diff --git a/.gitea/workflows/publish-psgallery.yml b/.gitea/workflows/publish-psgallery.yml index 435c250..8caa971 100644 --- a/.gitea/workflows/publish-psgallery.yml +++ b/.gitea/workflows/publish-psgallery.yml @@ -27,6 +27,23 @@ jobs: } Write-Host ("pwsh: " + (pwsh -NoProfile -Command '$PSVersionTable.PSVersion.ToString()')) Write-Host ("dotnet: " + (dotnet --version)) + Write-Host '--- dotnet --info ---' + dotnet --info + Write-Host '--- disk free ---' + df -h . + Write-Host '--- memory ---' + free -m + + - name: Restore NuGet packages + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + Write-Host '==> dotnet restore src/PSInfisicalAPI/PSInfisicalAPI.csproj' + dotnet restore src/PSInfisicalAPI/PSInfisicalAPI.csproj --verbosity normal + if ($LASTEXITCODE -ne 0) { throw "Restore of PSInfisicalAPI.csproj failed with exit code $LASTEXITCODE" } + Write-Host '==> dotnet restore src/PSInfisicalAPI.Tests/PSInfisicalAPI.Tests.csproj' + dotnet restore src/PSInfisicalAPI.Tests/PSInfisicalAPI.Tests.csproj --verbosity normal + if ($LASTEXITCODE -ne 0) { throw "Restore of PSInfisicalAPI.Tests.csproj failed with exit code $LASTEXITCODE" } - name: Build and test module shell: pwsh diff --git a/CHANGELOG.md b/CHANGELOG.md index da75758..ff6b704 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,198 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loos ## Unreleased -- **CI — Gitea artifact upload fix**: Replaced `actions/upload-artifact@v4` and `actions/download-artifact@v4` with the Gitea-compatible forks `christopherhx/gitea-upload-artifact@v4` and `christopherhx/gitea-download-artifact@v4` in `.gitea/workflows/publish-psgallery.yml`. The upstream v4 actions abort on Gitea because Gitea is detected as GHES, which the upstream v4 actions do not support (see [go-gitea/gitea#28853](https://github.com/go-gitea/gitea/issues/28853)). +## 2026.06.05.0117 + +- Build produced from commit cffda99591c9. + +## Unreleased (carried forward) + +## 2026.06.05.0015 + +- Build produced from commit fb27ab8a8503. + +## Unreleased (carried forward) + +- Fixed `ParameterNameConflictsWithAlias` registration error on `Get-InfisicalCertificateApplication`, `Get-InfisicalCertificateApplicationEnrollment`, and `New-InfisicalScepDynamicChallenge`. The cmdlets each declared an `[Alias]` entry that matched the parameter's own name, which PowerShell rejects at bind time and made the cmdlets unusable. + +## 2026.06.04.2335 + +- Build produced from commit 3c39a99b9a4c. + +## Unreleased (carried forward) + +## 2026.06.04.2305 + +- Build produced from commit 485ee8a7dd6a. + +## Unreleased (carried forward) + +- `Get-InfisicalCertificateApplication` added with `List` (default), `ById`, and `ByName` parameter sets. Binds to `/api/v1/cert-manager/applications` (list) and `/api/v1/cert-manager/applications/{applicationId}` / `/by-name/{name}` for single retrieval. Requests carry the `x-infisical-project-id` header so the certificate-manager scope resolves correctly. New `InfisicalCertificateApplication` model surfaces id, project, name, description, and counts. +- `Get-InfisicalCertificateApplicationEnrollment` added. Returns the API/EST/ACME/SCEP enrollment configuration for an application/profile pair (`GET /api/v1/cert-manager/applications/{applicationId}/profiles/{profileId}/enrollment`). The new `InfisicalCertificateApplicationEnrollment` model includes sub-blocks for each enrollment protocol; the SCEP block computes a SHA-1 `RaCertificateThumbprint` from the RA certificate PEM so it can be fed directly into MDM payloads. +- `New-InfisicalScepDynamicChallenge` added. Wraps `POST /scep/applications/{applicationId}/profiles/{profileId}/challenge` and returns the minted challenge as a `SecureString` (default) or string (`-AsPlainText`). The endpoint is gated by the dynamic-challenge feature on the target Infisical instance and by the calling identity's permission on `certificate-application-enrollment`. +- `Get-InfisicalScepMdmProfile` reworked into three parameter sets. `FromEnrollment` (new default) consumes an `InfisicalCertificateApplicationEnrollment` and auto-resolves `ServerUrl` from `scep.scepEndpointUrl`, `CAThumbprint` from the RA certificate, and the SCEP challenge (auto-minting when `challengeType=dynamic` and `-Challenge` is not supplied). `FromProfile` keeps the legacy projection from an `InfisicalCertificateProfile`, now requires `-ApplicationId`, and the default server URL is built against `/scep/applications/{appId}/profiles/{profileId}/pkiclient.exe`. `Manual` requires explicit `-ServerUrl`, `-Challenge`, and `-UniqueId`. +- `InfisicalApiInvoker` accepts an optional `extraHeaders` argument so callers can attach the `x-infisical-project-id` header and override `Accept` for plain-text responses (used by the new SCEP challenge endpoint). + +## 2026.06.04.2147 + +- Build produced from commit 183fb48c32ce. + +## Unreleased (carried forward) + +- `Get-InfisicalScepMdmProfile` added. Projects an `InfisicalCertificateProfile` (pipeline-bound) into a new `InfisicalScepMdmProfile` model that mirrors the Windows `ClientCertificateInstall/SCEP` CSP node set. `-ServerUrl` defaults to `{baseUri}/scep/{profileId}/pkiclient.exe` derived from the active connection (the `pkiclient.exe` suffix is the RFC 8894 / Cisco SCEP client compatibility holdover, not a server-side executable). `-UniqueId` defaults to a sanitized slug. `-Challenge` is a `SecureString` decrypted only when materializing the model. `KeyAlgorithm` and `EkuMapping` are inherited from the source profile defaults unless overridden. +- `Export-InfisicalScepMdmProfile` added. Serializes the model via `InfisicalScepMdmProfile.ToSyncMl()` (XDocument build, XmlWriter emit, XmlReader round-trip validation) and writes the result to `-Path` as UTF-8 without BOM. Auto-creates the target directory, honors `-WhatIf`/`-Confirm`, and follows the project rule for `-Force`: if the destination exists without `-Force`, the cmdlet logs a warning and returns instead of throwing. `-PassThru` emits the resulting `FileInfo`. +- `Write-InfisicalScepMdmProfileToWmi` added. Submits the same model to the local MDM Bridge WMI provider by invoking `New-CimInstance -Namespace root/cimv2/mdm/dmmap -ClassName MDM_ClientCertificateInstall_SCEP02 -Property ` through the host runspace (no new package references). Guards: throws `PlatformNotSupportedException` off Windows; device-scope enrollment requires an elevated session unless `-SkipElevationCheck` is passed; supports `-WhatIf`/`-Confirm`; `-PassThru` emits the returned CIM instance. Override `-ClassName` when targeting a different SCEP CSP version on the host. + +## 2026.06.04.2112 + +- Build produced from commit 3754de74f6c8. + +## Unreleased (carried forward) + +- Infisical API error responses are now parsed to surface the server-side `message`, `error`, and `reqId` fields. The 4xx/5xx exception message includes the human-readable explanation (e.g. "The project is of type secret-manager") instead of an opaque `Infisical API returned 400 (Bad Request)`. The `InfisicalApiException` gains `ApiErrorMessage` and `ApiRequestId` properties; `InfisicalErrorDetails` carries the same fields so PowerShell error records and logger output expose them. +- `Get-InfisicalCertificateProfile` added with `List` (default) and `ById` parameter sets. List binds to `GET /api/v1/cert-manager/certificate-profiles` (optional `-Limit`, `-Offset`, `-IncludeConfigs`); ById binds to `GET /api/v1/cert-manager/certificate-profiles/{certificateProfileId}`. New `InfisicalCertificateProfile` model surfaces ca/policy ids, slug, enrollment type, per-profile defaults (ttl, key/extended key usages), and the embedded CA/policy/apiConfig summaries. +- `Get-InfisicalCertificatePolicy` added with `List` (default) and `ById` parameter sets. List binds to `GET /api/v1/cert-manager/certificate-policies` (optional `-Limit`, `-Offset`); ById binds to `GET /api/v1/cert-manager/certificate-policies/{certificatePolicyId}`. New `InfisicalCertificatePolicy` model surfaces subject, SANs, key usages, extended key usages, algorithms, and validity. Polymorphic string-or-array fields (`allowed`, `required`, `keyAlgorithm`) are normalized to arrays; `sans` is normalized whether the API returns an object or an array. +- `Get-InfisicalCertificateAuthority` gains a `-Kind` parameter on the List parameter set with values `Internal` (default, preserves prior behavior against `/api/v1/cert-manager/ca/internal`), `Any` (binds to the generic `/api/v1/cert-manager/ca` endpoint which returns both internal and ACME CAs), and `Acme` (uses the generic endpoint and client-side filters to ACME issuers only). ById retrieval is unchanged and still resolves against the internal CA endpoint. +- `Request-InfisicalCertificate` gains a `ByProfile` parameter set bound by the new `-CertificateProfileId` parameter (alias `ProfileId`). The cmdlet generates a local keypair and CSR as usual, then POSTs to `/api/v1/cert-manager/certificates` with the profile id, the CSR, and a subject/attribute envelope (commonName, organization, organizationalUnit, country, state, locality, ttl, notBefore, notAfter, keyUsages, extendedKeyUsages). The wrapped response (`{certificate:{certificate,certificateChain,issuingCaCertificate,serialNumber,certificateId,privateKey}, certificateRequestId, status, message}`) is unwrapped into the existing `InfisicalSignedCertificate` shape so the install / reuse / chain-completion paths continue to work unchanged. Issuance that returns without a certificate body (e.g. status `pending_approval` or `pending_validation`) is logged as a warning and the cmdlet emits a status-only `InfisicalCertificateResult` (new `Status`, `StatusMessage`, `CertificateRequestId` properties) instead of throwing; install / chain / private-key-write steps are skipped in that case. Whether issuance is immediate or pending is dictated by the certificate policy bound to the profile (auto-approve vs. manual review and any required validation). + +## 2026.06.04.1920 + +- Build produced from commit 0f8f44afdb38. + +## Unreleased (carried forward) + +- `build.ps1` gains a `-CommitArtifacts` switch that, after a successful build, stages and commits only the build outputs (`Module/PSInfisicalAPI/bin/**`, `Module/PSInfisicalAPI/PSInfisicalAPI.psd1`, and the auto-inserted `CHANGELOG.md` build stamp) with a message that references the source commit whose hash is now embedded in `BuildCommitHash`. The switch is mutually exclusive with the older broader `-CommitOnSuccess` (which still uses `git add -A`). README extended with a "Committing source and build artifacts in lockstep" section describing the recommended two-commit workflow. + +## 2026.06.04.1917 + +- Build produced from commit a34db831d8bf. + +## Unreleased (carried forward) + +## 2026.06.04.1915 + +- Build produced from commit 2489b7adca98. + +## Unreleased (carried forward) + +## 2026.06.04.1911 + +- Build produced from commit 51bf819c37e5. + +## Unreleased (carried forward) + +## 2026.06.04.1906 + +- Build produced from commit 51bf819c37e5. + +## Unreleased (carried forward) + +- **BREAKING**: Removed the plural-noun discovery cmdlets `Get-InfisicalProjects`, `Get-InfisicalEnvironments`, `Get-InfisicalFolders`, `Get-InfisicalTags`, `Get-InfisicalSecrets`, and `Get-InfisicalCertificates`. Their behavior is now folded into the corresponding singular cmdlets via a `List` (default) / single-record parameter set pair, matching the existing `Get-InfisicalCertificateAuthority` precedent. Callers should drop the trailing `s`; invocation without the identity parameter (`-ProjectId`, `-EnvironmentSlugOrId`, `-FolderNameOrId`, `-TagSlugOrId`, `-SecretName`, `-SerialNumber`) now returns the list, and supplying the identity parameter returns the single record. No back-compat aliases were added. +- Added `Get-InfisicalPkiSubscriber` with `List` (default) and `ByName` parameter sets, backed by new `InfisicalPkiClient.ListPkiSubscribers` and `GetPkiSubscriber` methods, an `InfisicalPkiSubscriber` model, and corresponding DTOs/mapper. Use the emitted `Name` (slug) on `Request-InfisicalCertificate -PkiSubscriberSlug`. +- **Bug fix**: `Request-InfisicalCertificate -PkiSubscriberSlug ...` was returning 404 because the registry's `SignCertificateBySubscriber` endpoint pointed at `/api/v1/pki/pki-subscribers/{subscriberName}/sign-certificate` and `/api/v1/cert-manager/pki-subscribers/...`. Per Infisical's `v1/index.ts`, the subscriber router is mounted at `/pki/subscribers`, so the single correct path is `/api/v1/pki/subscribers/{subscriberName}/sign-certificate`. The redundant `cert-manager` template was removed; the PKI endpoint registry tests were updated to match. +- Updated MAML help in `Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml`: the six consolidated cmdlets and the new `Get-InfisicalPkiSubscriber` each ship three examples — two straight-line invocations (one per parameter set) plus one `OrderedDictionary` splat example. All in-text references to the removed plural cmdlets across other cmdlets' examples were updated to the singular form. +- `build.ps1`: `CmdletsToExport` and the `Test-ModuleImports` expected cmdlet list were updated to drop the six plural cmdlets and add `Get-InfisicalPkiSubscriber` (total: 34 exported cmdlets). + +## 2026.06.04.1825 + +- Build produced from commit 19615363e356. + +## Unreleased (carried forward) + +## 2026.06.04.1820 + +- Build produced from commit 19615363e356. + +## Unreleased (carried forward) + +- `Install-InfisicalCertificate` now routes chain certificates by self-signed status instead of dumping every chain entry into the Intermediate Certification Authorities store. Self-signed roots are installed into `StoreName.Root` (Trusted Root Certification Authorities) and non-self-signed intermediates are installed into `StoreName.CertificateAuthority` (Intermediate Certification Authorities). The leaf continues to use the user-specified `-StoreName`/`-StoreLocation` (default `My`/`CurrentUser`). `Request-InfisicalCertificate` already routed chain certs correctly; the same routing helper is now shared by both cmdlets. +- `InfisicalCertificateRequestHelpers` exposes a new public `GetChainCertificateTargetStore(X509Certificate2)` classifier and a new `InstallChain(IEnumerable, StoreLocation, bool, IInfisicalLogger, string)` overload. The existing `InstallChain(InfisicalSignedCertificate, ...)` overload now delegates to the new collection-based overload, so PKI chain-installation routing is centralized in one place. + +## 2026.06.04.1810 + +- Build produced from commit 19615363e356. + +## Unreleased (carried forward) + +- Authored MAML help (`Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml`) covering all 39 exported cmdlets. Every entry includes a synopsis, description, notes section, and two examples: a one-liner and an `OrderedDictionary` splat (with `OrdinalIgnoreCase`) that includes preceding `Get-` resolver commands wherever IDs or slugs are required. +- `build.ps1` now stages the cmdlet help XML next to the deployed binary. After the publish step, every culture directory under `Module/PSInfisicalAPI/` (matching `xx` or `xx-XX`) that contains `PSInfisicalAPI.dll-Help.xml` is mirrored into `bin//`. The script hard-fails if `bin/en-US/PSInfisicalAPI.dll-Help.xml` is missing or contains zero `` entries. +- `Test-ModuleImports` in `build.ps1` now dynamically enumerates exported cmdlets via `Get-Command -Module PSInfisicalAPI -CommandType Cmdlet`, cross-checks the result against an expected list of 39 cmdlet names (including the previously-missing `Copy-InfisicalSecret`), and for each cmdlet asserts that `Get-Help -Full` returns a non-empty synopsis (rejecting PowerShell's auto-generated cmdlet-name fallback), a non-empty description, and that `Get-Help -Examples` returns at least one example node whose `` block is non-empty. + +## 2026.06.04.1808 + +- Build produced from commit 19615363e356. + +## Unreleased (carried forward) + +## 2026.06.04.1658 + +- Build produced from commit 19615363e356. + +## Unreleased (carried forward) + +- `Request-InfisicalCertificate` reuse path now falls back to the Infisical certificate-bundle endpoint when the local trust stores do not contain the issuing intermediates or root. The cmdlet builds the local chain first; if the result has no intermediates and no root, it fetches `GetCertificateBundle(serialNumber)` and rebuilds the result with the bundle's chain PEM merged in. A new `-LocalChainOnly` switch opts out of the bundle fetch for strict offline behavior. Bundle-fetch failures are logged at verbose level and the cmdlet returns the local-only result. +- `InfisicalCertificateRequestHelpers.BuildResultFromExistingLocal` adds a second overload that accepts an `InfisicalCertificateBundle`; when supplied, chain certs from the bundle are deduplicated by thumbprint and merged with the locally-resolved chain before classification. + +## 2026.06.04.1652 + +- Build produced from commit 19615363e356. + +## Unreleased (carried forward) + +## 2026.06.04.1651 + +- Build produced from commit 19615363e356. + +## Unreleased (carried forward) + +## 2026.06.04.1634 + +- Build produced from commit 19615363e356. + +## Unreleased (carried forward) + +## 2026.06.04.1631 + +- Build produced from commit 19615363e356. + +## Unreleased (carried forward) + +## 2026.06.04.1622 + +- Build produced from commit 19615363e356. + +## Unreleased (carried forward) + +- **PKI contract fixes and cmdlet expansion**: + - `InfisicalPkiClient` no longer auto-injects `connection.ProjectId` into PKI CA list/retrieve calls; only the caller's explicit `-ProjectId` is forwarded so that cert-manager primary routes (which do not accept the query parameter) succeed. + - List/single CA and single certificate response parsing now tolerate raw arrays, wrapper objects (`{certificate: {...}}`, `{certificates: [...]}`), and nested `configuration` blocks. `InfisicalCaMapper` reads CA detail fields from `configuration` first, falling back to top-level. + - `RetrieveCertificate(connection, identifier)` added on `InfisicalPkiClient`. +- **New cmdlets**: + - **`Get-InfisicalCertificate`** — single-record retrieval by `-SerialNumber`/`-Id` (mandatory positional). + - **`Get-InfisicalCertificates`** — listing with light filtering (`-CommonName`, `-FriendlyName`, `-Status`, `-CaId`, `-Limit`, `-Offset`, `-NoAutoPage`). Auto-paginates by default. + - **`Request-InfisicalCertificate`** — generates a keypair locally (private key never leaves the device), submits a PKCS#10 CSR to either `pki-subscribers/{name}/sign-certificate` (`-PkiSubscriberSlug`) or `ca/{caId}/sign-certificate` (`-CertificateAuthorityId`), and returns a single `InfisicalCertificateResult` object with the leaf and chain pre-classified. The result exposes `Leaf : X509Certificate2`, `Intermediates : X509Certificate2[]`, `Root : X509Certificate2` (nullable), `Chain : X509Certificate2[]` (ordered leaf → intermediates → root, deduplicated by thumbprint), plus pass-through `SerialNumber`, `CertificatePem`, `CertificateChainPem`, and `PrivateKeyPem`. Supports `-Subject` (`IDictionary` with `CN`/`C`/`ST`/`L`/`O`/`OU`/`E` keys) merged with individual `-CommonName`/`-Country`/etc. parameters (individual params win), `-DnsName`/`-IpAddress` SANs (auto-populated from local FQDN when omitted). Idempotency: scans the local `X509Store` for an existing certificate matching `CN` and an Infisical-known serial number; returns the existing certificate wrapped in an `InfisicalCertificateResult` whose `Intermediates`/`Root`/`Chain` are populated by walking the local trust stores via `X509Chain` (no network calls, revocation checks disabled), and whose `CertificatePem`/`CertificateChainPem` are reconstructed from the resolved certs. Reuse is short-circuited unless `-Force` or `-AllowRenewal` (with optional `-RenewalThresholdDays`, default 30) requests a new one. Installation: `-Install` adds the leaf to `-StoreName`/`-StoreLocation` (default `My`/`CurrentUser`); `-InstallChain` additionally places intermediates into `CertificateAuthority` and self-signed roots into `Root` for the same `-StoreLocation`. `-KeyStorageFlags` is passed through to `X509Certificate2` import. + - **Multi-algorithm CSR support** on `Request-InfisicalCertificate` via split parameters: `-KeyAlgorithm` (`Rsa`/`Ecdsa`/`Ed25519`, default `Rsa`), `-KeySize` (`2048`/`3072`/`4096`, default `2048`, applies to RSA only), `-Curve` (`P256`/`P384`, default `P256`, applies to ECDSA only). Signature algorithms are picked automatically: SHA256WITHRSA for RSA, SHA256WITHECDSA / SHA384WITHECDSA for ECDSA P-256/P-384, and Ed25519 (pure-EdDSA) for Ed25519. The underlying `InfisicalCsrBuilder.Build(subject, dns, ip, options)` API was updated to take an `InfisicalCsrOptions` object in place of the prior `keySize` int. + - **Sign-certificate endpoint registrations**: `SignCertificateBySubscriber` and `SignCertificateByCa` registered with both `/api/v1/pki/...` and `/api/v1/cert-manager/...` candidate paths and marked `ContainsSecretMaterialInResponse = true`. + +## 2026.06.04.1554 + +- Build produced from commit 19615363e356. + +## Unreleased (carried forward) + +## 2026.06.04.1512 + +- Build produced from commit 19615363e356. + +## Unreleased (carried forward) + +## 2026.06.04.1508 + +- Build produced from commit 19615363e356. + +## Unreleased (carried forward) + +- **CI — Gitea artifact upload fix**: Replaced `actions/upload-artifact@v4` and `actions/download-artifact@v4` with the Gitea-compatible forks `christopherhx/gitea-upload-artifact@v4` and `christopherhx/gitea-download-artifact@v4` in `.gitea/workflows/publish-psgallery.yml`. The upstream v4 actions abort on Gitea because Gitea is detected as GHES, which the upstream v4 actions do not support (see [go-gitea/gitea#28853](https://github.com/go-gitea/gitea/issues/28853)). ## 2026.06.04.0123 @@ -14,7 +205,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loos ## Unreleased (carried forward) -- **M10 polish — formatting, type metadata, and PKI route aliases**: +- **M10 polish — formatting, type metadata, and PKI route aliases**: - Added default table views and `DefaultDisplayPropertySet` entries for `InfisicalCertificateAuthority`, `InfisicalCertificate`, and `InfisicalCertificateBundle` in the module `Format.ps1xml` / `Types.ps1xml`. - Realigned PKI endpoint registry to current Infisical paths: `ListInternalCertificateAuthorities` and `RetrieveInternalCertificateAuthority` now use `/api/v1/cert-manager/ca/internal[/{caId}]` as primary, with legacy `/api/v1/pki/ca/internal[/{caId}]` retained as a fallback alias. `GetCertificateBundle` and `RetrieveCertificate` similarly carry `cert-manager` fallback aliases. - `InfisicalApiInvoker.InvokeWithCandidateFallback` walks the candidate list and falls back on `404`/`405`, used by `InfisicalPkiClient` so older self-hosted Infisical instances are tolerated transparently. @@ -25,7 +216,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loos ## Unreleased (carried forward) -- **M10 — PKI Internal CAs, Certificates & Windows Store integration**: +- **M10 — PKI Internal CAs, Certificates & Windows Store integration**: - **`Get-InfisicalCertificateAuthority`** lists internal certificate authorities for the current project, or returns a single CA with `-CaId`. - **`Search-InfisicalCertificate`** wraps `POST /api/v1/projects/{projectId}/certificates/search` with rich filters (`-CommonName`, `-FriendlyName`, `-Search`, `-Status`, `-CaId`, `-ProfileId`, `-ApplicationId`, `-EnrollmentType`, `-KeyAlgorithm`, `-SignatureAlgorithm`, `-Source`, `-NotAfterFrom/To`, `-NotBeforeFrom/To`, `-SortBy/-SortOrder`, `-Limit/-Offset`). Auto-paginates unless `-NoAutoPage` is set. - **`ConvertTo-InfisicalCertificate`** accepts an `InfisicalCertificate`, `InfisicalCertificateBundle`, or `-SerialNumber`, fetches the bundle endpoint when needed, and emits a `System.Security.Cryptography.X509Certificates.X509Certificate2` with the private key attached. `-NoPrivateKey` skips key parsing; `-IncludeChain` additionally emits intermediates; `-KeyStorageFlags` controls import behavior. @@ -52,7 +243,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loos ## 2026.06.03.2207 - Build produced from commit 09c3d5c68bbc. -- **M9 — Bulk, Duplicate & Inheritance**: +- **M9 — Bulk, Duplicate & Inheritance**: - **Bulk parameter sets** added to `New-InfisicalSecret`, `Update-InfisicalSecret`, and `Remove-InfisicalSecret` accepting `-Secrets Hashtable[]`; client methods `CreateBatch`/`UpdateBatch`/`DeleteBatch` wrap `POST|PATCH|DELETE /api/v3/secrets/batch/raw`. - **`Copy-InfisicalSecret`** cmdlet added, wrapping `POST /api/v4/secrets/duplicate` with source/destination environment + path parameters and per-attribute copy toggles. - **Connection inheritance** centralized in `InfisicalCmdletBase` (`ResolveProjectId`/`ResolveEnvironment`/`ResolveSecretPath`/`ResolveApiVersion`/`ResolveOrganizationId`). Explicit parameters always win; missing values fall back to the active connection and emit a `-Verbose` line. diff --git a/Module/PSInfisicalAPI/PSInfisicalAPI.psd1 b/Module/PSInfisicalAPI/PSInfisicalAPI.psd1 index e0de7f0..a9c70cf 100644 --- a/Module/PSInfisicalAPI/PSInfisicalAPI.psd1 +++ b/Module/PSInfisicalAPI/PSInfisicalAPI.psd1 @@ -1,6 +1,6 @@ @{ RootModule = 'PSInfisicalAPI.psm1' - ModuleVersion = '2026.06.04.0123' + ModuleVersion = '2026.06.05.0117' GUID = 'b8a2f3d4-7c51-4d2f-9e6a-1f0c8b3d4e51' Author = 'Grace Solutions' CompanyName = 'Grace Solutions' @@ -12,7 +12,6 @@ CmdletsToExport = @( 'Connect-Infisical', 'Disconnect-Infisical', - 'Get-InfisicalSecrets', 'Get-InfisicalSecret', 'New-InfisicalSecret', 'Update-InfisicalSecret', @@ -20,32 +19,39 @@ 'Copy-InfisicalSecret', 'ConvertTo-InfisicalSecretDictionary', 'Export-InfisicalSecrets', - 'Get-InfisicalProjects', 'Get-InfisicalProject', 'New-InfisicalProject', 'Update-InfisicalProject', 'Remove-InfisicalProject', - 'Get-InfisicalEnvironments', 'Get-InfisicalEnvironment', 'New-InfisicalEnvironment', 'Update-InfisicalEnvironment', 'Remove-InfisicalEnvironment', - 'Get-InfisicalFolders', 'Get-InfisicalFolder', 'New-InfisicalFolder', 'Update-InfisicalFolder', 'Remove-InfisicalFolder', - 'Get-InfisicalTags', 'Get-InfisicalTag', 'New-InfisicalTag', 'Update-InfisicalTag', 'Remove-InfisicalTag', 'Get-InfisicalCertificateAuthority', + 'Get-InfisicalPkiSubscriber', + 'Get-InfisicalCertificateProfile', + 'Get-InfisicalCertificatePolicy', + 'Get-InfisicalCertificate', 'Search-InfisicalCertificate', + 'Request-InfisicalCertificate', 'ConvertTo-InfisicalCertificate', 'Install-InfisicalCertificate', 'Uninstall-InfisicalCertificate', - 'Export-InfisicalCertificate' + 'Export-InfisicalCertificate', + 'Get-InfisicalCertificateApplication', + 'Get-InfisicalCertificateApplicationEnrollment', + 'New-InfisicalScepDynamicChallenge', + 'Get-InfisicalScepMdmProfile', + 'Export-InfisicalScepMdmProfile', + 'Write-InfisicalScepMdmProfileToWmi' ) AliasesToExport = @() VariablesToExport = @() @@ -57,7 +63,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 = '2cbd5c2008f5' + CommitHash = 'cffda99591c9' } } } \ No newline at end of file diff --git a/Module/PSInfisicalAPI/bin/PSInfisicalAPI.dll b/Module/PSInfisicalAPI/bin/PSInfisicalAPI.dll index e4308f8..fd0a393 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 new file mode 100644 index 0000000..5d82499 --- /dev/null +++ b/Module/PSInfisicalAPI/bin/en-US/PSInfisicalAPI.dll-Help.xml @@ -0,0 +1,1696 @@ + + + + + + Connect-Infisical + Establishes an authenticated session with an Infisical server and stores it for use by subsequent cmdlets. + Connect + Infisical + + + Authenticates against an Infisical instance using one of the supported auth providers (UniversalAuth, Token, JWT, OIDC, LDAP, Azure, GCP IAM) and stores the resulting connection in the module-level session manager. Subsequent cmdlets pick up the connection automatically. If parameters such as BaseUri, OrganizationId, ClientId, or ClientSecret are not supplied, the cmdlet attempts to resolve them from a curated list of environment-variable name patterns across Process, User, and Machine scopes. The connection no longer carries a default ProjectId, Environment, or SecretPath; downstream cmdlets accept those as explicit (mandatory where applicable) parameters. + + + Notes + + Use -PassThru to emit the resulting InfisicalConnection object; by default the connection is stored silently. SecureString-typed parameters such as ClientSecret, AccessToken, Jwt, and Password are never logged. + The cmdlet pins the API version to the bound value when -ApiVersion is supplied explicitly; otherwise the default 'v4' is used and remains overridable per-call. + + + + + EXAMPLE 1 + Connect-Infisical -BaseUri 'https://app.infisical.com' -ClientId $ClientId -ClientSecret $ClientSecret -OrganizationId $OrgId + Performs a Universal-Auth machine-identity login and stores the resulting session for subsequent cmdlets. + + + EXAMPLE 2 + $ConnectInfisicalParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$ConnectInfisicalParameters.BaseUri = 'https://app.infisical.com' +$ConnectInfisicalParameters.OrganizationId = $OrganizationId +$ConnectInfisicalParameters.ClientId = $ClientId +$ConnectInfisicalParameters.ClientSecret = $ClientSecret +$ConnectInfisicalParameters.ApiVersion = 'v4' +$ConnectInfisicalParameters.PassThru = $True +$ConnectInfisicalParameters.Verbose = $True + +$ConnectInfisicalResult = Connect-Infisical @ConnectInfisicalParameters + Builds an ordered parameter dictionary, splats it onto Connect-Infisical, and captures the returned InfisicalConnection for later reuse. + + + + + + + Disconnect-Infisical + Clears the current Infisical session from the module-level session manager. + Disconnect + Infisical + + + Removes the cached InfisicalConnection so subsequent cmdlets that require an active session will fail until Connect-Infisical is invoked again. The cmdlet does not contact the Infisical server. + + + Notes + + Use -PassThru to receive a status object that includes the disconnect timestamp; by default the cmdlet returns no output. + + + + + EXAMPLE 1 + Disconnect-Infisical + Clears the active Infisical session silently. + + + EXAMPLE 2 + $DisconnectInfisicalParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$DisconnectInfisicalParameters.PassThru = $True +$DisconnectInfisicalParameters.Verbose = $True + +$DisconnectInfisicalResult = Disconnect-Infisical @DisconnectInfisicalParameters + Disconnects and captures a status object that includes IsConnected and DisconnectedAtUtc for logging. + + + + + + + Get-InfisicalSecret + Lists or retrieves Infisical secrets within a project, environment, and optional folder path. + Get + InfisicalSecret + + + Default (List parameter set) enumerates secrets under the supplied project and environment, optionally recursing through subfolders and filtering by metadata or tag slugs. When -SecretName is supplied (Single parameter set) the cmdlet returns one secret by name; -Version and -Type tune the single-record fetch. -ProjectId and -Environment are mandatory in both modes; -SecretPath defaults to '/' and -ApiVersion defaults to the value pinned on the active InfisicalConnection. + + + Notes + + Use -Recursive together with -SecretPath to walk an entire folder subtree in List mode. Pipe the result into ConvertTo-InfisicalSecretDictionary for hashtable-style lookup. The returned InfisicalSecret stores the value as SecureString; call .GetPlainTextValue() to materialize the cleartext value only when strictly required. + + + + + EXAMPLE 1 + Get-InfisicalSecret -ProjectId $ProjectId -Environment 'dev' -SecretPath '/Windows' -Recursive + Lists every secret under /Windows in the dev environment of the specified project. + + + EXAMPLE 2 + Get-InfisicalSecret -SecretName 'DATABASE_URL' + Retrieves the DATABASE_URL secret from the project and environment pinned by Connect-Infisical. + + + EXAMPLE 3 + $GetInfisicalSecretParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalSecretParameters.ProjectId = $ProjectId +$GetInfisicalSecretParameters.Environment = 'dev' +$GetInfisicalSecretParameters.SecretPath = "/Windows/$($CallingScriptPath.BaseName)" +$GetInfisicalSecretParameters.Recursive = $True +$GetInfisicalSecretParameters.ExpandSecretReferences = $True +$GetInfisicalSecretParameters.IncludeImports = $True +$GetInfisicalSecretParameters.IncludePersonalOverrides = $True +$GetInfisicalSecretParameters.Verbose = $True + +$GetInfisicalSecretResult = Get-InfisicalSecret @GetInfisicalSecretParameters + Lists secrets under a script-specific subpath with imports, personal overrides, and reference expansion enabled. + + + + + + + New-InfisicalSecret + Creates a new Infisical secret, with support for SecureString values and bulk creation. + New + InfisicalSecret + + + Creates one or many secrets. Three parameter sets are supported: PlainText (SecretName + SecretValue), SecureString (SecretName + SecureSecretValue), and Bulk (an array of hashtables piped or supplied via -Secrets). Honors -WhatIf and -Confirm. + + + Notes + + Pass -SkipMultilineEncoding when the value already contains literal newlines that the server should preserve verbatim. Use -TagIds to attach tag references at creation time. + + + + + EXAMPLE 1 + New-InfisicalSecret -SecretName 'API_KEY' -SecretValue 'super-secret-value' -ProjectId $ProjectId -Environment 'dev' + Creates a single shared secret in the specified project/environment. + + + EXAMPLE 2 + $GetInfisicalTagResult = Get-InfisicalTag -ProjectId $ProjectId + +$NewInfisicalSecretParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$NewInfisicalSecretParameters.SecretName = 'API_KEY' +$NewInfisicalSecretParameters.SecretValue = 'super-secret-value' +$NewInfisicalSecretParameters.SecretComment = 'Issued by deployment pipeline' +$NewInfisicalSecretParameters.ProjectId = $ProjectId +$NewInfisicalSecretParameters.Environment = 'dev' +$NewInfisicalSecretParameters.SecretPath = "/Windows/$($CallingScriptPath.BaseName)" +$NewInfisicalSecretParameters.TagIds = @($GetInfisicalTagResult[0].Id) +$NewInfisicalSecretParameters.Verbose = $True + +$NewInfisicalSecretResult = New-InfisicalSecret @NewInfisicalSecretParameters + Looks up tags to attach, then creates a single secret with a comment and tag association under a script-specific subpath. + + + + + + + Update-InfisicalSecret + Updates an existing Infisical secret value, comment, name, or tags. + Update + InfisicalSecret + + + Updates one or many secrets. Supports PlainText, SecureString, and Bulk parameter sets. Use -NewSecretName to rename a secret, -SecretComment to update its comment, and -TagIds to replace tag associations. Honors -WhatIf and -Confirm. + + + Notes + + Only the parameters you bind are sent; omitted scalar parameters are not modified server-side. The Bulk parameter set accepts pipeline input of hashtables containing SecretName/SecretValue/etc. + + + + + EXAMPLE 1 + Update-InfisicalSecret -SecretName 'API_KEY' -SecretValue 'rotated-value' -ProjectId $ProjectId -Environment 'dev' + Rotates the API_KEY secret in the specified project/environment. + + + EXAMPLE 2 + $UpdateInfisicalSecretParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$UpdateInfisicalSecretParameters.SecretName = 'API_KEY' +$UpdateInfisicalSecretParameters.NewSecretName = 'API_KEY_V2' +$UpdateInfisicalSecretParameters.SecretValue = 'rotated-value' +$UpdateInfisicalSecretParameters.SecretComment = 'Rotated by scheduled job' +$UpdateInfisicalSecretParameters.ProjectId = $ProjectId +$UpdateInfisicalSecretParameters.Environment = 'dev' +$UpdateInfisicalSecretParameters.SecretPath = "/Windows/$($CallingScriptPath.BaseName)" +$UpdateInfisicalSecretParameters.Verbose = $True + +$UpdateInfisicalSecretResult = Update-InfisicalSecret @UpdateInfisicalSecretParameters + Rotates the value, renames the secret, and updates its comment in a single call. + + + + + + + Remove-InfisicalSecret + Deletes one or many Infisical secrets by name. + Remove + InfisicalSecret + + + Deletes a single secret (Single parameter set) or a batch of secrets by name (Bulk parameter set). High ConfirmImpact triggers prompts by default. -PassThru emits the removed secret names. + + + Notes + + Removal is irreversible from this cmdlet's perspective; rely on Infisical's audit log or secret-version history for forensics. Honors -WhatIf and -Confirm. + + + + + EXAMPLE 1 + Remove-InfisicalSecret -SecretName 'API_KEY_V1' -ProjectId $ProjectId -Environment 'dev' -Confirm:$False + Deletes a single secret without prompting. + + + EXAMPLE 2 + $RemoveInfisicalSecretParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$RemoveInfisicalSecretParameters.SecretNames = @('LEGACY_KEY_1','LEGACY_KEY_2','LEGACY_KEY_3') +$RemoveInfisicalSecretParameters.ProjectId = $ProjectId +$RemoveInfisicalSecretParameters.Environment = 'dev' +$RemoveInfisicalSecretParameters.SecretPath = "/Windows/$($CallingScriptPath.BaseName)" +$RemoveInfisicalSecretParameters.PassThru = $True +$RemoveInfisicalSecretParameters.Confirm = $False +$RemoveInfisicalSecretParameters.Verbose = $True + +$RemoveInfisicalSecretResult = Remove-InfisicalSecret @RemoveInfisicalSecretParameters + Bulk-deletes three legacy secrets and returns the removed names for audit logging. + + + + + + + Copy-InfisicalSecret + Duplicates one or more secrets into a different environment or secret path. + Copy + InfisicalSecret + + + Server-side duplicates an array of secret IDs into a destination environment (and optional destination path), with switches that control whether the value, comment, tags, and metadata are copied. Use Get-InfisicalSecret followed by selection of the desired Id values to feed -SecretId. + + + Notes + + Set -OverwriteExisting to replace same-named secrets at the destination. Without -CopySecretValue, the destination secrets are created with empty values, preserving only metadata. + + + + + EXAMPLE 1 + Get-InfisicalSecret -ProjectId $ProjectId -Environment 'dev' | Select-Object -ExpandProperty Id | Copy-InfisicalSecret -ProjectId $ProjectId -SourceEnvironment 'dev' -DestinationEnvironment 'staging' -CopySecretValue + Copies all secrets from dev into staging, including their values. + + + EXAMPLE 2 + $GetInfisicalSecretResult = Get-InfisicalSecret -ProjectId $ProjectId -Environment 'dev' -SecretPath '/Windows' -Recursive + +$CopyInfisicalSecretParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$CopyInfisicalSecretParameters.SecretId = $GetInfisicalSecretResult.Id +$CopyInfisicalSecretParameters.ProjectId = $ProjectId +$CopyInfisicalSecretParameters.SourceEnvironment = 'dev' +$CopyInfisicalSecretParameters.SourceSecretPath = '/Windows' +$CopyInfisicalSecretParameters.DestinationEnvironment = 'staging' +$CopyInfisicalSecretParameters.DestinationSecretPath = '/Windows' +$CopyInfisicalSecretParameters.OverwriteExisting = $True +$CopyInfisicalSecretParameters.CopySecretValue = $True +$CopyInfisicalSecretParameters.CopySecretComment = $True +$CopyInfisicalSecretParameters.CopyTags = $True +$CopyInfisicalSecretParameters.CopyMetadata = $True +$CopyInfisicalSecretParameters.Verbose = $True + +$CopyInfisicalSecretResult = Copy-InfisicalSecret @CopyInfisicalSecretParameters + Promotes every Windows secret from dev into staging with full value/comment/tag/metadata propagation. + + + + + + + ConvertTo-InfisicalSecretDictionary + Converts a stream of InfisicalSecret objects into a name-keyed Dictionary of SecureString or plain text values. + ConvertTo + InfisicalSecretDictionary + + + Aggregates an incoming pipeline of InfisicalSecret objects into a case-insensitive Dictionary keyed by SecretName. By default values are SecureString; pass -AsPlainText to materialize string values. Duplicate keys are handled via the -DuplicateKeyBehavior parameter (Error, FirstWins, LastWins). + + + Notes + + Use this conversion before splatting secrets into another process (-AsPlainText) or before passing them to libraries that expect SecureString-keyed lookups (default). + + + + + EXAMPLE 1 + Get-InfisicalSecret -ProjectId $ProjectId -Environment 'dev' | ConvertTo-InfisicalSecretDictionary -AsPlainText + Builds a plain-text dictionary of every secret in the dev environment of the specified project. + + + EXAMPLE 2 + $GetInfisicalSecretResult = Get-InfisicalSecret -ProjectId $ProjectId -Environment 'dev' -SecretPath "/Windows/$($CallingScriptPath.BaseName)" -Recursive + +$ConvertToInfisicalSecretDictionaryParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$ConvertToInfisicalSecretDictionaryParameters.InputObject = $GetInfisicalSecretResult +$ConvertToInfisicalSecretDictionaryParameters.DuplicateKeyBehavior = 'LastWins' +$ConvertToInfisicalSecretDictionaryParameters.AsPlainText = $True +$ConvertToInfisicalSecretDictionaryParameters.Verbose = $True + +$ConvertToInfisicalSecretDictionaryResult = ConvertTo-InfisicalSecretDictionary @ConvertToInfisicalSecretDictionaryParameters + Aggregates recursive secret results into a plain-text dictionary, with the last value winning on key collisions. + + + + + + + Export-InfisicalSecrets + Exports InfisicalSecret objects to disk or environment variables in a chosen file format. + Export + InfisicalSecrets + + + Buffers an incoming pipeline of InfisicalSecret objects and writes them to a file in the requested format (DotEnv, Json, Yaml, EnvironmentVariables, etc.) or sets them as environment variables on the chosen scope (Process, User, Machine). -Encoding controls text encoding for file outputs. + + + Notes + + EnvironmentVariables format does not require -Path; all other formats do. User/Machine scopes require appropriate privileges (Machine scope requires elevation on Windows). + + + + + EXAMPLE 1 + Get-InfisicalSecret -ProjectId $ProjectId -Environment 'dev' | Export-InfisicalSecrets -Format DotEnv -Path '.\.env' -Force + Writes the dev environment's secrets for the specified project to a .env file. + + + EXAMPLE 2 + $GetInfisicalSecretResult = Get-InfisicalSecret -ProjectId $ProjectId -Environment 'dev' -SecretPath "/Windows/$($CallingScriptPath.BaseName)" -Recursive + +$ExportInfisicalSecretsParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$ExportInfisicalSecretsParameters.InputObject = $GetInfisicalSecretResult +$ExportInfisicalSecretsParameters.Format = 'EnvironmentVariables' +$ExportInfisicalSecretsParameters.Scope = 'Process' +$ExportInfisicalSecretsParameters.Force = $True +$ExportInfisicalSecretsParameters.Verbose = $True + +$ExportInfisicalSecretsResult = Export-InfisicalSecrets @ExportInfisicalSecretsParameters + Projects the recursive secret result into Process-scope environment variables for the current PowerShell session. + + + + + + + Get-InfisicalProject + Lists or retrieves Infisical projects accessible to the current identity. + Get + InfisicalProject + + + Default (List parameter set) returns every project the active session can see; project visibility is governed by Infisical's role assignments. -Type filters the list to a single product surface (secret-manager, cert-manager, kms, ssh, secret-scanning, pam, ai). -IncludeRoles asks the server to return the caller's role bindings on each project. When -ProjectId is supplied (Single parameter set) the cmdlet returns the one matching record. + + + Notes + + The List-mode result is an array of InfisicalProject objects; pipe into Where-Object or Select-Object to filter by Slug, Name, or Id. The cmdlet accepts pipeline input by property name on -ProjectId. + + + + + EXAMPLE 1 + Get-InfisicalProject + Lists every project the current session can see. + + + EXAMPLE 2 + Get-InfisicalProject -ProjectId $ProjectId + Retrieves the canonical record for a single project by id. + + + EXAMPLE 3 + Get-InfisicalProject -Type 'cert-manager' -IncludeRoles + Lists every Certificate Manager project visible to the session, including the caller's role bindings. + + + EXAMPLE 4 + $GetInfisicalProjectListResult = Get-InfisicalProject -Type 'secret-manager' | Where-Object { $_.Slug -ilike 'platform-*' } + +$GetInfisicalProjectParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalProjectParameters.ProjectId = $GetInfisicalProjectListResult[0].Id +$GetInfisicalProjectParameters.Verbose = $True + +$GetInfisicalProjectResult = Get-InfisicalProject @GetInfisicalProjectParameters + Filters Secret Manager projects to slugs that begin with 'platform-' and refetches the first match by id. + + + + + + + New-InfisicalProject + Creates a new Infisical project in the active organization. + New + InfisicalProject + + + Creates a project with the supplied name and optional slug, description, type, and organization id. If -OrganizationId is not supplied, the active session's organization is used. Honors -WhatIf and -Confirm. + + + Notes + + Slug must be unique within the organization; if not supplied, the server derives one from the project name. + + + + + EXAMPLE 1 + New-InfisicalProject -ProjectName 'Platform Telemetry' + Creates a new project named 'Platform Telemetry' in the active organization. + + + EXAMPLE 2 + $NewInfisicalProjectParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$NewInfisicalProjectParameters.ProjectName = 'Platform Telemetry' +$NewInfisicalProjectParameters.Slug = 'platform-telemetry' +$NewInfisicalProjectParameters.Description = 'Secrets for platform telemetry pipeline' +$NewInfisicalProjectParameters.Type = 'secret-manager' +$NewInfisicalProjectParameters.OrganizationId = $ConnectInfisicalParameters.OrganizationId +$NewInfisicalProjectParameters.Verbose = $True + +$NewInfisicalProjectResult = New-InfisicalProject @NewInfisicalProjectParameters + Creates a project with an explicit slug, description, and type bound to a specific organization id. + + + + + + + Update-InfisicalProject + Updates the name, description, or auto-capitalization flag on an existing project. + Update + InfisicalProject + + + Updates mutable attributes on a project. -ProjectId is required. Only parameters that are bound are sent to the server. Honors -WhatIf and -Confirm. + + + Notes + + AutoCapitalization controls whether secret names submitted in mixed case are stored uppercase server-side; setting it false preserves the literal case supplied by clients. + + + + + EXAMPLE 1 + Update-InfisicalProject -Name 'Platform Telemetry (v2)' + Renames the supplied project. + + + EXAMPLE 2 + $GetInfisicalProjectResult = Get-InfisicalProject | Where-Object { $_.Slug -eq 'platform-telemetry' } + +$UpdateInfisicalProjectParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$UpdateInfisicalProjectParameters.ProjectId = $GetInfisicalProjectResult.Id +$UpdateInfisicalProjectParameters.Name = 'Platform Telemetry (v2)' +$UpdateInfisicalProjectParameters.Description = 'Migrated to v2 pipeline' +$UpdateInfisicalProjectParameters.AutoCapitalization = $False +$UpdateInfisicalProjectParameters.Verbose = $True + +$UpdateInfisicalProjectResult = Update-InfisicalProject @UpdateInfisicalProjectParameters + Locates the project by slug, renames it, updates the description, and disables auto-capitalization. + + + + + + + Remove-InfisicalProject + Deletes an Infisical project. + Remove + InfisicalProject + + + Deletes a project by Id. -ProjectId is required. High ConfirmImpact prompts unless -Confirm:$False is supplied. -PassThru emits the removed project id. + + + Notes + + This is destructive and removes all secrets, environments, folders, and tags within the project. Honors -WhatIf and -Confirm. + + + + + EXAMPLE 1 + Remove-InfisicalProject -Confirm:$False + Deletes the supplied project without prompting. + + + EXAMPLE 2 + $GetInfisicalProjectResult = Get-InfisicalProject | Where-Object { $_.Slug -eq 'sandbox-temp' } + +$RemoveInfisicalProjectParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$RemoveInfisicalProjectParameters.ProjectId = $GetInfisicalProjectResult.Id +$RemoveInfisicalProjectParameters.PassThru = $True +$RemoveInfisicalProjectParameters.Confirm = $False +$RemoveInfisicalProjectParameters.Verbose = $True + +$RemoveInfisicalProjectResult = Remove-InfisicalProject @RemoveInfisicalProjectParameters + Finds the sandbox project by slug, removes it without confirmation, and emits the removed project id for logging. + + + + + + + Get-InfisicalEnvironment + Lists or retrieves Infisical environments defined on a project. + Get + InfisicalEnvironment + + + Default (List parameter set) returns every environment configured on the supplied project. When -EnvironmentSlugOrId is supplied (Single parameter set) the cmdlet returns one environment by slug or id. -ProjectId is required in both modes. + + + Notes + + Each InfisicalEnvironment carries both Id and Slug; downstream cmdlets accept either form on -Environment-like parameters. Accepts pipeline input by property name on -EnvironmentSlugOrId. + + + + + EXAMPLE 1 + Get-InfisicalEnvironment + Lists every environment defined on the supplied project. + + + EXAMPLE 2 + Get-InfisicalEnvironment -EnvironmentSlugOrId 'dev' + Retrieves the 'dev' environment from the supplied project. + + + EXAMPLE 3 + $GetInfisicalProjectResult = Get-InfisicalProject | Where-Object { $_.Slug -eq 'platform-telemetry' } + +$GetInfisicalEnvironmentParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalEnvironmentParameters.EnvironmentSlugOrId = 'dev' +$GetInfisicalEnvironmentParameters.ProjectId = $GetInfisicalProjectResult.Id +$GetInfisicalEnvironmentParameters.Verbose = $True + +$GetInfisicalEnvironmentResult = Get-InfisicalEnvironment @GetInfisicalEnvironmentParameters + Resolves a project by slug and re-fetches the dev environment record by slug under that project. + + + + + + + New-InfisicalEnvironment + Creates a new environment on an Infisical project. + New + InfisicalEnvironment + + + Creates an environment with the supplied display name and slug, optionally setting its sort -Position. -ProjectId is required. Honors -WhatIf and -Confirm. + + + Notes + + Slugs must be unique within the project and are used as the canonical -Environment value across all other cmdlets. + + + + + EXAMPLE 1 + New-InfisicalEnvironment -Name 'Staging' -Slug 'staging' + Adds a Staging environment to the supplied project. + + + EXAMPLE 2 + $GetInfisicalProjectResult = Get-InfisicalProject | Where-Object { $_.Slug -eq 'platform-telemetry' } + +$NewInfisicalEnvironmentParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$NewInfisicalEnvironmentParameters.ProjectId = $GetInfisicalProjectResult.Id +$NewInfisicalEnvironmentParameters.Name = 'Staging' +$NewInfisicalEnvironmentParameters.Slug = 'staging' +$NewInfisicalEnvironmentParameters.Position = 20 +$NewInfisicalEnvironmentParameters.Verbose = $True + +$NewInfisicalEnvironmentResult = New-InfisicalEnvironment @NewInfisicalEnvironmentParameters + Adds a Staging environment at sort position 20 on the resolved project. + + + + + + + Update-InfisicalEnvironment + Updates the name, slug, or sort order of an existing Infisical environment. + Update + InfisicalEnvironment + + + Updates an environment identified by -EnvironmentId. -ProjectId is required. Only bound parameters are sent to the server. Honors -WhatIf and -Confirm. + + + Notes + + Changing -Slug can break downstream automation that pins to the previous slug. Coordinate slug rotation with consumers. + + + + + EXAMPLE 1 + Update-InfisicalEnvironment -EnvironmentId $EnvId -Name 'Pre-Production' + Renames an environment in the supplied project. + + + EXAMPLE 2 + $GetInfisicalEnvironmentResult = Get-InfisicalEnvironment | Where-Object { $_.Slug -eq 'staging' } + +$UpdateInfisicalEnvironmentParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$UpdateInfisicalEnvironmentParameters.EnvironmentId = $GetInfisicalEnvironmentResult.Id +$UpdateInfisicalEnvironmentParameters.ProjectId = $ProjectId +$UpdateInfisicalEnvironmentParameters.Name = 'Pre-Production' +$UpdateInfisicalEnvironmentParameters.Slug = 'preprod' +$UpdateInfisicalEnvironmentParameters.Position = 25 +$UpdateInfisicalEnvironmentParameters.Verbose = $True + +$UpdateInfisicalEnvironmentResult = Update-InfisicalEnvironment @UpdateInfisicalEnvironmentParameters + Locates the staging environment, renames it to Pre-Production, rotates its slug, and updates its sort order. + + + + + + + Remove-InfisicalEnvironment + Deletes an Infisical environment from a project. + Remove + InfisicalEnvironment + + + Removes an environment by Id. -ProjectId is required. High ConfirmImpact prompts unless -Confirm:$False is supplied. -PassThru emits the removed environment id. + + + Notes + + Removing an environment deletes every secret and folder scoped to it. Honors -WhatIf and -Confirm. + + + + + EXAMPLE 1 + Remove-InfisicalEnvironment -EnvironmentId $EnvId -Confirm:$False + Deletes an environment without prompting. + + + EXAMPLE 2 + $GetInfisicalEnvironmentResult = Get-InfisicalEnvironment | Where-Object { $_.Slug -eq 'sandbox' } + +$RemoveInfisicalEnvironmentParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$RemoveInfisicalEnvironmentParameters.EnvironmentId = $GetInfisicalEnvironmentResult.Id +$RemoveInfisicalEnvironmentParameters.ProjectId = $ProjectId +$RemoveInfisicalEnvironmentParameters.PassThru = $True +$RemoveInfisicalEnvironmentParameters.Confirm = $False +$RemoveInfisicalEnvironmentParameters.Verbose = $True + +$RemoveInfisicalEnvironmentResult = Remove-InfisicalEnvironment @RemoveInfisicalEnvironmentParameters + Removes the sandbox environment without prompting and emits its id for the audit trail. + + + + + + + Get-InfisicalFolder + Lists or retrieves Infisical folders at a given secret path. + Get + InfisicalFolder + + + Default (List parameter set) enumerates folders directly under the supplied -Path within the project and environment. When -FolderNameOrId is supplied (Single parameter set) the cmdlet returns one folder by name or id under -Path. -ProjectId and -Environment are required in both modes; -Path defaults to '/'. + + + Notes + + List mode is a non-recursive listing of immediate subfolders. To enumerate secrets across a folder subtree use Get-InfisicalSecret -Recursive. Accepts pipeline input by property name on -FolderNameOrId. + + + + + EXAMPLE 1 + Get-InfisicalFolder -ProjectId $ProjectId -Environment 'dev' -Path '/Windows' + Lists every folder directly under /Windows in the supplied project and environment. + + + EXAMPLE 2 + Get-InfisicalFolder -FolderNameOrId 'Deployments' -ProjectId $ProjectId -Environment 'dev' -Path '/Windows' + Retrieves the Deployments folder under /Windows in the supplied project and environment. + + + EXAMPLE 3 + $GetInfisicalFolderListResult = Get-InfisicalFolder -Path '/Windows' | Where-Object { $_.Name -eq 'Deployments' } + +$GetInfisicalFolderParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalFolderParameters.FolderNameOrId = $GetInfisicalFolderListResult.Id +$GetInfisicalFolderParameters.ProjectId = $ProjectId +$GetInfisicalFolderParameters.Environment = 'dev' +$GetInfisicalFolderParameters.Path = '/Windows' +$GetInfisicalFolderParameters.Verbose = $True + +$GetInfisicalFolderResult = Get-InfisicalFolder @GetInfisicalFolderParameters + Locates the folder by name first, then re-fetches it by id to refresh the canonical record. + + + + + + + New-InfisicalFolder + Creates a new Infisical folder under the supplied parent path. + New + InfisicalFolder + + + Creates a folder with the supplied -Name beneath the supplied -Path. -ProjectId and -Environment are required; -Path defaults to '/'. Honors -WhatIf and -Confirm. + + + Notes + + Folder names are case-sensitive and must be unique within a parent path; the cmdlet does not create intermediate folders. + + + + + EXAMPLE 1 + New-InfisicalFolder -Name 'Deployments' -ProjectId $ProjectId -Environment 'dev' -Path '/Windows' + Creates the Deployments folder under /Windows in the supplied project and environment. + + + EXAMPLE 2 + $NewInfisicalFolderParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$NewInfisicalFolderParameters.Name = $CallingScriptPath.BaseName +$NewInfisicalFolderParameters.ProjectId = $ProjectId +$NewInfisicalFolderParameters.Environment = 'dev' +$NewInfisicalFolderParameters.Path = '/Windows' +$NewInfisicalFolderParameters.Verbose = $True + +$NewInfisicalFolderResult = New-InfisicalFolder @NewInfisicalFolderParameters + Creates a script-named folder under /Windows in the supplied project and environment. + + + + + + + Update-InfisicalFolder + Renames an existing Infisical folder. + Update + InfisicalFolder + + + Renames a folder identified by -FolderId to the supplied -Name. -ProjectId and -Environment are required; -Path defaults to '/'. Honors -WhatIf and -Confirm. + + + Notes + + Renaming a folder rewrites the path component for every secret beneath it; coordinate with consumers that pin to the previous path. + + + + + EXAMPLE 1 + Update-InfisicalFolder -FolderId $FolderId -Name 'Deployments-Archive' + Renames a folder in the supplied project/environment. + + + EXAMPLE 2 + $GetInfisicalFolderResult = Get-InfisicalFolder -Path '/Windows' | Where-Object { $_.Name -eq 'Deployments' } + +$UpdateInfisicalFolderParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$UpdateInfisicalFolderParameters.FolderId = $GetInfisicalFolderResult.Id +$UpdateInfisicalFolderParameters.Name = 'Deployments-Archive' +$UpdateInfisicalFolderParameters.ProjectId = $ProjectId +$UpdateInfisicalFolderParameters.Environment = 'dev' +$UpdateInfisicalFolderParameters.Path = '/Windows' +$UpdateInfisicalFolderParameters.Verbose = $True + +$UpdateInfisicalFolderResult = Update-InfisicalFolder @UpdateInfisicalFolderParameters + Resolves the folder by name and renames it to Deployments-Archive. + + + + + + + Remove-InfisicalFolder + Deletes an Infisical folder and all secrets it contains. + Remove + InfisicalFolder + + + Removes a folder by Id from the supplied -Path. -ProjectId and -Environment are required; -Path defaults to '/'. High ConfirmImpact prompts unless -Confirm:$False is supplied. -PassThru emits the removed folder id. + + + Notes + + This is destructive and removes every secret and subfolder under the target folder. Honors -WhatIf and -Confirm. + + + + + EXAMPLE 1 + Remove-InfisicalFolder -FolderId $FolderId -Confirm:$False + Deletes a folder from the supplied project/environment without prompting. + + + EXAMPLE 2 + $GetInfisicalFolderResult = Get-InfisicalFolder -Path '/Windows' | Where-Object { $_.Name -eq $CallingScriptPath.BaseName } + +$RemoveInfisicalFolderParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$RemoveInfisicalFolderParameters.FolderId = $GetInfisicalFolderResult.Id +$RemoveInfisicalFolderParameters.ProjectId = $ProjectId +$RemoveInfisicalFolderParameters.Environment = 'dev' +$RemoveInfisicalFolderParameters.Path = '/Windows' +$RemoveInfisicalFolderParameters.PassThru = $True +$RemoveInfisicalFolderParameters.Confirm = $False +$RemoveInfisicalFolderParameters.Verbose = $True + +$RemoveInfisicalFolderResult = Remove-InfisicalFolder @RemoveInfisicalFolderParameters + Resolves the script-named folder under /Windows and removes it without prompting, returning its id for logging. + + + + + + + Get-InfisicalTag + Lists or retrieves Infisical tags defined on a project. + Get + InfisicalTag + + + Default (List parameter set) returns every tag configured on the project. When -TagSlugOrId is supplied (Single parameter set) the cmdlet returns the one matching record. -ProjectId is required in both modes. + + + Notes + + Tag Ids returned here are the values to pass on -TagIds when creating or updating secrets. Accepts pipeline input by property name on -TagSlugOrId. + + + + + EXAMPLE 1 + Get-InfisicalTag + Lists every tag defined on the supplied project. + + + EXAMPLE 2 + Get-InfisicalTag -TagSlugOrId 'critical' + Retrieves the 'critical' tag from the supplied project. + + + EXAMPLE 3 + $GetInfisicalProjectResult = Get-InfisicalProject | Where-Object { $_.Slug -eq 'platform-telemetry' } + +$GetInfisicalTagParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalTagParameters.TagSlugOrId = 'critical' +$GetInfisicalTagParameters.ProjectId = $GetInfisicalProjectResult.Id +$GetInfisicalTagParameters.Verbose = $True + +$GetInfisicalTagResult = Get-InfisicalTag @GetInfisicalTagParameters + Resolves a project by slug and refetches the 'critical' tag from that project. + + + + + + + New-InfisicalTag + Creates a new Infisical tag on a project. + New + InfisicalTag + + + Creates a tag with the supplied -Slug, optional -Name and -Color. -ProjectId is required. Honors -WhatIf and -Confirm. + + + Notes + + Tag slugs must be unique within the project and are the canonical reference used by tag-filtered secret lookups. + + + + + EXAMPLE 1 + New-InfisicalTag -Slug 'critical' -Name 'Critical' -Color '#FF0000' + Creates a red Critical tag in the supplied project. + + + EXAMPLE 2 + $NewInfisicalTagParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$NewInfisicalTagParameters.Slug = 'critical' +$NewInfisicalTagParameters.Name = 'Critical' +$NewInfisicalTagParameters.Color = '#FF0000' +$NewInfisicalTagParameters.ProjectId = $ProjectId +$NewInfisicalTagParameters.Verbose = $True + +$NewInfisicalTagResult = New-InfisicalTag @NewInfisicalTagParameters + Creates a red Critical tag against an explicitly supplied project id. + + + + + + + Update-InfisicalTag + Updates the slug, name, or color of an existing Infisical tag. + Update + InfisicalTag + + + Updates a tag identified by -TagId. -ProjectId is required. Only bound parameters are sent to the server. Honors -WhatIf and -Confirm. + + + Notes + + Changing -Slug breaks tag-filtered automation that pins to the previous slug. Coordinate slug rotation with consumers. + + + + + EXAMPLE 1 + Update-InfisicalTag -TagId $TagId -Color '#FFA500' + Changes the display color of a tag in the supplied project. + + + EXAMPLE 2 + $GetInfisicalTagResult = Get-InfisicalTag | Where-Object { $_.Slug -eq 'critical' } + +$UpdateInfisicalTagParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$UpdateInfisicalTagParameters.TagId = $GetInfisicalTagResult.Id +$UpdateInfisicalTagParameters.Slug = 'critical-v2' +$UpdateInfisicalTagParameters.Name = 'Critical (v2)' +$UpdateInfisicalTagParameters.Color = '#FFA500' +$UpdateInfisicalTagParameters.ProjectId = $ProjectId +$UpdateInfisicalTagParameters.Verbose = $True + +$UpdateInfisicalTagResult = Update-InfisicalTag @UpdateInfisicalTagParameters + Locates the critical tag and rotates its slug, display name, and color. + + + + + + + Remove-InfisicalTag + Deletes an Infisical tag from a project. + Remove + InfisicalTag + + + Removes a tag by Id. -ProjectId is required. High ConfirmImpact prompts unless -Confirm:$False is supplied. -PassThru emits the removed tag id. + + + Notes + + Removing a tag detaches it from every secret it was applied to but does not delete the secrets themselves. Honors -WhatIf and -Confirm. + + + + + EXAMPLE 1 + Remove-InfisicalTag -TagId $TagId -Confirm:$False + Deletes a tag from the supplied project without prompting. + + + EXAMPLE 2 + $GetInfisicalTagResult = Get-InfisicalTag | Where-Object { $_.Slug -eq 'critical-v2' } + +$RemoveInfisicalTagParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$RemoveInfisicalTagParameters.TagId = $GetInfisicalTagResult.Id +$RemoveInfisicalTagParameters.ProjectId = $ProjectId +$RemoveInfisicalTagParameters.PassThru = $True +$RemoveInfisicalTagParameters.Confirm = $False +$RemoveInfisicalTagParameters.Verbose = $True + +$RemoveInfisicalTagResult = Remove-InfisicalTag @RemoveInfisicalTagParameters + Resolves a tag by slug and removes it without prompting, returning its id for the audit trail. + + + + + + + Get-InfisicalCertificateAuthority + Lists or retrieves Infisical Certificate Authorities. + Get + InfisicalCertificateAuthority + + + When -CaId is supplied (ById parameter set) returns a single internal CA. Otherwise (List parameter set) returns CAs scoped by -Kind: Internal (default, /api/v1/cert-manager/ca/internal), Any (/api/v1/cert-manager/ca returning both internal and ACME), or Acme (filters the generic endpoint to ACME issuers only). -ProjectId is required. + + + Notes + + 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. + + + + + EXAMPLE 1 + Get-InfisicalCertificateAuthority + Lists every internal CA visible in the supplied project. + + + EXAMPLE 2 + Get-InfisicalCertificateAuthority -Kind Any + Lists every CA (internal and ACME) visible in the supplied project; inspect the Type property to distinguish them. + + + EXAMPLE 3 + $GetInfisicalCertificateAuthorityListResult = Get-InfisicalCertificateAuthority | Where-Object { $_.FriendlyName -eq 'Issuing CA - Platform' } + +$GetInfisicalCertificateAuthorityParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalCertificateAuthorityParameters.CaId = $GetInfisicalCertificateAuthorityListResult.Id +$GetInfisicalCertificateAuthorityParameters.ProjectId = $ProjectId +$GetInfisicalCertificateAuthorityParameters.Verbose = $True + +$GetInfisicalCertificateAuthorityResult = Get-InfisicalCertificateAuthority @GetInfisicalCertificateAuthorityParameters + Filters the CA list by friendly name and then re-fetches the canonical CA record by id using a splatted parameter set. + + + + + + + Get-InfisicalCertificate + Lists or retrieves Infisical certificates in a project, with optional filters and automatic paging. + Get + InfisicalCertificate + + + Default (List parameter set) enumerates certificates with optional filters for -CommonName, -FriendlyName, -Status, and -CaId; -Limit and -Offset drive a single page and pages are walked automatically until exhausted unless -NoAutoPage is supplied. When -SerialNumber is supplied (Single parameter set) the cmdlet returns one certificate record. -ProjectId is required in both modes. + + + Notes + + For advanced filtering (validity window, key algorithm, extended key usage, etc.) use Search-InfisicalCertificate instead. Single mode returns metadata only; to obtain certificate and chain PEM material use ConvertTo-InfisicalCertificate or Export-InfisicalCertificate. Accepts pipeline input by property name on -SerialNumber. + + + + + EXAMPLE 1 + Get-InfisicalCertificate -Status 'active' + Lists every active certificate in the supplied project. + + + EXAMPLE 2 + Get-InfisicalCertificate -SerialNumber '7A:F2:1B:...:9E' + Retrieves the certificate record for the supplied serial number. + + + EXAMPLE 3 + $GetInfisicalCertificateAuthorityListResult = Get-InfisicalCertificateAuthority | Where-Object { $_.FriendlyName -eq 'Issuing CA - Platform' } + +$GetInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalCertificateParameters.ProjectId = $ProjectId +$GetInfisicalCertificateParameters.CommonName = $env:COMPUTERNAME +$GetInfisicalCertificateParameters.FriendlyName = 'web-tier' +$GetInfisicalCertificateParameters.Status = 'active' +$GetInfisicalCertificateParameters.CaId = @($GetInfisicalCertificateAuthorityListResult.Id) +$GetInfisicalCertificateParameters.Limit = 100 +$GetInfisicalCertificateParameters.Verbose = $True + +$GetInfisicalCertificateListResult = Get-InfisicalCertificate @GetInfisicalCertificateParameters + Resolves the issuing CA, then lists active certificates scoped to that CA, the local hostname, and the 'web-tier' friendly name. + + + + + + + Get-InfisicalPkiSubscriber + Lists or retrieves Infisical PKI subscribers in a project. + Get + InfisicalPkiSubscriber + + + Default (List parameter set) returns every PKI subscriber configured on the project. When -Name is supplied (ByName parameter set) the cmdlet returns one subscriber by its slug. -ProjectId is required in both modes. + + + Notes + + The -Name parameter is the subscriber slug; aliases SubscriberName and Slug are accepted. Pass the slug returned here on -PkiSubscriberSlug when calling Request-InfisicalCertificate. Accepts pipeline input by property name on -Name. + + + + + EXAMPLE 1 + Get-InfisicalPkiSubscriber + Lists every PKI subscriber defined on the supplied project. + + + EXAMPLE 2 + Get-InfisicalPkiSubscriber -Name 'mecm' + Retrieves the 'mecm' PKI subscriber from the supplied project. + + + EXAMPLE 3 + $GetInfisicalPkiSubscriberListResult = Get-InfisicalPkiSubscriber | Where-Object { $_.Name -ilike 'mecm*' } + +$GetInfisicalPkiSubscriberParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalPkiSubscriberParameters.Name = $GetInfisicalPkiSubscriberListResult[0].Name +$GetInfisicalPkiSubscriberParameters.ProjectId = $ProjectId +$GetInfisicalPkiSubscriberParameters.Verbose = $True + +$GetInfisicalPkiSubscriberResult = Get-InfisicalPkiSubscriber @GetInfisicalPkiSubscriberParameters + Filters subscribers whose name starts with 'mecm' and refetches the canonical record for the first match. + + + + + + + Get-InfisicalCertificateProfile + Lists or retrieves Infisical certificate profiles in a project. + Get + InfisicalCertificateProfile + + + Default (List parameter set) returns every certificate profile configured on the project via /api/v1/cert-manager/certificate-profiles, with optional -Limit, -Offset, and -IncludeConfigs. When -ProfileId is supplied (ById parameter set) the cmdlet returns one profile by its id. -ProjectId is required in both modes. + + + Notes + + Profiles bind a CA and a certificate policy and surface defaults (TtlDays, KeyAlgorithm, KeyUsages, ExtendedKeyUsages). Use the returned profile Id when wiring profile-based issuance against Request-InfisicalCertificate. + + + + + EXAMPLE 1 + Get-InfisicalCertificateProfile + Lists every certificate profile defined on the supplied project. + + + EXAMPLE 2 + Get-InfisicalCertificateProfile -ProfileId '8257641e-c808-454e-ac92-8dc920be865f' + Retrieves a single certificate profile by id from the supplied project. + + + EXAMPLE 3 + $GetInfisicalCertificateProfileListResult = Get-InfisicalCertificateProfile | Where-Object { $_.Slug -ieq 'codesigning' } + +$GetInfisicalCertificateProfileParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalCertificateProfileParameters.ProfileId = $GetInfisicalCertificateProfileListResult[0].Id +$GetInfisicalCertificateProfileParameters.ProjectId = $ProjectId +$GetInfisicalCertificateProfileParameters.Verbose = $True + +$GetInfisicalCertificateProfileResult = Get-InfisicalCertificateProfile @GetInfisicalCertificateProfileParameters + Filters profiles whose slug equals 'codesigning' and refetches the canonical record for the first match using a splatted parameter set. + + + + + + + Get-InfisicalCertificatePolicy + Lists or retrieves Infisical certificate policies in a project. + Get + InfisicalCertificatePolicy + + + Default (List parameter set) returns every certificate policy configured on the project via /api/v1/cert-manager/certificate-policies, with optional -Limit and -Offset. When -PolicyId is supplied (ById parameter set) the cmdlet returns one policy by its id. -ProjectId is required in both modes. + + + Notes + + Policies define the allowed/required subject, SANs, key usages, extended key usages, key algorithms, signature algorithm, and validity windows that certificate profiles enforce. Each profile binds exactly one policy via its CertificatePolicyId. + + + + + EXAMPLE 1 + Get-InfisicalCertificatePolicy + Lists every certificate policy defined on the supplied project. + + + EXAMPLE 2 + Get-InfisicalCertificatePolicy -PolicyId '3e69306a-e7c1-4fd2-a140-7fb300e53c43' + Retrieves a single certificate policy by id from the supplied project. + + + EXAMPLE 3 + $GetInfisicalCertificatePolicyListResult = Get-InfisicalCertificatePolicy | Where-Object { $_.Name -ieq 'codesigning' } + +$GetInfisicalCertificatePolicyParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalCertificatePolicyParameters.PolicyId = $GetInfisicalCertificatePolicyListResult[0].Id +$GetInfisicalCertificatePolicyParameters.ProjectId = $ProjectId +$GetInfisicalCertificatePolicyParameters.Verbose = $True + +$GetInfisicalCertificatePolicyResult = Get-InfisicalCertificatePolicy @GetInfisicalCertificatePolicyParameters + Filters policies whose name equals 'codesigning' and refetches the canonical record for the first match using a splatted parameter set. + + + + + + + Search-InfisicalCertificate + Searches Infisical certificates with advanced filters and automatic paging. + Search + InfisicalCertificate + + + Performs a server-side search across certificates with filters for friendly name, common name, free-text search, status, CA/profile/application/enrollment scope, key/signature algorithm, source, and validity window (-NotBeforeFrom/-NotBeforeTo/-NotAfterFrom/-NotAfterTo). Results are paged automatically unless -NoAutoPage is supplied. -ProjectId is required. + + + Notes + + Use -SortBy together with -SortOrder ('asc'/'desc') to control result ordering. Pair with Get-InfisicalCertificate or Export-InfisicalCertificate to drill into specific hits. + + + + + EXAMPLE 1 + Search-InfisicalCertificate -Search $env:COMPUTERNAME -Status 'active' + Finds active certificates whose searchable fields contain the local hostname. + + + EXAMPLE 2 + $GetInfisicalCertificateAuthorityListResult = Get-InfisicalCertificateAuthority | Where-Object { $_.FriendlyName -eq 'Issuing CA - Platform' } + +$SearchInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$SearchInfisicalCertificateParameters.ProjectId = $ProjectId +$SearchInfisicalCertificateParameters.CommonName = $env:COMPUTERNAME +$SearchInfisicalCertificateParameters.Status = 'active' +$SearchInfisicalCertificateParameters.CaId = @($GetInfisicalCertificateAuthorityListResult.Id) +$SearchInfisicalCertificateParameters.KeyAlgorithm = @('RSA') +$SearchInfisicalCertificateParameters.NotAfterTo = (Get-Date).AddDays(30) +$SearchInfisicalCertificateParameters.SortBy = 'notAfter' +$SearchInfisicalCertificateParameters.SortOrder = 'asc' +$SearchInfisicalCertificateParameters.Limit = 100 +$SearchInfisicalCertificateParameters.Verbose = $True + +$SearchInfisicalCertificateResult = Search-InfisicalCertificate @SearchInfisicalCertificateParameters + Searches for RSA certificates from a specific CA, scoped to the local hostname, that expire within the next 30 days, sorted soonest-first. + + + + + + + Request-InfisicalCertificate + Requests a new Infisical certificate (local CSR + sign) or reuses a still-valid existing one. + Request + InfisicalCertificate + + + Generates a keypair locally, builds a CSR, and submits it for signing via one of three parameter sets: a PKI subscriber (-PkiSubscriberSlug, default), direct CA signing (-CertificateAuthorityId), or a certificate profile (-CertificateProfileId, POSTs to /api/v1/cert-manager/certificates with the profile bound). On subsequent runs an existing certificate whose CN matches and whose remaining lifetime exceeds -RenewalThresholdDays is reused; pass -Force to always issue or -AllowRenewal to allow rotation inside the threshold. Optional flags install the leaf (-Install) and chain (-InstallChain) into a Windows certificate store, and control private-key protection (-PrivateKeyProtection, -PersistKey, -MachineKey, -PrivateKeyPath, -KeyStorageFlags). Honors -WhatIf and -Confirm. + + + Notes + + Default -PrivateKeyProtection is 'LocalOnly': the leaf is loaded into memory without persisting the private key and PrivateKeyPem is scrubbed from the emitted result unless -PrivateKeyPath or an explicit -KeyStorageFlags binding overrides it. The reuse path completes its chain from the Infisical bundle when local stores are incomplete; pass -LocalChainOnly to suppress that fetch entirely. + + + + + EXAMPLE 1 + Request-InfisicalCertificate -PkiSubscriberSlug 'web-tier' -Install + Requests (or reuses) a certificate for the 'web-tier' subscriber and installs it into CurrentUser\My. + + + EXAMPLE 2 + $RequestInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$RequestInfisicalCertificateParameters.PkiSubscriberSlug = 'web-tier' +$RequestInfisicalCertificateParameters.ProjectId = $ProjectId +$RequestInfisicalCertificateParameters.CommonName = ([System.Net.Dns]::GetHostEntry($env:COMPUTERNAME)).HostName +$RequestInfisicalCertificateParameters.DnsName = @(([System.Net.Dns]::GetHostEntry($env:COMPUTERNAME)).HostName, $env:COMPUTERNAME) +$RequestInfisicalCertificateParameters.KeyAlgorithm = 'Rsa' +$RequestInfisicalCertificateParameters.KeySize = 3072 +$RequestInfisicalCertificateParameters.Install = $True +$RequestInfisicalCertificateParameters.InstallChain = $True +$RequestInfisicalCertificateParameters.StoreName = 'My' +$RequestInfisicalCertificateParameters.StoreLocation = 'LocalMachine' +$RequestInfisicalCertificateParameters.PrivateKeyProtection = 'NonExportable' +$RequestInfisicalCertificateParameters.MachineKey = $True +$RequestInfisicalCertificateParameters.PersistKey = $True +$RequestInfisicalCertificateParameters.AllowRenewal = $True +$RequestInfisicalCertificateParameters.RenewalThresholdDays = 30 +$RequestInfisicalCertificateParameters.Verbose = $True + +$RequestInfisicalCertificateResult = Request-InfisicalCertificate @RequestInfisicalCertificateParameters + Issues (or renews within 30 days) a 3072-bit RSA certificate for the local FQDN, installs the leaf and chain into LocalMachine\My with a non-exportable machine-bound persistent key. + + + EXAMPLE 3 + $Profile = Get-InfisicalCertificateProfile | Where-Object { $_.Slug -eq 'web-tier-profile' } +Request-InfisicalCertificate -CertificateProfileId $Profile.Id -CommonName 'web01.contoso.com' -Ttl '90d' + Issues a certificate via the modern profile API (POST /api/v1/cert-manager/certificates). The profile binds the CA, policy, and defaults so no subscriber is required. + + + + + + + ConvertTo-InfisicalCertificate + Materializes an X509Certificate2 from an Infisical certificate record, bundle, or serial number. + ConvertTo + InfisicalCertificate + + + Fetches the certificate bundle (when given an InfisicalCertificate or -SerialNumber), or accepts an already-fetched -Bundle, and constructs an X509Certificate2 from the PEM material. Use -NoPrivateKey to omit the private key, -KeyStorageFlags to control how the key is loaded, and -IncludeChain to additionally emit each chain certificate as a separate X509Certificate2 in the pipeline. + + + Notes + + The bundle for any given certificate is typically retrievable only once after issuance; -SerialNumber and pipeline modes will fail with a bundle-not-available error for older certificates. Use -KeyStorageFlags Exportable when callers need to re-export the resulting cert as PFX. + + + + + EXAMPLE 1 + Get-InfisicalCertificate -SerialNumber $Serial | ConvertTo-InfisicalCertificate -IncludeChain + Materializes the certificate and emits each chain element individually. + + + EXAMPLE 2 + $GetInfisicalCertificateResult = Get-InfisicalCertificate -Status 'active' | Where-Object { $_.CommonName -eq $env:COMPUTERNAME } + +$ConvertToInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$ConvertToInfisicalCertificateParameters.SerialNumber = $GetInfisicalCertificateResult[0].SerialNumber +$ConvertToInfisicalCertificateParameters.NoPrivateKey = $False +$ConvertToInfisicalCertificateParameters.IncludeChain = $True +$ConvertToInfisicalCertificateParameters.KeyStorageFlags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable +$ConvertToInfisicalCertificateParameters.Verbose = $True + +$ConvertToInfisicalCertificateResult = ConvertTo-InfisicalCertificate @ConvertToInfisicalCertificateParameters + Selects the active certificate whose CN matches the host and materializes it (with private key and chain) as exportable X509Certificate2 objects. + + + + + + + Export-InfisicalCertificate + Exports an Infisical certificate to disk in PEM, PFX, or CER format. + Export + InfisicalCertificate + + + Writes a certificate to -Path in the supplied -Format. Accepts an X509Certificate2, an InfisicalCertificateBundle, an InfisicalCertificate (refetches bundle by serial), or a -SerialNumber. -Password (SecureString) supplies the PFX password. -IncludeChain appends chain certificates (PEM only). -NoPrivateKey omits the private key. -Force overwrites an existing file. Honors -WhatIf and -Confirm. + + + Notes + + PFX export requires the cert to have been loaded with X509KeyStorageFlags.Exportable; bundle/serial modes import with Exportable automatically. CER and PFX formats ignore -IncludeChain. + + + + + EXAMPLE 1 + Export-InfisicalCertificate -Path 'C:\Temp\web-tier.pem' -Format Pem -SerialNumber $Serial -IncludeChain + Exports a certificate, its chain, and private key (when available) as a single PEM bundle. + + + EXAMPLE 2 + $GetInfisicalCertificateResult = Get-InfisicalCertificate -Status 'active' | Where-Object { $_.CommonName -eq $env:COMPUTERNAME } + +$ExportInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$ExportInfisicalCertificateParameters.SerialNumber = $GetInfisicalCertificateResult[0].SerialNumber +$ExportInfisicalCertificateParameters.Path = "C:\Temp\$($env:COMPUTERNAME).pfx" +$ExportInfisicalCertificateParameters.Format = 'Pfx' +$ExportInfisicalCertificateParameters.Password = (Read-Host -AsSecureString -Prompt 'PFX password') +$ExportInfisicalCertificateParameters.Force = $True +$ExportInfisicalCertificateParameters.PassThru = $True +$ExportInfisicalCertificateParameters.Verbose = $True + +$ExportInfisicalCertificateResult = Export-InfisicalCertificate @ExportInfisicalCertificateParameters + Resolves the active host certificate by serial and exports it as a password-protected PFX, overwriting any existing file and emitting a FileInfo for downstream use. + + + + + + + Install-InfisicalCertificate + Installs an Infisical certificate (and optional chain) into a Windows certificate store. + Install + InfisicalCertificate + + + Adds a certificate to the supplied -StoreName and -StoreLocation. Accepts an X509Certificate2, an InfisicalCertificate (refetches bundle by serial), or a -SerialNumber. -KeyStorageFlags controls private-key loading. -IncludeChain installs each chain certificate to the CertificateAuthority store of the same -StoreLocation. -Force replaces an existing thumbprint. -PassThru emits the installed certificate. Honors -WhatIf and -Confirm. + + + Notes + + Installing into LocalMachine stores typically requires elevation. -IncludeChain only fires for serial/InfisicalCertificate inputs because the X509Certificate2 input has no associated bundle to walk. + + + + + EXAMPLE 1 + Install-InfisicalCertificate -SerialNumber $Serial -StoreLocation LocalMachine -IncludeChain + Installs the leaf into LocalMachine\My and each chain element into LocalMachine\CertificateAuthority. + + + EXAMPLE 2 + $GetInfisicalCertificateResult = Get-InfisicalCertificate -Status 'active' | Where-Object { $_.CommonName -eq $env:COMPUTERNAME } + +$InstallInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$InstallInfisicalCertificateParameters.SerialNumber = $GetInfisicalCertificateResult[0].SerialNumber +$InstallInfisicalCertificateParameters.StoreName = 'My' +$InstallInfisicalCertificateParameters.StoreLocation = 'LocalMachine' +$InstallInfisicalCertificateParameters.KeyStorageFlags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet -bor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet +$InstallInfisicalCertificateParameters.IncludeChain = $True +$InstallInfisicalCertificateParameters.Force = $True +$InstallInfisicalCertificateParameters.PassThru = $True +$InstallInfisicalCertificateParameters.Verbose = $True + +$InstallInfisicalCertificateResult = Install-InfisicalCertificate @InstallInfisicalCertificateParameters + Resolves the active host certificate and installs the leaf (with a machine-bound persistent key) plus its chain into LocalMachine, replacing any existing thumbprint match. + + + + + + + Uninstall-InfisicalCertificate + Removes a certificate from a Windows certificate store by thumbprint, subject, or pipeline input. + Uninstall + InfisicalCertificate + + + Removes matching certificates from the supplied -StoreName and -StoreLocation. Accepts -Thumbprint, -Subject, an X509Certificate2 (-Certificate), or an InfisicalCertificate (-InfisicalCertificate, uses FingerprintSha1). -Force allows removing multiple matches in one call; -PassThru emits each removed certificate. Honors -WhatIf and -Confirm. + + + Notes + + When more than one certificate matches -Subject and -Force is not supplied the cmdlet throws to prevent accidental bulk removal. Uninstalling from LocalMachine stores typically requires elevation. + + + + + EXAMPLE 1 + Uninstall-InfisicalCertificate -Thumbprint $Thumbprint -StoreLocation LocalMachine + Removes the certificate with the supplied thumbprint from LocalMachine\My. + + + EXAMPLE 2 + $GetInfisicalCertificateResult = Get-InfisicalCertificate -Status 'revoked' | Where-Object { $_.CommonName -eq $env:COMPUTERNAME } + +$UninstallInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$UninstallInfisicalCertificateParameters.InfisicalCertificate = $GetInfisicalCertificateResult[0] +$UninstallInfisicalCertificateParameters.StoreName = 'My' +$UninstallInfisicalCertificateParameters.StoreLocation = 'LocalMachine' +$UninstallInfisicalCertificateParameters.Force = $True +$UninstallInfisicalCertificateParameters.PassThru = $True +$UninstallInfisicalCertificateParameters.Verbose = $True + +$UninstallInfisicalCertificateResult = Uninstall-InfisicalCertificate @UninstallInfisicalCertificateParameters + Picks the revoked host certificate and removes it from LocalMachine\My using its SHA1 fingerprint, emitting the removed object for the audit trail. + + + + + + + Get-InfisicalCertificateApplication + Lists or retrieves an Infisical Certificate Manager Application from the supplied project. + Get + InfisicalCertificateApplication + + + Reads Infisical certificate-manager Applications (the join target used by EST/ACME/SCEP profile attachments) for the supplied project. The List parameter set returns all applications visible to the caller; the ById and ByName sets return a single application. -ProjectId is required. + + + + EXAMPLE 1 + Get-InfisicalCertificateApplication -ProjectId $ProjectId + Lists certificate-manager applications for the supplied project. + + + EXAMPLE 2 + Get-InfisicalCertificateApplication -ApplicationName 'workstation-mdm' -ProjectId $ProjectId + Retrieves a single application by name. + + + EXAMPLE 3 + $GetInfisicalCertificateApplicationParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalCertificateApplicationParameters.Id = $ApplicationId +$GetInfisicalCertificateApplicationParameters.ProjectId = $ProjectId +$GetInfisicalCertificateApplicationParameters.Verbose = $True + +$GetInfisicalCertificateApplicationResult = Get-InfisicalCertificateApplication @GetInfisicalCertificateApplicationParameters + Retrieves a single application by id from an explicit project. + + + + + + + Get-InfisicalCertificateApplicationEnrollment + Retrieves the API/EST/ACME/SCEP enrollment configuration attached to an application/profile pair. + Get + InfisicalCertificateApplicationEnrollment + + + Returns the InfisicalCertificateApplicationEnrollment for the given application and certificate profile, including any configured SCEP sub-block (server URL, RA certificate PEM, computed SHA-1 RaCertificateThumbprint, challenge type, and challenge endpoint URL when dynamic). + + + + EXAMPLE 1 + Get-InfisicalCertificateApplicationEnrollment -ApplicationId $AppId -ProfileId $ProfileId + Fetches the enrollment configuration for an application/profile pair. + + + EXAMPLE 2 + $GetInfisicalCertificateApplicationEnrollmentParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalCertificateApplicationEnrollmentParameters.ApplicationId = $ApplicationId +$GetInfisicalCertificateApplicationEnrollmentParameters.ProfileId = $ProfileId +$GetInfisicalCertificateApplicationEnrollmentParameters.Verbose = $True + +$GetInfisicalCertificateApplicationEnrollmentResult = Get-InfisicalCertificateApplicationEnrollment @GetInfisicalCertificateApplicationEnrollmentParameters + Retrieves the enrollment configuration and feeds it downstream to Get-InfisicalScepMdmProfile. + + + + + + + New-InfisicalScepDynamicChallenge + Generates a one-time SCEP challenge from an application/profile that is configured with dynamic challenge mode. + New + InfisicalScepDynamicChallenge + + + POSTs to /scep/applications/{applicationId}/profiles/{profileId}/challenge and returns the minted challenge as a SecureString. Use -AsPlainText to return a string instead. Requires the active machine identity to have read access on certificate-application-enrollment, and the target SCEP profile must be set to challengeType=dynamic. Dynamic challenges are an Enterprise-tier feature on managed Infisical deployments. + + + + EXAMPLE 1 + $Challenge = New-InfisicalScepDynamicChallenge -ApplicationId $AppId -ProfileId $ProfileId + Mints a single-use SCEP challenge and stores it as a SecureString. + + + EXAMPLE 2 + $NewInfisicalScepDynamicChallengeParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$NewInfisicalScepDynamicChallengeParameters.ApplicationId = $ApplicationId +$NewInfisicalScepDynamicChallengeParameters.ProfileId = $ProfileId +$NewInfisicalScepDynamicChallengeParameters.AsPlainText = $True +$NewInfisicalScepDynamicChallengeParameters.Verbose = $True + +$NewInfisicalScepDynamicChallengeResult = New-InfisicalScepDynamicChallenge @NewInfisicalScepDynamicChallengeParameters + Mints a plain-text challenge for use in environments where SecureString is inconvenient. + + + + + + + Get-InfisicalScepMdmProfile + Builds an Infisical SCEP MDM profile model from an application enrollment, certificate profile, or fully manual inputs. + Get + InfisicalScepMdmProfile + + + Produces an InfisicalScepMdmProfile that mirrors the Windows ClientCertificateInstall/SCEP CSP node set. FromEnrollment (default) consumes an InfisicalCertificateApplicationEnrollment and auto-fills ServerUrl from scep.scepEndpointUrl and CAThumbprint from the RA certificate; if the enrollment is configured for dynamic challenge mode, a fresh challenge is minted automatically when -Challenge is not supplied. FromProfile keeps the legacy projection from an InfisicalCertificateProfile and now requires -ApplicationId so the server URL can be built against /scep/applications/{appId}/profiles/{profileId}/pkiclient.exe. Manual requires explicit -ServerUrl, -Challenge, and -UniqueId. + + + Notes + + The SCEP endpoint URL ends in 'pkiclient.exe' for RFC 8894 / Cisco SCEP client compatibility. SecureString -Challenge is decrypted into the model only at write-time. + + + + + EXAMPLE 1 + Get-InfisicalCertificateApplicationEnrollment -ApplicationId $AppId -ProfileId $ProfileId | Get-InfisicalScepMdmProfile + Builds a SCEP MDM profile from an enrollment, auto-resolving ServerUrl, CAThumbprint, and (for dynamic mode) the challenge. + + + EXAMPLE 2 + Get-InfisicalCertificateProfile -CertificateProfileId $ProfileId | Get-InfisicalScepMdmProfile -ApplicationId $AppId -Challenge (Read-Host -AsSecureString 'SCEP challenge') + Builds a profile from a certificate profile (legacy path) with an explicit application id and static challenge. + + + EXAMPLE 3 + $GetInfisicalScepMdmProfileParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalScepMdmProfileParameters.EnrollmentObject = $Enrollment +$GetInfisicalScepMdmProfileParameters.UniqueId = 'WindowsClientAuth' +$GetInfisicalScepMdmProfileParameters.Scope = 'Device' +$GetInfisicalScepMdmProfileParameters.SubjectName = "CN=$($env:COMPUTERNAME)" +$GetInfisicalScepMdmProfileParameters.KeyLength = 2048 +$GetInfisicalScepMdmProfileParameters.HashAlgorithm = 'SHA256' +$GetInfisicalScepMdmProfileParameters.ValidPeriod = 'Years' +$GetInfisicalScepMdmProfileParameters.ValidPeriodUnits = 1 +$GetInfisicalScepMdmProfileParameters.Verbose = $True + +$GetInfisicalScepMdmProfileResult = Get-InfisicalScepMdmProfile @GetInfisicalScepMdmProfileParameters + Builds a device-scope SCEP MDM profile from an enrollment with overridden subject and key parameters. + + + + + + + Export-InfisicalScepMdmProfile + Writes an InfisicalScepMdmProfile to disk as a SyncML payload suitable for MDM delivery. + Export + InfisicalScepMdmProfile + + + Serializes the supplied InfisicalScepMdmProfile via ToSyncMl() and writes the result to -Path as UTF-8 (no BOM). Auto-creates the target directory. If the file exists and -Force is not specified the cmdlet logs a warning and returns instead of throwing. Honors -WhatIf and -Confirm. -PassThru emits the resulting FileInfo. + + + Notes + + The generated SyncML is round-trip-validated through XmlReader before being written. Pair with Write-InfisicalScepMdmProfileToWmi to apply the same model to the local MDM Bridge instead of exporting to a file. + + + + + EXAMPLE 1 + $Profile | Export-InfisicalScepMdmProfile -Path 'C:\Temp\scep.syncml' -Force + Writes the SyncML payload for the supplied SCEP MDM profile, overwriting any existing file. + + + EXAMPLE 2 + $ExportInfisicalScepMdmProfileParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$ExportInfisicalScepMdmProfileParameters.InputObject = $Profile +$ExportInfisicalScepMdmProfileParameters.Path = "C:\ProgramData\Infisical\scep-$($Profile.UniqueId).syncml" +$ExportInfisicalScepMdmProfileParameters.Force = $True +$ExportInfisicalScepMdmProfileParameters.PassThru = $True +$ExportInfisicalScepMdmProfileParameters.Verbose = $True + +$ExportInfisicalScepMdmProfileResult = Export-InfisicalScepMdmProfile @ExportInfisicalScepMdmProfileParameters + Writes the SyncML payload to a per-profile path under ProgramData and returns the resulting FileInfo. + + + + + + + Write-InfisicalScepMdmProfileToWmi + Submits an InfisicalScepMdmProfile to the local Windows MDM Bridge WMI provider to trigger SCEP enrollment. + Write + InfisicalScepMdmProfileToWmi + + + Creates a new CIM instance under the MDM Bridge namespace (default: root/cimv2/mdm/dmmap, class MDM_ClientCertificateInstall_SCEP02) by invoking New-CimInstance through the host runspace. Honors -WhatIf and -Confirm. -PassThru emits the resulting CIM instance. Throws PlatformNotSupportedException off Windows. Device-scope enrollment requires an elevated session; pass -SkipElevationCheck to bypass the guard. + + + Notes + + The MDM Bridge WMI provider runs the enrollment asynchronously; success here means the enrollment was submitted, not that a certificate has been issued. Inspect the corresponding ClientCertificateInstall/SCEP/<UniqueId>/Install nodes for status. Override -ClassName when targeting a different SCEP CSP version on the host. + + + + + EXAMPLE 1 + $Profile | Write-InfisicalScepMdmProfileToWmi -PassThru + Submits the SCEP MDM profile to the local MDM Bridge and emits the created CIM instance. + + + EXAMPLE 2 + $WriteInfisicalScepMdmProfileToWmiParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$WriteInfisicalScepMdmProfileToWmiParameters.InputObject = $Profile +$WriteInfisicalScepMdmProfileToWmiParameters.Namespace = 'root/cimv2/mdm/dmmap' +$WriteInfisicalScepMdmProfileToWmiParameters.ClassName = 'MDM_ClientCertificateInstall_SCEP02' +$WriteInfisicalScepMdmProfileToWmiParameters.SkipElevationCheck = $False +$WriteInfisicalScepMdmProfileToWmiParameters.PassThru = $True +$WriteInfisicalScepMdmProfileToWmiParameters.Verbose = $True + +$WriteInfisicalScepMdmProfileToWmiResult = Write-InfisicalScepMdmProfileToWmi @WriteInfisicalScepMdmProfileToWmiParameters + Submits a device-scope SCEP enrollment through the MDM Bridge and returns the CIM instance for downstream inspection. + + + + + diff --git a/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml b/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml new file mode 100644 index 0000000..5d82499 --- /dev/null +++ b/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml @@ -0,0 +1,1696 @@ + + + + + + Connect-Infisical + Establishes an authenticated session with an Infisical server and stores it for use by subsequent cmdlets. + Connect + Infisical + + + Authenticates against an Infisical instance using one of the supported auth providers (UniversalAuth, Token, JWT, OIDC, LDAP, Azure, GCP IAM) and stores the resulting connection in the module-level session manager. Subsequent cmdlets pick up the connection automatically. If parameters such as BaseUri, OrganizationId, ClientId, or ClientSecret are not supplied, the cmdlet attempts to resolve them from a curated list of environment-variable name patterns across Process, User, and Machine scopes. The connection no longer carries a default ProjectId, Environment, or SecretPath; downstream cmdlets accept those as explicit (mandatory where applicable) parameters. + + + Notes + + Use -PassThru to emit the resulting InfisicalConnection object; by default the connection is stored silently. SecureString-typed parameters such as ClientSecret, AccessToken, Jwt, and Password are never logged. + The cmdlet pins the API version to the bound value when -ApiVersion is supplied explicitly; otherwise the default 'v4' is used and remains overridable per-call. + + + + + EXAMPLE 1 + Connect-Infisical -BaseUri 'https://app.infisical.com' -ClientId $ClientId -ClientSecret $ClientSecret -OrganizationId $OrgId + Performs a Universal-Auth machine-identity login and stores the resulting session for subsequent cmdlets. + + + EXAMPLE 2 + $ConnectInfisicalParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$ConnectInfisicalParameters.BaseUri = 'https://app.infisical.com' +$ConnectInfisicalParameters.OrganizationId = $OrganizationId +$ConnectInfisicalParameters.ClientId = $ClientId +$ConnectInfisicalParameters.ClientSecret = $ClientSecret +$ConnectInfisicalParameters.ApiVersion = 'v4' +$ConnectInfisicalParameters.PassThru = $True +$ConnectInfisicalParameters.Verbose = $True + +$ConnectInfisicalResult = Connect-Infisical @ConnectInfisicalParameters + Builds an ordered parameter dictionary, splats it onto Connect-Infisical, and captures the returned InfisicalConnection for later reuse. + + + + + + + Disconnect-Infisical + Clears the current Infisical session from the module-level session manager. + Disconnect + Infisical + + + Removes the cached InfisicalConnection so subsequent cmdlets that require an active session will fail until Connect-Infisical is invoked again. The cmdlet does not contact the Infisical server. + + + Notes + + Use -PassThru to receive a status object that includes the disconnect timestamp; by default the cmdlet returns no output. + + + + + EXAMPLE 1 + Disconnect-Infisical + Clears the active Infisical session silently. + + + EXAMPLE 2 + $DisconnectInfisicalParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$DisconnectInfisicalParameters.PassThru = $True +$DisconnectInfisicalParameters.Verbose = $True + +$DisconnectInfisicalResult = Disconnect-Infisical @DisconnectInfisicalParameters + Disconnects and captures a status object that includes IsConnected and DisconnectedAtUtc for logging. + + + + + + + Get-InfisicalSecret + Lists or retrieves Infisical secrets within a project, environment, and optional folder path. + Get + InfisicalSecret + + + Default (List parameter set) enumerates secrets under the supplied project and environment, optionally recursing through subfolders and filtering by metadata or tag slugs. When -SecretName is supplied (Single parameter set) the cmdlet returns one secret by name; -Version and -Type tune the single-record fetch. -ProjectId and -Environment are mandatory in both modes; -SecretPath defaults to '/' and -ApiVersion defaults to the value pinned on the active InfisicalConnection. + + + Notes + + Use -Recursive together with -SecretPath to walk an entire folder subtree in List mode. Pipe the result into ConvertTo-InfisicalSecretDictionary for hashtable-style lookup. The returned InfisicalSecret stores the value as SecureString; call .GetPlainTextValue() to materialize the cleartext value only when strictly required. + + + + + EXAMPLE 1 + Get-InfisicalSecret -ProjectId $ProjectId -Environment 'dev' -SecretPath '/Windows' -Recursive + Lists every secret under /Windows in the dev environment of the specified project. + + + EXAMPLE 2 + Get-InfisicalSecret -SecretName 'DATABASE_URL' + Retrieves the DATABASE_URL secret from the project and environment pinned by Connect-Infisical. + + + EXAMPLE 3 + $GetInfisicalSecretParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalSecretParameters.ProjectId = $ProjectId +$GetInfisicalSecretParameters.Environment = 'dev' +$GetInfisicalSecretParameters.SecretPath = "/Windows/$($CallingScriptPath.BaseName)" +$GetInfisicalSecretParameters.Recursive = $True +$GetInfisicalSecretParameters.ExpandSecretReferences = $True +$GetInfisicalSecretParameters.IncludeImports = $True +$GetInfisicalSecretParameters.IncludePersonalOverrides = $True +$GetInfisicalSecretParameters.Verbose = $True + +$GetInfisicalSecretResult = Get-InfisicalSecret @GetInfisicalSecretParameters + Lists secrets under a script-specific subpath with imports, personal overrides, and reference expansion enabled. + + + + + + + New-InfisicalSecret + Creates a new Infisical secret, with support for SecureString values and bulk creation. + New + InfisicalSecret + + + Creates one or many secrets. Three parameter sets are supported: PlainText (SecretName + SecretValue), SecureString (SecretName + SecureSecretValue), and Bulk (an array of hashtables piped or supplied via -Secrets). Honors -WhatIf and -Confirm. + + + Notes + + Pass -SkipMultilineEncoding when the value already contains literal newlines that the server should preserve verbatim. Use -TagIds to attach tag references at creation time. + + + + + EXAMPLE 1 + New-InfisicalSecret -SecretName 'API_KEY' -SecretValue 'super-secret-value' -ProjectId $ProjectId -Environment 'dev' + Creates a single shared secret in the specified project/environment. + + + EXAMPLE 2 + $GetInfisicalTagResult = Get-InfisicalTag -ProjectId $ProjectId + +$NewInfisicalSecretParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$NewInfisicalSecretParameters.SecretName = 'API_KEY' +$NewInfisicalSecretParameters.SecretValue = 'super-secret-value' +$NewInfisicalSecretParameters.SecretComment = 'Issued by deployment pipeline' +$NewInfisicalSecretParameters.ProjectId = $ProjectId +$NewInfisicalSecretParameters.Environment = 'dev' +$NewInfisicalSecretParameters.SecretPath = "/Windows/$($CallingScriptPath.BaseName)" +$NewInfisicalSecretParameters.TagIds = @($GetInfisicalTagResult[0].Id) +$NewInfisicalSecretParameters.Verbose = $True + +$NewInfisicalSecretResult = New-InfisicalSecret @NewInfisicalSecretParameters + Looks up tags to attach, then creates a single secret with a comment and tag association under a script-specific subpath. + + + + + + + Update-InfisicalSecret + Updates an existing Infisical secret value, comment, name, or tags. + Update + InfisicalSecret + + + Updates one or many secrets. Supports PlainText, SecureString, and Bulk parameter sets. Use -NewSecretName to rename a secret, -SecretComment to update its comment, and -TagIds to replace tag associations. Honors -WhatIf and -Confirm. + + + Notes + + Only the parameters you bind are sent; omitted scalar parameters are not modified server-side. The Bulk parameter set accepts pipeline input of hashtables containing SecretName/SecretValue/etc. + + + + + EXAMPLE 1 + Update-InfisicalSecret -SecretName 'API_KEY' -SecretValue 'rotated-value' -ProjectId $ProjectId -Environment 'dev' + Rotates the API_KEY secret in the specified project/environment. + + + EXAMPLE 2 + $UpdateInfisicalSecretParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$UpdateInfisicalSecretParameters.SecretName = 'API_KEY' +$UpdateInfisicalSecretParameters.NewSecretName = 'API_KEY_V2' +$UpdateInfisicalSecretParameters.SecretValue = 'rotated-value' +$UpdateInfisicalSecretParameters.SecretComment = 'Rotated by scheduled job' +$UpdateInfisicalSecretParameters.ProjectId = $ProjectId +$UpdateInfisicalSecretParameters.Environment = 'dev' +$UpdateInfisicalSecretParameters.SecretPath = "/Windows/$($CallingScriptPath.BaseName)" +$UpdateInfisicalSecretParameters.Verbose = $True + +$UpdateInfisicalSecretResult = Update-InfisicalSecret @UpdateInfisicalSecretParameters + Rotates the value, renames the secret, and updates its comment in a single call. + + + + + + + Remove-InfisicalSecret + Deletes one or many Infisical secrets by name. + Remove + InfisicalSecret + + + Deletes a single secret (Single parameter set) or a batch of secrets by name (Bulk parameter set). High ConfirmImpact triggers prompts by default. -PassThru emits the removed secret names. + + + Notes + + Removal is irreversible from this cmdlet's perspective; rely on Infisical's audit log or secret-version history for forensics. Honors -WhatIf and -Confirm. + + + + + EXAMPLE 1 + Remove-InfisicalSecret -SecretName 'API_KEY_V1' -ProjectId $ProjectId -Environment 'dev' -Confirm:$False + Deletes a single secret without prompting. + + + EXAMPLE 2 + $RemoveInfisicalSecretParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$RemoveInfisicalSecretParameters.SecretNames = @('LEGACY_KEY_1','LEGACY_KEY_2','LEGACY_KEY_3') +$RemoveInfisicalSecretParameters.ProjectId = $ProjectId +$RemoveInfisicalSecretParameters.Environment = 'dev' +$RemoveInfisicalSecretParameters.SecretPath = "/Windows/$($CallingScriptPath.BaseName)" +$RemoveInfisicalSecretParameters.PassThru = $True +$RemoveInfisicalSecretParameters.Confirm = $False +$RemoveInfisicalSecretParameters.Verbose = $True + +$RemoveInfisicalSecretResult = Remove-InfisicalSecret @RemoveInfisicalSecretParameters + Bulk-deletes three legacy secrets and returns the removed names for audit logging. + + + + + + + Copy-InfisicalSecret + Duplicates one or more secrets into a different environment or secret path. + Copy + InfisicalSecret + + + Server-side duplicates an array of secret IDs into a destination environment (and optional destination path), with switches that control whether the value, comment, tags, and metadata are copied. Use Get-InfisicalSecret followed by selection of the desired Id values to feed -SecretId. + + + Notes + + Set -OverwriteExisting to replace same-named secrets at the destination. Without -CopySecretValue, the destination secrets are created with empty values, preserving only metadata. + + + + + EXAMPLE 1 + Get-InfisicalSecret -ProjectId $ProjectId -Environment 'dev' | Select-Object -ExpandProperty Id | Copy-InfisicalSecret -ProjectId $ProjectId -SourceEnvironment 'dev' -DestinationEnvironment 'staging' -CopySecretValue + Copies all secrets from dev into staging, including their values. + + + EXAMPLE 2 + $GetInfisicalSecretResult = Get-InfisicalSecret -ProjectId $ProjectId -Environment 'dev' -SecretPath '/Windows' -Recursive + +$CopyInfisicalSecretParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$CopyInfisicalSecretParameters.SecretId = $GetInfisicalSecretResult.Id +$CopyInfisicalSecretParameters.ProjectId = $ProjectId +$CopyInfisicalSecretParameters.SourceEnvironment = 'dev' +$CopyInfisicalSecretParameters.SourceSecretPath = '/Windows' +$CopyInfisicalSecretParameters.DestinationEnvironment = 'staging' +$CopyInfisicalSecretParameters.DestinationSecretPath = '/Windows' +$CopyInfisicalSecretParameters.OverwriteExisting = $True +$CopyInfisicalSecretParameters.CopySecretValue = $True +$CopyInfisicalSecretParameters.CopySecretComment = $True +$CopyInfisicalSecretParameters.CopyTags = $True +$CopyInfisicalSecretParameters.CopyMetadata = $True +$CopyInfisicalSecretParameters.Verbose = $True + +$CopyInfisicalSecretResult = Copy-InfisicalSecret @CopyInfisicalSecretParameters + Promotes every Windows secret from dev into staging with full value/comment/tag/metadata propagation. + + + + + + + ConvertTo-InfisicalSecretDictionary + Converts a stream of InfisicalSecret objects into a name-keyed Dictionary of SecureString or plain text values. + ConvertTo + InfisicalSecretDictionary + + + Aggregates an incoming pipeline of InfisicalSecret objects into a case-insensitive Dictionary keyed by SecretName. By default values are SecureString; pass -AsPlainText to materialize string values. Duplicate keys are handled via the -DuplicateKeyBehavior parameter (Error, FirstWins, LastWins). + + + Notes + + Use this conversion before splatting secrets into another process (-AsPlainText) or before passing them to libraries that expect SecureString-keyed lookups (default). + + + + + EXAMPLE 1 + Get-InfisicalSecret -ProjectId $ProjectId -Environment 'dev' | ConvertTo-InfisicalSecretDictionary -AsPlainText + Builds a plain-text dictionary of every secret in the dev environment of the specified project. + + + EXAMPLE 2 + $GetInfisicalSecretResult = Get-InfisicalSecret -ProjectId $ProjectId -Environment 'dev' -SecretPath "/Windows/$($CallingScriptPath.BaseName)" -Recursive + +$ConvertToInfisicalSecretDictionaryParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$ConvertToInfisicalSecretDictionaryParameters.InputObject = $GetInfisicalSecretResult +$ConvertToInfisicalSecretDictionaryParameters.DuplicateKeyBehavior = 'LastWins' +$ConvertToInfisicalSecretDictionaryParameters.AsPlainText = $True +$ConvertToInfisicalSecretDictionaryParameters.Verbose = $True + +$ConvertToInfisicalSecretDictionaryResult = ConvertTo-InfisicalSecretDictionary @ConvertToInfisicalSecretDictionaryParameters + Aggregates recursive secret results into a plain-text dictionary, with the last value winning on key collisions. + + + + + + + Export-InfisicalSecrets + Exports InfisicalSecret objects to disk or environment variables in a chosen file format. + Export + InfisicalSecrets + + + Buffers an incoming pipeline of InfisicalSecret objects and writes them to a file in the requested format (DotEnv, Json, Yaml, EnvironmentVariables, etc.) or sets them as environment variables on the chosen scope (Process, User, Machine). -Encoding controls text encoding for file outputs. + + + Notes + + EnvironmentVariables format does not require -Path; all other formats do. User/Machine scopes require appropriate privileges (Machine scope requires elevation on Windows). + + + + + EXAMPLE 1 + Get-InfisicalSecret -ProjectId $ProjectId -Environment 'dev' | Export-InfisicalSecrets -Format DotEnv -Path '.\.env' -Force + Writes the dev environment's secrets for the specified project to a .env file. + + + EXAMPLE 2 + $GetInfisicalSecretResult = Get-InfisicalSecret -ProjectId $ProjectId -Environment 'dev' -SecretPath "/Windows/$($CallingScriptPath.BaseName)" -Recursive + +$ExportInfisicalSecretsParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$ExportInfisicalSecretsParameters.InputObject = $GetInfisicalSecretResult +$ExportInfisicalSecretsParameters.Format = 'EnvironmentVariables' +$ExportInfisicalSecretsParameters.Scope = 'Process' +$ExportInfisicalSecretsParameters.Force = $True +$ExportInfisicalSecretsParameters.Verbose = $True + +$ExportInfisicalSecretsResult = Export-InfisicalSecrets @ExportInfisicalSecretsParameters + Projects the recursive secret result into Process-scope environment variables for the current PowerShell session. + + + + + + + Get-InfisicalProject + Lists or retrieves Infisical projects accessible to the current identity. + Get + InfisicalProject + + + Default (List parameter set) returns every project the active session can see; project visibility is governed by Infisical's role assignments. -Type filters the list to a single product surface (secret-manager, cert-manager, kms, ssh, secret-scanning, pam, ai). -IncludeRoles asks the server to return the caller's role bindings on each project. When -ProjectId is supplied (Single parameter set) the cmdlet returns the one matching record. + + + Notes + + The List-mode result is an array of InfisicalProject objects; pipe into Where-Object or Select-Object to filter by Slug, Name, or Id. The cmdlet accepts pipeline input by property name on -ProjectId. + + + + + EXAMPLE 1 + Get-InfisicalProject + Lists every project the current session can see. + + + EXAMPLE 2 + Get-InfisicalProject -ProjectId $ProjectId + Retrieves the canonical record for a single project by id. + + + EXAMPLE 3 + Get-InfisicalProject -Type 'cert-manager' -IncludeRoles + Lists every Certificate Manager project visible to the session, including the caller's role bindings. + + + EXAMPLE 4 + $GetInfisicalProjectListResult = Get-InfisicalProject -Type 'secret-manager' | Where-Object { $_.Slug -ilike 'platform-*' } + +$GetInfisicalProjectParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalProjectParameters.ProjectId = $GetInfisicalProjectListResult[0].Id +$GetInfisicalProjectParameters.Verbose = $True + +$GetInfisicalProjectResult = Get-InfisicalProject @GetInfisicalProjectParameters + Filters Secret Manager projects to slugs that begin with 'platform-' and refetches the first match by id. + + + + + + + New-InfisicalProject + Creates a new Infisical project in the active organization. + New + InfisicalProject + + + Creates a project with the supplied name and optional slug, description, type, and organization id. If -OrganizationId is not supplied, the active session's organization is used. Honors -WhatIf and -Confirm. + + + Notes + + Slug must be unique within the organization; if not supplied, the server derives one from the project name. + + + + + EXAMPLE 1 + New-InfisicalProject -ProjectName 'Platform Telemetry' + Creates a new project named 'Platform Telemetry' in the active organization. + + + EXAMPLE 2 + $NewInfisicalProjectParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$NewInfisicalProjectParameters.ProjectName = 'Platform Telemetry' +$NewInfisicalProjectParameters.Slug = 'platform-telemetry' +$NewInfisicalProjectParameters.Description = 'Secrets for platform telemetry pipeline' +$NewInfisicalProjectParameters.Type = 'secret-manager' +$NewInfisicalProjectParameters.OrganizationId = $ConnectInfisicalParameters.OrganizationId +$NewInfisicalProjectParameters.Verbose = $True + +$NewInfisicalProjectResult = New-InfisicalProject @NewInfisicalProjectParameters + Creates a project with an explicit slug, description, and type bound to a specific organization id. + + + + + + + Update-InfisicalProject + Updates the name, description, or auto-capitalization flag on an existing project. + Update + InfisicalProject + + + Updates mutable attributes on a project. -ProjectId is required. Only parameters that are bound are sent to the server. Honors -WhatIf and -Confirm. + + + Notes + + AutoCapitalization controls whether secret names submitted in mixed case are stored uppercase server-side; setting it false preserves the literal case supplied by clients. + + + + + EXAMPLE 1 + Update-InfisicalProject -Name 'Platform Telemetry (v2)' + Renames the supplied project. + + + EXAMPLE 2 + $GetInfisicalProjectResult = Get-InfisicalProject | Where-Object { $_.Slug -eq 'platform-telemetry' } + +$UpdateInfisicalProjectParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$UpdateInfisicalProjectParameters.ProjectId = $GetInfisicalProjectResult.Id +$UpdateInfisicalProjectParameters.Name = 'Platform Telemetry (v2)' +$UpdateInfisicalProjectParameters.Description = 'Migrated to v2 pipeline' +$UpdateInfisicalProjectParameters.AutoCapitalization = $False +$UpdateInfisicalProjectParameters.Verbose = $True + +$UpdateInfisicalProjectResult = Update-InfisicalProject @UpdateInfisicalProjectParameters + Locates the project by slug, renames it, updates the description, and disables auto-capitalization. + + + + + + + Remove-InfisicalProject + Deletes an Infisical project. + Remove + InfisicalProject + + + Deletes a project by Id. -ProjectId is required. High ConfirmImpact prompts unless -Confirm:$False is supplied. -PassThru emits the removed project id. + + + Notes + + This is destructive and removes all secrets, environments, folders, and tags within the project. Honors -WhatIf and -Confirm. + + + + + EXAMPLE 1 + Remove-InfisicalProject -Confirm:$False + Deletes the supplied project without prompting. + + + EXAMPLE 2 + $GetInfisicalProjectResult = Get-InfisicalProject | Where-Object { $_.Slug -eq 'sandbox-temp' } + +$RemoveInfisicalProjectParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$RemoveInfisicalProjectParameters.ProjectId = $GetInfisicalProjectResult.Id +$RemoveInfisicalProjectParameters.PassThru = $True +$RemoveInfisicalProjectParameters.Confirm = $False +$RemoveInfisicalProjectParameters.Verbose = $True + +$RemoveInfisicalProjectResult = Remove-InfisicalProject @RemoveInfisicalProjectParameters + Finds the sandbox project by slug, removes it without confirmation, and emits the removed project id for logging. + + + + + + + Get-InfisicalEnvironment + Lists or retrieves Infisical environments defined on a project. + Get + InfisicalEnvironment + + + Default (List parameter set) returns every environment configured on the supplied project. When -EnvironmentSlugOrId is supplied (Single parameter set) the cmdlet returns one environment by slug or id. -ProjectId is required in both modes. + + + Notes + + Each InfisicalEnvironment carries both Id and Slug; downstream cmdlets accept either form on -Environment-like parameters. Accepts pipeline input by property name on -EnvironmentSlugOrId. + + + + + EXAMPLE 1 + Get-InfisicalEnvironment + Lists every environment defined on the supplied project. + + + EXAMPLE 2 + Get-InfisicalEnvironment -EnvironmentSlugOrId 'dev' + Retrieves the 'dev' environment from the supplied project. + + + EXAMPLE 3 + $GetInfisicalProjectResult = Get-InfisicalProject | Where-Object { $_.Slug -eq 'platform-telemetry' } + +$GetInfisicalEnvironmentParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalEnvironmentParameters.EnvironmentSlugOrId = 'dev' +$GetInfisicalEnvironmentParameters.ProjectId = $GetInfisicalProjectResult.Id +$GetInfisicalEnvironmentParameters.Verbose = $True + +$GetInfisicalEnvironmentResult = Get-InfisicalEnvironment @GetInfisicalEnvironmentParameters + Resolves a project by slug and re-fetches the dev environment record by slug under that project. + + + + + + + New-InfisicalEnvironment + Creates a new environment on an Infisical project. + New + InfisicalEnvironment + + + Creates an environment with the supplied display name and slug, optionally setting its sort -Position. -ProjectId is required. Honors -WhatIf and -Confirm. + + + Notes + + Slugs must be unique within the project and are used as the canonical -Environment value across all other cmdlets. + + + + + EXAMPLE 1 + New-InfisicalEnvironment -Name 'Staging' -Slug 'staging' + Adds a Staging environment to the supplied project. + + + EXAMPLE 2 + $GetInfisicalProjectResult = Get-InfisicalProject | Where-Object { $_.Slug -eq 'platform-telemetry' } + +$NewInfisicalEnvironmentParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$NewInfisicalEnvironmentParameters.ProjectId = $GetInfisicalProjectResult.Id +$NewInfisicalEnvironmentParameters.Name = 'Staging' +$NewInfisicalEnvironmentParameters.Slug = 'staging' +$NewInfisicalEnvironmentParameters.Position = 20 +$NewInfisicalEnvironmentParameters.Verbose = $True + +$NewInfisicalEnvironmentResult = New-InfisicalEnvironment @NewInfisicalEnvironmentParameters + Adds a Staging environment at sort position 20 on the resolved project. + + + + + + + Update-InfisicalEnvironment + Updates the name, slug, or sort order of an existing Infisical environment. + Update + InfisicalEnvironment + + + Updates an environment identified by -EnvironmentId. -ProjectId is required. Only bound parameters are sent to the server. Honors -WhatIf and -Confirm. + + + Notes + + Changing -Slug can break downstream automation that pins to the previous slug. Coordinate slug rotation with consumers. + + + + + EXAMPLE 1 + Update-InfisicalEnvironment -EnvironmentId $EnvId -Name 'Pre-Production' + Renames an environment in the supplied project. + + + EXAMPLE 2 + $GetInfisicalEnvironmentResult = Get-InfisicalEnvironment | Where-Object { $_.Slug -eq 'staging' } + +$UpdateInfisicalEnvironmentParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$UpdateInfisicalEnvironmentParameters.EnvironmentId = $GetInfisicalEnvironmentResult.Id +$UpdateInfisicalEnvironmentParameters.ProjectId = $ProjectId +$UpdateInfisicalEnvironmentParameters.Name = 'Pre-Production' +$UpdateInfisicalEnvironmentParameters.Slug = 'preprod' +$UpdateInfisicalEnvironmentParameters.Position = 25 +$UpdateInfisicalEnvironmentParameters.Verbose = $True + +$UpdateInfisicalEnvironmentResult = Update-InfisicalEnvironment @UpdateInfisicalEnvironmentParameters + Locates the staging environment, renames it to Pre-Production, rotates its slug, and updates its sort order. + + + + + + + Remove-InfisicalEnvironment + Deletes an Infisical environment from a project. + Remove + InfisicalEnvironment + + + Removes an environment by Id. -ProjectId is required. High ConfirmImpact prompts unless -Confirm:$False is supplied. -PassThru emits the removed environment id. + + + Notes + + Removing an environment deletes every secret and folder scoped to it. Honors -WhatIf and -Confirm. + + + + + EXAMPLE 1 + Remove-InfisicalEnvironment -EnvironmentId $EnvId -Confirm:$False + Deletes an environment without prompting. + + + EXAMPLE 2 + $GetInfisicalEnvironmentResult = Get-InfisicalEnvironment | Where-Object { $_.Slug -eq 'sandbox' } + +$RemoveInfisicalEnvironmentParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$RemoveInfisicalEnvironmentParameters.EnvironmentId = $GetInfisicalEnvironmentResult.Id +$RemoveInfisicalEnvironmentParameters.ProjectId = $ProjectId +$RemoveInfisicalEnvironmentParameters.PassThru = $True +$RemoveInfisicalEnvironmentParameters.Confirm = $False +$RemoveInfisicalEnvironmentParameters.Verbose = $True + +$RemoveInfisicalEnvironmentResult = Remove-InfisicalEnvironment @RemoveInfisicalEnvironmentParameters + Removes the sandbox environment without prompting and emits its id for the audit trail. + + + + + + + Get-InfisicalFolder + Lists or retrieves Infisical folders at a given secret path. + Get + InfisicalFolder + + + Default (List parameter set) enumerates folders directly under the supplied -Path within the project and environment. When -FolderNameOrId is supplied (Single parameter set) the cmdlet returns one folder by name or id under -Path. -ProjectId and -Environment are required in both modes; -Path defaults to '/'. + + + Notes + + List mode is a non-recursive listing of immediate subfolders. To enumerate secrets across a folder subtree use Get-InfisicalSecret -Recursive. Accepts pipeline input by property name on -FolderNameOrId. + + + + + EXAMPLE 1 + Get-InfisicalFolder -ProjectId $ProjectId -Environment 'dev' -Path '/Windows' + Lists every folder directly under /Windows in the supplied project and environment. + + + EXAMPLE 2 + Get-InfisicalFolder -FolderNameOrId 'Deployments' -ProjectId $ProjectId -Environment 'dev' -Path '/Windows' + Retrieves the Deployments folder under /Windows in the supplied project and environment. + + + EXAMPLE 3 + $GetInfisicalFolderListResult = Get-InfisicalFolder -Path '/Windows' | Where-Object { $_.Name -eq 'Deployments' } + +$GetInfisicalFolderParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalFolderParameters.FolderNameOrId = $GetInfisicalFolderListResult.Id +$GetInfisicalFolderParameters.ProjectId = $ProjectId +$GetInfisicalFolderParameters.Environment = 'dev' +$GetInfisicalFolderParameters.Path = '/Windows' +$GetInfisicalFolderParameters.Verbose = $True + +$GetInfisicalFolderResult = Get-InfisicalFolder @GetInfisicalFolderParameters + Locates the folder by name first, then re-fetches it by id to refresh the canonical record. + + + + + + + New-InfisicalFolder + Creates a new Infisical folder under the supplied parent path. + New + InfisicalFolder + + + Creates a folder with the supplied -Name beneath the supplied -Path. -ProjectId and -Environment are required; -Path defaults to '/'. Honors -WhatIf and -Confirm. + + + Notes + + Folder names are case-sensitive and must be unique within a parent path; the cmdlet does not create intermediate folders. + + + + + EXAMPLE 1 + New-InfisicalFolder -Name 'Deployments' -ProjectId $ProjectId -Environment 'dev' -Path '/Windows' + Creates the Deployments folder under /Windows in the supplied project and environment. + + + EXAMPLE 2 + $NewInfisicalFolderParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$NewInfisicalFolderParameters.Name = $CallingScriptPath.BaseName +$NewInfisicalFolderParameters.ProjectId = $ProjectId +$NewInfisicalFolderParameters.Environment = 'dev' +$NewInfisicalFolderParameters.Path = '/Windows' +$NewInfisicalFolderParameters.Verbose = $True + +$NewInfisicalFolderResult = New-InfisicalFolder @NewInfisicalFolderParameters + Creates a script-named folder under /Windows in the supplied project and environment. + + + + + + + Update-InfisicalFolder + Renames an existing Infisical folder. + Update + InfisicalFolder + + + Renames a folder identified by -FolderId to the supplied -Name. -ProjectId and -Environment are required; -Path defaults to '/'. Honors -WhatIf and -Confirm. + + + Notes + + Renaming a folder rewrites the path component for every secret beneath it; coordinate with consumers that pin to the previous path. + + + + + EXAMPLE 1 + Update-InfisicalFolder -FolderId $FolderId -Name 'Deployments-Archive' + Renames a folder in the supplied project/environment. + + + EXAMPLE 2 + $GetInfisicalFolderResult = Get-InfisicalFolder -Path '/Windows' | Where-Object { $_.Name -eq 'Deployments' } + +$UpdateInfisicalFolderParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$UpdateInfisicalFolderParameters.FolderId = $GetInfisicalFolderResult.Id +$UpdateInfisicalFolderParameters.Name = 'Deployments-Archive' +$UpdateInfisicalFolderParameters.ProjectId = $ProjectId +$UpdateInfisicalFolderParameters.Environment = 'dev' +$UpdateInfisicalFolderParameters.Path = '/Windows' +$UpdateInfisicalFolderParameters.Verbose = $True + +$UpdateInfisicalFolderResult = Update-InfisicalFolder @UpdateInfisicalFolderParameters + Resolves the folder by name and renames it to Deployments-Archive. + + + + + + + Remove-InfisicalFolder + Deletes an Infisical folder and all secrets it contains. + Remove + InfisicalFolder + + + Removes a folder by Id from the supplied -Path. -ProjectId and -Environment are required; -Path defaults to '/'. High ConfirmImpact prompts unless -Confirm:$False is supplied. -PassThru emits the removed folder id. + + + Notes + + This is destructive and removes every secret and subfolder under the target folder. Honors -WhatIf and -Confirm. + + + + + EXAMPLE 1 + Remove-InfisicalFolder -FolderId $FolderId -Confirm:$False + Deletes a folder from the supplied project/environment without prompting. + + + EXAMPLE 2 + $GetInfisicalFolderResult = Get-InfisicalFolder -Path '/Windows' | Where-Object { $_.Name -eq $CallingScriptPath.BaseName } + +$RemoveInfisicalFolderParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$RemoveInfisicalFolderParameters.FolderId = $GetInfisicalFolderResult.Id +$RemoveInfisicalFolderParameters.ProjectId = $ProjectId +$RemoveInfisicalFolderParameters.Environment = 'dev' +$RemoveInfisicalFolderParameters.Path = '/Windows' +$RemoveInfisicalFolderParameters.PassThru = $True +$RemoveInfisicalFolderParameters.Confirm = $False +$RemoveInfisicalFolderParameters.Verbose = $True + +$RemoveInfisicalFolderResult = Remove-InfisicalFolder @RemoveInfisicalFolderParameters + Resolves the script-named folder under /Windows and removes it without prompting, returning its id for logging. + + + + + + + Get-InfisicalTag + Lists or retrieves Infisical tags defined on a project. + Get + InfisicalTag + + + Default (List parameter set) returns every tag configured on the project. When -TagSlugOrId is supplied (Single parameter set) the cmdlet returns the one matching record. -ProjectId is required in both modes. + + + Notes + + Tag Ids returned here are the values to pass on -TagIds when creating or updating secrets. Accepts pipeline input by property name on -TagSlugOrId. + + + + + EXAMPLE 1 + Get-InfisicalTag + Lists every tag defined on the supplied project. + + + EXAMPLE 2 + Get-InfisicalTag -TagSlugOrId 'critical' + Retrieves the 'critical' tag from the supplied project. + + + EXAMPLE 3 + $GetInfisicalProjectResult = Get-InfisicalProject | Where-Object { $_.Slug -eq 'platform-telemetry' } + +$GetInfisicalTagParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalTagParameters.TagSlugOrId = 'critical' +$GetInfisicalTagParameters.ProjectId = $GetInfisicalProjectResult.Id +$GetInfisicalTagParameters.Verbose = $True + +$GetInfisicalTagResult = Get-InfisicalTag @GetInfisicalTagParameters + Resolves a project by slug and refetches the 'critical' tag from that project. + + + + + + + New-InfisicalTag + Creates a new Infisical tag on a project. + New + InfisicalTag + + + Creates a tag with the supplied -Slug, optional -Name and -Color. -ProjectId is required. Honors -WhatIf and -Confirm. + + + Notes + + Tag slugs must be unique within the project and are the canonical reference used by tag-filtered secret lookups. + + + + + EXAMPLE 1 + New-InfisicalTag -Slug 'critical' -Name 'Critical' -Color '#FF0000' + Creates a red Critical tag in the supplied project. + + + EXAMPLE 2 + $NewInfisicalTagParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$NewInfisicalTagParameters.Slug = 'critical' +$NewInfisicalTagParameters.Name = 'Critical' +$NewInfisicalTagParameters.Color = '#FF0000' +$NewInfisicalTagParameters.ProjectId = $ProjectId +$NewInfisicalTagParameters.Verbose = $True + +$NewInfisicalTagResult = New-InfisicalTag @NewInfisicalTagParameters + Creates a red Critical tag against an explicitly supplied project id. + + + + + + + Update-InfisicalTag + Updates the slug, name, or color of an existing Infisical tag. + Update + InfisicalTag + + + Updates a tag identified by -TagId. -ProjectId is required. Only bound parameters are sent to the server. Honors -WhatIf and -Confirm. + + + Notes + + Changing -Slug breaks tag-filtered automation that pins to the previous slug. Coordinate slug rotation with consumers. + + + + + EXAMPLE 1 + Update-InfisicalTag -TagId $TagId -Color '#FFA500' + Changes the display color of a tag in the supplied project. + + + EXAMPLE 2 + $GetInfisicalTagResult = Get-InfisicalTag | Where-Object { $_.Slug -eq 'critical' } + +$UpdateInfisicalTagParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$UpdateInfisicalTagParameters.TagId = $GetInfisicalTagResult.Id +$UpdateInfisicalTagParameters.Slug = 'critical-v2' +$UpdateInfisicalTagParameters.Name = 'Critical (v2)' +$UpdateInfisicalTagParameters.Color = '#FFA500' +$UpdateInfisicalTagParameters.ProjectId = $ProjectId +$UpdateInfisicalTagParameters.Verbose = $True + +$UpdateInfisicalTagResult = Update-InfisicalTag @UpdateInfisicalTagParameters + Locates the critical tag and rotates its slug, display name, and color. + + + + + + + Remove-InfisicalTag + Deletes an Infisical tag from a project. + Remove + InfisicalTag + + + Removes a tag by Id. -ProjectId is required. High ConfirmImpact prompts unless -Confirm:$False is supplied. -PassThru emits the removed tag id. + + + Notes + + Removing a tag detaches it from every secret it was applied to but does not delete the secrets themselves. Honors -WhatIf and -Confirm. + + + + + EXAMPLE 1 + Remove-InfisicalTag -TagId $TagId -Confirm:$False + Deletes a tag from the supplied project without prompting. + + + EXAMPLE 2 + $GetInfisicalTagResult = Get-InfisicalTag | Where-Object { $_.Slug -eq 'critical-v2' } + +$RemoveInfisicalTagParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$RemoveInfisicalTagParameters.TagId = $GetInfisicalTagResult.Id +$RemoveInfisicalTagParameters.ProjectId = $ProjectId +$RemoveInfisicalTagParameters.PassThru = $True +$RemoveInfisicalTagParameters.Confirm = $False +$RemoveInfisicalTagParameters.Verbose = $True + +$RemoveInfisicalTagResult = Remove-InfisicalTag @RemoveInfisicalTagParameters + Resolves a tag by slug and removes it without prompting, returning its id for the audit trail. + + + + + + + Get-InfisicalCertificateAuthority + Lists or retrieves Infisical Certificate Authorities. + Get + InfisicalCertificateAuthority + + + When -CaId is supplied (ById parameter set) returns a single internal CA. Otherwise (List parameter set) returns CAs scoped by -Kind: Internal (default, /api/v1/cert-manager/ca/internal), Any (/api/v1/cert-manager/ca returning both internal and ACME), or Acme (filters the generic endpoint to ACME issuers only). -ProjectId is required. + + + Notes + + 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. + + + + + EXAMPLE 1 + Get-InfisicalCertificateAuthority + Lists every internal CA visible in the supplied project. + + + EXAMPLE 2 + Get-InfisicalCertificateAuthority -Kind Any + Lists every CA (internal and ACME) visible in the supplied project; inspect the Type property to distinguish them. + + + EXAMPLE 3 + $GetInfisicalCertificateAuthorityListResult = Get-InfisicalCertificateAuthority | Where-Object { $_.FriendlyName -eq 'Issuing CA - Platform' } + +$GetInfisicalCertificateAuthorityParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalCertificateAuthorityParameters.CaId = $GetInfisicalCertificateAuthorityListResult.Id +$GetInfisicalCertificateAuthorityParameters.ProjectId = $ProjectId +$GetInfisicalCertificateAuthorityParameters.Verbose = $True + +$GetInfisicalCertificateAuthorityResult = Get-InfisicalCertificateAuthority @GetInfisicalCertificateAuthorityParameters + Filters the CA list by friendly name and then re-fetches the canonical CA record by id using a splatted parameter set. + + + + + + + Get-InfisicalCertificate + Lists or retrieves Infisical certificates in a project, with optional filters and automatic paging. + Get + InfisicalCertificate + + + Default (List parameter set) enumerates certificates with optional filters for -CommonName, -FriendlyName, -Status, and -CaId; -Limit and -Offset drive a single page and pages are walked automatically until exhausted unless -NoAutoPage is supplied. When -SerialNumber is supplied (Single parameter set) the cmdlet returns one certificate record. -ProjectId is required in both modes. + + + Notes + + For advanced filtering (validity window, key algorithm, extended key usage, etc.) use Search-InfisicalCertificate instead. Single mode returns metadata only; to obtain certificate and chain PEM material use ConvertTo-InfisicalCertificate or Export-InfisicalCertificate. Accepts pipeline input by property name on -SerialNumber. + + + + + EXAMPLE 1 + Get-InfisicalCertificate -Status 'active' + Lists every active certificate in the supplied project. + + + EXAMPLE 2 + Get-InfisicalCertificate -SerialNumber '7A:F2:1B:...:9E' + Retrieves the certificate record for the supplied serial number. + + + EXAMPLE 3 + $GetInfisicalCertificateAuthorityListResult = Get-InfisicalCertificateAuthority | Where-Object { $_.FriendlyName -eq 'Issuing CA - Platform' } + +$GetInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalCertificateParameters.ProjectId = $ProjectId +$GetInfisicalCertificateParameters.CommonName = $env:COMPUTERNAME +$GetInfisicalCertificateParameters.FriendlyName = 'web-tier' +$GetInfisicalCertificateParameters.Status = 'active' +$GetInfisicalCertificateParameters.CaId = @($GetInfisicalCertificateAuthorityListResult.Id) +$GetInfisicalCertificateParameters.Limit = 100 +$GetInfisicalCertificateParameters.Verbose = $True + +$GetInfisicalCertificateListResult = Get-InfisicalCertificate @GetInfisicalCertificateParameters + Resolves the issuing CA, then lists active certificates scoped to that CA, the local hostname, and the 'web-tier' friendly name. + + + + + + + Get-InfisicalPkiSubscriber + Lists or retrieves Infisical PKI subscribers in a project. + Get + InfisicalPkiSubscriber + + + Default (List parameter set) returns every PKI subscriber configured on the project. When -Name is supplied (ByName parameter set) the cmdlet returns one subscriber by its slug. -ProjectId is required in both modes. + + + Notes + + The -Name parameter is the subscriber slug; aliases SubscriberName and Slug are accepted. Pass the slug returned here on -PkiSubscriberSlug when calling Request-InfisicalCertificate. Accepts pipeline input by property name on -Name. + + + + + EXAMPLE 1 + Get-InfisicalPkiSubscriber + Lists every PKI subscriber defined on the supplied project. + + + EXAMPLE 2 + Get-InfisicalPkiSubscriber -Name 'mecm' + Retrieves the 'mecm' PKI subscriber from the supplied project. + + + EXAMPLE 3 + $GetInfisicalPkiSubscriberListResult = Get-InfisicalPkiSubscriber | Where-Object { $_.Name -ilike 'mecm*' } + +$GetInfisicalPkiSubscriberParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalPkiSubscriberParameters.Name = $GetInfisicalPkiSubscriberListResult[0].Name +$GetInfisicalPkiSubscriberParameters.ProjectId = $ProjectId +$GetInfisicalPkiSubscriberParameters.Verbose = $True + +$GetInfisicalPkiSubscriberResult = Get-InfisicalPkiSubscriber @GetInfisicalPkiSubscriberParameters + Filters subscribers whose name starts with 'mecm' and refetches the canonical record for the first match. + + + + + + + Get-InfisicalCertificateProfile + Lists or retrieves Infisical certificate profiles in a project. + Get + InfisicalCertificateProfile + + + Default (List parameter set) returns every certificate profile configured on the project via /api/v1/cert-manager/certificate-profiles, with optional -Limit, -Offset, and -IncludeConfigs. When -ProfileId is supplied (ById parameter set) the cmdlet returns one profile by its id. -ProjectId is required in both modes. + + + Notes + + Profiles bind a CA and a certificate policy and surface defaults (TtlDays, KeyAlgorithm, KeyUsages, ExtendedKeyUsages). Use the returned profile Id when wiring profile-based issuance against Request-InfisicalCertificate. + + + + + EXAMPLE 1 + Get-InfisicalCertificateProfile + Lists every certificate profile defined on the supplied project. + + + EXAMPLE 2 + Get-InfisicalCertificateProfile -ProfileId '8257641e-c808-454e-ac92-8dc920be865f' + Retrieves a single certificate profile by id from the supplied project. + + + EXAMPLE 3 + $GetInfisicalCertificateProfileListResult = Get-InfisicalCertificateProfile | Where-Object { $_.Slug -ieq 'codesigning' } + +$GetInfisicalCertificateProfileParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalCertificateProfileParameters.ProfileId = $GetInfisicalCertificateProfileListResult[0].Id +$GetInfisicalCertificateProfileParameters.ProjectId = $ProjectId +$GetInfisicalCertificateProfileParameters.Verbose = $True + +$GetInfisicalCertificateProfileResult = Get-InfisicalCertificateProfile @GetInfisicalCertificateProfileParameters + Filters profiles whose slug equals 'codesigning' and refetches the canonical record for the first match using a splatted parameter set. + + + + + + + Get-InfisicalCertificatePolicy + Lists or retrieves Infisical certificate policies in a project. + Get + InfisicalCertificatePolicy + + + Default (List parameter set) returns every certificate policy configured on the project via /api/v1/cert-manager/certificate-policies, with optional -Limit and -Offset. When -PolicyId is supplied (ById parameter set) the cmdlet returns one policy by its id. -ProjectId is required in both modes. + + + Notes + + Policies define the allowed/required subject, SANs, key usages, extended key usages, key algorithms, signature algorithm, and validity windows that certificate profiles enforce. Each profile binds exactly one policy via its CertificatePolicyId. + + + + + EXAMPLE 1 + Get-InfisicalCertificatePolicy + Lists every certificate policy defined on the supplied project. + + + EXAMPLE 2 + Get-InfisicalCertificatePolicy -PolicyId '3e69306a-e7c1-4fd2-a140-7fb300e53c43' + Retrieves a single certificate policy by id from the supplied project. + + + EXAMPLE 3 + $GetInfisicalCertificatePolicyListResult = Get-InfisicalCertificatePolicy | Where-Object { $_.Name -ieq 'codesigning' } + +$GetInfisicalCertificatePolicyParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalCertificatePolicyParameters.PolicyId = $GetInfisicalCertificatePolicyListResult[0].Id +$GetInfisicalCertificatePolicyParameters.ProjectId = $ProjectId +$GetInfisicalCertificatePolicyParameters.Verbose = $True + +$GetInfisicalCertificatePolicyResult = Get-InfisicalCertificatePolicy @GetInfisicalCertificatePolicyParameters + Filters policies whose name equals 'codesigning' and refetches the canonical record for the first match using a splatted parameter set. + + + + + + + Search-InfisicalCertificate + Searches Infisical certificates with advanced filters and automatic paging. + Search + InfisicalCertificate + + + Performs a server-side search across certificates with filters for friendly name, common name, free-text search, status, CA/profile/application/enrollment scope, key/signature algorithm, source, and validity window (-NotBeforeFrom/-NotBeforeTo/-NotAfterFrom/-NotAfterTo). Results are paged automatically unless -NoAutoPage is supplied. -ProjectId is required. + + + Notes + + Use -SortBy together with -SortOrder ('asc'/'desc') to control result ordering. Pair with Get-InfisicalCertificate or Export-InfisicalCertificate to drill into specific hits. + + + + + EXAMPLE 1 + Search-InfisicalCertificate -Search $env:COMPUTERNAME -Status 'active' + Finds active certificates whose searchable fields contain the local hostname. + + + EXAMPLE 2 + $GetInfisicalCertificateAuthorityListResult = Get-InfisicalCertificateAuthority | Where-Object { $_.FriendlyName -eq 'Issuing CA - Platform' } + +$SearchInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$SearchInfisicalCertificateParameters.ProjectId = $ProjectId +$SearchInfisicalCertificateParameters.CommonName = $env:COMPUTERNAME +$SearchInfisicalCertificateParameters.Status = 'active' +$SearchInfisicalCertificateParameters.CaId = @($GetInfisicalCertificateAuthorityListResult.Id) +$SearchInfisicalCertificateParameters.KeyAlgorithm = @('RSA') +$SearchInfisicalCertificateParameters.NotAfterTo = (Get-Date).AddDays(30) +$SearchInfisicalCertificateParameters.SortBy = 'notAfter' +$SearchInfisicalCertificateParameters.SortOrder = 'asc' +$SearchInfisicalCertificateParameters.Limit = 100 +$SearchInfisicalCertificateParameters.Verbose = $True + +$SearchInfisicalCertificateResult = Search-InfisicalCertificate @SearchInfisicalCertificateParameters + Searches for RSA certificates from a specific CA, scoped to the local hostname, that expire within the next 30 days, sorted soonest-first. + + + + + + + Request-InfisicalCertificate + Requests a new Infisical certificate (local CSR + sign) or reuses a still-valid existing one. + Request + InfisicalCertificate + + + Generates a keypair locally, builds a CSR, and submits it for signing via one of three parameter sets: a PKI subscriber (-PkiSubscriberSlug, default), direct CA signing (-CertificateAuthorityId), or a certificate profile (-CertificateProfileId, POSTs to /api/v1/cert-manager/certificates with the profile bound). On subsequent runs an existing certificate whose CN matches and whose remaining lifetime exceeds -RenewalThresholdDays is reused; pass -Force to always issue or -AllowRenewal to allow rotation inside the threshold. Optional flags install the leaf (-Install) and chain (-InstallChain) into a Windows certificate store, and control private-key protection (-PrivateKeyProtection, -PersistKey, -MachineKey, -PrivateKeyPath, -KeyStorageFlags). Honors -WhatIf and -Confirm. + + + Notes + + Default -PrivateKeyProtection is 'LocalOnly': the leaf is loaded into memory without persisting the private key and PrivateKeyPem is scrubbed from the emitted result unless -PrivateKeyPath or an explicit -KeyStorageFlags binding overrides it. The reuse path completes its chain from the Infisical bundle when local stores are incomplete; pass -LocalChainOnly to suppress that fetch entirely. + + + + + EXAMPLE 1 + Request-InfisicalCertificate -PkiSubscriberSlug 'web-tier' -Install + Requests (or reuses) a certificate for the 'web-tier' subscriber and installs it into CurrentUser\My. + + + EXAMPLE 2 + $RequestInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$RequestInfisicalCertificateParameters.PkiSubscriberSlug = 'web-tier' +$RequestInfisicalCertificateParameters.ProjectId = $ProjectId +$RequestInfisicalCertificateParameters.CommonName = ([System.Net.Dns]::GetHostEntry($env:COMPUTERNAME)).HostName +$RequestInfisicalCertificateParameters.DnsName = @(([System.Net.Dns]::GetHostEntry($env:COMPUTERNAME)).HostName, $env:COMPUTERNAME) +$RequestInfisicalCertificateParameters.KeyAlgorithm = 'Rsa' +$RequestInfisicalCertificateParameters.KeySize = 3072 +$RequestInfisicalCertificateParameters.Install = $True +$RequestInfisicalCertificateParameters.InstallChain = $True +$RequestInfisicalCertificateParameters.StoreName = 'My' +$RequestInfisicalCertificateParameters.StoreLocation = 'LocalMachine' +$RequestInfisicalCertificateParameters.PrivateKeyProtection = 'NonExportable' +$RequestInfisicalCertificateParameters.MachineKey = $True +$RequestInfisicalCertificateParameters.PersistKey = $True +$RequestInfisicalCertificateParameters.AllowRenewal = $True +$RequestInfisicalCertificateParameters.RenewalThresholdDays = 30 +$RequestInfisicalCertificateParameters.Verbose = $True + +$RequestInfisicalCertificateResult = Request-InfisicalCertificate @RequestInfisicalCertificateParameters + Issues (or renews within 30 days) a 3072-bit RSA certificate for the local FQDN, installs the leaf and chain into LocalMachine\My with a non-exportable machine-bound persistent key. + + + EXAMPLE 3 + $Profile = Get-InfisicalCertificateProfile | Where-Object { $_.Slug -eq 'web-tier-profile' } +Request-InfisicalCertificate -CertificateProfileId $Profile.Id -CommonName 'web01.contoso.com' -Ttl '90d' + Issues a certificate via the modern profile API (POST /api/v1/cert-manager/certificates). The profile binds the CA, policy, and defaults so no subscriber is required. + + + + + + + ConvertTo-InfisicalCertificate + Materializes an X509Certificate2 from an Infisical certificate record, bundle, or serial number. + ConvertTo + InfisicalCertificate + + + Fetches the certificate bundle (when given an InfisicalCertificate or -SerialNumber), or accepts an already-fetched -Bundle, and constructs an X509Certificate2 from the PEM material. Use -NoPrivateKey to omit the private key, -KeyStorageFlags to control how the key is loaded, and -IncludeChain to additionally emit each chain certificate as a separate X509Certificate2 in the pipeline. + + + Notes + + The bundle for any given certificate is typically retrievable only once after issuance; -SerialNumber and pipeline modes will fail with a bundle-not-available error for older certificates. Use -KeyStorageFlags Exportable when callers need to re-export the resulting cert as PFX. + + + + + EXAMPLE 1 + Get-InfisicalCertificate -SerialNumber $Serial | ConvertTo-InfisicalCertificate -IncludeChain + Materializes the certificate and emits each chain element individually. + + + EXAMPLE 2 + $GetInfisicalCertificateResult = Get-InfisicalCertificate -Status 'active' | Where-Object { $_.CommonName -eq $env:COMPUTERNAME } + +$ConvertToInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$ConvertToInfisicalCertificateParameters.SerialNumber = $GetInfisicalCertificateResult[0].SerialNumber +$ConvertToInfisicalCertificateParameters.NoPrivateKey = $False +$ConvertToInfisicalCertificateParameters.IncludeChain = $True +$ConvertToInfisicalCertificateParameters.KeyStorageFlags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable +$ConvertToInfisicalCertificateParameters.Verbose = $True + +$ConvertToInfisicalCertificateResult = ConvertTo-InfisicalCertificate @ConvertToInfisicalCertificateParameters + Selects the active certificate whose CN matches the host and materializes it (with private key and chain) as exportable X509Certificate2 objects. + + + + + + + Export-InfisicalCertificate + Exports an Infisical certificate to disk in PEM, PFX, or CER format. + Export + InfisicalCertificate + + + Writes a certificate to -Path in the supplied -Format. Accepts an X509Certificate2, an InfisicalCertificateBundle, an InfisicalCertificate (refetches bundle by serial), or a -SerialNumber. -Password (SecureString) supplies the PFX password. -IncludeChain appends chain certificates (PEM only). -NoPrivateKey omits the private key. -Force overwrites an existing file. Honors -WhatIf and -Confirm. + + + Notes + + PFX export requires the cert to have been loaded with X509KeyStorageFlags.Exportable; bundle/serial modes import with Exportable automatically. CER and PFX formats ignore -IncludeChain. + + + + + EXAMPLE 1 + Export-InfisicalCertificate -Path 'C:\Temp\web-tier.pem' -Format Pem -SerialNumber $Serial -IncludeChain + Exports a certificate, its chain, and private key (when available) as a single PEM bundle. + + + EXAMPLE 2 + $GetInfisicalCertificateResult = Get-InfisicalCertificate -Status 'active' | Where-Object { $_.CommonName -eq $env:COMPUTERNAME } + +$ExportInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$ExportInfisicalCertificateParameters.SerialNumber = $GetInfisicalCertificateResult[0].SerialNumber +$ExportInfisicalCertificateParameters.Path = "C:\Temp\$($env:COMPUTERNAME).pfx" +$ExportInfisicalCertificateParameters.Format = 'Pfx' +$ExportInfisicalCertificateParameters.Password = (Read-Host -AsSecureString -Prompt 'PFX password') +$ExportInfisicalCertificateParameters.Force = $True +$ExportInfisicalCertificateParameters.PassThru = $True +$ExportInfisicalCertificateParameters.Verbose = $True + +$ExportInfisicalCertificateResult = Export-InfisicalCertificate @ExportInfisicalCertificateParameters + Resolves the active host certificate by serial and exports it as a password-protected PFX, overwriting any existing file and emitting a FileInfo for downstream use. + + + + + + + Install-InfisicalCertificate + Installs an Infisical certificate (and optional chain) into a Windows certificate store. + Install + InfisicalCertificate + + + Adds a certificate to the supplied -StoreName and -StoreLocation. Accepts an X509Certificate2, an InfisicalCertificate (refetches bundle by serial), or a -SerialNumber. -KeyStorageFlags controls private-key loading. -IncludeChain installs each chain certificate to the CertificateAuthority store of the same -StoreLocation. -Force replaces an existing thumbprint. -PassThru emits the installed certificate. Honors -WhatIf and -Confirm. + + + Notes + + Installing into LocalMachine stores typically requires elevation. -IncludeChain only fires for serial/InfisicalCertificate inputs because the X509Certificate2 input has no associated bundle to walk. + + + + + EXAMPLE 1 + Install-InfisicalCertificate -SerialNumber $Serial -StoreLocation LocalMachine -IncludeChain + Installs the leaf into LocalMachine\My and each chain element into LocalMachine\CertificateAuthority. + + + EXAMPLE 2 + $GetInfisicalCertificateResult = Get-InfisicalCertificate -Status 'active' | Where-Object { $_.CommonName -eq $env:COMPUTERNAME } + +$InstallInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$InstallInfisicalCertificateParameters.SerialNumber = $GetInfisicalCertificateResult[0].SerialNumber +$InstallInfisicalCertificateParameters.StoreName = 'My' +$InstallInfisicalCertificateParameters.StoreLocation = 'LocalMachine' +$InstallInfisicalCertificateParameters.KeyStorageFlags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet -bor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet +$InstallInfisicalCertificateParameters.IncludeChain = $True +$InstallInfisicalCertificateParameters.Force = $True +$InstallInfisicalCertificateParameters.PassThru = $True +$InstallInfisicalCertificateParameters.Verbose = $True + +$InstallInfisicalCertificateResult = Install-InfisicalCertificate @InstallInfisicalCertificateParameters + Resolves the active host certificate and installs the leaf (with a machine-bound persistent key) plus its chain into LocalMachine, replacing any existing thumbprint match. + + + + + + + Uninstall-InfisicalCertificate + Removes a certificate from a Windows certificate store by thumbprint, subject, or pipeline input. + Uninstall + InfisicalCertificate + + + Removes matching certificates from the supplied -StoreName and -StoreLocation. Accepts -Thumbprint, -Subject, an X509Certificate2 (-Certificate), or an InfisicalCertificate (-InfisicalCertificate, uses FingerprintSha1). -Force allows removing multiple matches in one call; -PassThru emits each removed certificate. Honors -WhatIf and -Confirm. + + + Notes + + When more than one certificate matches -Subject and -Force is not supplied the cmdlet throws to prevent accidental bulk removal. Uninstalling from LocalMachine stores typically requires elevation. + + + + + EXAMPLE 1 + Uninstall-InfisicalCertificate -Thumbprint $Thumbprint -StoreLocation LocalMachine + Removes the certificate with the supplied thumbprint from LocalMachine\My. + + + EXAMPLE 2 + $GetInfisicalCertificateResult = Get-InfisicalCertificate -Status 'revoked' | Where-Object { $_.CommonName -eq $env:COMPUTERNAME } + +$UninstallInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$UninstallInfisicalCertificateParameters.InfisicalCertificate = $GetInfisicalCertificateResult[0] +$UninstallInfisicalCertificateParameters.StoreName = 'My' +$UninstallInfisicalCertificateParameters.StoreLocation = 'LocalMachine' +$UninstallInfisicalCertificateParameters.Force = $True +$UninstallInfisicalCertificateParameters.PassThru = $True +$UninstallInfisicalCertificateParameters.Verbose = $True + +$UninstallInfisicalCertificateResult = Uninstall-InfisicalCertificate @UninstallInfisicalCertificateParameters + Picks the revoked host certificate and removes it from LocalMachine\My using its SHA1 fingerprint, emitting the removed object for the audit trail. + + + + + + + Get-InfisicalCertificateApplication + Lists or retrieves an Infisical Certificate Manager Application from the supplied project. + Get + InfisicalCertificateApplication + + + Reads Infisical certificate-manager Applications (the join target used by EST/ACME/SCEP profile attachments) for the supplied project. The List parameter set returns all applications visible to the caller; the ById and ByName sets return a single application. -ProjectId is required. + + + + EXAMPLE 1 + Get-InfisicalCertificateApplication -ProjectId $ProjectId + Lists certificate-manager applications for the supplied project. + + + EXAMPLE 2 + Get-InfisicalCertificateApplication -ApplicationName 'workstation-mdm' -ProjectId $ProjectId + Retrieves a single application by name. + + + EXAMPLE 3 + $GetInfisicalCertificateApplicationParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalCertificateApplicationParameters.Id = $ApplicationId +$GetInfisicalCertificateApplicationParameters.ProjectId = $ProjectId +$GetInfisicalCertificateApplicationParameters.Verbose = $True + +$GetInfisicalCertificateApplicationResult = Get-InfisicalCertificateApplication @GetInfisicalCertificateApplicationParameters + Retrieves a single application by id from an explicit project. + + + + + + + Get-InfisicalCertificateApplicationEnrollment + Retrieves the API/EST/ACME/SCEP enrollment configuration attached to an application/profile pair. + Get + InfisicalCertificateApplicationEnrollment + + + Returns the InfisicalCertificateApplicationEnrollment for the given application and certificate profile, including any configured SCEP sub-block (server URL, RA certificate PEM, computed SHA-1 RaCertificateThumbprint, challenge type, and challenge endpoint URL when dynamic). + + + + EXAMPLE 1 + Get-InfisicalCertificateApplicationEnrollment -ApplicationId $AppId -ProfileId $ProfileId + Fetches the enrollment configuration for an application/profile pair. + + + EXAMPLE 2 + $GetInfisicalCertificateApplicationEnrollmentParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalCertificateApplicationEnrollmentParameters.ApplicationId = $ApplicationId +$GetInfisicalCertificateApplicationEnrollmentParameters.ProfileId = $ProfileId +$GetInfisicalCertificateApplicationEnrollmentParameters.Verbose = $True + +$GetInfisicalCertificateApplicationEnrollmentResult = Get-InfisicalCertificateApplicationEnrollment @GetInfisicalCertificateApplicationEnrollmentParameters + Retrieves the enrollment configuration and feeds it downstream to Get-InfisicalScepMdmProfile. + + + + + + + New-InfisicalScepDynamicChallenge + Generates a one-time SCEP challenge from an application/profile that is configured with dynamic challenge mode. + New + InfisicalScepDynamicChallenge + + + POSTs to /scep/applications/{applicationId}/profiles/{profileId}/challenge and returns the minted challenge as a SecureString. Use -AsPlainText to return a string instead. Requires the active machine identity to have read access on certificate-application-enrollment, and the target SCEP profile must be set to challengeType=dynamic. Dynamic challenges are an Enterprise-tier feature on managed Infisical deployments. + + + + EXAMPLE 1 + $Challenge = New-InfisicalScepDynamicChallenge -ApplicationId $AppId -ProfileId $ProfileId + Mints a single-use SCEP challenge and stores it as a SecureString. + + + EXAMPLE 2 + $NewInfisicalScepDynamicChallengeParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$NewInfisicalScepDynamicChallengeParameters.ApplicationId = $ApplicationId +$NewInfisicalScepDynamicChallengeParameters.ProfileId = $ProfileId +$NewInfisicalScepDynamicChallengeParameters.AsPlainText = $True +$NewInfisicalScepDynamicChallengeParameters.Verbose = $True + +$NewInfisicalScepDynamicChallengeResult = New-InfisicalScepDynamicChallenge @NewInfisicalScepDynamicChallengeParameters + Mints a plain-text challenge for use in environments where SecureString is inconvenient. + + + + + + + Get-InfisicalScepMdmProfile + Builds an Infisical SCEP MDM profile model from an application enrollment, certificate profile, or fully manual inputs. + Get + InfisicalScepMdmProfile + + + Produces an InfisicalScepMdmProfile that mirrors the Windows ClientCertificateInstall/SCEP CSP node set. FromEnrollment (default) consumes an InfisicalCertificateApplicationEnrollment and auto-fills ServerUrl from scep.scepEndpointUrl and CAThumbprint from the RA certificate; if the enrollment is configured for dynamic challenge mode, a fresh challenge is minted automatically when -Challenge is not supplied. FromProfile keeps the legacy projection from an InfisicalCertificateProfile and now requires -ApplicationId so the server URL can be built against /scep/applications/{appId}/profiles/{profileId}/pkiclient.exe. Manual requires explicit -ServerUrl, -Challenge, and -UniqueId. + + + Notes + + The SCEP endpoint URL ends in 'pkiclient.exe' for RFC 8894 / Cisco SCEP client compatibility. SecureString -Challenge is decrypted into the model only at write-time. + + + + + EXAMPLE 1 + Get-InfisicalCertificateApplicationEnrollment -ApplicationId $AppId -ProfileId $ProfileId | Get-InfisicalScepMdmProfile + Builds a SCEP MDM profile from an enrollment, auto-resolving ServerUrl, CAThumbprint, and (for dynamic mode) the challenge. + + + EXAMPLE 2 + Get-InfisicalCertificateProfile -CertificateProfileId $ProfileId | Get-InfisicalScepMdmProfile -ApplicationId $AppId -Challenge (Read-Host -AsSecureString 'SCEP challenge') + Builds a profile from a certificate profile (legacy path) with an explicit application id and static challenge. + + + EXAMPLE 3 + $GetInfisicalScepMdmProfileParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalScepMdmProfileParameters.EnrollmentObject = $Enrollment +$GetInfisicalScepMdmProfileParameters.UniqueId = 'WindowsClientAuth' +$GetInfisicalScepMdmProfileParameters.Scope = 'Device' +$GetInfisicalScepMdmProfileParameters.SubjectName = "CN=$($env:COMPUTERNAME)" +$GetInfisicalScepMdmProfileParameters.KeyLength = 2048 +$GetInfisicalScepMdmProfileParameters.HashAlgorithm = 'SHA256' +$GetInfisicalScepMdmProfileParameters.ValidPeriod = 'Years' +$GetInfisicalScepMdmProfileParameters.ValidPeriodUnits = 1 +$GetInfisicalScepMdmProfileParameters.Verbose = $True + +$GetInfisicalScepMdmProfileResult = Get-InfisicalScepMdmProfile @GetInfisicalScepMdmProfileParameters + Builds a device-scope SCEP MDM profile from an enrollment with overridden subject and key parameters. + + + + + + + Export-InfisicalScepMdmProfile + Writes an InfisicalScepMdmProfile to disk as a SyncML payload suitable for MDM delivery. + Export + InfisicalScepMdmProfile + + + Serializes the supplied InfisicalScepMdmProfile via ToSyncMl() and writes the result to -Path as UTF-8 (no BOM). Auto-creates the target directory. If the file exists and -Force is not specified the cmdlet logs a warning and returns instead of throwing. Honors -WhatIf and -Confirm. -PassThru emits the resulting FileInfo. + + + Notes + + The generated SyncML is round-trip-validated through XmlReader before being written. Pair with Write-InfisicalScepMdmProfileToWmi to apply the same model to the local MDM Bridge instead of exporting to a file. + + + + + EXAMPLE 1 + $Profile | Export-InfisicalScepMdmProfile -Path 'C:\Temp\scep.syncml' -Force + Writes the SyncML payload for the supplied SCEP MDM profile, overwriting any existing file. + + + EXAMPLE 2 + $ExportInfisicalScepMdmProfileParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$ExportInfisicalScepMdmProfileParameters.InputObject = $Profile +$ExportInfisicalScepMdmProfileParameters.Path = "C:\ProgramData\Infisical\scep-$($Profile.UniqueId).syncml" +$ExportInfisicalScepMdmProfileParameters.Force = $True +$ExportInfisicalScepMdmProfileParameters.PassThru = $True +$ExportInfisicalScepMdmProfileParameters.Verbose = $True + +$ExportInfisicalScepMdmProfileResult = Export-InfisicalScepMdmProfile @ExportInfisicalScepMdmProfileParameters + Writes the SyncML payload to a per-profile path under ProgramData and returns the resulting FileInfo. + + + + + + + Write-InfisicalScepMdmProfileToWmi + Submits an InfisicalScepMdmProfile to the local Windows MDM Bridge WMI provider to trigger SCEP enrollment. + Write + InfisicalScepMdmProfileToWmi + + + Creates a new CIM instance under the MDM Bridge namespace (default: root/cimv2/mdm/dmmap, class MDM_ClientCertificateInstall_SCEP02) by invoking New-CimInstance through the host runspace. Honors -WhatIf and -Confirm. -PassThru emits the resulting CIM instance. Throws PlatformNotSupportedException off Windows. Device-scope enrollment requires an elevated session; pass -SkipElevationCheck to bypass the guard. + + + Notes + + The MDM Bridge WMI provider runs the enrollment asynchronously; success here means the enrollment was submitted, not that a certificate has been issued. Inspect the corresponding ClientCertificateInstall/SCEP/<UniqueId>/Install nodes for status. Override -ClassName when targeting a different SCEP CSP version on the host. + + + + + EXAMPLE 1 + $Profile | Write-InfisicalScepMdmProfileToWmi -PassThru + Submits the SCEP MDM profile to the local MDM Bridge and emits the created CIM instance. + + + EXAMPLE 2 + $WriteInfisicalScepMdmProfileToWmiParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$WriteInfisicalScepMdmProfileToWmiParameters.InputObject = $Profile +$WriteInfisicalScepMdmProfileToWmiParameters.Namespace = 'root/cimv2/mdm/dmmap' +$WriteInfisicalScepMdmProfileToWmiParameters.ClassName = 'MDM_ClientCertificateInstall_SCEP02' +$WriteInfisicalScepMdmProfileToWmiParameters.SkipElevationCheck = $False +$WriteInfisicalScepMdmProfileToWmiParameters.PassThru = $True +$WriteInfisicalScepMdmProfileToWmiParameters.Verbose = $True + +$WriteInfisicalScepMdmProfileToWmiResult = Write-InfisicalScepMdmProfileToWmi @WriteInfisicalScepMdmProfileToWmiParameters + Submits a device-scope SCEP enrollment through the MDM Bridge and returns the CIM instance for downstream inspection. + + + + + diff --git a/README.md b/README.md index 835c2b5..f155e8c 100644 --- a/README.md +++ b/README.md @@ -26,14 +26,79 @@ Import-Module -Name .\Module\PSInfisicalAPI ## Cmdlets -| Cmdlet | Purpose | -| ------------------------------------- | -------------------------------------------------------------------------- | -| `Connect-Infisical` | Establish a session using Universal Auth or a pre-issued access token. | -| `Disconnect-Infisical` | Clear the current session. | -| `Get-InfisicalSecrets` | List secrets at a given path / environment. | -| `Get-InfisicalSecret` | Retrieve a single secret by name. | -| `ConvertTo-InfisicalSecretDictionary` | Convert secret objects into a `Hashtable` keyed by `SecretKey`. | -| `Export-InfisicalSecrets` | Export secrets to JSON, YAML, XML, or `.env` format. | +The module exports 37 cmdlets. Discovery cmdlets (`Get-Infisical*`) use a `List` (default) / single-record parameter-set pair: invoking without the identity parameter returns the collection, supplying the identity parameter returns one record. + +### Session + +| Cmdlet | Purpose | +| ---------------------- | -------------------------------------------------------------------------------------------------- | +| `Connect-Infisical` | Establishes an authenticated session with an Infisical server and stores it for use by subsequent cmdlets. | +| `Disconnect-Infisical` | Clears the current Infisical session from the module-level session manager. | + +### Secrets + +| Cmdlet | Purpose | +| ------------------------------------- | -------------------------------------------------------------------------------------------------- | +| `Get-InfisicalSecret` | Lists or retrieves Infisical secrets within a project, environment, and optional folder path. | +| `New-InfisicalSecret` | Creates a new Infisical secret, with support for SecureString values and bulk creation. | +| `Update-InfisicalSecret` | Updates an existing Infisical secret value, comment, name, or tags. | +| `Remove-InfisicalSecret` | Deletes one or many Infisical secrets by name. | +| `Copy-InfisicalSecret` | Duplicates one or more secrets into a different environment or secret path. | +| `ConvertTo-InfisicalSecretDictionary` | Converts a stream of InfisicalSecret objects into a name-keyed Dictionary of SecureString or plain text values. | +| `Export-InfisicalSecrets` | Exports InfisicalSecret objects to disk or environment variables in a chosen file format. | + +### Projects + +| Cmdlet | Purpose | +| ------------------------- | -------------------------------------------------------------------------------------------------- | +| `Get-InfisicalProject` | Lists or retrieves Infisical projects accessible to the current identity. | +| `New-InfisicalProject` | Creates a new Infisical project in the active organization. | +| `Update-InfisicalProject` | Updates the name, description, or auto-capitalization flag on an existing project. | +| `Remove-InfisicalProject` | Deletes an Infisical project. | + +### Environments + +| Cmdlet | Purpose | +| ----------------------------- | -------------------------------------------------------------------------------------------------- | +| `Get-InfisicalEnvironment` | Lists or retrieves Infisical environments defined on a project. | +| `New-InfisicalEnvironment` | Creates a new environment on an Infisical project. | +| `Update-InfisicalEnvironment` | Updates the name, slug, or sort order of an existing Infisical environment. | +| `Remove-InfisicalEnvironment` | Deletes an Infisical environment from a project. | + +### Folders + +| Cmdlet | Purpose | +| ------------------------ | -------------------------------------------------------------------------------------------------- | +| `Get-InfisicalFolder` | Lists or retrieves Infisical folders at a given secret path. | +| `New-InfisicalFolder` | Creates a new Infisical folder under the supplied parent path. | +| `Update-InfisicalFolder` | Renames an existing Infisical folder. | +| `Remove-InfisicalFolder` | Deletes an Infisical folder and all secrets it contains. | + +### Tags + +| Cmdlet | Purpose | +| --------------------- | -------------------------------------------------------------------------------------------------- | +| `Get-InfisicalTag` | Lists or retrieves Infisical tags defined on a project. | +| `New-InfisicalTag` | Creates a new Infisical tag on a project. | +| `Update-InfisicalTag` | Updates the slug, name, or color of an existing Infisical tag. | +| `Remove-InfisicalTag` | Deletes an Infisical tag from a project. | + +### PKI + +| Cmdlet | Purpose | +| ----------------------------------- | -------------------------------------------------------------------------------------------------- | +| `Get-InfisicalCertificateAuthority` | Lists or retrieves Infisical internal Certificate Authorities. | +| `Get-InfisicalPkiSubscriber` | Lists or retrieves Infisical PKI subscribers in a project. | +| `Get-InfisicalCertificate` | Lists or retrieves Infisical certificates in a project, with optional filters and automatic paging. | +| `Search-InfisicalCertificate` | Searches Infisical certificates with advanced filters and automatic paging. | +| `Request-InfisicalCertificate` | Requests a new Infisical certificate (local CSR + sign) or reuses a still-valid existing one. | +| `ConvertTo-InfisicalCertificate` | Materializes an X509Certificate2 from an Infisical certificate record, bundle, or serial number. | +| `Install-InfisicalCertificate` | Installs an Infisical certificate (and optional chain) into a Windows certificate store. | +| `Uninstall-InfisicalCertificate` | Removes a certificate from a Windows certificate store by thumbprint, subject, or pipeline input. | +| `Export-InfisicalCertificate` | Exports an Infisical certificate to disk in PEM, PFX, or CER format. | +| `Get-InfisicalScepMdmProfile` | Projects an Infisical certificate profile into a Windows SCEP MDM profile model. | +| `Export-InfisicalScepMdmProfile` | Writes a SCEP MDM profile to disk as a SyncML payload suitable for MDM delivery. | +| `Write-InfisicalScepMdmProfileToWmi`| Submits a SCEP MDM profile to the local MDM Bridge WMI provider to trigger enrollment. | Use `Get-Help -Full` for parameter details and `Get-Help about_PSInfisicalAPI` for the module overview. @@ -51,7 +116,7 @@ $connection = Connect-Infisical ` -ClientSecret $secureSecret ` -PassThru -Get-InfisicalSecrets -SecretPath '/' +Get-InfisicalSecret -SecretPath '/' Disconnect-Infisical ``` @@ -96,7 +161,7 @@ Sensitive values (`ClientSecret`, `AccessToken`) are read directly into a read-o [Environment]::SetEnvironmentVariable('INFISICAL_CLIENT_SECRET', 'super-secret-value', 'User') Connect-Infisical -Get-InfisicalSecrets +Get-InfisicalSecret ``` ### Mixed example (explicit values override discovery) @@ -119,6 +184,65 @@ pwsh -NoProfile -ExecutionPolicy Bypass -File .\build.ps1 -RunTests The script builds the binary, runs unit tests, publishes binaries into `Module/PSInfisicalAPI/bin/`, regenerates the manifest, and validates that the module imports. +## Extending the module + +### Adding a new API endpoint + +All HTTP routes live in two files under `src/PSInfisicalAPI/Endpoints/`: + +- `InfisicalEndpointNames.cs` declares a `const string` identifier for each endpoint. +- `InfisicalEndpointRegistry.cs` maps each identifier to one or more `InfisicalEndpointDefinition` records grouped by resource (`RegisterAuthentication`, `RegisterSecrets`, `RegisterPki`, etc.). + +To add a route: + +1. Add a constant in `InfisicalEndpointNames.cs` (e.g., `public const string ListPkiSubscribers = "ListPkiSubscribers";`). +2. In the matching `Register` method, call `Add(map, new InfisicalEndpointDefinition { ... })` with `Name`, `Resource`, `Version`, `Method`, `Template`, and the `RequiresAuthorization` / `ContainsSecretMaterialInRequest` / `ContainsSecretMaterialInResponse` flags. Use `{placeholder}` tokens in `Template`; they are substituted from the `pathParameters` dictionary passed by the caller. +3. If the same logical operation has more than one upstream path (legacy + current), register both definitions under the same `Name` — `InvokeWithCandidateFallback` tries each in order until one succeeds. +4. Invoke the endpoint from the appropriate client (`InfisicalPkiClient`, `InfisicalSecretsClient`, etc.) via `_invoker.InvokeWithCandidateFallback(connection, InfisicalEndpointNames.XYZ, "XYZ", pathParameters, query, body)`. + +### Adding a new cmdlet + +Cmdlets live in `src/PSInfisicalAPI/Cmdlets/` and derive from `InfisicalCmdletBase`, which exposes `HttpClient`, `Logger`, `ResolveProjectId`, and `ThrowTerminatingForException`. Follow the consolidated discovery pattern when the cmdlet supports both list and single-record retrieval: + +```csharp +[Cmdlet(VerbsCommon.Get, "InfisicalPkiSubscriber", DefaultParameterSetName = "List")] +[OutputType(typeof(InfisicalPkiSubscriber))] +public sealed class GetInfisicalPkiSubscriberCmdlet : InfisicalCmdletBase +{ + [Parameter(ParameterSetName = "ByName", Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)] + [Alias("SubscriberName", "Slug")] + public string Name { get; set; } + + [Parameter] public string ProjectId { get; set; } + + protected override void ProcessRecord() { /* dispatch on ParameterSetName */ } +} +``` + +After adding (or removing) a cmdlet: + +1. Update `build.ps1` in **two** places — the `CmdletsToExport` array inside the generated manifest block, and the `$expectedCmds` array used by `Test-ModuleImports`. Both must list the same cmdlets; the build fails fast if they drift. +2. Add a `` entry in `Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml`. Each entry must include a non-empty `` synopsis (do not let it start with the cmdlet name — the validation gate rejects PowerShell's auto-generated fallback), a non-empty `` body, and at least one `` with a non-empty `` block. +3. For consolidated `List` / single-record cmdlets, ship **three examples**: two straight-line invocations (one per parameter set) and one `OrderedDictionary` splat. The splat must construct the dictionary with `OrdinalIgnoreCase` so parameter names round-trip case-insensitively: + + ```powershell + $Params = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) + $Params.ProjectId = (Get-InfisicalProject | Select-Object -First 1).Id + $Result = Get-InfisicalPkiSubscriber @Params + ``` +4. Add a `## Unreleased` entry to `CHANGELOG.md` describing the change (mark removals of public cmdlets or parameters as **BREAKING**). +5. Run `./build.ps1 -RunTests`. The script enforces the cmdlet list, runs the xUnit suite, and verifies that every exported cmdlet has a valid synopsis, description, and at least one non-empty example. + +### Committing source and build artifacts in lockstep + +The embedded `BuildCommitHash` in `Module/PSInfisicalAPI/PSInfisicalAPI.psd1` and the bundled DLL is captured from `git rev-parse HEAD` at build time. To keep the embedded hash truthful, commit source and build artifacts as two ordered commits: + +1. Stage and commit your source changes first. Suppose this produces commit `S`. +2. Run `./build.ps1 -RunTests -CommitArtifacts`. The build picks up `S` as `HEAD`, embeds it as `BuildCommitHash`, then stages and commits **only** the build outputs (`Module/PSInfisicalAPI/bin/**`, `Module/PSInfisicalAPI/PSInfisicalAPI.psd1`, and the `CHANGELOG.md` build-stamp insertion). The commit message references `S` so the binary commit always traces back to its source. +3. `git push`. + +`-CommitArtifacts` only touches the three artifact paths above; any other dirty files in your working tree are left alone. Use the older `-CommitOnSuccess` switch only when you intentionally want a single commit covering everything (`git add -A` + `git commit -m "Build "`); the two switches are mutually exclusive. + ## Continuous integration `.gitea/workflows/publish-psgallery.yml` publishes the module to the PowerShell Gallery whenever a pull request is merged into `main`. The workflow expects a repository secret named `PSGALLERY_API_KEY` containing a valid Gallery API key. diff --git a/build.ps1 b/build.ps1 index ae27921..5e70b47 100644 --- a/build.ps1 +++ b/build.ps1 @@ -15,9 +15,15 @@ param( [switch]$CommitOnSuccess, + [switch]$CommitArtifacts, + [switch]$Force ) +if ($CommitOnSuccess.IsPresent -and $CommitArtifacts.IsPresent) { + throw "-CommitOnSuccess and -CommitArtifacts are mutually exclusive." +} + $ErrorActionPreference = 'Stop' Set-StrictMode -Version Latest @@ -100,7 +106,6 @@ function Write-Manifest { CmdletsToExport = @( 'Connect-Infisical', 'Disconnect-Infisical', - 'Get-InfisicalSecrets', 'Get-InfisicalSecret', 'New-InfisicalSecret', 'Update-InfisicalSecret', @@ -108,32 +113,39 @@ function Write-Manifest { 'Copy-InfisicalSecret', 'ConvertTo-InfisicalSecretDictionary', 'Export-InfisicalSecrets', - 'Get-InfisicalProjects', 'Get-InfisicalProject', 'New-InfisicalProject', 'Update-InfisicalProject', 'Remove-InfisicalProject', - 'Get-InfisicalEnvironments', 'Get-InfisicalEnvironment', 'New-InfisicalEnvironment', 'Update-InfisicalEnvironment', 'Remove-InfisicalEnvironment', - 'Get-InfisicalFolders', 'Get-InfisicalFolder', 'New-InfisicalFolder', 'Update-InfisicalFolder', 'Remove-InfisicalFolder', - 'Get-InfisicalTags', 'Get-InfisicalTag', 'New-InfisicalTag', 'Update-InfisicalTag', 'Remove-InfisicalTag', 'Get-InfisicalCertificateAuthority', + 'Get-InfisicalPkiSubscriber', + 'Get-InfisicalCertificateProfile', + 'Get-InfisicalCertificatePolicy', + 'Get-InfisicalCertificate', 'Search-InfisicalCertificate', + 'Request-InfisicalCertificate', 'ConvertTo-InfisicalCertificate', 'Install-InfisicalCertificate', 'Uninstall-InfisicalCertificate', - 'Export-InfisicalCertificate' + 'Export-InfisicalCertificate', + 'Get-InfisicalCertificateApplication', + 'Get-InfisicalCertificateApplicationEnrollment', + 'New-InfisicalScepDynamicChallenge', + 'Get-InfisicalScepMdmProfile', + 'Export-InfisicalScepMdmProfile', + 'Write-InfisicalScepMdmProfileToWmi' ) AliasesToExport = @() VariablesToExport = @() @@ -193,15 +205,50 @@ if (`$null -eq `$manifest) { Import-Module -Name '$($ModuleDirectory.FullName)' -Force -`$cmds = @('Connect-Infisical','Disconnect-Infisical','Get-InfisicalSecrets','Get-InfisicalSecret','New-InfisicalSecret','Update-InfisicalSecret','Remove-InfisicalSecret','ConvertTo-InfisicalSecretDictionary','Export-InfisicalSecrets','Get-InfisicalProjects','Get-InfisicalProject','New-InfisicalProject','Update-InfisicalProject','Remove-InfisicalProject','Get-InfisicalEnvironments','Get-InfisicalEnvironment','New-InfisicalEnvironment','Update-InfisicalEnvironment','Remove-InfisicalEnvironment','Get-InfisicalFolders','Get-InfisicalFolder','New-InfisicalFolder','Update-InfisicalFolder','Remove-InfisicalFolder','Get-InfisicalTags','Get-InfisicalTag','New-InfisicalTag','Update-InfisicalTag','Remove-InfisicalTag','Get-InfisicalCertificateAuthority','Search-InfisicalCertificate','ConvertTo-InfisicalCertificate','Install-InfisicalCertificate','Uninstall-InfisicalCertificate','Export-InfisicalCertificate') -foreach (`$c in `$cmds) { - if (-not (Get-Command -Name `$c -Module PSInfisicalAPI -ErrorAction SilentlyContinue)) { - throw "Cmdlet not found: `$c" +`$cmds = @(Get-Command -Module PSInfisicalAPI -CommandType Cmdlet) +if (`$cmds.Count -eq 0) { + throw "No cmdlets were exported by the PSInfisicalAPI module." +} + +`$expectedCmds = @('Connect-Infisical','Disconnect-Infisical','Get-InfisicalSecret','New-InfisicalSecret','Update-InfisicalSecret','Remove-InfisicalSecret','Copy-InfisicalSecret','ConvertTo-InfisicalSecretDictionary','Export-InfisicalSecrets','Get-InfisicalProject','New-InfisicalProject','Update-InfisicalProject','Remove-InfisicalProject','Get-InfisicalEnvironment','New-InfisicalEnvironment','Update-InfisicalEnvironment','Remove-InfisicalEnvironment','Get-InfisicalFolder','New-InfisicalFolder','Update-InfisicalFolder','Remove-InfisicalFolder','Get-InfisicalTag','New-InfisicalTag','Update-InfisicalTag','Remove-InfisicalTag','Get-InfisicalCertificateAuthority','Get-InfisicalPkiSubscriber','Get-InfisicalCertificateProfile','Get-InfisicalCertificatePolicy','Get-InfisicalCertificate','Search-InfisicalCertificate','Request-InfisicalCertificate','ConvertTo-InfisicalCertificate','Install-InfisicalCertificate','Uninstall-InfisicalCertificate','Export-InfisicalCertificate','Get-InfisicalCertificateApplication','Get-InfisicalCertificateApplicationEnrollment','New-InfisicalScepDynamicChallenge','Get-InfisicalScepMdmProfile','Export-InfisicalScepMdmProfile','Write-InfisicalScepMdmProfileToWmi') +foreach (`$expected in `$expectedCmds) { + if (-not (Get-Command -Name `$expected -Module PSInfisicalAPI -ErrorAction SilentlyContinue)) { + throw "Cmdlet not found: `$expected" + } +} + +foreach (`$cmd in `$cmds) { + `$name = `$cmd.Name + `$help = Get-Help -Name `$name -Full -ErrorAction SilentlyContinue + if (`$null -eq `$help) { + throw "Get-Help returned nothing for cmdlet: `$name" } - `$help = Get-Help -Name `$c -ErrorAction SilentlyContinue - if (`$null -eq `$help) { - throw "Get-Help returned nothing for cmdlet: `$c" + `$synopsis = (`$help.Synopsis | Out-String).Trim() + if ([string]::IsNullOrWhiteSpace(`$synopsis) -or `$synopsis.StartsWith(`$name, [System.StringComparison]::OrdinalIgnoreCase)) { + throw "Get-Help synopsis is missing or auto-generated for cmdlet: `$name" + } + + `$description = (`$help.description | Out-String).Trim() + if ([string]::IsNullOrWhiteSpace(`$description)) { + throw "Get-Help description is empty for cmdlet: `$name" + } + + `$examples = Get-Help -Name `$name -Examples -ErrorAction SilentlyContinue + if (`$null -eq `$examples -or `$null -eq `$examples.examples -or `$null -eq `$examples.examples.example) { + throw "Get-Help -Examples returned no examples for cmdlet: `$name" + } + + `$exampleNodes = @(`$examples.examples.example) + if (`$exampleNodes.Count -lt 1) { + throw "Get-Help -Examples returned zero examples for cmdlet: `$name" + } + + foreach (`$example in `$exampleNodes) { + `$code = (`$example.code | Out-String).Trim() + if ([string]::IsNullOrWhiteSpace(`$code)) { + throw "Example with empty code block found for cmdlet: `$name" + } } } @@ -297,6 +344,35 @@ foreach ($assembly in $desiredAssemblies) { } } +Write-Step "Staging cmdlet help XML next to module binary" +$moduleCultureDirs = Get-ChildItem -LiteralPath $ModuleRoot.FullName -Directory -Force -ErrorAction SilentlyContinue | + Where-Object { $_.Name -match '^[a-z]{2}(-[A-Za-z0-9]+)*$' } +foreach ($cultureDir in $moduleCultureDirs) { + $helpXmlSource = [System.IO.FileInfo][System.IO.Path]::Combine($cultureDir.FullName, 'PSInfisicalAPI.dll-Help.xml') + if (-not $helpXmlSource.Exists) { continue } + + $binCultureDir = [System.IO.DirectoryInfo][System.IO.Path]::Combine($ModuleBinDir.FullName, $cultureDir.Name) + Ensure-Directory -Directory $binCultureDir + Copy-Item -LiteralPath $helpXmlSource.FullName -Destination $binCultureDir.FullName -Force +} + +$primaryHelpXml = [System.IO.FileInfo][System.IO.Path]::Combine($ModuleBinDir.FullName, 'en-US', 'PSInfisicalAPI.dll-Help.xml') +if (-not $primaryHelpXml.Exists) { + throw "Help XML not found at '$($primaryHelpXml.FullName)'. Ensure Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml exists." +} + +try { + [xml]$helpDocument = Get-Content -LiteralPath $primaryHelpXml.FullName -Raw +} catch { + throw "Help XML at '$($primaryHelpXml.FullName)' failed to parse as XML: $_" +} + +$helpCommandCount = @($helpDocument.helpItems.command).Count +if ($helpCommandCount -lt 1) { + throw "Help XML at '$($primaryHelpXml.FullName)' contains no entries." +} +Write-Step "Help XML contains $helpCommandCount cmdlet entries." + $manifestPath = [System.IO.FileInfo][System.IO.Path]::Combine($ModuleRoot.FullName, 'PSInfisicalAPI.psd1') Write-Manifest -Path $manifestPath -ModuleVersion $buildVersion -CommitHash $commitHash @@ -328,4 +404,30 @@ if ($CommitOnSuccess.IsPresent) { if ($LASTEXITCODE -ne 0) { throw "git commit failed." } } +if ($CommitArtifacts.IsPresent) { + Write-Step "Committing build artifacts (embedded BuildCommitHash=$commitHash)" + $artifactPaths = @( + [System.IO.Path]::Combine('Module', 'PSInfisicalAPI', 'bin'), + [System.IO.Path]::Combine('Module', 'PSInfisicalAPI', 'PSInfisicalAPI.psd1'), + 'CHANGELOG.md' + ) + + foreach ($artifactPath in $artifactPaths) { + & git -C $RepositoryRoot.FullName add -- $artifactPath + if ($LASTEXITCODE -ne 0) { throw "git add '$artifactPath' failed." } + } + + $stagedOutput = & git -C $RepositoryRoot.FullName diff --cached --name-only + if ($LASTEXITCODE -ne 0) { throw "git diff --cached failed." } + $stagedFiles = @($stagedOutput | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) + if ($stagedFiles.Count -eq 0) { + Write-Step "No build artifact changes to commit." + } else { + $subject = "Build artifacts for $commitHash" + $body = "Auto-generated by build.ps1 -CommitArtifacts. Build $buildVersion. Module DLL and manifest embed BuildCommitHash=$commitHash, matching the source commit they were produced from." + & git -C $RepositoryRoot.FullName commit -m $subject -m $body + if ($LASTEXITCODE -ne 0) { throw "git commit failed." } + } +} + Write-Step "Build complete." diff --git a/src/PSInfisicalAPI.Tests/CertificateMapperTests.cs b/src/PSInfisicalAPI.Tests/CertificateMapperTests.cs index a9653b3..13d21f4 100644 --- a/src/PSInfisicalAPI.Tests/CertificateMapperTests.cs +++ b/src/PSInfisicalAPI.Tests/CertificateMapperTests.cs @@ -12,6 +12,7 @@ namespace PSInfisicalAPI.Tests private static readonly Type CertDtoType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateResponseDto", true); private static readonly Type CaMapperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCaMapper", true); private static readonly Type CaDtoType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalInternalCaResponseDto", true); + private static readonly Type CaConfigDtoType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalInternalCaConfigurationDto", true); private static readonly Type BundleDtoType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateBundleResponseDto", true); private static InfisicalCertificate InvokeCertMap(object dto, string fallbackProjectId) @@ -90,6 +91,43 @@ namespace PSInfisicalAPI.Tests Assert.Equal("proj-fallback", mapped.ProjectId); } + [Fact] + public void CaMap_Prefers_Configuration_Fields_Over_TopLevel() + { + object cfg = Activator.CreateInstance(CaConfigDtoType); + CaConfigDtoType.GetProperty("FriendlyName").SetValue(cfg, "C=US, CN=GSPA Intermediate"); + CaConfigDtoType.GetProperty("CommonName").SetValue(cfg, "GSPA Intermediate"); + CaConfigDtoType.GetProperty("OrganizationName").SetValue(cfg, "GSPA"); + CaConfigDtoType.GetProperty("OrganizationUnit").SetValue(cfg, "MECM"); + CaConfigDtoType.GetProperty("Country").SetValue(cfg, "US"); + CaConfigDtoType.GetProperty("KeyAlgorithm").SetValue(cfg, "RSA_2048"); + CaConfigDtoType.GetProperty("DistinguishedName").SetValue(cfg, "CN=GSPA Intermediate"); + CaConfigDtoType.GetProperty("SerialNumber").SetValue(cfg, "74a4b62197ad"); + CaConfigDtoType.GetProperty("MaxPathLength").SetValue(cfg, 0); + CaConfigDtoType.GetProperty("Type").SetValue(cfg, "intermediate"); + + object dto = Activator.CreateInstance(CaDtoType); + CaDtoType.GetProperty("Id").SetValue(dto, "ca-9"); + CaDtoType.GetProperty("Name").SetValue(dto, "intermediate-ca"); + CaDtoType.GetProperty("Type").SetValue(dto, "internal"); + CaDtoType.GetProperty("Status").SetValue(dto, "active"); + CaDtoType.GetProperty("Configuration").SetValue(dto, cfg); + + InfisicalCertificateAuthority mapped = InvokeCaMap(dto, "proj-fallback"); + Assert.Equal("ca-9", mapped.Id); + Assert.Equal("intermediate-ca", mapped.Name); + Assert.Equal("internal", mapped.Type); + Assert.Equal("C=US, CN=GSPA Intermediate", mapped.FriendlyName); + Assert.Equal("GSPA Intermediate", mapped.CommonName); + Assert.Equal("GSPA", mapped.OrganizationName); + Assert.Equal("MECM", mapped.OrganizationUnit); + Assert.Equal("US", mapped.Country); + Assert.Equal("RSA_2048", mapped.KeyAlgorithm); + Assert.Equal("CN=GSPA Intermediate", mapped.DistinguishedName); + Assert.Equal("74a4b62197ad", mapped.SerialNumber); + Assert.Equal(0, mapped.MaxPathLength); + } + [Fact] public void BundleMap_Maps_All_Pem_Fields() { diff --git a/src/PSInfisicalAPI.Tests/CmdletBaseInheritanceTests.cs b/src/PSInfisicalAPI.Tests/CmdletBaseInheritanceTests.cs index 6ae42cd..92840be 100644 --- a/src/PSInfisicalAPI.Tests/CmdletBaseInheritanceTests.cs +++ b/src/PSInfisicalAPI.Tests/CmdletBaseInheritanceTests.cs @@ -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); } } } diff --git a/src/PSInfisicalAPI.Tests/CsrAndRequestCmdletTests.cs b/src/PSInfisicalAPI.Tests/CsrAndRequestCmdletTests.cs new file mode 100644 index 0000000..48f32ab --- /dev/null +++ b/src/PSInfisicalAPI.Tests/CsrAndRequestCmdletTests.cs @@ -0,0 +1,478 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Management.Automation; +using System.Reflection; +using PSInfisicalAPI.Endpoints; +using PSInfisicalAPI.Pki; +using Xunit; + +namespace PSInfisicalAPI.Tests +{ + public class CsrAndRequestCmdletTests + { + private static readonly Assembly ModuleAssembly = typeof(PSInfisicalAPI.Connections.InfisicalConnection).Assembly; + + [Fact] + public void CsrBuilder_Rsa2048_Produces_Pem_Csr_And_PrivateKey_With_Subject_And_Sans() + { + InfisicalCsrSubject subject = new InfisicalCsrSubject + { + CommonName = "test.contoso.local", + Organization = "Contoso", + Country = "US" + }; + + InfisicalCsrOptions options = new InfisicalCsrOptions { KeyAlgorithm = InfisicalKeyAlgorithm.Rsa, RsaKeySize = 2048 }; + InfisicalCsrResult result = InfisicalCsrBuilder.Build(subject, new[] { "test.contoso.local", "alt.contoso.local" }, new[] { "10.0.0.5" }, options); + + Assert.NotNull(result); + Assert.Contains("BEGIN CERTIFICATE REQUEST", result.CsrPem); + Assert.Contains("END CERTIFICATE REQUEST", result.CsrPem); + Assert.Contains("BEGIN RSA PRIVATE KEY", result.PrivateKeyPem); + + Org.BouncyCastle.Pkcs.Pkcs10CertificationRequest pkcs10 = ReadCsr(result.CsrPem); + Assert.True(pkcs10.Verify()); + Org.BouncyCastle.Crypto.Parameters.RsaKeyParameters rsa = Assert.IsAssignableFrom(pkcs10.GetPublicKey()); + Assert.Equal(2048, rsa.Modulus.BitLength); + } + + [Theory] + [InlineData(InfisicalEcCurve.P256, "1.2.840.10045.3.1.7")] + [InlineData(InfisicalEcCurve.P384, "1.3.132.0.34")] + public void CsrBuilder_Ecdsa_Produces_Verifiable_Csr(InfisicalEcCurve curve, string expectedCurveOid) + { + InfisicalCsrSubject subject = new InfisicalCsrSubject { CommonName = "ec.contoso.local" }; + InfisicalCsrOptions options = new InfisicalCsrOptions { KeyAlgorithm = InfisicalKeyAlgorithm.Ecdsa, EcCurve = curve }; + InfisicalCsrResult result = InfisicalCsrBuilder.Build(subject, new[] { "ec.contoso.local" }, null, options); + + Assert.Contains("BEGIN CERTIFICATE REQUEST", result.CsrPem); + Assert.True(result.PrivateKeyPem.Contains("BEGIN EC PRIVATE KEY") || result.PrivateKeyPem.Contains("BEGIN PRIVATE KEY")); + + Org.BouncyCastle.Pkcs.Pkcs10CertificationRequest pkcs10 = ReadCsr(result.CsrPem); + Assert.True(pkcs10.Verify()); + Org.BouncyCastle.Crypto.Parameters.ECPublicKeyParameters ec = Assert.IsAssignableFrom(pkcs10.GetPublicKey()); + Assert.Equal(expectedCurveOid, ec.PublicKeyParamSet.Id); + } + + [Fact] + public void CsrBuilder_Ed25519_Produces_Verifiable_Csr() + { + InfisicalCsrSubject subject = new InfisicalCsrSubject { CommonName = "ed.contoso.local" }; + InfisicalCsrOptions options = new InfisicalCsrOptions { KeyAlgorithm = InfisicalKeyAlgorithm.Ed25519 }; + InfisicalCsrResult result = InfisicalCsrBuilder.Build(subject, new[] { "ed.contoso.local" }, null, options); + + Assert.Contains("BEGIN CERTIFICATE REQUEST", result.CsrPem); + Assert.Contains("BEGIN PRIVATE KEY", result.PrivateKeyPem); + + Org.BouncyCastle.Pkcs.Pkcs10CertificationRequest pkcs10 = ReadCsr(result.CsrPem); + Assert.True(pkcs10.Verify()); + Assert.IsAssignableFrom(pkcs10.GetPublicKey()); + } + + [Fact] + public void CsrBuilder_Rsa_Rejects_Invalid_KeySize() + { + InfisicalCsrSubject subject = new InfisicalCsrSubject { CommonName = "test.local" }; + InfisicalCsrOptions options = new InfisicalCsrOptions { KeyAlgorithm = InfisicalKeyAlgorithm.Rsa, RsaKeySize = 1024 }; + Assert.Throws(() => InfisicalCsrBuilder.Build(subject, null, null, options)); + } + + [Fact] + public void CsrBuilder_Throws_When_CommonName_Missing() + { + InfisicalCsrSubject subject = new InfisicalCsrSubject { Organization = "Contoso" }; + Assert.Throws(() => InfisicalCsrBuilder.Build(subject, null, null, new InfisicalCsrOptions())); + } + + private static Org.BouncyCastle.Pkcs.Pkcs10CertificationRequest ReadCsr(string pem) + { + using (System.IO.StringReader reader = new System.IO.StringReader(pem)) + { + Org.BouncyCastle.OpenSsl.PemReader pemReader = new Org.BouncyCastle.OpenSsl.PemReader(reader); + object obj = pemReader.ReadObject(); + return Assert.IsType(obj); + } + } + + [Fact] + public void MergeSubject_Hashtable_Then_Individual_Params_Override() + { + Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true); + MethodInfo merge = helperType.GetMethod("MergeSubject", BindingFlags.Public | BindingFlags.Static); + Assert.NotNull(merge); + + Hashtable subject = new Hashtable { { "CN", "fallback.local" }, { "O", "FallbackOrg" }, { "C", "DE" } }; + object result = merge.Invoke(null, new object[] { subject, "explicit.local", null, null, null, "ExplicitOrg", null, null }); + + PropertyInfo commonNameProp = result.GetType().GetProperty("CommonName"); + PropertyInfo organizationProp = result.GetType().GetProperty("Organization"); + PropertyInfo countryProp = result.GetType().GetProperty("Country"); + + Assert.Equal("explicit.local", commonNameProp.GetValue(result)); + Assert.Equal("ExplicitOrg", organizationProp.GetValue(result)); + Assert.Equal("DE", countryProp.GetValue(result)); + } + + [Fact] + public void SignCertificateBySubscriber_Uses_Pki_Subscribers_Template() + { + IReadOnlyList candidates = InfisicalEndpointRegistry.GetCandidates(InfisicalEndpointNames.SignCertificateBySubscriber); + Assert.Single(candidates); + InfisicalEndpointDefinition only = candidates[0]; + Assert.Equal("/api/v1/pki/subscribers/{subscriberName}/sign-certificate", only.Template); + Assert.Equal("POST", only.Method); + Assert.True(only.RequiresAuthorization); + Assert.True(only.ContainsSecretMaterialInResponse); + } + + [Fact] + public void Candidates_For_SignCertificateByCa_Include_Pki_And_CertManager() + { + IReadOnlyList candidates = InfisicalEndpointRegistry.GetCandidates(InfisicalEndpointNames.SignCertificateByCa); + Assert.Contains(candidates, c => c.Template == "/api/v1/pki/ca/{caId}/sign-certificate"); + Assert.Contains(candidates, c => c.Template == "/api/v1/cert-manager/ca/{caId}/sign-certificate"); + } + + [Fact] + public void RequestInfisicalCertificate_Cmdlet_Has_Both_Parameter_Sets() + { + Type cmdletType = ModuleAssembly.GetType("PSInfisicalAPI.Cmdlets.RequestInfisicalCertificateCmdlet", true); + Assert.True(typeof(PSInfisicalAPI.Cmdlets.InfisicalCmdletBase).IsAssignableFrom(cmdletType)); + + CustomAttributeData cmdletData = null; + foreach (CustomAttributeData candidate in cmdletType.GetCustomAttributesData()) + { + if (candidate.AttributeType == typeof(CmdletAttribute)) { cmdletData = candidate; break; } + } + Assert.NotNull(cmdletData); + Assert.Equal(VerbsLifecycle.Request, cmdletData.ConstructorArguments[0].Value); + Assert.Equal("InfisicalCertificate", cmdletData.ConstructorArguments[1].Value); + + string defaultParameterSetName = null; + foreach (CustomAttributeNamedArgument named in cmdletData.NamedArguments) + { + if (named.MemberName == "DefaultParameterSetName") { defaultParameterSetName = (string)named.TypedValue.Value; break; } + } + Assert.Equal("BySubscriber", defaultParameterSetName); + + Assert.NotNull(cmdletType.GetProperty("PkiSubscriberSlug")); + Assert.NotNull(cmdletType.GetProperty("CertificateAuthorityId")); + Assert.NotNull(cmdletType.GetProperty("CertificateProfileId")); + Assert.NotNull(cmdletType.GetProperty("Subject")); + Assert.NotNull(cmdletType.GetProperty("CommonName")); + Assert.NotNull(cmdletType.GetProperty("DnsName")); + Assert.NotNull(cmdletType.GetProperty("IpAddress")); + Assert.NotNull(cmdletType.GetProperty("Install")); + Assert.NotNull(cmdletType.GetProperty("StoreName")); + Assert.NotNull(cmdletType.GetProperty("StoreLocation")); + Assert.NotNull(cmdletType.GetProperty("AllowRenewal")); + Assert.NotNull(cmdletType.GetProperty("RenewalThresholdDays")); + Assert.NotNull(cmdletType.GetProperty("Force")); + Assert.NotNull(cmdletType.GetProperty("InstallChain")); + + PropertyInfo keyAlgorithmProp = cmdletType.GetProperty("KeyAlgorithm"); + PropertyInfo curveProp = cmdletType.GetProperty("Curve"); + Assert.NotNull(keyAlgorithmProp); + Assert.NotNull(curveProp); + Assert.Equal(typeof(InfisicalKeyAlgorithm), keyAlgorithmProp.PropertyType); + Assert.Equal(typeof(InfisicalEcCurve), curveProp.PropertyType); + + PropertyInfo protectionProp = cmdletType.GetProperty("PrivateKeyProtection"); + Assert.NotNull(protectionProp); + Assert.Equal(typeof(InfisicalPrivateKeyProtection), protectionProp.PropertyType); + Assert.NotNull(cmdletType.GetProperty("PersistKey")); + Assert.NotNull(cmdletType.GetProperty("MachineKey")); + Assert.NotNull(cmdletType.GetProperty("PrivateKeyPath")); + Assert.NotNull(cmdletType.GetProperty("LocalChainOnly")); + + CustomAttributeData outputTypeData = null; + foreach (CustomAttributeData candidate in cmdletType.GetCustomAttributesData()) + { + if (candidate.AttributeType == typeof(OutputTypeAttribute)) { outputTypeData = candidate; break; } + } + Assert.NotNull(outputTypeData); + IList outputTypeArgs = (IList)outputTypeData.ConstructorArguments[0].Value; + Assert.Contains(outputTypeArgs, a => (Type)a.Value == typeof(PSInfisicalAPI.Models.InfisicalCertificateResult)); + } + + [Fact] + public void BuildResult_Splits_Chain_Into_Leaf_Intermediates_And_Root() + { + (string leafPem, _, string leafThumb) = PemCertificateBuilderTests.CreateSelfSignedExposed("BuildResult.Leaf"); + (string intermediatePem, _, string intermediateThumb) = PemCertificateBuilderTests.CreateSelfSignedExposed("BuildResult.Intermediate"); + (string rootPem, _, string rootThumb) = PemCertificateBuilderTests.CreateSelfSignedExposed("BuildResult.Root"); + + PSInfisicalAPI.Models.InfisicalSignedCertificate signed = new PSInfisicalAPI.Models.InfisicalSignedCertificate + { + SerialNumber = "ABC123", + CertificatePem = leafPem, + CertificateChainPem = intermediatePem + rootPem, + IssuingCaCertificatePem = rootPem + }; + + using (System.Security.Cryptography.X509Certificates.X509Certificate2 leaf = new System.Security.Cryptography.X509Certificates.X509Certificate2(System.Text.Encoding.ASCII.GetBytes(leafPem))) + { + Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true); + MethodInfo buildResult = helperType.GetMethod("BuildResult", BindingFlags.Public | BindingFlags.Static); + Assert.NotNull(buildResult); + + PSInfisicalAPI.Models.InfisicalCertificateResult result = (PSInfisicalAPI.Models.InfisicalCertificateResult)buildResult.Invoke(null, new object[] { leaf, signed }); + + Assert.Same(leaf, result.Leaf); + Assert.Equal("ABC123", result.SerialNumber); + Assert.Empty(result.Intermediates); + Assert.NotNull(result.Root); + Assert.Equal(2, result.Chain.Length); + Assert.Same(leaf, result.Chain[0]); + } + } + + [Theory] + [InlineData(InfisicalPrivateKeyProtection.LocalOnly, false, false, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.DefaultKeySet)] + [InlineData(InfisicalPrivateKeyProtection.Exportable, false, false, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.Exportable)] + [InlineData(InfisicalPrivateKeyProtection.NonExportable, false, false, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.DefaultKeySet)] + [InlineData(InfisicalPrivateKeyProtection.LocalOnly, true, false, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.PersistKeySet)] + [InlineData(InfisicalPrivateKeyProtection.LocalOnly, false, true, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.MachineKeySet)] + [InlineData(InfisicalPrivateKeyProtection.Exportable, true, true, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.Exportable | System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.MachineKeySet | System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.PersistKeySet)] + public void ResolveKeyStorageFlags_Maps_Protection_And_Switches(InfisicalPrivateKeyProtection protection, bool persist, bool machine, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags expected) + { + Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true); + MethodInfo method = helperType.GetMethod("ResolveKeyStorageFlags", BindingFlags.Public | BindingFlags.Static); + Assert.NotNull(method); + + System.Security.Cryptography.X509Certificates.X509KeyStorageFlags actual = (System.Security.Cryptography.X509Certificates.X509KeyStorageFlags)method.Invoke(null, new object[] { protection, persist, machine }); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(InfisicalPrivateKeyProtection.LocalOnly, false, false)] + [InlineData(InfisicalPrivateKeyProtection.Exportable, false, false)] + [InlineData(InfisicalPrivateKeyProtection.NonExportable, false, true)] + [InlineData(InfisicalPrivateKeyProtection.Ephemeral, false, true)] + [InlineData(InfisicalPrivateKeyProtection.LocalOnly, true, true)] + [InlineData(InfisicalPrivateKeyProtection.Exportable, true, true)] + public void ShouldScrubPrivateKeyPem_Returns_Expected(InfisicalPrivateKeyProtection protection, bool hasPath, bool expected) + { + Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true); + MethodInfo method = helperType.GetMethod("ShouldScrubPrivateKeyPem", BindingFlags.Public | BindingFlags.Static); + Assert.NotNull(method); + + bool actual = (bool)method.Invoke(null, new object[] { protection, hasPath }); + Assert.Equal(expected, actual); + } + + [Fact] + public void WritePrivateKeyPem_Writes_File_And_Creates_Directory() + { + Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true); + MethodInfo method = helperType.GetMethod("WritePrivateKeyPem", BindingFlags.Public | BindingFlags.Static); + Assert.NotNull(method); + + string tempRoot = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "PSInfisicalAPI_PemWrite_" + Guid.NewGuid().ToString("N")); + string nested = System.IO.Path.Combine(tempRoot, "nested", "key.pem"); + const string pem = "-----BEGIN PRIVATE KEY-----\nMIIBVgIBADANBgkqhkiG9w0BAQEFAA==\n-----END PRIVATE KEY-----\n"; + try + { + method.Invoke(null, new object[] { pem, nested }); + Assert.True(System.IO.File.Exists(nested)); + Assert.Equal(pem, System.IO.File.ReadAllText(nested)); + } + finally + { + if (System.IO.Directory.Exists(tempRoot)) { System.IO.Directory.Delete(tempRoot, true); } + } + } + + [Fact] + public void BuildResultFromExistingLocal_Populates_Leaf_And_Pem_For_Selfsigned() + { + (string leafPem, _, string leafThumb) = PemCertificateBuilderTests.CreateSelfSignedExposed("ReuseLookup.Leaf"); + + using (System.Security.Cryptography.X509Certificates.X509Certificate2 leaf = new System.Security.Cryptography.X509Certificates.X509Certificate2(System.Text.Encoding.ASCII.GetBytes(leafPem))) + { + Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true); + MethodInfo build = helperType.GetMethod("BuildResultFromExistingLocal", BindingFlags.Public | BindingFlags.Static, null, new Type[] { typeof(System.Security.Cryptography.X509Certificates.X509Certificate2) }, null); + Assert.NotNull(build); + + PSInfisicalAPI.Models.InfisicalCertificateResult result = (PSInfisicalAPI.Models.InfisicalCertificateResult)build.Invoke(null, new object[] { leaf }); + + Assert.Same(leaf, result.Leaf); + Assert.Equal(leaf.SerialNumber, result.SerialNumber); + Assert.Contains("BEGIN CERTIFICATE", result.CertificatePem); + Assert.NotNull(result.Chain); + Assert.NotEmpty(result.Chain); + Assert.Same(leaf, result.Chain[0]); + Assert.Empty(result.Intermediates); + } + } + + [Fact] + public void BuildResultFromExistingLocal_Has_Bundle_Fallback_Overload() + { + Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true); + MethodInfo overload = helperType.GetMethod( + "BuildResultFromExistingLocal", + BindingFlags.Public | BindingFlags.Static, + null, + new Type[] { typeof(System.Security.Cryptography.X509Certificates.X509Certificate2), typeof(PSInfisicalAPI.Models.InfisicalCertificateBundle) }, + null); + Assert.NotNull(overload); + Assert.Equal(typeof(PSInfisicalAPI.Models.InfisicalCertificateResult), overload.ReturnType); + } + + [Fact] + public void BuildResultFromExistingLocal_With_Null_Bundle_Matches_LocalOnly_Behavior() + { + (string leafPem, _, _) = PemCertificateBuilderTests.CreateSelfSignedExposed("ReuseLookup.Bundle.Null.Leaf"); + + using (System.Security.Cryptography.X509Certificates.X509Certificate2 leaf = new System.Security.Cryptography.X509Certificates.X509Certificate2(System.Text.Encoding.ASCII.GetBytes(leafPem))) + { + Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true); + MethodInfo overload = helperType.GetMethod( + "BuildResultFromExistingLocal", + BindingFlags.Public | BindingFlags.Static, + null, + new Type[] { typeof(System.Security.Cryptography.X509Certificates.X509Certificate2), typeof(PSInfisicalAPI.Models.InfisicalCertificateBundle) }, + null); + + PSInfisicalAPI.Models.InfisicalCertificateResult result = (PSInfisicalAPI.Models.InfisicalCertificateResult)overload.Invoke(null, new object[] { leaf, null }); + + Assert.Same(leaf, result.Leaf); + Assert.Empty(result.Intermediates); + Assert.Single(result.Chain); + } + } + + [Fact] + public void BuildResultFromExistingLocal_With_Bundle_Merges_Chain_From_Bundle() + { + (string leafPem, _, string leafThumb) = PemCertificateBuilderTests.CreateSelfSignedExposed("ReuseLookup.Bundle.Leaf"); + (string caPem, _, string caThumb) = PemCertificateBuilderTests.CreateSelfSignedExposed("ReuseLookup.Bundle.Ca"); + + using (System.Security.Cryptography.X509Certificates.X509Certificate2 leaf = new System.Security.Cryptography.X509Certificates.X509Certificate2(System.Text.Encoding.ASCII.GetBytes(leafPem))) + { + PSInfisicalAPI.Models.InfisicalCertificateBundle bundle = new PSInfisicalAPI.Models.InfisicalCertificateBundle + { + SerialNumber = leaf.SerialNumber, + CertificatePem = leafPem, + CertificateChainPem = caPem + }; + + Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true); + MethodInfo overload = helperType.GetMethod( + "BuildResultFromExistingLocal", + BindingFlags.Public | BindingFlags.Static, + null, + new Type[] { typeof(System.Security.Cryptography.X509Certificates.X509Certificate2), typeof(PSInfisicalAPI.Models.InfisicalCertificateBundle) }, + null); + + PSInfisicalAPI.Models.InfisicalCertificateResult result = (PSInfisicalAPI.Models.InfisicalCertificateResult)overload.Invoke(null, new object[] { leaf, bundle }); + + Assert.Same(leaf, result.Leaf); + Assert.NotNull(result.Root); + Assert.Equal(caThumb, result.Root.Thumbprint); + Assert.Equal(2, result.Chain.Length); + Assert.Same(leaf, result.Chain[0]); + Assert.Equal(caThumb, result.Chain[1].Thumbprint); + + Assert.NotNull(result.CertificateChainPem); + Assert.Contains("BEGIN CERTIFICATE", result.CertificateChainPem); + } + } + + [Fact] + public void GetChainCertificateTargetStore_SelfSigned_Returns_Root() + { + using (System.Security.Cryptography.RSA rsa = System.Security.Cryptography.RSA.Create(2048)) + { + System.Security.Cryptography.X509Certificates.CertificateRequest request = new System.Security.Cryptography.X509Certificates.CertificateRequest( + "CN=ChainRouting.SelfSigned", + rsa, + System.Security.Cryptography.HashAlgorithmName.SHA256, + System.Security.Cryptography.RSASignaturePadding.Pkcs1); + using (System.Security.Cryptography.X509Certificates.X509Certificate2 selfSigned = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-5), DateTimeOffset.UtcNow.AddDays(1))) + { + Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true); + MethodInfo classify = helperType.GetMethod("GetChainCertificateTargetStore", BindingFlags.Public | BindingFlags.Static); + Assert.NotNull(classify); + + object result = classify.Invoke(null, new object[] { selfSigned }); + Assert.Equal(System.Security.Cryptography.X509Certificates.StoreName.Root, result); + } + } + } + + [Fact] + public void GetChainCertificateTargetStore_NonSelfSigned_Returns_CertificateAuthority() + { + using (System.Security.Cryptography.RSA rootRsa = System.Security.Cryptography.RSA.Create(2048)) + using (System.Security.Cryptography.RSA intermediateRsa = System.Security.Cryptography.RSA.Create(2048)) + { + System.Security.Cryptography.X509Certificates.CertificateRequest rootRequest = new System.Security.Cryptography.X509Certificates.CertificateRequest( + "CN=ChainRouting.Root", + rootRsa, + System.Security.Cryptography.HashAlgorithmName.SHA256, + System.Security.Cryptography.RSASignaturePadding.Pkcs1); + rootRequest.CertificateExtensions.Add(new System.Security.Cryptography.X509Certificates.X509BasicConstraintsExtension(true, false, 0, true)); + + using (System.Security.Cryptography.X509Certificates.X509Certificate2 rootCert = rootRequest.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-5), DateTimeOffset.UtcNow.AddDays(1))) + { + System.Security.Cryptography.X509Certificates.CertificateRequest intermediateRequest = new System.Security.Cryptography.X509Certificates.CertificateRequest( + "CN=ChainRouting.Intermediate", + intermediateRsa, + System.Security.Cryptography.HashAlgorithmName.SHA256, + System.Security.Cryptography.RSASignaturePadding.Pkcs1); + intermediateRequest.CertificateExtensions.Add(new System.Security.Cryptography.X509Certificates.X509BasicConstraintsExtension(true, false, 0, true)); + + byte[] serial = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }; + using (System.Security.Cryptography.X509Certificates.X509Certificate2 intermediate = intermediateRequest.Create(rootCert, DateTimeOffset.UtcNow.AddMinutes(-5), DateTimeOffset.UtcNow.AddDays(1), serial)) + { + Assert.NotEqual(intermediate.Subject, intermediate.Issuer); + + Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true); + MethodInfo classify = helperType.GetMethod("GetChainCertificateTargetStore", BindingFlags.Public | BindingFlags.Static); + + object result = classify.Invoke(null, new object[] { intermediate }); + Assert.Equal(System.Security.Cryptography.X509Certificates.StoreName.CertificateAuthority, result); + } + } + } + } + + [Fact] + public void InstallChain_Has_X509Collection_Overload() + { + Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true); + Type loggerType = ModuleAssembly.GetType("PSInfisicalAPI.Logging.IInfisicalLogger", true); + + MethodInfo overload = helperType.GetMethod( + "InstallChain", + BindingFlags.Public | BindingFlags.Static, + null, + new Type[] + { + typeof(System.Collections.Generic.IEnumerable), + typeof(System.Security.Cryptography.X509Certificates.StoreLocation), + typeof(bool), + loggerType, + typeof(string) + }, + null); + + Assert.NotNull(overload); + Assert.Equal(typeof(void), overload.ReturnType); + } + + [Fact] + public void InstallInfisicalCertificateCmdlet_Uses_ChainRouting_Helper() + { + Type cmdletType = ModuleAssembly.GetType("PSInfisicalAPI.Cmdlets.InstallInfisicalCertificateCmdlet", true); + Assert.NotNull(cmdletType); + + Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true); + MethodInfo classify = helperType.GetMethod("GetChainCertificateTargetStore", BindingFlags.Public | BindingFlags.Static); + Assert.NotNull(classify); + } + } +} diff --git a/src/PSInfisicalAPI.Tests/InfisicalEnvironmentPatternTests.cs b/src/PSInfisicalAPI.Tests/InfisicalEnvironmentPatternTests.cs index 1bedc43..babd0f0 100644 --- a/src/PSInfisicalAPI.Tests/InfisicalEnvironmentPatternTests.cs +++ b/src/PSInfisicalAPI.Tests/InfisicalEnvironmentPatternTests.cs @@ -26,26 +26,6 @@ namespace PSInfisicalAPI.Tests Assert.True(MatchesAny(name, InfisicalEnvironmentResolver.OrganizationIdPatterns), "Expected match for " + name); } - [Theory] - [InlineData("INFISICAL_PROJECT_ID")] - [InlineData("INFISICAL_WORKSPACE_ID")] - [InlineData("CLOUDINIT_INFISICAL_PROJECTID")] - public void ProjectIdPatterns_Match_Expected_Names(string name) - { - Assert.True(MatchesAny(name, InfisicalEnvironmentResolver.ProjectIdPatterns), "Expected match for " + name); - } - - [Theory] - [InlineData("INFISICAL_ENVIRONMENT")] - [InlineData("INFISICAL_ENVIRONMENT_NAME")] - [InlineData("INFISICAL_ENV")] - [InlineData("INFISICAL_ENV_SLUG")] - [InlineData("CLOUDINIT_INFISICAL_ENVIRONMENT")] - public void EnvironmentPatterns_Match_Expected_Names(string name) - { - Assert.True(MatchesAny(name, InfisicalEnvironmentResolver.EnvironmentPatterns), "Expected match for " + name); - } - [Theory] [InlineData("INFISICAL_CLIENT_ID")] [InlineData("INFISICAL_UNIVERSAL_AUTH_CLIENT_ID")] @@ -78,15 +58,6 @@ namespace PSInfisicalAPI.Tests Assert.True(MatchesAny(name, InfisicalEnvironmentResolver.AccessTokenPatterns), "Expected match for " + name); } - [Theory] - [InlineData("INFISICAL_SECRET_PATH")] - [InlineData("INFISICAL_DEFAULT_SECRET_PATH")] - [InlineData("CLOUDINIT_INFISICAL_SECRETPATH")] - public void SecretPathPatterns_Match_Expected_Names(string name) - { - Assert.True(MatchesAny(name, InfisicalEnvironmentResolver.SecretPathPatterns), "Expected match for " + name); - } - [Theory] [InlineData("INFISICAL_SECRET_PATH")] [InlineData("INFISICAL_DEFAULT_SECRET_PATH")] @@ -108,9 +79,6 @@ namespace PSInfisicalAPI.Tests Assert.False(MatchesAny(name, InfisicalEnvironmentResolver.AccessTokenPatterns)); Assert.False(MatchesAny(name, InfisicalEnvironmentResolver.BaseUriPatterns)); Assert.False(MatchesAny(name, InfisicalEnvironmentResolver.OrganizationIdPatterns)); - Assert.False(MatchesAny(name, InfisicalEnvironmentResolver.ProjectIdPatterns)); - Assert.False(MatchesAny(name, InfisicalEnvironmentResolver.EnvironmentPatterns)); - Assert.False(MatchesAny(name, InfisicalEnvironmentResolver.SecretPathPatterns)); Assert.False(MatchesAny(name, InfisicalEnvironmentResolver.ApiVersionPatterns)); } diff --git a/src/PSInfisicalAPI.Tests/PemCertificateBuilderTests.cs b/src/PSInfisicalAPI.Tests/PemCertificateBuilderTests.cs index b147725..17d3d01 100644 --- a/src/PSInfisicalAPI.Tests/PemCertificateBuilderTests.cs +++ b/src/PSInfisicalAPI.Tests/PemCertificateBuilderTests.cs @@ -8,6 +8,11 @@ namespace PSInfisicalAPI.Tests { public class PemCertificateBuilderTests { + public static (string CertPem, string KeyPem, string Thumbprint) CreateSelfSignedExposed(string commonName) + { + return CreateSelfSigned(commonName); + } + private static (string CertPem, string KeyPem, string Thumbprint) CreateSelfSigned(string commonName) { using (RSA rsa = RSA.Create(2048)) diff --git a/src/PSInfisicalAPI.Tests/PkiClientParseTests.cs b/src/PSInfisicalAPI.Tests/PkiClientParseTests.cs new file mode 100644 index 0000000..8ccbf5d --- /dev/null +++ b/src/PSInfisicalAPI.Tests/PkiClientParseTests.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections; +using System.Reflection; +using PSInfisicalAPI.Http; +using PSInfisicalAPI.Logging; +using PSInfisicalAPI.Pki; +using Xunit; + +namespace PSInfisicalAPI.Tests +{ + public class PkiClientParseTests + { + private sealed class NoopHttpClient : IInfisicalHttpClient + { + public InfisicalHttpResponse Send(InfisicalHttpRequest request) { throw new NotImplementedException(); } + } + + private static InfisicalPkiClient CreateClient() + { + return new InfisicalPkiClient(new NoopHttpClient(), NullInfisicalLogger.Instance); + } + + private static object InvokeNonPublic(InfisicalPkiClient client, string methodName, string body) + { + MethodInfo method = typeof(InfisicalPkiClient).GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance); + return method.Invoke(client, new object[] { body }); + } + + [Fact] + public void ParseCaListBody_Reads_Raw_Json_Array() + { + string body = "[{\"id\":\"ca-1\",\"name\":\"intermediate\",\"projectId\":\"p1\",\"configuration\":{\"commonName\":\"Intermediate CA\",\"keyAlgorithm\":\"RSA_2048\"}}]"; + object result = InvokeNonPublic(CreateClient(), "ParseCaListBody", body); + IList list = (IList)result; + Assert.Single(list); + object dto = list[0]; + Assert.Equal("ca-1", dto.GetType().GetProperty("Id").GetValue(dto)); + object cfg = dto.GetType().GetProperty("Configuration").GetValue(dto); + Assert.NotNull(cfg); + Assert.Equal("Intermediate CA", cfg.GetType().GetProperty("CommonName").GetValue(cfg)); + } + + [Fact] + public void ParseCaListBody_Reads_CertificateAuthorities_Wrapper() + { + string body = "{\"certificateAuthorities\":[{\"id\":\"ca-2\",\"name\":\"root\"}]}"; + object result = InvokeNonPublic(CreateClient(), "ParseCaListBody", body); + IList list = (IList)result; + Assert.Single(list); + object dto = list[0]; + Assert.Equal("ca-2", dto.GetType().GetProperty("Id").GetValue(dto)); + } + + [Fact] + public void ParseCaSingleBody_Reads_Raw_Object_With_Configuration() + { + string body = "{\"id\":\"ca-9\",\"name\":\"intermediate-ca\",\"status\":\"active\",\"configuration\":{\"commonName\":\"GSPA Intermediate\",\"organization\":\"GSPA\"}}"; + object result = InvokeNonPublic(CreateClient(), "ParseCaSingleBody", body); + Assert.NotNull(result); + Assert.Equal("ca-9", result.GetType().GetProperty("Id").GetValue(result)); + object cfg = result.GetType().GetProperty("Configuration").GetValue(result); + Assert.NotNull(cfg); + Assert.Equal("GSPA Intermediate", cfg.GetType().GetProperty("CommonName").GetValue(cfg)); + Assert.Equal("GSPA", cfg.GetType().GetProperty("OrganizationName").GetValue(cfg)); + } + + [Fact] + public void ParseCaSingleBody_Reads_CertificateAuthority_Wrapper() + { + string body = "{\"certificateAuthority\":{\"id\":\"ca-7\",\"name\":\"root\"}}"; + object result = InvokeNonPublic(CreateClient(), "ParseCaSingleBody", body); + Assert.NotNull(result); + Assert.Equal("ca-7", result.GetType().GetProperty("Id").GetValue(result)); + } + + [Fact] + public void ParseCertificateSingleBody_Reads_Certificate_Wrapper() + { + string body = "{\"certificate\":{\"id\":\"cert-1\",\"serialNumber\":\"ABCD\",\"commonName\":\"host.example\"}}"; + object result = InvokeNonPublic(CreateClient(), "ParseCertificateSingleBody", body); + Assert.NotNull(result); + Assert.Equal("cert-1", result.GetType().GetProperty("Id").GetValue(result)); + Assert.Equal("ABCD", result.GetType().GetProperty("SerialNumber").GetValue(result)); + } + } +} diff --git a/src/PSInfisicalAPI.Tests/PkiEndpointRegistryTests.cs b/src/PSInfisicalAPI.Tests/PkiEndpointRegistryTests.cs index 70c3ca2..dae7d4b 100644 --- a/src/PSInfisicalAPI.Tests/PkiEndpointRegistryTests.cs +++ b/src/PSInfisicalAPI.Tests/PkiEndpointRegistryTests.cs @@ -1,4 +1,7 @@ +using System; using System.Collections.Generic; +using System.Management.Automation; +using System.Reflection; using PSInfisicalAPI.Endpoints; using Xunit; @@ -6,6 +9,65 @@ namespace PSInfisicalAPI.Tests { public class PkiEndpointRegistryTests { + private static readonly Assembly ModuleAssembly = typeof(PSInfisicalAPI.Connections.InfisicalConnection).Assembly; + + [Fact] + public void GetInfisicalCertificate_Cmdlet_Is_Singular_With_SerialNumber_In_Single_ParameterSet() + { + Type cmdletType = ModuleAssembly.GetType("PSInfisicalAPI.Cmdlets.GetInfisicalCertificateCmdlet", true); + Assert.True(typeof(PSInfisicalAPI.Cmdlets.InfisicalCmdletBase).IsAssignableFrom(cmdletType)); + + CustomAttributeData cmdletData = null; + foreach (CustomAttributeData candidate in cmdletType.GetCustomAttributesData()) + { + if (candidate.AttributeType == typeof(CmdletAttribute)) { cmdletData = candidate; break; } + } + Assert.NotNull(cmdletData); + Assert.Equal(2, cmdletData.ConstructorArguments.Count); + Assert.Equal(VerbsCommon.Get, cmdletData.ConstructorArguments[0].Value); + Assert.Equal("InfisicalCertificate", cmdletData.ConstructorArguments[1].Value); + + string defaultParameterSetName = null; + foreach (CustomAttributeNamedArgument named in cmdletData.NamedArguments) + { + if (named.MemberName == "DefaultParameterSetName") { defaultParameterSetName = (string)named.TypedValue.Value; break; } + } + Assert.Equal("List", defaultParameterSetName); + + PropertyInfo serialProp = cmdletType.GetProperty("SerialNumber"); + Assert.NotNull(serialProp); + + CustomAttributeData parameterAttr = null; + foreach (CustomAttributeData candidate in serialProp.GetCustomAttributesData()) + { + if (candidate.AttributeType == typeof(ParameterAttribute)) { parameterAttr = candidate; break; } + } + Assert.NotNull(parameterAttr); + + bool mandatory = false; + string parameterSetName = null; + foreach (CustomAttributeNamedArgument named in parameterAttr.NamedArguments) + { + if (named.MemberName == "Mandatory") { mandatory = (bool)named.TypedValue.Value; } + else if (named.MemberName == "ParameterSetName") { parameterSetName = (string)named.TypedValue.Value; } + } + Assert.True(mandatory); + Assert.Equal("Single", parameterSetName); + } + + [Fact] + public void GetInfisicalCertificate_Cmdlet_Exposes_List_Filter_Properties() + { + Type cmdletType = ModuleAssembly.GetType("PSInfisicalAPI.Cmdlets.GetInfisicalCertificateCmdlet", true); + Assert.NotNull(cmdletType.GetProperty("CommonName")); + Assert.NotNull(cmdletType.GetProperty("FriendlyName")); + Assert.NotNull(cmdletType.GetProperty("CaId")); + Assert.NotNull(cmdletType.GetProperty("Limit")); + Assert.NotNull(cmdletType.GetProperty("Offset")); + Assert.NotNull(cmdletType.GetProperty("NoAutoPage")); + Assert.NotNull(cmdletType.GetProperty("List")); + } + [Fact] public void Get_ListInternalCertificateAuthorities_Returns_CertManager_Primary() { diff --git a/src/PSInfisicalAPI/Authentication/InfisicalEnvironmentResolver.cs b/src/PSInfisicalAPI/Authentication/InfisicalEnvironmentResolver.cs index dee5c0d..0e091c4 100644 --- a/src/PSInfisicalAPI/Authentication/InfisicalEnvironmentResolver.cs +++ b/src/PSInfisicalAPI/Authentication/InfisicalEnvironmentResolver.cs @@ -33,18 +33,6 @@ namespace PSInfisicalAPI.Authentication new Regex(@".*INFISICAL.*ORG(ANIZATION)?.*ID.*", DefaultRegexOptions) }; - public static readonly Regex[] ProjectIdPatterns = new[] - { - new Regex(@".*INFISICAL.*(PROJECT|WORKSPACE).*ID.*", DefaultRegexOptions) - }; - - public static readonly Regex[] EnvironmentPatterns = new[] - { - new Regex(@".*INFISICAL.*ENV(IRONMENT)?.*NAME.*", DefaultRegexOptions), - new Regex(@".*INFISICAL.*ENV(IRONMENT)?.*SLUG.*", DefaultRegexOptions), - new Regex(@".*INFISICAL.*ENV(IRONMENT)?.*", DefaultRegexOptions) - }; - public static readonly Regex[] ClientIdPatterns = new[] { new Regex(@".*INFISICAL.*CLIENT.*ID.*", DefaultRegexOptions), @@ -64,12 +52,6 @@ namespace PSInfisicalAPI.Authentication new Regex(@".*INFISICAL.*TOKEN.*", DefaultRegexOptions) }; - public static readonly Regex[] SecretPathPatterns = new[] - { - new Regex(@".*INFISICAL.*SECRET.*PATH.*", DefaultRegexOptions), - new Regex(@".*INFISICAL.*DEFAULT.*PATH.*", DefaultRegexOptions) - }; - public static readonly Regex[] ApiVersionPatterns = new[] { new Regex(@".*INFISICAL.*API.*VERSION.*", DefaultRegexOptions) diff --git a/src/PSInfisicalAPI/Cmdlets/ConnectInfisicalCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/ConnectInfisicalCmdlet.cs index 08834df..d199fb9 100644 --- a/src/PSInfisicalAPI/Cmdlets/ConnectInfisicalCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/ConnectInfisicalCmdlet.cs @@ -28,12 +28,6 @@ namespace PSInfisicalAPI.Cmdlets [Parameter] public string OrganizationId { get; set; } - [Parameter] - public string ProjectId { get; set; } - - [Parameter] - public string Environment { get; set; } - [Parameter(ParameterSetName = ParameterSetUniversalAuth)] public string ClientId { get; set; } @@ -62,9 +56,6 @@ namespace PSInfisicalAPI.Cmdlets [Parameter(Mandatory = true, ParameterSetName = ParameterSetLdap)] public SecureString Password { get; set; } - [Parameter] - public string SecretPath { get; set; } = "/"; - [Parameter] public string ApiVersion { get; set; } = "v4"; @@ -185,9 +176,6 @@ namespace PSInfisicalAPI.Cmdlets PinnedApiVersion = apiVersionExplicitlyBound ? ApiVersion : null, AuthType = authType, OrganizationId = OrganizationId, - ProjectId = ProjectId, - Environment = Environment, - DefaultSecretPath = string.IsNullOrEmpty(SecretPath) ? "/" : SecretPath, ConnectedAtUtc = DateTimeOffset.UtcNow, ExpiresAtUtc = authResult.ExpiresAtUtc, IsConnected = true, @@ -215,8 +203,6 @@ namespace PSInfisicalAPI.Cmdlets bool needsScan = BaseUri == null || string.IsNullOrWhiteSpace(OrganizationId) || - string.IsNullOrWhiteSpace(ProjectId) || - string.IsNullOrWhiteSpace(Environment) || (tokenSet && (AccessToken == null || AccessToken.Length == 0)) || (universalSet && string.IsNullOrWhiteSpace(ClientId)) || (universalSet && (ClientSecret == null || ClientSecret.Length == 0)); @@ -242,8 +228,6 @@ namespace PSInfisicalAPI.Cmdlets } OrganizationId = InfisicalEnvironmentResolver.ResolveString("OrganizationId", InfisicalEnvironmentResolver.OrganizationIdPatterns, OrganizationId, Logger); - ProjectId = InfisicalEnvironmentResolver.ResolveString("ProjectId", InfisicalEnvironmentResolver.ProjectIdPatterns, ProjectId, Logger); - Environment = InfisicalEnvironmentResolver.ResolveString("Environment", InfisicalEnvironmentResolver.EnvironmentPatterns, Environment, Logger); if (tokenSet) { @@ -255,15 +239,6 @@ namespace PSInfisicalAPI.Cmdlets ClientSecret = InfisicalEnvironmentResolver.ResolveSecureString("ClientSecret", InfisicalEnvironmentResolver.ClientSecretPatterns, ClientSecret, Logger); } - if (!MyInvocation.BoundParameters.ContainsKey("SecretPath")) - { - string resolvedPath = InfisicalEnvironmentResolver.ResolveString("SecretPath", InfisicalEnvironmentResolver.SecretPathPatterns, null, Logger); - if (!string.IsNullOrWhiteSpace(resolvedPath)) - { - SecretPath = resolvedPath; - } - } - if (!MyInvocation.BoundParameters.ContainsKey("ApiVersion")) { string resolvedVersion = InfisicalEnvironmentResolver.ResolveString("ApiVersion", InfisicalEnvironmentResolver.ApiVersionPatterns, null, Logger); @@ -280,8 +255,6 @@ namespace PSInfisicalAPI.Cmdlets if (BaseUri == null) { missing.Add("BaseUri"); } if (string.IsNullOrWhiteSpace(OrganizationId)) { missing.Add("OrganizationId"); } - if (string.IsNullOrWhiteSpace(ProjectId)) { missing.Add("ProjectId"); } - if (string.IsNullOrWhiteSpace(Environment)) { missing.Add("Environment"); } if (string.Equals(ParameterSetName, ParameterSetToken, StringComparison.Ordinal)) { diff --git a/src/PSInfisicalAPI/Cmdlets/CopyInfisicalSecretCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/CopyInfisicalSecretCmdlet.cs index ce4baea..ea85a76 100644 --- a/src/PSInfisicalAPI/Cmdlets/CopyInfisicalSecretCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/CopyInfisicalSecretCmdlet.cs @@ -18,9 +18,9 @@ namespace PSInfisicalAPI.Cmdlets public string DestinationEnvironment { get; set; } [Parameter] public string DestinationSecretPath { get; set; } - [Parameter] public string SourceEnvironment { get; set; } + [Parameter(Mandatory = true)] public string SourceEnvironment { get; set; } [Parameter] public string SourceSecretPath { get; set; } - [Parameter] public string ProjectId { get; set; } + [Parameter(Mandatory = true)] public string ProjectId { get; set; } [Parameter] public string ApiVersion { get; set; } [Parameter] public SwitchParameter OverwriteExisting { get; set; } [Parameter] public SwitchParameter CopySecretValue { get; set; } @@ -35,9 +35,6 @@ namespace PSInfisicalAPI.Cmdlets if (SecretId == null || SecretId.Length == 0) { return; } InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); - string resolvedProjectId = ResolveProjectId(connection, ProjectId); - string resolvedSourceEnv = ResolveEnvironment(connection, SourceEnvironment); - string resolvedSourcePath = ResolveSecretPath(connection, SourceSecretPath); string resolvedApiVersion = ResolveApiVersion(connection, ApiVersion); string target = string.Concat(SecretId.Length, " secret(s) -> ", DestinationEnvironment); @@ -45,10 +42,10 @@ namespace PSInfisicalAPI.Cmdlets InfisicalDuplicateSecretsRequest request = new InfisicalDuplicateSecretsRequest { - ProjectId = resolvedProjectId, - SourceEnvironment = resolvedSourceEnv, + ProjectId = ProjectId, + SourceEnvironment = SourceEnvironment, DestinationEnvironment = DestinationEnvironment, - SourceSecretPath = resolvedSourcePath, + SourceSecretPath = SourceSecretPath, DestinationSecretPath = DestinationSecretPath, SecretIds = SecretId, ApiVersion = resolvedApiVersion, diff --git a/src/PSInfisicalAPI/Cmdlets/ExportInfisicalScepMdmProfileCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/ExportInfisicalScepMdmProfileCmdlet.cs new file mode 100644 index 0000000..3daef68 --- /dev/null +++ b/src/PSInfisicalAPI/Cmdlets/ExportInfisicalScepMdmProfileCmdlet.cs @@ -0,0 +1,65 @@ +using System; +using System.IO; +using System.Management.Automation; +using System.Text; +using PSInfisicalAPI.Models; + +namespace PSInfisicalAPI.Cmdlets +{ + [Cmdlet(VerbsData.Export, "InfisicalScepMdmProfile", SupportsShouldProcess = true)] + [OutputType(typeof(FileInfo))] + public sealed class ExportInfisicalScepMdmProfileCmdlet : InfisicalCmdletBase + { + private const string Component = "ExportInfisicalScepMdmProfileCmdlet"; + + [Parameter(Mandatory = true, ValueFromPipeline = true, Position = 0)] + [Alias("Profile", "ScepProfile")] + public InfisicalScepMdmProfile InputObject { get; set; } + + [Parameter(Mandatory = true, Position = 1)] + public string Path { get; set; } + + [Parameter] public SwitchParameter Force { get; set; } + [Parameter] public SwitchParameter PassThru { get; set; } + + protected override void ProcessRecord() + { + try + { + if (InputObject == null) { throw new InvalidOperationException("InputObject is required."); } + + string resolvedPath = SessionState.Path.GetUnresolvedProviderPathFromPSPath(Path); + if (File.Exists(resolvedPath) && !Force.IsPresent) + { + Logger.Warning(Component, string.Concat("File '", resolvedPath, "' already exists. Pass -Force to overwrite. Skipping export.")); + return; + } + + if (!ShouldProcess(resolvedPath, "Write SyncML SCEP MDM profile")) + { + return; + } + + string directory = System.IO.Path.GetDirectoryName(resolvedPath); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + Logger.Verbose(Component, string.Concat("Created directory '", directory, "'.")); + } + + string syncMl = InputObject.ToSyncMl(); + File.WriteAllText(resolvedPath, syncMl, new UTF8Encoding(false)); + + Logger.Information(Component, string.Concat("Wrote SCEP MDM profile to '", resolvedPath, "' (UniqueId=", InputObject.UniqueId, ").")); + if (PassThru.IsPresent) + { + WriteObject(new FileInfo(resolvedPath)); + } + } + catch (Exception exception) + { + ThrowTerminatingForException(Component, "ExportScepMdmProfile", exception); + } + } + } +} diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateApplicationCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateApplicationCmdlet.cs new file mode 100644 index 0000000..9fa6433 --- /dev/null +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateApplicationCmdlet.cs @@ -0,0 +1,60 @@ +using System; +using System.Management.Automation; +using PSInfisicalAPI.Connections; +using PSInfisicalAPI.Models; +using PSInfisicalAPI.Pki; + +namespace PSInfisicalAPI.Cmdlets +{ + [Cmdlet(VerbsCommon.Get, "InfisicalCertificateApplication", DefaultParameterSetName = "List")] + [OutputType(typeof(InfisicalCertificateApplication))] + public sealed class GetInfisicalCertificateApplicationCmdlet : InfisicalCmdletBase + { + [Parameter(ParameterSetName = "ById", Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)] + [Alias("ApplicationId")] + public string Id { get; set; } + + [Parameter(ParameterSetName = "ByName", Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)] + [Alias("Name")] + public string ApplicationName { get; set; } + + [Parameter(Mandatory = true)] public string ProjectId { get; set; } + + [Parameter(ParameterSetName = "List")] public int? Limit { get; set; } + + [Parameter(ParameterSetName = "List")] public int? Offset { get; set; } + + protected override void ProcessRecord() + { + try + { + InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); + InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger); + + if (string.Equals(ParameterSetName, "ById", StringComparison.Ordinal)) + { + InfisicalCertificateApplication app = client.GetCertificateApplication(connection, Id, ProjectId); + if (app != null) { WriteObject(app); } + return; + } + + if (string.Equals(ParameterSetName, "ByName", StringComparison.Ordinal)) + { + InfisicalCertificateApplication app = client.GetCertificateApplicationByName(connection, ApplicationName, ProjectId); + if (app != null) { WriteObject(app); } + return; + } + + InfisicalCertificateApplication[] all = client.ListCertificateApplications(connection, ProjectId, Limit, Offset); + foreach (InfisicalCertificateApplication app in all) + { + WriteObject(app); + } + } + catch (Exception exception) + { + ThrowTerminatingForException("GetInfisicalCertificateApplicationCmdlet", "GetCertificateApplication", exception); + } + } + } +} diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateApplicationEnrollmentCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateApplicationEnrollmentCmdlet.cs new file mode 100644 index 0000000..6001970 --- /dev/null +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateApplicationEnrollmentCmdlet.cs @@ -0,0 +1,42 @@ +using System; +using System.Management.Automation; +using PSInfisicalAPI.Connections; +using PSInfisicalAPI.Models; +using PSInfisicalAPI.Pki; + +namespace PSInfisicalAPI.Cmdlets +{ + [Cmdlet(VerbsCommon.Get, "InfisicalCertificateApplicationEnrollment")] + [OutputType(typeof(InfisicalCertificateApplicationEnrollment))] + public sealed class GetInfisicalCertificateApplicationEnrollmentCmdlet : InfisicalCmdletBase + { + [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)] + [Alias("Id")] + public string ApplicationId { get; set; } + + [Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true)] + [Alias("CertificateProfileId")] + public string ProfileId { get; set; } + + [Parameter(Mandatory = true)] public string ProjectId { get; set; } + + protected override void ProcessRecord() + { + try + { + InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); + InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger); + + InfisicalCertificateApplicationEnrollment enrollment = client.GetCertificateApplicationEnrollment(connection, ApplicationId, ProfileId, ProjectId); + if (enrollment != null) + { + WriteObject(enrollment); + } + } + catch (Exception exception) + { + ThrowTerminatingForException("GetInfisicalCertificateApplicationEnrollmentCmdlet", "GetCertificateApplicationEnrollment", exception); + } + } + } +} diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateAuthorityCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateAuthorityCmdlet.cs index d80992e..d712ac4 100644 --- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateAuthorityCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateAuthorityCmdlet.cs @@ -14,19 +14,22 @@ namespace PSInfisicalAPI.Cmdlets [Alias("Id")] public string CaId { get; set; } - [Parameter] public string ProjectId { get; set; } + [Parameter(Mandatory = true)] public string ProjectId { get; set; } + + [Parameter(ParameterSetName = "List")] + [ValidateSet("Internal", "Acme", "Any")] + public string Kind { get; set; } = "Internal"; protected override void ProcessRecord() { try { InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); - string resolvedProjectId = ResolveProjectId(connection, ProjectId); InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger); if (string.Equals(ParameterSetName, "ById", StringComparison.Ordinal)) { - InfisicalCertificateAuthority ca = client.GetInternalCertificateAuthority(connection, CaId, resolvedProjectId); + InfisicalCertificateAuthority ca = client.GetInternalCertificateAuthority(connection, CaId, ProjectId); if (ca != null) { WriteObject(ca); @@ -35,7 +38,20 @@ namespace PSInfisicalAPI.Cmdlets return; } - InfisicalCertificateAuthority[] all = client.ListInternalCertificateAuthorities(connection, resolvedProjectId); + InfisicalCertificateAuthority[] all; + if (string.Equals(Kind, "Internal", StringComparison.OrdinalIgnoreCase)) + { + all = client.ListInternalCertificateAuthorities(connection, ProjectId); + } + else + { + all = client.ListAllCertificateAuthorities(connection, ProjectId); + if (string.Equals(Kind, "Acme", StringComparison.OrdinalIgnoreCase)) + { + all = FilterByType(all, "acme"); + } + } + foreach (InfisicalCertificateAuthority ca in all) { WriteObject(ca); @@ -46,5 +62,20 @@ namespace PSInfisicalAPI.Cmdlets ThrowTerminatingForException("GetInfisicalCertificateAuthorityCmdlet", "GetCertificateAuthority", exception); } } + + private static InfisicalCertificateAuthority[] FilterByType(InfisicalCertificateAuthority[] source, string type) + { + if (source == null || source.Length == 0) { return Array.Empty(); } + System.Collections.Generic.List kept = new System.Collections.Generic.List(); + foreach (InfisicalCertificateAuthority ca in source) + { + if (ca != null && string.Equals(ca.Type, type, StringComparison.OrdinalIgnoreCase)) + { + kept.Add(ca); + } + } + + return kept.ToArray(); + } } } diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateCmdlet.cs new file mode 100644 index 0000000..a9b98d7 --- /dev/null +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateCmdlet.cs @@ -0,0 +1,91 @@ +using System; +using System.Management.Automation; +using PSInfisicalAPI.Connections; +using PSInfisicalAPI.Models; +using PSInfisicalAPI.Pki; + +namespace PSInfisicalAPI.Cmdlets +{ + [Cmdlet(VerbsCommon.Get, "InfisicalCertificate", DefaultParameterSetName = "List")] + [OutputType(typeof(InfisicalCertificate))] + public sealed class GetInfisicalCertificateCmdlet : InfisicalCmdletBase + { + [Parameter(ParameterSetName = "Single", Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)] + [Alias("Id", "Identifier")] + public string SerialNumber { get; set; } + + [Parameter(ParameterSetName = "List")] public SwitchParameter List { get; set; } + [Parameter(ParameterSetName = "List", Mandatory = true)] 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 Status { get; set; } + [Parameter(ParameterSetName = "List")] public string[] CaId { get; set; } + [Parameter(ParameterSetName = "List")] public int? Limit { get; set; } + [Parameter(ParameterSetName = "List")] public int? Offset { get; set; } + [Parameter(ParameterSetName = "List")] public SwitchParameter NoAutoPage { get; set; } + + protected override void ProcessRecord() + { + try + { + InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); + InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger); + + if (string.Equals(ParameterSetName, "Single", StringComparison.Ordinal)) + { + InfisicalCertificate cert = client.RetrieveCertificate(connection, SerialNumber); + if (cert != null) + { + WriteObject(cert); + } + + return; + } + + InfisicalCertificateSearchQuery query = new InfisicalCertificateSearchQuery + { + ProjectId = ProjectId, + CommonName = CommonName, + FriendlyName = FriendlyName, + Status = Status, + CaIds = CaId, + Limit = Limit ?? 100, + Offset = Offset ?? 0 + }; + + int requestedLimit = query.Limit ?? 100; + int emitted = 0; + while (true) + { + InfisicalCertificateSearchResult page = client.SearchCertificates(connection, query); + if (page == null || page.Certificates == null || page.Certificates.Length == 0) + { + break; + } + + foreach (InfisicalCertificate cert in page.Certificates) + { + WriteObject(cert); + emitted++; + } + + if (NoAutoPage.IsPresent || page.Certificates.Length < requestedLimit) + { + break; + } + + if (page.TotalCount > 0 && emitted >= page.TotalCount) + { + break; + } + + query.Offset = (query.Offset ?? 0) + page.Certificates.Length; + } + } + catch (Exception exception) + { + ThrowTerminatingForException("GetInfisicalCertificateCmdlet", "GetCertificate", exception); + } + } + } +} diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificatePolicyCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificatePolicyCmdlet.cs new file mode 100644 index 0000000..0472507 --- /dev/null +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificatePolicyCmdlet.cs @@ -0,0 +1,53 @@ +using System; +using System.Management.Automation; +using PSInfisicalAPI.Connections; +using PSInfisicalAPI.Models; +using PSInfisicalAPI.Pki; + +namespace PSInfisicalAPI.Cmdlets +{ + [Cmdlet(VerbsCommon.Get, "InfisicalCertificatePolicy", DefaultParameterSetName = "List")] + [OutputType(typeof(InfisicalCertificatePolicy))] + public sealed class GetInfisicalCertificatePolicyCmdlet : InfisicalCmdletBase + { + [Parameter(ParameterSetName = "ById", Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)] + [Alias("Id", "CertificatePolicyId")] + public string PolicyId { get; set; } + + [Parameter(Mandatory = true)] public string ProjectId { get; set; } + + [Parameter(ParameterSetName = "List")] public int? Limit { get; set; } + + [Parameter(ParameterSetName = "List")] public int? Offset { get; set; } + + protected override void ProcessRecord() + { + try + { + InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); + InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger); + + if (string.Equals(ParameterSetName, "ById", StringComparison.Ordinal)) + { + InfisicalCertificatePolicy policy = client.GetCertificatePolicy(connection, PolicyId, ProjectId); + if (policy != null) + { + WriteObject(policy); + } + + return; + } + + InfisicalCertificatePolicy[] all = client.ListCertificatePolicies(connection, ProjectId, Limit, Offset); + foreach (InfisicalCertificatePolicy policy in all) + { + WriteObject(policy); + } + } + catch (Exception exception) + { + ThrowTerminatingForException("GetInfisicalCertificatePolicyCmdlet", "GetCertificatePolicy", exception); + } + } + } +} diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateProfileCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateProfileCmdlet.cs new file mode 100644 index 0000000..02cf1ae --- /dev/null +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateProfileCmdlet.cs @@ -0,0 +1,56 @@ +using System; +using System.Management.Automation; +using PSInfisicalAPI.Connections; +using PSInfisicalAPI.Models; +using PSInfisicalAPI.Pki; + +namespace PSInfisicalAPI.Cmdlets +{ + [Cmdlet(VerbsCommon.Get, "InfisicalCertificateProfile", DefaultParameterSetName = "List")] + [OutputType(typeof(InfisicalCertificateProfile))] + public sealed class GetInfisicalCertificateProfileCmdlet : InfisicalCmdletBase + { + [Parameter(ParameterSetName = "ById", Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)] + [Alias("Id", "CertificateProfileId")] + public string ProfileId { get; set; } + + [Parameter(Mandatory = true)] public string ProjectId { get; set; } + + [Parameter(ParameterSetName = "List")] public int? Limit { get; set; } + + [Parameter(ParameterSetName = "List")] public int? Offset { get; set; } + + [Parameter(ParameterSetName = "List")] public SwitchParameter IncludeConfigs { get; set; } + + protected override void ProcessRecord() + { + try + { + InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); + InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger); + + if (string.Equals(ParameterSetName, "ById", StringComparison.Ordinal)) + { + InfisicalCertificateProfile profile = client.GetCertificateProfile(connection, ProfileId, ProjectId); + if (profile != null) + { + WriteObject(profile); + } + + return; + } + + bool? includeConfigs = MyInvocation.BoundParameters.ContainsKey("IncludeConfigs") ? (bool?)IncludeConfigs.IsPresent : null; + InfisicalCertificateProfile[] all = client.ListCertificateProfiles(connection, ProjectId, Limit, Offset, includeConfigs); + foreach (InfisicalCertificateProfile profile in all) + { + WriteObject(profile); + } + } + catch (Exception exception) + { + ThrowTerminatingForException("GetInfisicalCertificateProfileCmdlet", "GetCertificateProfile", exception); + } + } + } +} diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalEnvironmentCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalEnvironmentCmdlet.cs index 728ec32..c6b4013 100644 --- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalEnvironmentCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalEnvironmentCmdlet.cs @@ -6,32 +6,45 @@ using PSInfisicalAPI.Models; namespace PSInfisicalAPI.Cmdlets { - [Cmdlet(VerbsCommon.Get, "InfisicalEnvironment")] + [Cmdlet(VerbsCommon.Get, "InfisicalEnvironment", DefaultParameterSetName = "List")] [OutputType(typeof(InfisicalEnvironment))] public sealed class GetInfisicalEnvironmentCmdlet : InfisicalCmdletBase { - [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)] + [Parameter(ParameterSetName = "Single", Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)] [Alias("Slug", "Id", "Environment")] public string EnvironmentSlugOrId { get; set; } - [Parameter] public string ProjectId { get; set; } + [Parameter(Mandatory = true)] public string ProjectId { get; set; } + + [Parameter(ParameterSetName = "List")] public SwitchParameter List { get; set; } protected override void ProcessRecord() { try { InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); - string resolvedProjectId = ResolveProjectId(connection, ProjectId); InfisicalEnvironmentClient client = new InfisicalEnvironmentClient(HttpClient, Logger); - InfisicalEnvironment env = client.Retrieve(connection, resolvedProjectId, EnvironmentSlugOrId); - if (env != null) + + if (string.Equals(ParameterSetName, "Single", StringComparison.Ordinal)) + { + InfisicalEnvironment env = client.Retrieve(connection, ProjectId, EnvironmentSlugOrId); + if (env != null) + { + WriteObject(env); + } + + return; + } + + InfisicalEnvironment[] envs = client.List(connection, ProjectId); + foreach (InfisicalEnvironment env in envs) { WriteObject(env); } } catch (Exception exception) { - ThrowTerminatingForException("GetInfisicalEnvironmentCmdlet", "RetrieveEnvironment", exception); + ThrowTerminatingForException("GetInfisicalEnvironmentCmdlet", "GetEnvironment", exception); } } } diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalEnvironmentsCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalEnvironmentsCmdlet.cs deleted file mode 100644 index 2128cda..0000000 --- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalEnvironmentsCmdlet.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Management.Automation; -using PSInfisicalAPI.Connections; -using PSInfisicalAPI.Environments; -using PSInfisicalAPI.Models; - -namespace PSInfisicalAPI.Cmdlets -{ - [Cmdlet(VerbsCommon.Get, "InfisicalEnvironments")] - [OutputType(typeof(InfisicalEnvironment))] - public sealed class GetInfisicalEnvironmentsCmdlet : InfisicalCmdletBase - { - [Parameter] public string ProjectId { get; set; } - - protected override void ProcessRecord() - { - try - { - InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); - string resolvedProjectId = ResolveProjectId(connection, ProjectId); - InfisicalEnvironmentClient client = new InfisicalEnvironmentClient(HttpClient, Logger); - InfisicalEnvironment[] envs = client.List(connection, resolvedProjectId); - foreach (InfisicalEnvironment env in envs) - { - WriteObject(env); - } - } - catch (Exception exception) - { - ThrowTerminatingForException("GetInfisicalEnvironmentsCmdlet", "ListEnvironments", exception); - } - } - } -} diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalFolderCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalFolderCmdlet.cs index 22ff5ee..2e877c6 100644 --- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalFolderCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalFolderCmdlet.cs @@ -6,36 +6,47 @@ using PSInfisicalAPI.Models; namespace PSInfisicalAPI.Cmdlets { - [Cmdlet(VerbsCommon.Get, "InfisicalFolder")] + [Cmdlet(VerbsCommon.Get, "InfisicalFolder", DefaultParameterSetName = "List")] [OutputType(typeof(InfisicalFolder))] public sealed class GetInfisicalFolderCmdlet : InfisicalCmdletBase { - [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)] + [Parameter(ParameterSetName = "Single", Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)] [Alias("Name", "Id")] public string FolderNameOrId { get; set; } - [Parameter] public string ProjectId { get; set; } - [Parameter] public string Environment { get; set; } + [Parameter(Mandatory = true)] public string ProjectId { get; set; } + [Parameter(Mandatory = true)] public string Environment { get; set; } [Parameter] public string Path { get; set; } + [Parameter(ParameterSetName = "List")] public SwitchParameter List { get; set; } + protected override void ProcessRecord() { try { InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); - string resolvedProjectId = ResolveProjectId(connection, ProjectId); - string resolvedEnvironment = ResolveEnvironment(connection, Environment); - string resolvedPath = ResolveSecretPath(connection, Path); InfisicalFolderClient client = new InfisicalFolderClient(HttpClient, Logger); - InfisicalFolder folder = client.Retrieve(connection, resolvedProjectId, resolvedEnvironment, resolvedPath, FolderNameOrId); - if (folder != null) + + if (string.Equals(ParameterSetName, "Single", StringComparison.Ordinal)) + { + InfisicalFolder folder = client.Retrieve(connection, ProjectId, Environment, Path, FolderNameOrId); + if (folder != null) + { + WriteObject(folder); + } + + return; + } + + InfisicalFolder[] folders = client.List(connection, ProjectId, Environment, Path); + foreach (InfisicalFolder folder in folders) { WriteObject(folder); } } catch (Exception exception) { - ThrowTerminatingForException("GetInfisicalFolderCmdlet", "RetrieveFolder", exception); + ThrowTerminatingForException("GetInfisicalFolderCmdlet", "GetFolder", exception); } } } diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalFoldersCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalFoldersCmdlet.cs deleted file mode 100644 index 4c3b2a6..0000000 --- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalFoldersCmdlet.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Management.Automation; -using PSInfisicalAPI.Connections; -using PSInfisicalAPI.Folders; -using PSInfisicalAPI.Models; - -namespace PSInfisicalAPI.Cmdlets -{ - [Cmdlet(VerbsCommon.Get, "InfisicalFolders")] - [OutputType(typeof(InfisicalFolder))] - public sealed class GetInfisicalFoldersCmdlet : InfisicalCmdletBase - { - [Parameter] public string ProjectId { get; set; } - [Parameter] public string Environment { get; set; } - [Parameter] public string Path { get; set; } - - protected override void ProcessRecord() - { - try - { - InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); - string resolvedProjectId = ResolveProjectId(connection, ProjectId); - string resolvedEnvironment = ResolveEnvironment(connection, Environment); - string resolvedPath = ResolveSecretPath(connection, Path); - InfisicalFolderClient client = new InfisicalFolderClient(HttpClient, Logger); - InfisicalFolder[] folders = client.List(connection, resolvedProjectId, resolvedEnvironment, resolvedPath); - foreach (InfisicalFolder folder in folders) - { - WriteObject(folder); - } - } - catch (Exception exception) - { - ThrowTerminatingForException("GetInfisicalFoldersCmdlet", "ListFolders", exception); - } - } - } -} diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalPkiSubscriberCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalPkiSubscriberCmdlet.cs new file mode 100644 index 0000000..5cfebda --- /dev/null +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalPkiSubscriberCmdlet.cs @@ -0,0 +1,49 @@ +using System; +using System.Management.Automation; +using PSInfisicalAPI.Connections; +using PSInfisicalAPI.Models; +using PSInfisicalAPI.Pki; + +namespace PSInfisicalAPI.Cmdlets +{ + [Cmdlet(VerbsCommon.Get, "InfisicalPkiSubscriber", DefaultParameterSetName = "List")] + [OutputType(typeof(InfisicalPkiSubscriber))] + public sealed class GetInfisicalPkiSubscriberCmdlet : InfisicalCmdletBase + { + [Parameter(ParameterSetName = "ByName", Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)] + [Alias("SubscriberName", "Slug")] + public string Name { get; set; } + + [Parameter(Mandatory = true)] public string ProjectId { get; set; } + + protected override void ProcessRecord() + { + try + { + InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); + InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger); + + if (string.Equals(ParameterSetName, "ByName", StringComparison.Ordinal)) + { + InfisicalPkiSubscriber subscriber = client.GetPkiSubscriber(connection, Name, ProjectId); + if (subscriber != null) + { + WriteObject(subscriber); + } + + return; + } + + InfisicalPkiSubscriber[] all = client.ListPkiSubscribers(connection, ProjectId); + foreach (InfisicalPkiSubscriber subscriber in all) + { + WriteObject(subscriber); + } + } + catch (Exception exception) + { + ThrowTerminatingForException("GetInfisicalPkiSubscriberCmdlet", "GetPkiSubscriber", exception); + } + } + } +} diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalProjectCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalProjectCmdlet.cs index 93ec71e..4ab1598 100644 --- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalProjectCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalProjectCmdlet.cs @@ -6,30 +6,49 @@ using PSInfisicalAPI.Projects; namespace PSInfisicalAPI.Cmdlets { - [Cmdlet(VerbsCommon.Get, "InfisicalProject")] + [Cmdlet(VerbsCommon.Get, "InfisicalProject", DefaultParameterSetName = "List")] [OutputType(typeof(InfisicalProject))] public sealed class GetInfisicalProjectCmdlet : InfisicalCmdletBase { - [Parameter(ValueFromPipelineByPropertyName = true, Position = 0)] + [Parameter(ParameterSetName = "Single", Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)] [Alias("Id")] public string ProjectId { get; set; } + [Parameter(ParameterSetName = "List")] public SwitchParameter List { get; set; } + + [Parameter(ParameterSetName = "List")] + [ValidateSet("secret-manager", "cert-manager", "kms", "ssh", "secret-scanning", "pam", "ai")] + public string Type { get; set; } + + [Parameter(ParameterSetName = "List")] public SwitchParameter IncludeRoles { get; set; } + protected override void ProcessRecord() { try { InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); - string resolvedProjectId = ResolveProjectId(connection, ProjectId); InfisicalProjectClient client = new InfisicalProjectClient(HttpClient, Logger); - InfisicalProject project = client.Retrieve(connection, resolvedProjectId); - if (project != null) + + if (string.Equals(ParameterSetName, "Single", StringComparison.Ordinal)) + { + InfisicalProject project = client.Retrieve(connection, ProjectId); + if (project != null) + { + WriteObject(project); + } + + return; + } + + InfisicalProject[] projects = client.List(connection, Type, IncludeRoles.IsPresent); + foreach (InfisicalProject project in projects) { WriteObject(project); } } catch (Exception exception) { - ThrowTerminatingForException("GetInfisicalProjectCmdlet", "RetrieveProject", exception); + ThrowTerminatingForException("GetInfisicalProjectCmdlet", "GetProject", exception); } } } diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalProjectsCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalProjectsCmdlet.cs deleted file mode 100644 index fa6150a..0000000 --- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalProjectsCmdlet.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Management.Automation; -using PSInfisicalAPI.Connections; -using PSInfisicalAPI.Models; -using PSInfisicalAPI.Projects; - -namespace PSInfisicalAPI.Cmdlets -{ - [Cmdlet(VerbsCommon.Get, "InfisicalProjects")] - [OutputType(typeof(InfisicalProject))] - public sealed class GetInfisicalProjectsCmdlet : InfisicalCmdletBase - { - protected override void ProcessRecord() - { - try - { - InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); - InfisicalProjectClient client = new InfisicalProjectClient(HttpClient, Logger); - InfisicalProject[] projects = client.List(connection); - - foreach (InfisicalProject project in projects) - { - WriteObject(project); - } - } - catch (Exception exception) - { - ThrowTerminatingForException("GetInfisicalProjectsCmdlet", "ListProjects", exception); - } - } - } -} diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalScepMdmProfileCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalScepMdmProfileCmdlet.cs new file mode 100644 index 0000000..59336e0 --- /dev/null +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalScepMdmProfileCmdlet.cs @@ -0,0 +1,241 @@ +using System; +using System.Globalization; +using System.Management.Automation; +using System.Net; +using System.Runtime.InteropServices; +using System.Security; +using PSInfisicalAPI.Connections; +using PSInfisicalAPI.Models; +using PSInfisicalAPI.Pki; + +namespace PSInfisicalAPI.Cmdlets +{ + [Cmdlet(VerbsCommon.Get, "InfisicalScepMdmProfile", DefaultParameterSetName = "FromEnrollment")] + [OutputType(typeof(InfisicalScepMdmProfile))] + public sealed class GetInfisicalScepMdmProfileCmdlet : InfisicalCmdletBase + { + private const string Component = "GetInfisicalScepMdmProfileCmdlet"; + + [Parameter(ParameterSetName = "FromEnrollment", Mandatory = true, ValueFromPipeline = true, Position = 0)] + [Alias("Enrollment")] + public InfisicalCertificateApplicationEnrollment EnrollmentObject { get; set; } + + [Parameter(ParameterSetName = "FromProfile", Mandatory = true, ValueFromPipeline = true, Position = 0)] + [Alias("Profile", "CertificateProfile")] + public InfisicalCertificateProfile InputObject { get; set; } + + [Parameter(ParameterSetName = "FromProfile", Mandatory = true)] + [Alias("AppId")] + public string ApplicationId { get; set; } + + [Parameter(ParameterSetName = "FromEnrollment")] + [Parameter(ParameterSetName = "FromProfile")] + [Parameter(ParameterSetName = "Manual", Mandatory = true)] + public SecureString Challenge { get; set; } + + [Parameter(ParameterSetName = "Manual", Mandatory = true)] + [Parameter(ParameterSetName = "FromProfile")] + [Parameter(ParameterSetName = "FromEnrollment")] + public string ServerUrl { get; set; } + + [Parameter] public string UniqueId { get; set; } + + [Parameter] + [ValidateSet("Device", "User")] + public string Scope { get; set; } = "Device"; + + [Parameter] public string SubjectName { get; set; } + [Parameter] public string SubjectAlternativeNames { get; set; } + [Parameter] public string EkuMapping { get; set; } + [Parameter] public int? KeyUsage { get; set; } + + [Parameter] public int? KeyLength { get; set; } + [Parameter] public string KeyAlgorithm { get; set; } + [Parameter] public string HashAlgorithm { get; set; } + [Parameter] public int? KeyProtection { get; set; } + [Parameter] public string ContainerName { get; set; } + + [Parameter] public string ValidPeriod { get; set; } + [Parameter] public int? ValidPeriodUnits { get; set; } + [Parameter] public int? RetryCount { get; set; } + [Parameter] public int? RetryDelay { get; set; } + + [Parameter] public string TemplateName { get; set; } + [Parameter] public string CAThumbprint { get; set; } + [Parameter] public string CustomTextToShowInPrompt { get; set; } + + protected override void ProcessRecord() + { + try + { + InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); + + if (string.Equals(ParameterSetName, "FromEnrollment", StringComparison.Ordinal)) + { + WriteObject(BuildFromEnrollment(connection)); + return; + } + + if (string.Equals(ParameterSetName, "FromProfile", StringComparison.Ordinal)) + { + WriteObject(BuildFromProfile(connection)); + return; + } + + WriteObject(BuildManual(connection)); + } + catch (Exception exception) + { + ThrowTerminatingForException(Component, "GetScepMdmProfile", exception); + } + } + + private InfisicalScepMdmProfile BuildFromEnrollment(InfisicalConnection connection) + { + if (EnrollmentObject == null) { throw new InvalidOperationException("EnrollmentObject is required."); } + if (string.IsNullOrEmpty(EnrollmentObject.ApplicationId)) { throw new InvalidOperationException("EnrollmentObject.ApplicationId is required."); } + if (string.IsNullOrEmpty(EnrollmentObject.ProfileId)) { throw new InvalidOperationException("EnrollmentObject.ProfileId is required."); } + + InfisicalCertificateApplicationScepEnrollment scep = EnrollmentObject.Scep; + if (scep == null) { throw new InvalidOperationException("Enrollment does not have SCEP configured."); } + + string resolvedServerUrl = FirstNonEmpty(ServerUrl, scep.ScepEndpointUrl, BuildDefaultServerUrl(connection, EnrollmentObject.ApplicationId, EnrollmentObject.ProfileId)); + string resolvedUniqueId = !string.IsNullOrEmpty(UniqueId) ? UniqueId : SanitizeForCspId(EnrollmentObject.ProfileId); + string resolvedThumbprint = !string.IsNullOrEmpty(CAThumbprint) ? CAThumbprint : scep.RaCertificateThumbprint; + string resolvedChallenge = ResolveChallengeFromEnrollment(connection, scep); + + InfisicalScepMdmProfile result = NewProfileShell(resolvedUniqueId, resolvedServerUrl, resolvedChallenge, resolvedThumbprint, null, null); + result.SourceProfileId = EnrollmentObject.ProfileId; + Logger.Verbose(Component, string.Concat("Built SCEP MDM profile from enrollment for application '", EnrollmentObject.ApplicationId, "' / profile '", EnrollmentObject.ProfileId, "' targeting ", result.ServerUrl, " (UniqueId=", result.UniqueId, ", Scope=", result.Scope, ", ChallengeType=", scep.ChallengeType ?? "", ").")); + return result; + } + + private InfisicalScepMdmProfile BuildFromProfile(InfisicalConnection connection) + { + if (InputObject == null) { throw new InvalidOperationException("InputObject is required."); } + if (string.IsNullOrEmpty(InputObject.Id)) { throw new InvalidOperationException("InputObject.Id is required."); } + if (string.IsNullOrEmpty(ApplicationId)) { throw new InvalidOperationException("ApplicationId is required when binding by certificate profile."); } + if (Challenge == null) { throw new InvalidOperationException("Challenge is required when building from a certificate profile."); } + + string resolvedServerUrl = !string.IsNullOrEmpty(ServerUrl) ? ServerUrl : BuildDefaultServerUrl(connection, ApplicationId, InputObject.Id); + string resolvedUniqueId = !string.IsNullOrEmpty(UniqueId) ? UniqueId : SanitizeForCspId(!string.IsNullOrEmpty(InputObject.Slug) ? InputObject.Slug : InputObject.Id); + InfisicalCertificateProfileDefaults defaults = InputObject.Defaults; + string resolvedKeyAlgorithm = !string.IsNullOrEmpty(KeyAlgorithm) ? KeyAlgorithm : MapKeyAlgorithm(defaults != null ? defaults.KeyAlgorithm : null); + string resolvedEku = !string.IsNullOrEmpty(EkuMapping) ? EkuMapping : JoinEkuOids(defaults != null ? defaults.ExtendedKeyUsages : null); + + InfisicalScepMdmProfile result = NewProfileShell(resolvedUniqueId, resolvedServerUrl, SecureStringToPlainText(Challenge), CAThumbprint, resolvedKeyAlgorithm, resolvedEku); + result.SourceProfileId = InputObject.Id; + result.SourceProfileSlug = InputObject.Slug; + Logger.Verbose(Component, string.Concat("Built SCEP MDM profile for source profile '", InputObject.Slug ?? InputObject.Id, "' targeting ", result.ServerUrl, " (UniqueId=", result.UniqueId, ", Scope=", result.Scope, ").")); + return result; + } + + private InfisicalScepMdmProfile BuildManual(InfisicalConnection connection) + { + if (string.IsNullOrEmpty(UniqueId)) { throw new InvalidOperationException("UniqueId is required in Manual mode."); } + string resolvedChallenge = SecureStringToPlainText(Challenge); + InfisicalScepMdmProfile result = NewProfileShell(UniqueId, ServerUrl, resolvedChallenge, CAThumbprint, KeyAlgorithm, EkuMapping); + Logger.Verbose(Component, string.Concat("Built SCEP MDM profile in Manual mode targeting ", result.ServerUrl, " (UniqueId=", result.UniqueId, ", Scope=", result.Scope, ").")); + return result; + } + + private InfisicalScepMdmProfile NewProfileShell(string uniqueId, string serverUrl, string challenge, string thumbprint, string keyAlgorithm, string ekuMapping) + { + return new InfisicalScepMdmProfile + { + UniqueId = uniqueId, + Scope = Scope, + ServerUrl = serverUrl, + Challenge = challenge, + SubjectName = SubjectName, + SubjectAlternativeNames = SubjectAlternativeNames, + EkuMapping = ekuMapping, + KeyUsage = KeyUsage, + KeyLength = KeyLength, + KeyAlgorithm = keyAlgorithm, + HashAlgorithm = HashAlgorithm, + KeyProtection = KeyProtection, + ContainerName = ContainerName, + ValidPeriod = ValidPeriod, + ValidPeriodUnits = ValidPeriodUnits, + RetryCount = RetryCount, + RetryDelay = RetryDelay, + TemplateName = TemplateName, + CAThumbprint = thumbprint, + CustomTextToShowInPrompt = CustomTextToShowInPrompt + }; + } + + private string ResolveChallengeFromEnrollment(InfisicalConnection connection, InfisicalCertificateApplicationScepEnrollment scep) + { + if (Challenge != null) { return SecureStringToPlainText(Challenge); } + + string challengeType = scep.ChallengeType ?? string.Empty; + if (string.Equals(challengeType, "dynamic", StringComparison.OrdinalIgnoreCase)) + { + InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger); + Logger.Verbose(Component, "Minting SCEP dynamic challenge for enrollment."); + return client.GenerateScepDynamicChallenge(connection, EnrollmentObject.ApplicationId, EnrollmentObject.ProfileId); + } + + throw new InvalidOperationException(string.Concat("Enrollment uses challengeType '", challengeType, "'. Supply -Challenge with the configured static challenge password.")); + } + + private static string BuildDefaultServerUrl(InfisicalConnection connection, string applicationId, string profileId) + { + if (connection == null || connection.BaseUri == null) { throw new InvalidOperationException("Active Infisical connection is required to derive ServerUrl."); } + string baseUrl = connection.BaseUri.GetLeftPart(UriPartial.Authority); + return string.Concat(baseUrl, "/scep/applications/", applicationId, "/profiles/", profileId, "/pkiclient.exe"); + } + + private static string FirstNonEmpty(params string[] values) + { + if (values == null) { return null; } + foreach (string value in values) { if (!string.IsNullOrEmpty(value)) { return value; } } + return null; + } + + private static string SanitizeForCspId(string input) + { + if (string.IsNullOrEmpty(input)) { return "Infisical"; } + char[] buffer = new char[input.Length]; + for (int i = 0; i < input.Length; i++) + { + char c = input[i]; + buffer[i] = (char.IsLetterOrDigit(c) || c == '-' || c == '_') ? c : '_'; + } + return new string(buffer); + } + + private static string MapKeyAlgorithm(string fromDefaults) + { + if (string.IsNullOrEmpty(fromDefaults)) { return null; } + if (fromDefaults.IndexOf("rsa", StringComparison.OrdinalIgnoreCase) >= 0) { return "RSA"; } + if (fromDefaults.IndexOf("ec", StringComparison.OrdinalIgnoreCase) >= 0) { return "ECDSA_P256"; } + return null; + } + + private static string JoinEkuOids(string[] values) + { + if (values == null || values.Length == 0) { return null; } + System.Text.StringBuilder sb = new System.Text.StringBuilder(); + bool first = true; + foreach (string v in values) + { + if (string.IsNullOrEmpty(v)) { continue; } + if (!first) { sb.Append('+'); } + sb.Append(v); + first = false; + } + return sb.Length > 0 ? sb.ToString() : null; + } + + private static string SecureStringToPlainText(SecureString value) + { + if (value == null) { return null; } + IntPtr ptr = Marshal.SecureStringToGlobalAllocUnicode(value); + try { return Marshal.PtrToStringUni(ptr); } + finally { if (ptr != IntPtr.Zero) { Marshal.ZeroFreeGlobalAllocUnicode(ptr); } } + } + } +} diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalSecretCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalSecretCmdlet.cs index 2493296..724a4d3 100644 --- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalSecretCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalSecretCmdlet.cs @@ -1,4 +1,6 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.Management.Automation; using PSInfisicalAPI.Connections; using PSInfisicalAPI.Models; @@ -6,55 +8,101 @@ using PSInfisicalAPI.Secrets; namespace PSInfisicalAPI.Cmdlets { - [Cmdlet(VerbsCommon.Get, "InfisicalSecret")] + [Cmdlet(VerbsCommon.Get, "InfisicalSecret", DefaultParameterSetName = "List")] [OutputType(typeof(InfisicalSecret))] public sealed class GetInfisicalSecretCmdlet : InfisicalCmdletBase { - [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)] + [Parameter(ParameterSetName = "Single", Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)] public string SecretName { get; set; } - [Parameter] public string ProjectId { get; set; } - [Parameter] public string Environment { get; set; } + [Parameter(Mandatory = true)] public string ProjectId { get; set; } + [Parameter(Mandatory = true)] public string Environment { get; set; } [Parameter] public string SecretPath { get; set; } [Parameter] public string ApiVersion { get; set; } - [Parameter] public int? Version { get; set; } - [Parameter] public InfisicalSecretType Type { get; set; } = InfisicalSecretType.Shared; [Parameter] public SwitchParameter ViewSecretValue { get; set; } = SwitchParameter.Present; [Parameter] public SwitchParameter ExpandSecretReferences { get; set; } [Parameter] public SwitchParameter IncludeImports { get; set; } + [Parameter(ParameterSetName = "Single")] public int? Version { get; set; } + [Parameter(ParameterSetName = "Single")] public InfisicalSecretType Type { get; set; } = InfisicalSecretType.Shared; + + [Parameter(ParameterSetName = "List")] public SwitchParameter List { get; set; } + [Parameter(ParameterSetName = "List")] public SwitchParameter Recursive { get; set; } + [Parameter(ParameterSetName = "List")] public SwitchParameter IncludePersonalOverrides { get; set; } + [Parameter(ParameterSetName = "List")] public Hashtable MetadataFilter { get; set; } + [Parameter(ParameterSetName = "List")] public string[] TagSlugs { get; set; } + protected override void ProcessRecord() { try { InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); + InfisicalSecretsClient client = new InfisicalSecretsClient(HttpClient, Logger); - InfisicalRetrieveSecretQuery query = new InfisicalRetrieveSecretQuery + if (string.Equals(ParameterSetName, "Single", StringComparison.Ordinal)) { - SecretName = SecretName, - ProjectId = ResolveProjectId(connection, ProjectId), - Environment = ResolveEnvironment(connection, Environment), - SecretPath = ResolveSecretPath(connection, SecretPath), + InfisicalRetrieveSecretQuery query = new InfisicalRetrieveSecretQuery + { + SecretName = SecretName, + ProjectId = ProjectId, + Environment = Environment, + SecretPath = SecretPath, + ApiVersion = ResolveApiVersion(connection, ApiVersion), + Version = Version, + Type = Type.ToString(), + ViewSecretValue = ViewSecretValue.IsPresent, + ExpandSecretReferences = ExpandSecretReferences.IsPresent, + IncludeImports = IncludeImports.IsPresent + }; + + InfisicalSecret secret = client.Retrieve(connection, query); + if (secret != null) + { + WriteObject(secret); + } + + return; + } + + InfisicalListSecretsQuery listQuery = new InfisicalListSecretsQuery + { + ProjectId = ProjectId, + Environment = Environment, + SecretPath = SecretPath, ApiVersion = ResolveApiVersion(connection, ApiVersion), - Version = Version, - Type = Type.ToString(), - ViewSecretValue = ViewSecretValue.IsPresent, + Recursive = Recursive.IsPresent, + IncludeImports = IncludeImports.IsPresent, + IncludePersonalOverrides = IncludePersonalOverrides.IsPresent, ExpandSecretReferences = ExpandSecretReferences.IsPresent, - IncludeImports = IncludeImports.IsPresent + ViewSecretValue = ViewSecretValue.IsPresent, + MetadataFilter = ToStringDictionary(MetadataFilter), + TagSlugs = TagSlugs }; - InfisicalSecretsClient client = new InfisicalSecretsClient(HttpClient, Logger); - InfisicalSecret secret = client.Retrieve(connection, query); - - if (secret != null) + InfisicalSecret[] secrets = client.List(connection, listQuery); + foreach (InfisicalSecret secret in secrets) { WriteObject(secret); } } catch (Exception exception) { - ThrowTerminatingForException("GetInfisicalSecretCmdlet", "RetrieveSecret", exception); + ThrowTerminatingForException("GetInfisicalSecretCmdlet", "GetSecret", exception); } } + + private static Dictionary ToStringDictionary(Hashtable hashtable) + { + if (hashtable == null) { return null; } + + Dictionary result = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (DictionaryEntry entry in hashtable) + { + if (entry.Key == null) { continue; } + result[entry.Key.ToString()] = entry.Value != null ? entry.Value.ToString() : null; + } + + return result; + } } } diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalSecretsCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalSecretsCmdlet.cs deleted file mode 100644 index 599b8a9..0000000 --- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalSecretsCmdlet.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Management.Automation; -using PSInfisicalAPI.Connections; -using PSInfisicalAPI.Models; -using PSInfisicalAPI.Secrets; - -namespace PSInfisicalAPI.Cmdlets -{ - [Cmdlet(VerbsCommon.Get, "InfisicalSecrets")] - [OutputType(typeof(InfisicalSecret))] - public sealed class GetInfisicalSecretsCmdlet : InfisicalCmdletBase - { - [Parameter] public string ProjectId { get; set; } - [Parameter] public string Environment { get; set; } - [Parameter] public string SecretPath { get; set; } - [Parameter] public string ApiVersion { get; set; } - [Parameter] public SwitchParameter Recursive { get; set; } - [Parameter] public SwitchParameter IncludeImports { get; set; } - [Parameter] public SwitchParameter IncludePersonalOverrides { get; set; } - [Parameter] public SwitchParameter ExpandSecretReferences { get; set; } - [Parameter] public SwitchParameter ViewSecretValue { get; set; } = SwitchParameter.Present; - [Parameter] public Hashtable MetadataFilter { get; set; } - [Parameter] public string[] TagSlugs { get; set; } - - protected override void ProcessRecord() - { - try - { - InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); - - InfisicalListSecretsQuery query = new InfisicalListSecretsQuery - { - ProjectId = ResolveProjectId(connection, ProjectId), - Environment = ResolveEnvironment(connection, Environment), - SecretPath = ResolveSecretPath(connection, SecretPath), - ApiVersion = ResolveApiVersion(connection, ApiVersion), - Recursive = Recursive.IsPresent, - IncludeImports = IncludeImports.IsPresent, - IncludePersonalOverrides = IncludePersonalOverrides.IsPresent, - ExpandSecretReferences = ExpandSecretReferences.IsPresent, - ViewSecretValue = ViewSecretValue.IsPresent, - MetadataFilter = ToStringDictionary(MetadataFilter), - TagSlugs = TagSlugs - }; - - InfisicalSecretsClient client = new InfisicalSecretsClient(HttpClient, Logger); - InfisicalSecret[] secrets = client.List(connection, query); - - foreach (InfisicalSecret secret in secrets) - { - WriteObject(secret); - } - } - catch (Exception exception) - { - ThrowTerminatingForException("GetInfisicalSecretsCmdlet", "RetrieveSecrets", exception); - } - } - - private static Dictionary ToStringDictionary(Hashtable hashtable) - { - if (hashtable == null) { return null; } - - Dictionary result = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (DictionaryEntry entry in hashtable) - { - if (entry.Key == null) { continue; } - result[entry.Key.ToString()] = entry.Value != null ? entry.Value.ToString() : null; - } - - return result; - } - } -} diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalTagCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalTagCmdlet.cs index 8c7837f..eefe56d 100644 --- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalTagCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalTagCmdlet.cs @@ -6,32 +6,45 @@ using PSInfisicalAPI.Tags; namespace PSInfisicalAPI.Cmdlets { - [Cmdlet(VerbsCommon.Get, "InfisicalTag")] + [Cmdlet(VerbsCommon.Get, "InfisicalTag", DefaultParameterSetName = "List")] [OutputType(typeof(InfisicalTag))] public sealed class GetInfisicalTagCmdlet : InfisicalCmdletBase { - [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)] + [Parameter(ParameterSetName = "Single", Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)] [Alias("Slug", "Id")] public string TagSlugOrId { get; set; } - [Parameter] public string ProjectId { get; set; } + [Parameter(Mandatory = true)] public string ProjectId { get; set; } + + [Parameter(ParameterSetName = "List")] public SwitchParameter List { get; set; } protected override void ProcessRecord() { try { InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); - string resolvedProjectId = ResolveProjectId(connection, ProjectId); InfisicalTagClient client = new InfisicalTagClient(HttpClient, Logger); - InfisicalTag tag = client.Retrieve(connection, resolvedProjectId, TagSlugOrId); - if (tag != null) + + if (string.Equals(ParameterSetName, "Single", StringComparison.Ordinal)) + { + InfisicalTag tag = client.Retrieve(connection, ProjectId, TagSlugOrId); + if (tag != null) + { + WriteObject(tag); + } + + return; + } + + InfisicalTag[] tags = client.List(connection, ProjectId); + foreach (InfisicalTag tag in tags) { WriteObject(tag); } } catch (Exception exception) { - ThrowTerminatingForException("GetInfisicalTagCmdlet", "RetrieveTag", exception); + ThrowTerminatingForException("GetInfisicalTagCmdlet", "GetTag", exception); } } } diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalTagsCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalTagsCmdlet.cs deleted file mode 100644 index a4b736c..0000000 --- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalTagsCmdlet.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Management.Automation; -using PSInfisicalAPI.Connections; -using PSInfisicalAPI.Models; -using PSInfisicalAPI.Tags; - -namespace PSInfisicalAPI.Cmdlets -{ - [Cmdlet(VerbsCommon.Get, "InfisicalTags")] - [OutputType(typeof(InfisicalTag))] - public sealed class GetInfisicalTagsCmdlet : InfisicalCmdletBase - { - [Parameter] public string ProjectId { get; set; } - - protected override void ProcessRecord() - { - try - { - InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); - string resolvedProjectId = ResolveProjectId(connection, ProjectId); - InfisicalTagClient client = new InfisicalTagClient(HttpClient, Logger); - InfisicalTag[] tags = client.List(connection, resolvedProjectId); - foreach (InfisicalTag tag in tags) - { - WriteObject(tag); - } - } - catch (Exception exception) - { - ThrowTerminatingForException("GetInfisicalTagsCmdlet", "ListTags", exception); - } - } - } -} diff --git a/src/PSInfisicalAPI/Cmdlets/InfisicalCmdletBase.cs b/src/PSInfisicalAPI/Cmdlets/InfisicalCmdletBase.cs index 4a3c37b..4e35b17 100644 --- a/src/PSInfisicalAPI/Cmdlets/InfisicalCmdletBase.cs +++ b/src/PSInfisicalAPI/Cmdlets/InfisicalCmdletBase.cs @@ -46,21 +46,6 @@ namespace PSInfisicalAPI.Cmdlets ThrowTerminatingError(record); } - protected string ResolveProjectId(InfisicalConnection connection, string explicitValue) - { - return ResolveValue("ProjectId", explicitValue, connection != null ? connection.ProjectId : null, null); - } - - protected string ResolveEnvironment(InfisicalConnection connection, string explicitValue) - { - return ResolveValue("Environment", explicitValue, connection != null ? connection.Environment : null, null); - } - - protected string ResolveSecretPath(InfisicalConnection connection, string explicitValue) - { - return ResolveValue("SecretPath", explicitValue, connection != null ? connection.DefaultSecretPath : null, "/"); - } - protected string ResolveApiVersion(InfisicalConnection connection, string explicitValue) { string fromConnection = connection != null ? (!string.IsNullOrEmpty(connection.PinnedApiVersion) ? connection.PinnedApiVersion : connection.ApiVersion) : null; diff --git a/src/PSInfisicalAPI/Cmdlets/InstallInfisicalCertificateCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/InstallInfisicalCertificateCmdlet.cs index af32a3e..03b635a 100644 --- a/src/PSInfisicalAPI/Cmdlets/InstallInfisicalCertificateCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/InstallInfisicalCertificateCmdlet.cs @@ -44,7 +44,8 @@ namespace PSInfisicalAPI.Cmdlets { foreach (X509Certificate2 chainCert in ResolveChain()) { - InstallCertificate(chainCert, StoreName.CertificateAuthority, StoreLocation); + StoreName chainStore = InfisicalCertificateRequestHelpers.GetChainCertificateTargetStore(chainCert); + InstallCertificate(chainCert, chainStore, StoreLocation); } } diff --git a/src/PSInfisicalAPI/Cmdlets/NewInfisicalEnvironmentCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/NewInfisicalEnvironmentCmdlet.cs index 6a00664..ad11bef 100644 --- a/src/PSInfisicalAPI/Cmdlets/NewInfisicalEnvironmentCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/NewInfisicalEnvironmentCmdlet.cs @@ -12,7 +12,7 @@ namespace PSInfisicalAPI.Cmdlets { [Parameter(Mandatory = true, Position = 0)] public string Name { get; set; } [Parameter(Mandatory = true, Position = 1)] public string Slug { get; set; } - [Parameter] public string ProjectId { get; set; } + [Parameter(Mandatory = true)] public string ProjectId { get; set; } [Parameter] public int? Position { get; set; } protected override void ProcessRecord() @@ -25,9 +25,8 @@ namespace PSInfisicalAPI.Cmdlets } InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); - string resolvedProjectId = ResolveProjectId(connection, ProjectId); InfisicalEnvironmentClient client = new InfisicalEnvironmentClient(HttpClient, Logger); - InfisicalEnvironment env = client.Create(connection, resolvedProjectId, Name, Slug, Position); + InfisicalEnvironment env = client.Create(connection, ProjectId, Name, Slug, Position); if (env != null) { WriteObject(env); diff --git a/src/PSInfisicalAPI/Cmdlets/NewInfisicalFolderCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/NewInfisicalFolderCmdlet.cs index f31ff1d..46b6f97 100644 --- a/src/PSInfisicalAPI/Cmdlets/NewInfisicalFolderCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/NewInfisicalFolderCmdlet.cs @@ -11,8 +11,8 @@ namespace PSInfisicalAPI.Cmdlets public sealed class NewInfisicalFolderCmdlet : InfisicalCmdletBase { [Parameter(Mandatory = true, Position = 0)] public string Name { get; set; } - [Parameter] public string ProjectId { get; set; } - [Parameter] public string Environment { get; set; } + [Parameter(Mandatory = true)] public string ProjectId { get; set; } + [Parameter(Mandatory = true)] public string Environment { get; set; } [Parameter] public string Path { get; set; } protected override void ProcessRecord() @@ -25,11 +25,8 @@ namespace PSInfisicalAPI.Cmdlets } InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); - string resolvedProjectId = ResolveProjectId(connection, ProjectId); - string resolvedEnvironment = ResolveEnvironment(connection, Environment); - string resolvedPath = ResolveSecretPath(connection, Path); InfisicalFolderClient client = new InfisicalFolderClient(HttpClient, Logger); - InfisicalFolder folder = client.Create(connection, resolvedProjectId, resolvedEnvironment, Name, resolvedPath); + InfisicalFolder folder = client.Create(connection, ProjectId, Environment, Name, Path); if (folder != null) { WriteObject(folder); diff --git a/src/PSInfisicalAPI/Cmdlets/NewInfisicalScepDynamicChallengeCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/NewInfisicalScepDynamicChallengeCmdlet.cs new file mode 100644 index 0000000..abaee4e --- /dev/null +++ b/src/PSInfisicalAPI/Cmdlets/NewInfisicalScepDynamicChallengeCmdlet.cs @@ -0,0 +1,49 @@ +using System; +using System.Management.Automation; +using System.Security; +using PSInfisicalAPI.Connections; +using PSInfisicalAPI.Pki; + +namespace PSInfisicalAPI.Cmdlets +{ + [Cmdlet(VerbsCommon.New, "InfisicalScepDynamicChallenge")] + [OutputType(typeof(SecureString))] + [OutputType(typeof(string))] + public sealed class NewInfisicalScepDynamicChallengeCmdlet : InfisicalCmdletBase + { + [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)] + [Alias("Id")] + public string ApplicationId { get; set; } + + [Parameter(Mandatory = true, Position = 1, ValueFromPipelineByPropertyName = true)] + [Alias("CertificateProfileId")] + public string ProfileId { get; set; } + + [Parameter] public SwitchParameter AsPlainText { get; set; } + + protected override void ProcessRecord() + { + try + { + InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); + InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger); + + string challenge = client.GenerateScepDynamicChallenge(connection, ApplicationId, ProfileId); + if (AsPlainText.IsPresent) + { + WriteObject(challenge); + return; + } + + SecureString secure = new SecureString(); + foreach (char c in challenge) { secure.AppendChar(c); } + secure.MakeReadOnly(); + WriteObject(secure); + } + catch (Exception exception) + { + ThrowTerminatingForException("NewInfisicalScepDynamicChallengeCmdlet", "GenerateScepDynamicChallenge", exception); + } + } + } +} diff --git a/src/PSInfisicalAPI/Cmdlets/NewInfisicalSecretCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/NewInfisicalSecretCmdlet.cs index 2d8962a..2b41f51 100644 --- a/src/PSInfisicalAPI/Cmdlets/NewInfisicalSecretCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/NewInfisicalSecretCmdlet.cs @@ -28,8 +28,8 @@ namespace PSInfisicalAPI.Cmdlets public IDictionary[] Secrets { get; set; } [Parameter] public string SecretComment { get; set; } - [Parameter] public string ProjectId { get; set; } - [Parameter] public string Environment { get; set; } + [Parameter(Mandatory = true)] public string ProjectId { get; set; } + [Parameter(Mandatory = true)] public string Environment { get; set; } [Parameter] public string SecretPath { get; set; } [Parameter] public string ApiVersion { get; set; } [Parameter] public InfisicalSecretType Type { get; set; } = InfisicalSecretType.Shared; @@ -41,9 +41,6 @@ namespace PSInfisicalAPI.Cmdlets try { InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); - string resolvedProjectId = ResolveProjectId(connection, ProjectId); - string resolvedEnvironment = ResolveEnvironment(connection, Environment); - string resolvedSecretPath = ResolveSecretPath(connection, SecretPath); string resolvedApiVersion = ResolveApiVersion(connection, ApiVersion); if (string.Equals(ParameterSetName, "Bulk", StringComparison.Ordinal)) @@ -54,9 +51,9 @@ namespace PSInfisicalAPI.Cmdlets InfisicalBulkCreateSecretsRequest bulk = new InfisicalBulkCreateSecretsRequest { - ProjectId = resolvedProjectId, - Environment = resolvedEnvironment, - SecretPath = resolvedSecretPath, + ProjectId = ProjectId, + Environment = Environment, + SecretPath = SecretPath, ApiVersion = resolvedApiVersion, Secrets = InfisicalBulkSecretConverter.ToCreateItems(Secrets) }; @@ -82,9 +79,9 @@ namespace PSInfisicalAPI.Cmdlets SecretName = SecretName, SecretValue = plainValue, SecretComment = SecretComment, - ProjectId = resolvedProjectId, - Environment = resolvedEnvironment, - SecretPath = resolvedSecretPath, + ProjectId = ProjectId, + Environment = Environment, + SecretPath = SecretPath, Type = Type.ToString(), ApiVersion = resolvedApiVersion, SkipMultilineEncoding = SkipMultilineEncoding.IsPresent ? (bool?)true : null, diff --git a/src/PSInfisicalAPI/Cmdlets/NewInfisicalTagCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/NewInfisicalTagCmdlet.cs index bf26869..3014fcb 100644 --- a/src/PSInfisicalAPI/Cmdlets/NewInfisicalTagCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/NewInfisicalTagCmdlet.cs @@ -13,7 +13,7 @@ namespace PSInfisicalAPI.Cmdlets [Parameter(Mandatory = true, Position = 0)] public string Slug { get; set; } [Parameter] public string Name { get; set; } [Parameter] public string Color { get; set; } - [Parameter] public string ProjectId { get; set; } + [Parameter(Mandatory = true)] public string ProjectId { get; set; } protected override void ProcessRecord() { @@ -25,9 +25,8 @@ namespace PSInfisicalAPI.Cmdlets } InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); - string resolvedProjectId = ResolveProjectId(connection, ProjectId); InfisicalTagClient client = new InfisicalTagClient(HttpClient, Logger); - InfisicalTag tag = client.Create(connection, resolvedProjectId, Slug, Name, Color); + InfisicalTag tag = client.Create(connection, ProjectId, Slug, Name, Color); if (tag != null) { WriteObject(tag); diff --git a/src/PSInfisicalAPI/Cmdlets/RemoveInfisicalEnvironmentCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/RemoveInfisicalEnvironmentCmdlet.cs index 2716bc4..48b3c24 100644 --- a/src/PSInfisicalAPI/Cmdlets/RemoveInfisicalEnvironmentCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/RemoveInfisicalEnvironmentCmdlet.cs @@ -12,7 +12,7 @@ namespace PSInfisicalAPI.Cmdlets [Alias("Id")] public string EnvironmentId { get; set; } - [Parameter] public string ProjectId { get; set; } + [Parameter(Mandatory = true)] public string ProjectId { get; set; } [Parameter] public SwitchParameter PassThru { get; set; } protected override void ProcessRecord() @@ -25,9 +25,8 @@ namespace PSInfisicalAPI.Cmdlets } InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); - string resolvedProjectId = ResolveProjectId(connection, ProjectId); InfisicalEnvironmentClient client = new InfisicalEnvironmentClient(HttpClient, Logger); - client.Delete(connection, resolvedProjectId, EnvironmentId); + client.Delete(connection, ProjectId, EnvironmentId); if (PassThru.IsPresent) { diff --git a/src/PSInfisicalAPI/Cmdlets/RemoveInfisicalFolderCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/RemoveInfisicalFolderCmdlet.cs index 7dde5d7..597defc 100644 --- a/src/PSInfisicalAPI/Cmdlets/RemoveInfisicalFolderCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/RemoveInfisicalFolderCmdlet.cs @@ -12,8 +12,8 @@ namespace PSInfisicalAPI.Cmdlets [Alias("Id")] public string FolderId { get; set; } - [Parameter] public string ProjectId { get; set; } - [Parameter] public string Environment { get; set; } + [Parameter(Mandatory = true)] public string ProjectId { get; set; } + [Parameter(Mandatory = true)] public string Environment { get; set; } [Parameter] public string Path { get; set; } [Parameter] public SwitchParameter PassThru { get; set; } @@ -27,11 +27,8 @@ namespace PSInfisicalAPI.Cmdlets } InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); - string resolvedProjectId = ResolveProjectId(connection, ProjectId); - string resolvedEnvironment = ResolveEnvironment(connection, Environment); - string resolvedPath = ResolveSecretPath(connection, Path); InfisicalFolderClient client = new InfisicalFolderClient(HttpClient, Logger); - client.Delete(connection, resolvedProjectId, resolvedEnvironment, FolderId, resolvedPath); + client.Delete(connection, ProjectId, Environment, FolderId, Path); if (PassThru.IsPresent) { diff --git a/src/PSInfisicalAPI/Cmdlets/RemoveInfisicalProjectCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/RemoveInfisicalProjectCmdlet.cs index fbab178..86bf47a 100644 --- a/src/PSInfisicalAPI/Cmdlets/RemoveInfisicalProjectCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/RemoveInfisicalProjectCmdlet.cs @@ -8,7 +8,7 @@ namespace PSInfisicalAPI.Cmdlets [Cmdlet(VerbsCommon.Remove, "InfisicalProject", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)] public sealed class RemoveInfisicalProjectCmdlet : InfisicalCmdletBase { - [Parameter(ValueFromPipelineByPropertyName = true, Position = 0)] + [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)] [Alias("Id")] public string ProjectId { get; set; } @@ -19,19 +19,18 @@ namespace PSInfisicalAPI.Cmdlets try { InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); - string resolvedProjectId = ResolveProjectId(connection, ProjectId); - if (!ShouldProcess(resolvedProjectId, "Remove Infisical project")) + if (!ShouldProcess(ProjectId, "Remove Infisical project")) { return; } InfisicalProjectClient client = new InfisicalProjectClient(HttpClient, Logger); - client.Delete(connection, resolvedProjectId); + client.Delete(connection, ProjectId); if (PassThru.IsPresent) { - WriteObject(resolvedProjectId); + WriteObject(ProjectId); } } catch (Exception exception) diff --git a/src/PSInfisicalAPI/Cmdlets/RemoveInfisicalSecretCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/RemoveInfisicalSecretCmdlet.cs index d2154c7..e02d6ab 100644 --- a/src/PSInfisicalAPI/Cmdlets/RemoveInfisicalSecretCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/RemoveInfisicalSecretCmdlet.cs @@ -16,8 +16,8 @@ namespace PSInfisicalAPI.Cmdlets [Alias("Names", "SecretKeys")] public string[] SecretNames { get; set; } - [Parameter] public string ProjectId { get; set; } - [Parameter] public string Environment { get; set; } + [Parameter(Mandatory = true)] public string ProjectId { get; set; } + [Parameter(Mandatory = true)] public string Environment { get; set; } [Parameter] public string SecretPath { get; set; } [Parameter] public string ApiVersion { get; set; } [Parameter] public InfisicalSecretType Type { get; set; } = InfisicalSecretType.Shared; @@ -28,9 +28,6 @@ namespace PSInfisicalAPI.Cmdlets try { InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); - string resolvedProjectId = ResolveProjectId(connection, ProjectId); - string resolvedEnvironment = ResolveEnvironment(connection, Environment); - string resolvedSecretPath = ResolveSecretPath(connection, SecretPath); string resolvedApiVersion = ResolveApiVersion(connection, ApiVersion); InfisicalSecretsClient client = new InfisicalSecretsClient(HttpClient, Logger); @@ -43,9 +40,9 @@ namespace PSInfisicalAPI.Cmdlets InfisicalBulkDeleteSecretsRequest bulk = new InfisicalBulkDeleteSecretsRequest { - ProjectId = resolvedProjectId, - Environment = resolvedEnvironment, - SecretPath = resolvedSecretPath, + ProjectId = ProjectId, + Environment = Environment, + SecretPath = SecretPath, ApiVersion = resolvedApiVersion, SecretNames = SecretNames }; @@ -65,9 +62,9 @@ namespace PSInfisicalAPI.Cmdlets InfisicalDeleteSecretRequest request = new InfisicalDeleteSecretRequest { SecretName = SecretName, - ProjectId = resolvedProjectId, - Environment = resolvedEnvironment, - SecretPath = resolvedSecretPath, + ProjectId = ProjectId, + Environment = Environment, + SecretPath = SecretPath, Type = Type.ToString(), ApiVersion = resolvedApiVersion }; diff --git a/src/PSInfisicalAPI/Cmdlets/RemoveInfisicalTagCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/RemoveInfisicalTagCmdlet.cs index 96b3b7e..9280a22 100644 --- a/src/PSInfisicalAPI/Cmdlets/RemoveInfisicalTagCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/RemoveInfisicalTagCmdlet.cs @@ -12,7 +12,7 @@ namespace PSInfisicalAPI.Cmdlets [Alias("Id")] public string TagId { get; set; } - [Parameter] public string ProjectId { get; set; } + [Parameter(Mandatory = true)] public string ProjectId { get; set; } [Parameter] public SwitchParameter PassThru { get; set; } protected override void ProcessRecord() @@ -25,9 +25,8 @@ namespace PSInfisicalAPI.Cmdlets } InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); - string resolvedProjectId = ResolveProjectId(connection, ProjectId); InfisicalTagClient client = new InfisicalTagClient(HttpClient, Logger); - client.Delete(connection, resolvedProjectId, TagId); + client.Delete(connection, ProjectId, TagId); if (PassThru.IsPresent) { diff --git a/src/PSInfisicalAPI/Cmdlets/RequestInfisicalCertificateCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/RequestInfisicalCertificateCmdlet.cs new file mode 100644 index 0000000..2a761de --- /dev/null +++ b/src/PSInfisicalAPI/Cmdlets/RequestInfisicalCertificateCmdlet.cs @@ -0,0 +1,227 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Management.Automation; +using System.Security.Cryptography.X509Certificates; +using PSInfisicalAPI.Connections; +using PSInfisicalAPI.Models; +using PSInfisicalAPI.Pki; + +namespace PSInfisicalAPI.Cmdlets +{ + [Cmdlet(VerbsLifecycle.Request, "InfisicalCertificate", SupportsShouldProcess = true, DefaultParameterSetName = "BySubscriber")] + [OutputType(typeof(InfisicalCertificateResult))] + public sealed class RequestInfisicalCertificateCmdlet : InfisicalCmdletBase + { + private const string Component = "RequestInfisicalCertificateCmdlet"; + + [Parameter(ParameterSetName = "BySubscriber", Mandatory = true, Position = 0)] + [Alias("Subscriber")] + public string PkiSubscriberSlug { get; set; } + + [Parameter(ParameterSetName = "ByCa", Mandatory = true, Position = 0)] + [Alias("CaId")] + public string CertificateAuthorityId { get; set; } + + [Parameter(ParameterSetName = "ByProfile", Mandatory = true, Position = 0)] + [Alias("ProfileId")] + public string CertificateProfileId { get; set; } + + [Parameter(Mandatory = true)] public string ProjectId { get; set; } + [Parameter] public IDictionary Subject { get; set; } + [Parameter] public string CommonName { get; set; } + [Parameter] public string Country { get; set; } + [Parameter] public string State { get; set; } + [Parameter] public string Locality { get; set; } + [Parameter] public string Organization { get; set; } + [Parameter] public string OrganizationalUnit { get; set; } + [Parameter] public string EmailAddress { get; set; } + [Parameter] public string[] DnsName { get; set; } + [Parameter] public string[] IpAddress { get; set; } + [Parameter] public InfisicalKeyAlgorithm KeyAlgorithm { get; set; } = InfisicalKeyAlgorithm.Rsa; + [Parameter] public int KeySize { get; set; } = 2048; + [Parameter] public InfisicalEcCurve Curve { get; set; } = InfisicalEcCurve.P256; + + [Parameter(ParameterSetName = "ByCa")] + [Parameter(ParameterSetName = "ByProfile")] public string Ttl { get; set; } + [Parameter(ParameterSetName = "ByCa")] + [Parameter(ParameterSetName = "ByProfile")] public string NotBefore { get; set; } + [Parameter(ParameterSetName = "ByCa")] + [Parameter(ParameterSetName = "ByProfile")] public string NotAfter { get; set; } + [Parameter(ParameterSetName = "ByCa")] public string FriendlyName { get; set; } + [Parameter(ParameterSetName = "ByCa")] public string PkiCollectionId { get; set; } + [Parameter(ParameterSetName = "ByCa")] + [Parameter(ParameterSetName = "ByProfile")] public string[] KeyUsage { get; set; } + [Parameter(ParameterSetName = "ByCa")] + [Parameter(ParameterSetName = "ByProfile")] public string[] ExtendedKeyUsage { get; set; } + + [Parameter] public SwitchParameter Install { get; set; } + [Parameter] public StoreName StoreName { get; set; } = StoreName.My; + [Parameter] public StoreLocation StoreLocation { get; set; } = StoreLocation.CurrentUser; + [Parameter] public X509KeyStorageFlags KeyStorageFlags { get; set; } = X509KeyStorageFlags.DefaultKeySet; + [Parameter] public SwitchParameter InstallChain { get; set; } + + [Parameter] public InfisicalPrivateKeyProtection PrivateKeyProtection { get; set; } = InfisicalPrivateKeyProtection.LocalOnly; + [Parameter] public SwitchParameter PersistKey { get; set; } + [Parameter] public SwitchParameter MachineKey { get; set; } + [Parameter] public string PrivateKeyPath { get; set; } + + [Parameter] public SwitchParameter AllowRenewal { get; set; } + [Parameter] public int RenewalThresholdDays { get; set; } = 30; + [Parameter] public SwitchParameter Force { get; set; } + [Parameter] public SwitchParameter LocalChainOnly { get; set; } + + protected override void ProcessRecord() + { + try + { + InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); + InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger); + + InfisicalCsrSubject csrSubject = InfisicalCertificateRequestHelpers.MergeSubject(Subject, CommonName, Country, State, Locality, Organization, OrganizationalUnit, EmailAddress); + List dnsNames = BuildDnsNames(csrSubject); + if (string.IsNullOrEmpty(csrSubject.CommonName) && dnsNames.Count > 0) { csrSubject.CommonName = dnsNames[0]; } + if (string.IsNullOrEmpty(csrSubject.CommonName)) { throw new InvalidOperationException("Subject CommonName could not be determined and no DnsName was provided."); } + + X509Certificate2 existing = TryFindExisting(client, connection, ProjectId, csrSubject.CommonName); + if (existing != null && !Force.IsPresent && !(AllowRenewal.IsPresent && InfisicalLocalCertificateLookup.IsRenewable(existing, RenewalThresholdDays))) + { + Logger.Information(Component, string.Concat("Reusing existing certificate (Thumbprint=", existing.Thumbprint, ", NotAfter=", existing.NotAfter.ToString("u"), ").")); + InfisicalCertificateResult reuseResult = InfisicalCertificateRequestHelpers.BuildResultFromExistingLocal(existing); + + if (!LocalChainOnly.IsPresent + && (reuseResult.Root == null || reuseResult.Intermediates == null || reuseResult.Intermediates.Length == 0) + && !string.IsNullOrEmpty(existing.SerialNumber)) + { + try + { + InfisicalCertificateBundle bundle = client.GetCertificateBundle(connection, existing.SerialNumber); + if (bundle != null && !string.IsNullOrEmpty(bundle.CertificateChainPem)) + { + reuseResult = InfisicalCertificateRequestHelpers.BuildResultFromExistingLocal(existing, bundle); + Logger.Information(Component, "Reused certificate chain completed from Infisical bundle."); + } + } + catch (Exception bundleException) + { + Logger.Verbose(Component, string.Concat("Infisical bundle fetch for reuse path failed (continuing with local-only chain): ", bundleException.Message)); + } + } + + WriteObject(reuseResult); + return; + } + + string target = string.Concat("PKI subscriber '", PkiSubscriberSlug ?? "(n/a)", "', CA '", CertificateAuthorityId ?? "(n/a)", "', or profile '", CertificateProfileId ?? "(n/a)", "' for CN=", csrSubject.CommonName); + if (!ShouldProcess(target, "Request new certificate")) { return; } + + InfisicalCsrOptions csrOptions = new InfisicalCsrOptions { KeyAlgorithm = KeyAlgorithm, RsaKeySize = KeySize, EcCurve = Curve }; + InfisicalCsrResult csr = InfisicalCsrBuilder.Build(csrSubject, dnsNames, IpAddress, csrOptions); + InfisicalSignedCertificate signed = SignCertificate(client, connection, ProjectId, csr.CsrPem); + signed.PrivateKeyPem = csr.PrivateKeyPem; + + if (string.IsNullOrEmpty(signed.CertificatePem)) + { + Logger.Warning(Component, string.Concat("Issuance returned without a certificate (status='", signed.Status ?? "unknown", "'", string.IsNullOrEmpty(signed.StatusMessage) ? "" : string.Concat(", message='", signed.StatusMessage, "'"), string.IsNullOrEmpty(signed.CertificateRequestId) ? "" : string.Concat(", certificateRequestId='", signed.CertificateRequestId, "'"), "). Install / chain / key-write steps are skipped; emitting status-only result.")); + InfisicalCertificateResult pending = InfisicalCertificateRequestHelpers.BuildResult(null, signed); + pending.PrivateKeyPem = null; + WriteObject(pending); + return; + } + + X509KeyStorageFlags resolvedFlags = ResolveEffectiveKeyStorageFlags(); + X509Certificate2 cert = PemCertificateBuilder.Build(signed.CertificatePem, signed.PrivateKeyPem, signed.CertificateChainPem, resolvedFlags); + + if (Install.IsPresent) + { + InfisicalCertificateRequestHelpers.InstallToStore(cert, StoreName, StoreLocation, Force.IsPresent, Logger, Component); + if (InstallChain.IsPresent) + { + InfisicalCertificateRequestHelpers.InstallChain(signed, StoreLocation, Force.IsPresent, Logger, Component); + } + } + + InfisicalCertificateResult resultObj = InfisicalCertificateRequestHelpers.BuildResult(cert, signed); + + bool hasExplicitPath = !string.IsNullOrEmpty(PrivateKeyPath); + if (hasExplicitPath && !string.IsNullOrEmpty(resultObj.PrivateKeyPem)) + { + InfisicalCertificateRequestHelpers.WritePrivateKeyPem(resultObj.PrivateKeyPem, PrivateKeyPath); + Logger.Information(Component, string.Concat("Wrote private key PEM to '", PrivateKeyPath, "'.")); + } + + if (!MyInvocation.BoundParameters.ContainsKey("KeyStorageFlags") + && InfisicalCertificateRequestHelpers.ShouldScrubPrivateKeyPem(PrivateKeyProtection, hasExplicitPath)) + { + resultObj.PrivateKeyPem = null; + } + + WriteObject(resultObj); + } + catch (Exception exception) + { + ThrowTerminatingForException(Component, "RequestCertificate", exception); + } + } + + private List BuildDnsNames(InfisicalCsrSubject subject) + { + List result = new List(); + if (DnsName != null) { foreach (string dns in DnsName) { if (!string.IsNullOrEmpty(dns)) { result.Add(dns); } } } + if (result.Count == 0) + { + string fqdn = InfisicalCertificateRequestHelpers.ResolveLocalFqdn(); + if (!string.IsNullOrEmpty(fqdn)) { result.Add(fqdn); } + } + + if (!string.IsNullOrEmpty(subject.CommonName) && !result.Contains(subject.CommonName)) { result.Insert(0, subject.CommonName); } + return result; + } + + private X509Certificate2 TryFindExisting(InfisicalPkiClient client, InfisicalConnection connection, string projectId, string commonName) + { + List candidateSerials = new List(); + try + { + InfisicalCertificateSearchQuery query = new InfisicalCertificateSearchQuery { ProjectId = projectId, CommonName = commonName, Status = "active", Limit = 50 }; + InfisicalCertificateSearchResult page = client.SearchCertificates(connection, query); + if (page != null && page.Certificates != null) + { + foreach (InfisicalCertificate hit in page.Certificates) { if (!string.IsNullOrEmpty(hit.SerialNumber)) { candidateSerials.Add(hit.SerialNumber); } } + } + } + catch (Exception searchException) + { + Logger.Verbose(Component, string.Concat("Infisical search for idempotency check failed: ", searchException.Message)); + } + + return InfisicalLocalCertificateLookup.FindMatch(StoreName, StoreLocation, commonName, candidateSerials); + } + + private X509KeyStorageFlags ResolveEffectiveKeyStorageFlags() + { + if (MyInvocation.BoundParameters.ContainsKey("KeyStorageFlags")) + { + return KeyStorageFlags; + } + + return InfisicalCertificateRequestHelpers.ResolveKeyStorageFlags(PrivateKeyProtection, PersistKey.IsPresent, MachineKey.IsPresent); + } + + private InfisicalSignedCertificate SignCertificate(InfisicalPkiClient client, InfisicalConnection connection, string projectId, string csrPem) + { + if (string.Equals(ParameterSetName, "BySubscriber", StringComparison.Ordinal)) + { + return client.SignCertificateBySubscriber(connection, PkiSubscriberSlug, projectId, csrPem); + } + + if (string.Equals(ParameterSetName, "ByProfile", StringComparison.Ordinal)) + { + InfisicalCsrSubject subject = InfisicalCertificateRequestHelpers.MergeSubject(Subject, CommonName, Country, State, Locality, Organization, OrganizationalUnit, EmailAddress); + return client.IssueCertificateByProfile(connection, CertificateProfileId, csrPem, subject.CommonName, subject.Organization, subject.OrganizationalUnit, subject.Country, subject.State, subject.Locality, Ttl, NotBefore, NotAfter, KeyUsage, ExtendedKeyUsage); + } + + return client.SignCertificateByCa(connection, CertificateAuthorityId, csrPem, CommonName, null, Ttl, NotBefore, NotAfter, FriendlyName, PkiCollectionId, KeyUsage, ExtendedKeyUsage); + } + } +} diff --git a/src/PSInfisicalAPI/Cmdlets/SearchInfisicalCertificateCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/SearchInfisicalCertificateCmdlet.cs index 4b3d3d7..1bd04b5 100644 --- a/src/PSInfisicalAPI/Cmdlets/SearchInfisicalCertificateCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/SearchInfisicalCertificateCmdlet.cs @@ -11,7 +11,7 @@ namespace PSInfisicalAPI.Cmdlets [OutputType(typeof(InfisicalCertificate))] public sealed class SearchInfisicalCertificateCmdlet : InfisicalCmdletBase { - [Parameter] public string ProjectId { get; set; } + [Parameter(Mandatory = true)] public string ProjectId { get; set; } [Parameter] public string FriendlyName { get; set; } [Parameter] public string CommonName { get; set; } [Parameter] public string Search { get; set; } @@ -39,10 +39,9 @@ namespace PSInfisicalAPI.Cmdlets try { InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); - string resolvedProjectId = ResolveProjectId(connection, ProjectId); InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger); - InfisicalCertificateSearchQuery query = BuildQuery(resolvedProjectId); + InfisicalCertificateSearchQuery query = BuildQuery(ProjectId); int requestedLimit = query.Limit ?? 100; query.Limit = requestedLimit; query.Offset = query.Offset ?? 0; diff --git a/src/PSInfisicalAPI/Cmdlets/UpdateInfisicalEnvironmentCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/UpdateInfisicalEnvironmentCmdlet.cs index 76de675..d042e01 100644 --- a/src/PSInfisicalAPI/Cmdlets/UpdateInfisicalEnvironmentCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/UpdateInfisicalEnvironmentCmdlet.cs @@ -14,7 +14,7 @@ namespace PSInfisicalAPI.Cmdlets [Alias("Id")] public string EnvironmentId { get; set; } - [Parameter] public string ProjectId { get; set; } + [Parameter(Mandatory = true)] public string ProjectId { get; set; } [Parameter] public string Name { get; set; } [Parameter] public string Slug { get; set; } [Parameter] public int? Position { get; set; } @@ -29,9 +29,8 @@ namespace PSInfisicalAPI.Cmdlets } InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); - string resolvedProjectId = ResolveProjectId(connection, ProjectId); InfisicalEnvironmentClient client = new InfisicalEnvironmentClient(HttpClient, Logger); - InfisicalEnvironment env = client.Update(connection, resolvedProjectId, EnvironmentId, Name, Slug, Position); + InfisicalEnvironment env = client.Update(connection, ProjectId, EnvironmentId, Name, Slug, Position); if (env != null) { WriteObject(env); diff --git a/src/PSInfisicalAPI/Cmdlets/UpdateInfisicalFolderCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/UpdateInfisicalFolderCmdlet.cs index bb5fe36..9305ae2 100644 --- a/src/PSInfisicalAPI/Cmdlets/UpdateInfisicalFolderCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/UpdateInfisicalFolderCmdlet.cs @@ -15,8 +15,8 @@ namespace PSInfisicalAPI.Cmdlets public string FolderId { get; set; } [Parameter(Mandatory = true, Position = 1)] public string Name { get; set; } - [Parameter] public string ProjectId { get; set; } - [Parameter] public string Environment { get; set; } + [Parameter(Mandatory = true)] public string ProjectId { get; set; } + [Parameter(Mandatory = true)] public string Environment { get; set; } [Parameter] public string Path { get; set; } protected override void ProcessRecord() @@ -29,11 +29,8 @@ namespace PSInfisicalAPI.Cmdlets } InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); - string resolvedProjectId = ResolveProjectId(connection, ProjectId); - string resolvedEnvironment = ResolveEnvironment(connection, Environment); - string resolvedPath = ResolveSecretPath(connection, Path); InfisicalFolderClient client = new InfisicalFolderClient(HttpClient, Logger); - InfisicalFolder folder = client.Update(connection, resolvedProjectId, resolvedEnvironment, FolderId, Name, resolvedPath); + InfisicalFolder folder = client.Update(connection, ProjectId, Environment, FolderId, Name, Path); if (folder != null) { WriteObject(folder); diff --git a/src/PSInfisicalAPI/Cmdlets/UpdateInfisicalProjectCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/UpdateInfisicalProjectCmdlet.cs index a76cb6b..3d929d0 100644 --- a/src/PSInfisicalAPI/Cmdlets/UpdateInfisicalProjectCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/UpdateInfisicalProjectCmdlet.cs @@ -10,7 +10,7 @@ namespace PSInfisicalAPI.Cmdlets [OutputType(typeof(InfisicalProject))] public sealed class UpdateInfisicalProjectCmdlet : InfisicalCmdletBase { - [Parameter(ValueFromPipelineByPropertyName = true, Position = 0)] + [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)] [Alias("Id")] public string ProjectId { get; set; } @@ -23,15 +23,14 @@ namespace PSInfisicalAPI.Cmdlets try { InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); - string resolvedProjectId = ResolveProjectId(connection, ProjectId); - if (!ShouldProcess(resolvedProjectId, "Update Infisical project")) + if (!ShouldProcess(ProjectId, "Update Infisical project")) { return; } InfisicalProjectClient client = new InfisicalProjectClient(HttpClient, Logger); - InfisicalProject project = client.Update(connection, resolvedProjectId, Name, Description, AutoCapitalization); + InfisicalProject project = client.Update(connection, ProjectId, Name, Description, AutoCapitalization); if (project != null) { WriteObject(project); diff --git a/src/PSInfisicalAPI/Cmdlets/UpdateInfisicalSecretCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/UpdateInfisicalSecretCmdlet.cs index 0449af7..30047b1 100644 --- a/src/PSInfisicalAPI/Cmdlets/UpdateInfisicalSecretCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/UpdateInfisicalSecretCmdlet.cs @@ -26,8 +26,8 @@ namespace PSInfisicalAPI.Cmdlets [Parameter] public string NewSecretName { get; set; } [Parameter] public string SecretComment { get; set; } - [Parameter] public string ProjectId { get; set; } - [Parameter] public string Environment { get; set; } + [Parameter(Mandatory = true)] public string ProjectId { get; set; } + [Parameter(Mandatory = true)] public string Environment { get; set; } [Parameter] public string SecretPath { get; set; } [Parameter] public string ApiVersion { get; set; } [Parameter] public InfisicalSecretType Type { get; set; } = InfisicalSecretType.Shared; @@ -39,9 +39,6 @@ namespace PSInfisicalAPI.Cmdlets try { InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); - string resolvedProjectId = ResolveProjectId(connection, ProjectId); - string resolvedEnvironment = ResolveEnvironment(connection, Environment); - string resolvedSecretPath = ResolveSecretPath(connection, SecretPath); string resolvedApiVersion = ResolveApiVersion(connection, ApiVersion); if (string.Equals(ParameterSetName, "Bulk", StringComparison.Ordinal)) @@ -52,9 +49,9 @@ namespace PSInfisicalAPI.Cmdlets InfisicalBulkUpdateSecretsRequest bulk = new InfisicalBulkUpdateSecretsRequest { - ProjectId = resolvedProjectId, - Environment = resolvedEnvironment, - SecretPath = resolvedSecretPath, + ProjectId = ProjectId, + Environment = Environment, + SecretPath = SecretPath, ApiVersion = resolvedApiVersion, Secrets = InfisicalBulkSecretConverter.ToUpdateItems(Secrets) }; @@ -81,9 +78,9 @@ namespace PSInfisicalAPI.Cmdlets NewSecretName = NewSecretName, SecretValue = plainValue, SecretComment = SecretComment, - ProjectId = resolvedProjectId, - Environment = resolvedEnvironment, - SecretPath = resolvedSecretPath, + ProjectId = ProjectId, + Environment = Environment, + SecretPath = SecretPath, Type = Type.ToString(), ApiVersion = resolvedApiVersion, SkipMultilineEncoding = SkipMultilineEncoding.IsPresent ? (bool?)true : null, diff --git a/src/PSInfisicalAPI/Cmdlets/UpdateInfisicalTagCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/UpdateInfisicalTagCmdlet.cs index 15aefe6..7b5f01c 100644 --- a/src/PSInfisicalAPI/Cmdlets/UpdateInfisicalTagCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/UpdateInfisicalTagCmdlet.cs @@ -17,7 +17,7 @@ namespace PSInfisicalAPI.Cmdlets [Parameter] public string Slug { get; set; } [Parameter] public string Name { get; set; } [Parameter] public string Color { get; set; } - [Parameter] public string ProjectId { get; set; } + [Parameter(Mandatory = true)] public string ProjectId { get; set; } protected override void ProcessRecord() { @@ -29,9 +29,8 @@ namespace PSInfisicalAPI.Cmdlets } InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); - string resolvedProjectId = ResolveProjectId(connection, ProjectId); InfisicalTagClient client = new InfisicalTagClient(HttpClient, Logger); - InfisicalTag tag = client.Update(connection, resolvedProjectId, TagId, Slug, Name, Color); + InfisicalTag tag = client.Update(connection, ProjectId, TagId, Slug, Name, Color); if (tag != null) { WriteObject(tag); diff --git a/src/PSInfisicalAPI/Cmdlets/WriteInfisicalScepMdmProfileToWmiCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/WriteInfisicalScepMdmProfileToWmiCmdlet.cs new file mode 100644 index 0000000..87bbbdb --- /dev/null +++ b/src/PSInfisicalAPI/Cmdlets/WriteInfisicalScepMdmProfileToWmiCmdlet.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Globalization; +using System.Management.Automation; +using System.Text; +using PSInfisicalAPI.Models; + +namespace PSInfisicalAPI.Cmdlets +{ + [Cmdlet(VerbsCommunications.Write, "InfisicalScepMdmProfileToWmi", SupportsShouldProcess = true)] + [OutputType(typeof(PSObject))] + public sealed class WriteInfisicalScepMdmProfileToWmiCmdlet : InfisicalCmdletBase + { + private const string Component = "WriteInfisicalScepMdmProfileToWmiCmdlet"; + + [Parameter(Mandatory = true, ValueFromPipeline = true, Position = 0)] + [Alias("Profile", "ScepProfile")] + public InfisicalScepMdmProfile InputObject { get; set; } + + [Parameter] public string Namespace { get; set; } = "root/cimv2/mdm/dmmap"; + [Parameter] public string ClassName { get; set; } = "MDM_ClientCertificateInstall_SCEP02"; + [Parameter] public SwitchParameter SkipElevationCheck { get; set; } + [Parameter] public SwitchParameter PassThru { get; set; } + + protected override void ProcessRecord() + { + try + { + if (InputObject == null) { throw new InvalidOperationException("InputObject is required."); } + if (string.IsNullOrEmpty(InputObject.UniqueId)) { throw new InvalidOperationException("InputObject.UniqueId is required."); } + if (string.IsNullOrEmpty(InputObject.ServerUrl)) { throw new InvalidOperationException("InputObject.ServerUrl is required."); } + + if (Environment.OSVersion.Platform != PlatformID.Win32NT) + { + throw new PlatformNotSupportedException("Write-InfisicalScepMdmProfileToWmi requires Windows (MDM Bridge WMI provider)."); + } + + bool deviceScope = !string.Equals(InputObject.Scope, "User", StringComparison.OrdinalIgnoreCase); + if (deviceScope && !SkipElevationCheck.IsPresent && !IsElevated()) + { + throw new UnauthorizedAccessException("Device-scope SCEP enrollment requires an elevated session (run as Administrator or SYSTEM). Pass -SkipElevationCheck to bypass this guard."); + } + + string parentId = string.Concat("./Vendor/MSFT/ClientCertificateInstall/SCEP/", InputObject.UniqueId); + Hashtable properties = BuildProperties(InputObject, parentId); + string target = string.Concat(Namespace, " ", ClassName, " ParentID=", parentId); + if (!ShouldProcess(target, "New-CimInstance MDM SCEP enrollment")) + { + return; + } + + Logger.Verbose(Component, string.Concat("Creating CIM instance in namespace '", Namespace, "' for class '", ClassName, "' with ParentID '", parentId, "'.")); + Collection results = InvokeNewCimInstance(Namespace, ClassName, properties); + + Logger.Information(Component, string.Concat("Submitted SCEP MDM profile '", InputObject.UniqueId, "' to MDM Bridge WMI provider (results=", results != null ? results.Count : 0, ").")); + if (PassThru.IsPresent && results != null) + { + foreach (PSObject result in results) { WriteObject(result); } + } + } + catch (Exception exception) + { + ThrowTerminatingForException(Component, "WriteScepMdmProfileToWmi", exception); + } + } + + private bool IsElevated() + { + try + { + Collection results = InvokeCommand.InvokeScript("[bool]([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)"); + if (results == null || results.Count == 0 || results[0] == null || results[0].BaseObject == null) { return false; } + return Convert.ToBoolean(results[0].BaseObject, CultureInfo.InvariantCulture); + } + catch (Exception ex) + { + Logger.Verbose(Component, string.Concat("Elevation check failed; assuming non-elevated. ", ex.Message)); + return false; + } + } + + private Collection InvokeNewCimInstance(string ns, string className, Hashtable properties) + { + Dictionary variables = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "ns", ns }, + { "class", className }, + { "props", properties } + }; + + foreach (KeyValuePair kv in variables) + { + SessionState.PSVariable.Set(kv.Key, kv.Value); + } + + try + { + return InvokeCommand.InvokeScript("New-CimInstance -Namespace $ns -ClassName $class -Property $props -ErrorAction Stop"); + } + finally + { + foreach (KeyValuePair kv in variables) + { + SessionState.PSVariable.Remove(kv.Key); + } + } + } + + private static Hashtable BuildProperties(InfisicalScepMdmProfile profile, string parentId) + { + Hashtable h = new Hashtable(StringComparer.OrdinalIgnoreCase); + h["ParentID"] = parentId; + h["InstanceID"] = "Install"; + + AddString(h, "ServerURL", profile.ServerUrl); + AddString(h, "Challenge", profile.Challenge); + AddString(h, "SubjectName", profile.SubjectName); + AddString(h, "SubjectAlternativeNames", profile.SubjectAlternativeNames); + AddString(h, "EKUMapping", profile.EkuMapping); + AddInt(h, "KeyUsage", profile.KeyUsage); + AddInt(h, "KeyLength", profile.KeyLength); + AddString(h, "KeyAlgorithm", profile.KeyAlgorithm); + AddString(h, "HashAlgorithm", profile.HashAlgorithm); + AddInt(h, "KeyProtection", profile.KeyProtection); + AddString(h, "ContainerName", profile.ContainerName); + AddString(h, "ValidPeriod", profile.ValidPeriod); + AddInt(h, "ValidPeriodUnits", profile.ValidPeriodUnits); + AddInt(h, "RetryCount", profile.RetryCount); + AddInt(h, "RetryDelay", profile.RetryDelay); + AddString(h, "TemplateName", profile.TemplateName); + AddString(h, "CAThumbprint", profile.CAThumbprint); + AddString(h, "CustomTextToShowInPrompt", profile.CustomTextToShowInPrompt); + + return h; + } + + private static void AddString(Hashtable h, string key, string value) + { + if (string.IsNullOrEmpty(value)) { return; } + h[key] = value; + } + + private static void AddInt(Hashtable h, string key, int? value) + { + if (!value.HasValue) { return; } + h[key] = value.Value; + } + } +} diff --git a/src/PSInfisicalAPI/Connections/InfisicalConnection.cs b/src/PSInfisicalAPI/Connections/InfisicalConnection.cs index ef4d04c..7e6362d 100644 --- a/src/PSInfisicalAPI/Connections/InfisicalConnection.cs +++ b/src/PSInfisicalAPI/Connections/InfisicalConnection.cs @@ -12,9 +12,6 @@ namespace PSInfisicalAPI.Connections public string PinnedApiVersion { get; set; } public InfisicalAuthType AuthType { get; set; } public string OrganizationId { get; set; } - public string ProjectId { get; set; } - public string Environment { get; set; } - public string DefaultSecretPath { get; set; } public DateTimeOffset ConnectedAtUtc { get; set; } public DateTimeOffset? ExpiresAtUtc { get; set; } public bool IsConnected { get; set; } @@ -26,8 +23,8 @@ namespace PSInfisicalAPI.Connections public override string ToString() { return string.Concat( - "Project=", ProjectId ?? "", - " Environment=", Environment ?? "", + "BaseUri=", BaseUri != null ? BaseUri.ToString() : "", + " AuthType=", AuthType.ToString(), " Connected=", IsConnected ? "true" : "false"); } } diff --git a/src/PSInfisicalAPI/Endpoints/InfisicalEndpointNames.cs b/src/PSInfisicalAPI/Endpoints/InfisicalEndpointNames.cs index 6d01d4a..678514f 100644 --- a/src/PSInfisicalAPI/Endpoints/InfisicalEndpointNames.cs +++ b/src/PSInfisicalAPI/Endpoints/InfisicalEndpointNames.cs @@ -49,5 +49,27 @@ namespace PSInfisicalAPI.Endpoints public const string SearchCertificates = "SearchCertificates"; public const string RetrieveCertificate = "RetrieveCertificate"; public const string GetCertificateBundle = "GetCertificateBundle"; + public const string SignCertificateBySubscriber = "SignCertificateBySubscriber"; + public const string SignCertificateByCa = "SignCertificateByCa"; + public const string IssueCertificateByProfile = "IssueCertificateByProfile"; + + public const string ListPkiSubscribers = "ListPkiSubscribers"; + public const string GetPkiSubscriber = "GetPkiSubscriber"; + + public const string ListCertificateProfiles = "ListCertificateProfiles"; + public const string GetCertificateProfile = "GetCertificateProfile"; + + public const string ListCertificatePolicies = "ListCertificatePolicies"; + public const string GetCertificatePolicy = "GetCertificatePolicy"; + + public const string ListCertificateAuthorities = "ListCertificateAuthorities"; + + public const string ListCertificateApplications = "ListCertificateApplications"; + public const string GetCertificateApplication = "GetCertificateApplication"; + public const string GetCertificateApplicationByName = "GetCertificateApplicationByName"; + public const string ListCertificateApplicationProfiles = "ListCertificateApplicationProfiles"; + public const string GetCertificateApplicationEnrollment = "GetCertificateApplicationEnrollment"; + + public const string GenerateScepDynamicChallenge = "GenerateScepDynamicChallenge"; } } diff --git a/src/PSInfisicalAPI/Endpoints/InfisicalEndpointRegistry.cs b/src/PSInfisicalAPI/Endpoints/InfisicalEndpointRegistry.cs index 9d1e305..e3f5ac4 100644 --- a/src/PSInfisicalAPI/Endpoints/InfisicalEndpointRegistry.cs +++ b/src/PSInfisicalAPI/Endpoints/InfisicalEndpointRegistry.cs @@ -589,6 +589,181 @@ namespace PSInfisicalAPI.Endpoints RequiresAuthorization = true, ContainsSecretMaterialInResponse = true }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.SignCertificateBySubscriber, + Resource = "Pki", + Version = "v1", + Method = "POST", + Template = "/api/v1/pki/subscribers/{subscriberName}/sign-certificate", + RequiresAuthorization = true, + ContainsSecretMaterialInResponse = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.SignCertificateByCa, + Resource = "Pki", + Version = "v1", + Method = "POST", + Template = "/api/v1/pki/ca/{caId}/sign-certificate", + RequiresAuthorization = true, + ContainsSecretMaterialInResponse = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.SignCertificateByCa, + Resource = "Pki", + Version = "v1", + Method = "POST", + Template = "/api/v1/cert-manager/ca/{caId}/sign-certificate", + RequiresAuthorization = true, + ContainsSecretMaterialInResponse = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.IssueCertificateByProfile, + Resource = "Pki", + Version = "v1", + Method = "POST", + Template = "/api/v1/cert-manager/certificates", + RequiresAuthorization = true, + ContainsSecretMaterialInResponse = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.ListPkiSubscribers, + Resource = "Pki", + Version = "v1", + Method = "GET", + Template = "/api/v1/projects/{projectId}/pki-subscribers", + RequiresAuthorization = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.GetPkiSubscriber, + Resource = "Pki", + Version = "v1", + Method = "GET", + Template = "/api/v1/pki/subscribers/{subscriberName}", + RequiresAuthorization = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.ListCertificateProfiles, + Resource = "Pki", + Version = "v1", + Method = "GET", + Template = "/api/v1/cert-manager/certificate-profiles", + RequiresAuthorization = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.GetCertificateProfile, + Resource = "Pki", + Version = "v1", + Method = "GET", + Template = "/api/v1/cert-manager/certificate-profiles/{certificateProfileId}", + RequiresAuthorization = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.ListCertificatePolicies, + Resource = "Pki", + Version = "v1", + Method = "GET", + Template = "/api/v1/cert-manager/certificate-policies", + RequiresAuthorization = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.GetCertificatePolicy, + Resource = "Pki", + Version = "v1", + Method = "GET", + Template = "/api/v1/cert-manager/certificate-policies/{certificatePolicyId}", + RequiresAuthorization = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.ListCertificateAuthorities, + Resource = "Pki", + Version = "v1", + Method = "GET", + Template = "/api/v1/cert-manager/ca", + RequiresAuthorization = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.ListCertificateApplications, + Resource = "Pki", + Version = "v1", + Method = "GET", + Template = "/api/v1/cert-manager/applications", + RequiresAuthorization = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.GetCertificateApplication, + Resource = "Pki", + Version = "v1", + Method = "GET", + Template = "/api/v1/cert-manager/applications/{applicationId}", + RequiresAuthorization = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.GetCertificateApplicationByName, + Resource = "Pki", + Version = "v1", + Method = "GET", + Template = "/api/v1/cert-manager/applications/by-name/{name}", + RequiresAuthorization = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.ListCertificateApplicationProfiles, + Resource = "Pki", + Version = "v1", + Method = "GET", + Template = "/api/v1/cert-manager/applications/{applicationId}/profiles", + RequiresAuthorization = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.GetCertificateApplicationEnrollment, + Resource = "Pki", + Version = "v1", + Method = "GET", + Template = "/api/v1/cert-manager/applications/{applicationId}/profiles/{profileId}/enrollment", + RequiresAuthorization = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.GenerateScepDynamicChallenge, + Resource = "Pki", + Version = "v1", + Method = "POST", + Template = "/scep/applications/{applicationId}/profiles/{profileId}/challenge", + RequiresAuthorization = true, + ContainsSecretMaterialInResponse = true + }); } public static InfisicalEndpointDefinition Get(string name) diff --git a/src/PSInfisicalAPI/Environments/InfisicalEnvironmentClient.cs b/src/PSInfisicalAPI/Environments/InfisicalEnvironmentClient.cs index 3a917bc..455852c 100644 --- a/src/PSInfisicalAPI/Environments/InfisicalEnvironmentClient.cs +++ b/src/PSInfisicalAPI/Environments/InfisicalEnvironmentClient.cs @@ -29,10 +29,9 @@ namespace PSInfisicalAPI.Environments public InfisicalEnvironment[] List(InfisicalConnection connection, string projectId) { if (connection == null) { throw new ArgumentNullException(nameof(connection)); } - string resolvedProjectId = FirstNonEmpty(projectId, connection.ProjectId); - if (string.IsNullOrEmpty(resolvedProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + if (string.IsNullOrEmpty(projectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } - Dictionary pathParameters = new Dictionary { { "projectId", resolvedProjectId } }; + Dictionary pathParameters = new Dictionary { { "projectId", projectId } }; try { @@ -43,7 +42,7 @@ namespace PSInfisicalAPI.Environments InfisicalEnvironmentWorkspaceDto workspace = dto != null ? (dto.Workspace ?? dto.Project) : null; List envs = workspace != null ? workspace.Environments : null; - InfisicalEnvironment[] mapped = InfisicalEnvironmentMapper.MapMany(envs, resolvedProjectId); + InfisicalEnvironment[] mapped = InfisicalEnvironmentMapper.MapMany(envs, projectId); _logger.Information(Component, "Infisical environment list retrieval was successful."); return mapped; } @@ -57,11 +56,10 @@ namespace PSInfisicalAPI.Environments public InfisicalEnvironment Retrieve(InfisicalConnection connection, string projectId, string environmentSlugOrId) { if (connection == null) { throw new ArgumentNullException(nameof(connection)); } - string resolvedProjectId = FirstNonEmpty(projectId, connection.ProjectId); - if (string.IsNullOrEmpty(resolvedProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + if (string.IsNullOrEmpty(projectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } if (string.IsNullOrEmpty(environmentSlugOrId)) { throw new InfisicalConfigurationException("Environment is required."); } - InfisicalEnvironment[] all = List(connection, resolvedProjectId); + InfisicalEnvironment[] all = List(connection, projectId); foreach (InfisicalEnvironment env in all) { if (string.Equals(env.Id, environmentSlugOrId, StringComparison.OrdinalIgnoreCase) || @@ -77,12 +75,11 @@ namespace PSInfisicalAPI.Environments public InfisicalEnvironment Create(InfisicalConnection connection, string projectId, string name, string slug, int? position) { if (connection == null) { throw new ArgumentNullException(nameof(connection)); } - string resolvedProjectId = FirstNonEmpty(projectId, connection.ProjectId); - if (string.IsNullOrEmpty(resolvedProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + if (string.IsNullOrEmpty(projectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } if (string.IsNullOrEmpty(name)) { throw new InfisicalConfigurationException("Name is required."); } if (string.IsNullOrEmpty(slug)) { throw new InfisicalConfigurationException("Slug is required."); } - Dictionary pathParameters = new Dictionary { { "projectId", resolvedProjectId } }; + Dictionary pathParameters = new Dictionary { { "projectId", projectId } }; InfisicalEnvironmentCreateRequestDto request = new InfisicalEnvironmentCreateRequestDto { Name = name, Slug = slug, Position = position }; string body = _serializer.Serialize(request); @@ -93,7 +90,7 @@ namespace PSInfisicalAPI.Environments InfisicalEnvironmentSingleResponseDto dto = _serializer.Deserialize(response.Body); response.Clear(); - InfisicalEnvironment mapped = InfisicalEnvironmentMapper.Map(dto != null ? dto.Environment : null, resolvedProjectId); + InfisicalEnvironment mapped = InfisicalEnvironmentMapper.Map(dto != null ? dto.Environment : null, projectId); _logger.Information(Component, "Infisical environment creation was successful."); return mapped; } @@ -107,11 +104,10 @@ namespace PSInfisicalAPI.Environments public InfisicalEnvironment Update(InfisicalConnection connection, string projectId, string environmentId, string name, string slug, int? position) { if (connection == null) { throw new ArgumentNullException(nameof(connection)); } - string resolvedProjectId = FirstNonEmpty(projectId, connection.ProjectId); - if (string.IsNullOrEmpty(resolvedProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + if (string.IsNullOrEmpty(projectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } if (string.IsNullOrEmpty(environmentId)) { throw new InfisicalConfigurationException("EnvironmentId is required."); } - Dictionary pathParameters = new Dictionary { { "projectId", resolvedProjectId }, { "environmentId", environmentId } }; + Dictionary pathParameters = new Dictionary { { "projectId", projectId }, { "environmentId", environmentId } }; InfisicalEnvironmentUpdateRequestDto request = new InfisicalEnvironmentUpdateRequestDto { Name = name, Slug = slug, Position = position }; string body = _serializer.Serialize(request); @@ -122,7 +118,7 @@ namespace PSInfisicalAPI.Environments InfisicalEnvironmentSingleResponseDto dto = _serializer.Deserialize(response.Body); response.Clear(); - InfisicalEnvironment mapped = InfisicalEnvironmentMapper.Map(dto != null ? dto.Environment : null, resolvedProjectId); + InfisicalEnvironment mapped = InfisicalEnvironmentMapper.Map(dto != null ? dto.Environment : null, projectId); _logger.Information(Component, "Infisical environment update was successful."); return mapped; } @@ -136,11 +132,10 @@ namespace PSInfisicalAPI.Environments public void Delete(InfisicalConnection connection, string projectId, string environmentId) { if (connection == null) { throw new ArgumentNullException(nameof(connection)); } - string resolvedProjectId = FirstNonEmpty(projectId, connection.ProjectId); - if (string.IsNullOrEmpty(resolvedProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + if (string.IsNullOrEmpty(projectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } if (string.IsNullOrEmpty(environmentId)) { throw new InfisicalConfigurationException("EnvironmentId is required."); } - Dictionary pathParameters = new Dictionary { { "projectId", resolvedProjectId }, { "environmentId", environmentId } }; + Dictionary pathParameters = new Dictionary { { "projectId", projectId }, { "environmentId", environmentId } }; try { @@ -156,11 +151,5 @@ namespace PSInfisicalAPI.Environments } } - private static string FirstNonEmpty(params string[] values) - { - if (values == null) { return null; } - foreach (string value in values) { if (!string.IsNullOrEmpty(value)) { return value; } } - return null; - } } } diff --git a/src/PSInfisicalAPI/Errors/InfisicalApiErrorEnvelope.cs b/src/PSInfisicalAPI/Errors/InfisicalApiErrorEnvelope.cs new file mode 100644 index 0000000..831a1ed --- /dev/null +++ b/src/PSInfisicalAPI/Errors/InfisicalApiErrorEnvelope.cs @@ -0,0 +1,119 @@ +using System; +using Newtonsoft.Json.Linq; + +namespace PSInfisicalAPI.Errors +{ + internal static class InfisicalApiErrorEnvelope + { + public static void Enrich(InfisicalApiException exception, string body) + { + if (exception == null || string.IsNullOrEmpty(body)) + { + return; + } + + string trimmed = body.TrimStart(); + if (trimmed.Length == 0 || (trimmed[0] != '{' && trimmed[0] != '[')) + { + return; + } + + JObject obj; + try + { + JToken token = JToken.Parse(body); + if (token.Type != JTokenType.Object) { return; } + obj = (JObject)token; + } + catch (Exception) + { + return; + } + + string message = ReadString(obj, "message"); + string error = ReadString(obj, "error"); + string reqId = ReadString(obj, "reqId"); + + if (!string.IsNullOrEmpty(message)) { exception.ApiErrorMessage = message; } + if (!string.IsNullOrEmpty(error) && string.IsNullOrEmpty(exception.ApiErrorCode)) { exception.ApiErrorCode = error; } + if (!string.IsNullOrEmpty(reqId)) { exception.ApiRequestId = reqId; } + } + + public static string BuildExceptionMessage(int statusCode, string reasonPhrase, string body) + { + string baseMessage = string.Concat( + "Infisical API returned ", + statusCode.ToString(System.Globalization.CultureInfo.InvariantCulture), + " (", reasonPhrase ?? string.Empty, ")."); + + string apiMessage = null; + string apiError = null; + string reqId = null; + + if (!string.IsNullOrEmpty(body)) + { + string trimmed = body.TrimStart(); + if (trimmed.Length > 0 && trimmed[0] == '{') + { + try + { + JToken token = JToken.Parse(body); + if (token.Type == JTokenType.Object) + { + JObject obj = (JObject)token; + apiMessage = ReadString(obj, "message"); + apiError = ReadString(obj, "error"); + reqId = ReadString(obj, "reqId"); + } + } + catch (Exception) + { + } + } + } + + if (string.IsNullOrEmpty(apiMessage) && string.IsNullOrEmpty(apiError) && string.IsNullOrEmpty(reqId)) + { + return baseMessage; + } + + System.Text.StringBuilder builder = new System.Text.StringBuilder(baseMessage); + if (!string.IsNullOrEmpty(apiMessage)) + { + builder.Append(' ').Append(apiMessage); + } + + if (!string.IsNullOrEmpty(apiError) || !string.IsNullOrEmpty(reqId)) + { + builder.Append(" ["); + bool needsSeparator = false; + if (!string.IsNullOrEmpty(apiError)) + { + builder.Append("error=").Append(apiError); + needsSeparator = true; + } + + if (!string.IsNullOrEmpty(reqId)) + { + if (needsSeparator) { builder.Append("; "); } + builder.Append("reqId=").Append(reqId); + } + + builder.Append(']'); + } + + return builder.ToString(); + } + + private static string ReadString(JObject obj, string name) + { + JToken token; + if (obj.TryGetValue(name, StringComparison.OrdinalIgnoreCase, out token) && token != null && token.Type == JTokenType.String) + { + return (string)token; + } + + return null; + } + } +} diff --git a/src/PSInfisicalAPI/Errors/InfisicalErrorDetails.cs b/src/PSInfisicalAPI/Errors/InfisicalErrorDetails.cs index c02a0ae..2cd0aca 100644 --- a/src/PSInfisicalAPI/Errors/InfisicalErrorDetails.cs +++ b/src/PSInfisicalAPI/Errors/InfisicalErrorDetails.cs @@ -10,6 +10,8 @@ namespace PSInfisicalAPI.Errors public int? StatusCode { get; set; } public string ReasonPhrase { get; set; } public string ApiErrorCode { get; set; } + public string ApiErrorMessage { get; set; } + public string ApiRequestId { get; set; } public string SanitizedBody { get; set; } public int? LineNumber { get; set; } public int? LinePosition { get; set; } diff --git a/src/PSInfisicalAPI/Errors/InfisicalErrorHandler.cs b/src/PSInfisicalAPI/Errors/InfisicalErrorHandler.cs index 0da799e..33bc109 100644 --- a/src/PSInfisicalAPI/Errors/InfisicalErrorHandler.cs +++ b/src/PSInfisicalAPI/Errors/InfisicalErrorHandler.cs @@ -26,6 +26,8 @@ namespace PSInfisicalAPI.Errors details.StatusCode = apiException.StatusCode; details.ReasonPhrase = apiException.ReasonPhrase; details.ApiErrorCode = apiException.ApiErrorCode; + details.ApiErrorMessage = apiException.ApiErrorMessage; + details.ApiRequestId = apiException.ApiRequestId; details.SanitizedBody = apiException.SanitizedBody; details.EndpointName = apiException.EndpointName; details.RequestMethod = apiException.RequestMethod; @@ -70,6 +72,16 @@ namespace PSInfisicalAPI.Errors logger.Error(Component, string.Concat("API Error Code: ", details.ApiErrorCode)); } + if (!string.IsNullOrEmpty(details.ApiErrorMessage)) + { + logger.Error(Component, string.Concat("API Error Message: ", details.ApiErrorMessage)); + } + + if (!string.IsNullOrEmpty(details.ApiRequestId)) + { + logger.Error(Component, string.Concat("API Request Id: ", details.ApiRequestId)); + } + if (details.LineNumber.HasValue) { logger.Error(Component, string.Concat("Line: ", details.LineNumber.Value.ToString(CultureInfo.InvariantCulture))); diff --git a/src/PSInfisicalAPI/Errors/InfisicalException.cs b/src/PSInfisicalAPI/Errors/InfisicalException.cs index 88979e9..98f2c1f 100644 --- a/src/PSInfisicalAPI/Errors/InfisicalException.cs +++ b/src/PSInfisicalAPI/Errors/InfisicalException.cs @@ -33,6 +33,8 @@ namespace PSInfisicalAPI.Errors public int StatusCode { get; set; } public string ReasonPhrase { get; set; } public string ApiErrorCode { get; set; } + public string ApiErrorMessage { get; set; } + public string ApiRequestId { get; set; } public string SanitizedBody { get; set; } public string EndpointName { get; set; } public string RequestMethod { get; set; } diff --git a/src/PSInfisicalAPI/Folders/InfisicalFolderClient.cs b/src/PSInfisicalAPI/Folders/InfisicalFolderClient.cs index 4722add..88f4d06 100644 --- a/src/PSInfisicalAPI/Folders/InfisicalFolderClient.cs +++ b/src/PSInfisicalAPI/Folders/InfisicalFolderClient.cs @@ -29,16 +29,14 @@ namespace PSInfisicalAPI.Folders public InfisicalFolder[] List(InfisicalConnection connection, string projectId, string environment, string path) { if (connection == null) { throw new ArgumentNullException(nameof(connection)); } - string resolvedProjectId = FirstNonEmpty(projectId, connection.ProjectId); - string resolvedEnvironment = FirstNonEmpty(environment, connection.Environment); - string resolvedPath = FirstNonEmpty(path, connection.DefaultSecretPath, "/"); - if (string.IsNullOrEmpty(resolvedProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } - if (string.IsNullOrEmpty(resolvedEnvironment)) { throw new InfisicalConfigurationException("Environment is required."); } + if (string.IsNullOrEmpty(projectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + if (string.IsNullOrEmpty(environment)) { throw new InfisicalConfigurationException("Environment is required."); } + string resolvedPath = FirstNonEmpty(path, "/"); List> queryParameters = new List> { - new KeyValuePair("workspaceId", resolvedProjectId), - new KeyValuePair("environment", resolvedEnvironment), + new KeyValuePair("workspaceId", projectId), + new KeyValuePair("environment", environment), new KeyValuePair("path", resolvedPath) }; @@ -49,7 +47,7 @@ namespace PSInfisicalAPI.Folders InfisicalFolderListResponseDto dto = _serializer.Deserialize(response.Body); response.Clear(); - InfisicalFolder[] mapped = InfisicalFolderMapper.MapMany(dto != null ? dto.Folders : null, resolvedProjectId, resolvedEnvironment); + InfisicalFolder[] mapped = InfisicalFolderMapper.MapMany(dto != null ? dto.Folders : null, projectId, environment); _logger.Information(Component, "Infisical folder list retrieval was successful."); return mapped; } @@ -80,17 +78,15 @@ namespace PSInfisicalAPI.Folders public InfisicalFolder Create(InfisicalConnection connection, string projectId, string environment, string name, string path) { if (connection == null) { throw new ArgumentNullException(nameof(connection)); } - string resolvedProjectId = FirstNonEmpty(projectId, connection.ProjectId); - string resolvedEnvironment = FirstNonEmpty(environment, connection.Environment); - string resolvedPath = FirstNonEmpty(path, connection.DefaultSecretPath, "/"); - if (string.IsNullOrEmpty(resolvedProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } - if (string.IsNullOrEmpty(resolvedEnvironment)) { throw new InfisicalConfigurationException("Environment is required."); } + if (string.IsNullOrEmpty(projectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + if (string.IsNullOrEmpty(environment)) { throw new InfisicalConfigurationException("Environment is required."); } if (string.IsNullOrEmpty(name)) { throw new InfisicalConfigurationException("Name is required."); } + string resolvedPath = FirstNonEmpty(path, "/"); InfisicalFolderCreateRequestDto request = new InfisicalFolderCreateRequestDto { - WorkspaceId = resolvedProjectId, - Environment = resolvedEnvironment, + WorkspaceId = projectId, + Environment = environment, Name = name, Path = resolvedPath }; @@ -103,7 +99,7 @@ namespace PSInfisicalAPI.Folders InfisicalFolderSingleResponseDto dto = _serializer.Deserialize(response.Body); response.Clear(); - InfisicalFolder mapped = InfisicalFolderMapper.Map(dto != null ? dto.Folder : null, resolvedProjectId, resolvedEnvironment); + InfisicalFolder mapped = InfisicalFolderMapper.Map(dto != null ? dto.Folder : null, projectId, environment); _logger.Information(Component, "Infisical folder creation was successful."); return mapped; } @@ -117,19 +113,17 @@ namespace PSInfisicalAPI.Folders public InfisicalFolder Update(InfisicalConnection connection, string projectId, string environment, string folderId, string name, string path) { if (connection == null) { throw new ArgumentNullException(nameof(connection)); } - string resolvedProjectId = FirstNonEmpty(projectId, connection.ProjectId); - string resolvedEnvironment = FirstNonEmpty(environment, connection.Environment); - string resolvedPath = FirstNonEmpty(path, connection.DefaultSecretPath, "/"); - if (string.IsNullOrEmpty(resolvedProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } - if (string.IsNullOrEmpty(resolvedEnvironment)) { throw new InfisicalConfigurationException("Environment is required."); } + if (string.IsNullOrEmpty(projectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + if (string.IsNullOrEmpty(environment)) { throw new InfisicalConfigurationException("Environment is required."); } if (string.IsNullOrEmpty(folderId)) { throw new InfisicalConfigurationException("FolderId is required."); } + string resolvedPath = FirstNonEmpty(path, "/"); if (string.IsNullOrEmpty(name)) { throw new InfisicalConfigurationException("Name is required."); } Dictionary pathParameters = new Dictionary { { "folderId", folderId } }; InfisicalFolderUpdateRequestDto request = new InfisicalFolderUpdateRequestDto { - WorkspaceId = resolvedProjectId, - Environment = resolvedEnvironment, + WorkspaceId = projectId, + Environment = environment, Name = name, Path = resolvedPath }; @@ -142,7 +136,7 @@ namespace PSInfisicalAPI.Folders InfisicalFolderSingleResponseDto dto = _serializer.Deserialize(response.Body); response.Clear(); - InfisicalFolder mapped = InfisicalFolderMapper.Map(dto != null ? dto.Folder : null, resolvedProjectId, resolvedEnvironment); + InfisicalFolder mapped = InfisicalFolderMapper.Map(dto != null ? dto.Folder : null, projectId, environment); _logger.Information(Component, "Infisical folder update was successful."); return mapped; } @@ -156,18 +150,16 @@ namespace PSInfisicalAPI.Folders public void Delete(InfisicalConnection connection, string projectId, string environment, string folderId, string path) { if (connection == null) { throw new ArgumentNullException(nameof(connection)); } - string resolvedProjectId = FirstNonEmpty(projectId, connection.ProjectId); - string resolvedEnvironment = FirstNonEmpty(environment, connection.Environment); - string resolvedPath = FirstNonEmpty(path, connection.DefaultSecretPath, "/"); - if (string.IsNullOrEmpty(resolvedProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } - if (string.IsNullOrEmpty(resolvedEnvironment)) { throw new InfisicalConfigurationException("Environment is required."); } + if (string.IsNullOrEmpty(projectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + if (string.IsNullOrEmpty(environment)) { throw new InfisicalConfigurationException("Environment is required."); } if (string.IsNullOrEmpty(folderId)) { throw new InfisicalConfigurationException("FolderId is required."); } + string resolvedPath = FirstNonEmpty(path, "/"); Dictionary pathParameters = new Dictionary { { "folderId", folderId } }; List> queryParameters = new List> { - new KeyValuePair("workspaceId", resolvedProjectId), - new KeyValuePair("environment", resolvedEnvironment), + new KeyValuePair("workspaceId", projectId), + new KeyValuePair("environment", environment), new KeyValuePair("path", resolvedPath) }; diff --git a/src/PSInfisicalAPI/Http/InfisicalApiInvoker.cs b/src/PSInfisicalAPI/Http/InfisicalApiInvoker.cs index ffd988e..7e88cf7 100644 --- a/src/PSInfisicalAPI/Http/InfisicalApiInvoker.cs +++ b/src/PSInfisicalAPI/Http/InfisicalApiInvoker.cs @@ -23,7 +23,8 @@ namespace PSInfisicalAPI.Http string operationName, IDictionary pathParameters, IEnumerable> queryParameters, - string body) + string body, + IDictionary extraHeaders = null) { if (connection == null) { throw new ArgumentNullException(nameof(connection)); } if (string.IsNullOrEmpty(endpointName)) { throw new ArgumentNullException(nameof(endpointName)); } @@ -31,7 +32,7 @@ namespace PSInfisicalAPI.Http InfisicalEndpointDefinition definition = InfisicalEndpointRegistry.Get(endpointName); Uri uri = InfisicalUriBuilder.Build(connection.BaseUri, definition, pathParameters, queryParameters); - InfisicalHttpResponse response = ExecuteAuthorized(connection, definition, operationName, uri, body); + InfisicalHttpResponse response = ExecuteAuthorized(connection, definition, operationName, uri, body, extraHeaders); if (response.StatusCode >= 200 && response.StatusCode < 300) { @@ -49,7 +50,8 @@ namespace PSInfisicalAPI.Http string operationName, IDictionary pathParameters, IEnumerable> queryParameters, - string body) + string body, + IDictionary extraHeaders = null) { if (connection == null) { throw new ArgumentNullException(nameof(connection)); } if (string.IsNullOrEmpty(endpointName)) { throw new ArgumentNullException(nameof(endpointName)); } @@ -61,7 +63,7 @@ namespace PSInfisicalAPI.Http { InfisicalEndpointDefinition definition = candidates[index]; Uri uri = InfisicalUriBuilder.Build(connection.BaseUri, definition, pathParameters, queryParameters); - InfisicalHttpResponse response = ExecuteAuthorized(connection, definition, operationName, uri, body); + InfisicalHttpResponse response = ExecuteAuthorized(connection, definition, operationName, uri, body, extraHeaders); if (response.StatusCode >= 200 && response.StatusCode < 300) { @@ -95,7 +97,8 @@ namespace PSInfisicalAPI.Http InfisicalEndpointDefinition definition, string operationName, Uri uri, - string body) + string body, + IDictionary extraHeaders = null) { Dictionary headers = new Dictionary(StringComparer.OrdinalIgnoreCase); headers["Accept"] = "application/json"; @@ -118,6 +121,15 @@ namespace PSInfisicalAPI.Http }); } + if (extraHeaders != null) + { + foreach (KeyValuePair entry in extraHeaders) + { + if (string.IsNullOrEmpty(entry.Key)) { continue; } + headers[entry.Key] = entry.Value; + } + } + InfisicalHttpRequest request = new InfisicalHttpRequest { OperationName = operationName, @@ -135,15 +147,14 @@ namespace PSInfisicalAPI.Http private static InfisicalApiException BuildApiException(InfisicalHttpResponse response, InfisicalEndpointDefinition definition) { - InfisicalApiException exception = new InfisicalApiException(string.Concat( - "Infisical API returned ", - response.StatusCode.ToString(CultureInfo.InvariantCulture), - " (", response.ReasonPhrase ?? string.Empty, ").")); + string message = InfisicalApiErrorEnvelope.BuildExceptionMessage(response.StatusCode, response.ReasonPhrase, response.Body); + InfisicalApiException exception = new InfisicalApiException(message); exception.StatusCode = response.StatusCode; exception.ReasonPhrase = response.ReasonPhrase; exception.EndpointName = definition.Name; exception.RequestMethod = definition.Method; exception.SanitizedBody = response.Body; + InfisicalApiErrorEnvelope.Enrich(exception, response.Body); return exception; } } diff --git a/src/PSInfisicalAPI/Models/InfisicalCertificateApplication.cs b/src/PSInfisicalAPI/Models/InfisicalCertificateApplication.cs new file mode 100644 index 0000000..170e366 --- /dev/null +++ b/src/PSInfisicalAPI/Models/InfisicalCertificateApplication.cs @@ -0,0 +1,31 @@ +using System; + +namespace PSInfisicalAPI.Models +{ + public sealed class InfisicalCertificateApplication + { + public string Id { get; set; } + public string ProjectId { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public int? ProfileCount { get; set; } + public int? MemberCount { get; set; } + public int? CertificateCount { get; set; } + public DateTimeOffset? CreatedAtUtc { get; set; } + public DateTimeOffset? UpdatedAtUtc { get; set; } + } + + public sealed class InfisicalCertificateApplicationProfileAttachment + { + public string ApplicationId { get; set; } + public string ProfileId { get; set; } + public string ProfileSlug { get; set; } + public string ProfileDescription { get; set; } + public string ApiConfigId { get; set; } + public string EstConfigId { get; set; } + public string AcmeConfigId { get; set; } + public string ScepConfigId { get; set; } + public DateTimeOffset? CreatedAtUtc { get; set; } + public DateTimeOffset? UpdatedAtUtc { get; set; } + } +} diff --git a/src/PSInfisicalAPI/Models/InfisicalCertificateApplicationEnrollment.cs b/src/PSInfisicalAPI/Models/InfisicalCertificateApplicationEnrollment.cs new file mode 100644 index 0000000..1a5a9ef --- /dev/null +++ b/src/PSInfisicalAPI/Models/InfisicalCertificateApplicationEnrollment.cs @@ -0,0 +1,55 @@ +using System; + +namespace PSInfisicalAPI.Models +{ + public sealed class InfisicalCertificateApplicationEnrollment + { + public string ApplicationId { get; set; } + public string ProfileId { get; set; } + public InfisicalCertificateApplicationApiEnrollment Api { get; set; } + public InfisicalCertificateApplicationEstEnrollment Est { get; set; } + public InfisicalCertificateApplicationAcmeEnrollment Acme { get; set; } + public InfisicalCertificateApplicationScepEnrollment Scep { get; set; } + public bool ApiConfigured { get { return Api != null; } } + public bool EstConfigured { get; set; } + public bool AcmeConfigured { get; set; } + public bool ScepConfigured { get; set; } + } + + public sealed class InfisicalCertificateApplicationApiEnrollment + { + public string Id { get; set; } + public bool? AutoRenew { get; set; } + public int? RenewBeforeDays { get; set; } + } + + public sealed class InfisicalCertificateApplicationEstEnrollment + { + public string Id { get; set; } + public bool? DisableBootstrapCaValidation { get; set; } + public string EstEndpointUrl { get; set; } + } + + public sealed class InfisicalCertificateApplicationAcmeEnrollment + { + public string Id { get; set; } + public bool? SkipDnsOwnershipVerification { get; set; } + public bool? SkipEabBinding { get; set; } + public string DirectoryUrl { get; set; } + } + + public sealed class InfisicalCertificateApplicationScepEnrollment + { + public string Id { get; set; } + public string ChallengeType { get; set; } + public bool? IncludeCaCertInResponse { get; set; } + public bool? AllowCertBasedRenewal { get; set; } + public int? DynamicChallengeExpiryMinutes { get; set; } + public int? DynamicChallengeMaxPending { get; set; } + public string ScepEndpointUrl { get; set; } + public string ChallengeEndpointUrl { get; set; } + public string RaCertificatePem { get; set; } + public string RaCertificateThumbprint { get; set; } + public DateTimeOffset? RaCertExpiresAtUtc { get; set; } + } +} diff --git a/src/PSInfisicalAPI/Models/InfisicalCertificatePolicy.cs b/src/PSInfisicalAPI/Models/InfisicalCertificatePolicy.cs new file mode 100644 index 0000000..5708648 --- /dev/null +++ b/src/PSInfisicalAPI/Models/InfisicalCertificatePolicy.cs @@ -0,0 +1,50 @@ +using System; + +namespace PSInfisicalAPI.Models +{ + public sealed class InfisicalCertificatePolicy + { + public string Id { get; set; } + public string ProjectId { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public InfisicalCertificatePolicySubject Subject { get; set; } + public InfisicalCertificatePolicySan[] Sans { get; set; } + public InfisicalCertificatePolicyUsages KeyUsages { get; set; } + public InfisicalCertificatePolicyUsages ExtendedKeyUsages { get; set; } + public InfisicalCertificatePolicyAlgorithms Algorithms { get; set; } + public InfisicalCertificatePolicyValidity Validity { get; set; } + public DateTimeOffset? CreatedAtUtc { get; set; } + public DateTimeOffset? UpdatedAtUtc { get; set; } + } + + public sealed class InfisicalCertificatePolicySubject + { + public string Type { get; set; } + public string[] Allowed { get; set; } + } + + public sealed class InfisicalCertificatePolicySan + { + public string Type { get; set; } + public string[] Allowed { get; set; } + public string[] Required { get; set; } + } + + public sealed class InfisicalCertificatePolicyUsages + { + public string[] Allowed { get; set; } + public string[] Required { get; set; } + } + + public sealed class InfisicalCertificatePolicyAlgorithms + { + public string Signature { get; set; } + public string[] KeyAlgorithms { get; set; } + } + + public sealed class InfisicalCertificatePolicyValidity + { + public string Max { get; set; } + } +} diff --git a/src/PSInfisicalAPI/Models/InfisicalCertificateProfile.cs b/src/PSInfisicalAPI/Models/InfisicalCertificateProfile.cs new file mode 100644 index 0000000..0497414 --- /dev/null +++ b/src/PSInfisicalAPI/Models/InfisicalCertificateProfile.cs @@ -0,0 +1,58 @@ +using System; + +namespace PSInfisicalAPI.Models +{ + public sealed class InfisicalCertificateProfile + { + public string Id { get; set; } + public string ProjectId { get; set; } + public string CaId { get; set; } + public string CertificatePolicyId { get; set; } + public string Slug { get; set; } + public string Description { get; set; } + public string EnrollmentType { get; set; } + public string IssuerType { get; set; } + public string EstConfigId { get; set; } + public string ApiConfigId { get; set; } + public string AcmeConfigId { get; set; } + public string ScepConfigId { get; set; } + public DateTimeOffset? CreatedAtUtc { get; set; } + public DateTimeOffset? UpdatedAtUtc { get; set; } + public InfisicalCertificateProfileDefaults Defaults { get; set; } + public InfisicalCertificateAuthoritySummary CertificateAuthority { get; set; } + public InfisicalCertificatePolicySummary CertificatePolicy { get; set; } + public InfisicalCertificateProfileApiConfig ApiConfig { get; set; } + } + + public sealed class InfisicalCertificateProfileDefaults + { + public int? TtlDays { get; set; } + public string KeyAlgorithm { get; set; } + public string SignatureAlgorithm { get; set; } + public string[] KeyUsages { get; set; } + public string[] ExtendedKeyUsages { get; set; } + } + + public sealed class InfisicalCertificateAuthoritySummary + { + public string Id { get; set; } + public string Status { get; set; } + public string Name { get; set; } + public bool? IsExternal { get; set; } + public string ExternalType { get; set; } + } + + public sealed class InfisicalCertificatePolicySummary + { + public string Id { get; set; } + public string ProjectId { get; set; } + public string Name { get; set; } + } + + public sealed class InfisicalCertificateProfileApiConfig + { + public string Id { get; set; } + public bool? AutoRenew { get; set; } + public int? RenewBeforeDays { get; set; } + } +} diff --git a/src/PSInfisicalAPI/Models/InfisicalCertificateResult.cs b/src/PSInfisicalAPI/Models/InfisicalCertificateResult.cs new file mode 100644 index 0000000..7dc5404 --- /dev/null +++ b/src/PSInfisicalAPI/Models/InfisicalCertificateResult.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; + +namespace PSInfisicalAPI.Models +{ + public sealed class InfisicalCertificateResult + { + public X509Certificate2 Leaf { get; set; } + public X509Certificate2[] Intermediates { get; set; } + public X509Certificate2 Root { get; set; } + public X509Certificate2[] Chain { get; set; } + public string SerialNumber { get; set; } + public string CertificatePem { get; set; } + public string CertificateChainPem { get; set; } + public string PrivateKeyPem { get; set; } + public string Status { get; set; } + public string StatusMessage { get; set; } + public string CertificateRequestId { get; set; } + + public override string ToString() + { + if (Leaf != null) { return Leaf.Subject; } + return SerialNumber; + } + } +} diff --git a/src/PSInfisicalAPI/Models/InfisicalPkiSubscriber.cs b/src/PSInfisicalAPI/Models/InfisicalPkiSubscriber.cs new file mode 100644 index 0000000..66a1953 --- /dev/null +++ b/src/PSInfisicalAPI/Models/InfisicalPkiSubscriber.cs @@ -0,0 +1,37 @@ +using System; + +namespace PSInfisicalAPI.Models +{ + public sealed class InfisicalPkiSubscriber + { + public string Id { get; set; } + public string ProjectId { get; set; } + public string CaId { get; set; } + public string Name { get; set; } + public string CommonName { get; set; } + public string Status { get; set; } + public string Ttl { get; set; } + public string[] SubjectAlternativeNames { get; set; } + public string[] KeyUsages { get; set; } + public string[] ExtendedKeyUsages { get; set; } + public bool? EnableAutoRenewal { get; set; } + public int? AutoRenewalPeriodInDays { get; set; } + public string LastOperationStatus { get; set; } + public string LastOperationMessage { get; set; } + public DateTimeOffset? LastOperationAtUtc { get; set; } + public DateTimeOffset? CreatedAtUtc { get; set; } + public DateTimeOffset? UpdatedAtUtc { get; set; } + public InfisicalPkiSubscriberProperties Properties { get; set; } + } + + public sealed class InfisicalPkiSubscriberProperties + { + public string AzureTemplateType { get; set; } + public string Organization { get; set; } + public string OrganizationalUnit { get; set; } + public string Country { get; set; } + public string State { get; set; } + public string Locality { get; set; } + public string EmailAddress { get; set; } + } +} diff --git a/src/PSInfisicalAPI/Models/InfisicalScepMdmProfile.cs b/src/PSInfisicalAPI/Models/InfisicalScepMdmProfile.cs new file mode 100644 index 0000000..7c591f1 --- /dev/null +++ b/src/PSInfisicalAPI/Models/InfisicalScepMdmProfile.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Xml; +using System.Xml.Linq; + +namespace PSInfisicalAPI.Models +{ + public sealed class InfisicalScepMdmProfile + { + private const string SyncMlMetInfNamespace = "syncml:metinf"; + + public string UniqueId { get; set; } + public string Scope { get; set; } + + public string ServerUrl { get; set; } + public string Challenge { get; set; } + + public string SubjectName { get; set; } + public string SubjectAlternativeNames { get; set; } + public string EkuMapping { get; set; } + public int? KeyUsage { get; set; } + + public int? KeyLength { get; set; } + public string KeyAlgorithm { get; set; } + public string HashAlgorithm { get; set; } + public int? KeyProtection { get; set; } + public string ContainerName { get; set; } + + public string ValidPeriod { get; set; } + public int? ValidPeriodUnits { get; set; } + public int? RetryCount { get; set; } + public int? RetryDelay { get; set; } + + public string TemplateName { get; set; } + public string CAThumbprint { get; set; } + public string CustomTextToShowInPrompt { get; set; } + + public string SourceProfileId { get; set; } + public string SourceProfileSlug { get; set; } + + public string ToSyncMl() + { + if (string.IsNullOrEmpty(UniqueId)) { throw new InvalidOperationException("UniqueId is required."); } + if (string.IsNullOrEmpty(ServerUrl)) { throw new InvalidOperationException("ServerUrl is required."); } + + string scopeSegment = string.Equals(Scope, "User", StringComparison.OrdinalIgnoreCase) ? "./User" : "./Device"; + string nodeBase = string.Concat(scopeSegment, "/Vendor/MSFT/ClientCertificateInstall/SCEP/", UniqueId, "/Install/"); + + List nodes = new List(); + AddString(nodes, "ServerURL", ServerUrl); + AddString(nodes, "Challenge", Challenge); + AddString(nodes, "SubjectName", SubjectName); + AddString(nodes, "SubjectAlternativeNames", SubjectAlternativeNames); + AddString(nodes, "EKUMapping", EkuMapping); + AddInt(nodes, "KeyUsage", KeyUsage); + AddInt(nodes, "KeyLength", KeyLength); + AddString(nodes, "KeyAlgorithm", KeyAlgorithm); + AddString(nodes, "HashAlgorithm", HashAlgorithm); + AddInt(nodes, "KeyProtection", KeyProtection); + AddString(nodes, "ContainerName", ContainerName); + AddString(nodes, "ValidPeriod", ValidPeriod); + AddInt(nodes, "ValidPeriodUnits", ValidPeriodUnits); + AddInt(nodes, "RetryCount", RetryCount); + AddInt(nodes, "RetryDelay", RetryDelay); + AddString(nodes, "TemplateName", TemplateName); + AddString(nodes, "CAThumbprint", CAThumbprint); + AddString(nodes, "CustomTextToShowInPrompt", CustomTextToShowInPrompt); + + XDocument document = new XDocument(new XDeclaration("1.0", "utf-8", null)); + XElement syncBody = new XElement("SyncBody"); + XElement atomic = new XElement("Atomic", new XElement("CmdID", "1")); + + int cmdId = 2; + foreach (CspNode node in nodes) + { + XElement meta = new XElement("Meta", new XElement(XName.Get("Format", SyncMlMetInfNamespace), node.Format)); + XElement item = new XElement("Item", + new XElement("Target", new XElement("LocURI", string.Concat(nodeBase, node.Suffix))), + meta, + new XElement("Data", node.Value)); + atomic.Add(new XElement("Replace", new XElement("CmdID", cmdId.ToString(System.Globalization.CultureInfo.InvariantCulture)), item)); + cmdId++; + } + + XElement enrollItem = new XElement("Item", + new XElement("Target", new XElement("LocURI", string.Concat(nodeBase, "Enroll"))), + new XElement("Meta", new XElement(XName.Get("Format", SyncMlMetInfNamespace), "node"))); + atomic.Add(new XElement("Exec", new XElement("CmdID", cmdId.ToString(System.Globalization.CultureInfo.InvariantCulture)), enrollItem)); + + syncBody.Add(atomic); + document.Add(syncBody); + + XmlWriterSettings writerSettings = new XmlWriterSettings + { + Indent = true, + IndentChars = " ", + NewLineHandling = NewLineHandling.Replace, + Encoding = new UTF8Encoding(false), + OmitXmlDeclaration = false, + CloseOutput = false + }; + + string serialized; + using (MemoryStream buffer = new MemoryStream()) + { + using (XmlWriter writer = XmlWriter.Create(buffer, writerSettings)) + { + document.Save(writer); + } + serialized = writerSettings.Encoding.GetString(buffer.ToArray()); + } + + using (StringReader stringReader = new StringReader(serialized)) + { + XmlReaderSettings readerSettings = new XmlReaderSettings { DtdProcessing = DtdProcessing.Prohibit, XmlResolver = null }; + using (XmlReader reader = XmlReader.Create(stringReader, readerSettings)) + { + XDocument.Load(reader); + } + } + + return serialized; + } + + private static void AddString(List nodes, string suffix, string value) + { + if (string.IsNullOrEmpty(value)) { return; } + nodes.Add(new CspNode { Suffix = suffix, Value = value, Format = "chr" }); + } + + private static void AddInt(List nodes, string suffix, int? value) + { + if (!value.HasValue) { return; } + nodes.Add(new CspNode { Suffix = suffix, Value = value.Value.ToString(System.Globalization.CultureInfo.InvariantCulture), Format = "int" }); + } + + private sealed class CspNode + { + public string Suffix; + public string Value; + public string Format; + } + } +} diff --git a/src/PSInfisicalAPI/Models/InfisicalSignedCertificate.cs b/src/PSInfisicalAPI/Models/InfisicalSignedCertificate.cs new file mode 100644 index 0000000..24b1a91 --- /dev/null +++ b/src/PSInfisicalAPI/Models/InfisicalSignedCertificate.cs @@ -0,0 +1,19 @@ +namespace PSInfisicalAPI.Models +{ + public sealed class InfisicalSignedCertificate + { + public string SerialNumber { get; set; } + public string CertificatePem { get; set; } + public string CertificateChainPem { get; set; } + public string IssuingCaCertificatePem { get; set; } + public string PrivateKeyPem { get; set; } + public string Status { get; set; } + public string StatusMessage { get; set; } + public string CertificateRequestId { get; set; } + + public override string ToString() + { + return SerialNumber; + } + } +} diff --git a/src/PSInfisicalAPI/Pki/InfisicalCaDtos.cs b/src/PSInfisicalAPI/Pki/InfisicalCaDtos.cs index 01c45fc..553dd5e 100644 --- a/src/PSInfisicalAPI/Pki/InfisicalCaDtos.cs +++ b/src/PSInfisicalAPI/Pki/InfisicalCaDtos.cs @@ -3,6 +3,26 @@ using Newtonsoft.Json; namespace PSInfisicalAPI.Pki { + internal sealed class InfisicalInternalCaConfigurationDto + { + [JsonProperty("type")] public string Type { get; set; } + [JsonProperty("friendlyName")] public string FriendlyName { get; set; } + [JsonProperty("commonName")] public string CommonName { get; set; } + [JsonProperty("organization")] public string OrganizationName { get; set; } + [JsonProperty("ou")] public string OrganizationUnit { get; set; } + [JsonProperty("country")] public string Country { get; set; } + [JsonProperty("province")] public string State { get; set; } + [JsonProperty("locality")] public string Locality { get; set; } + [JsonProperty("notBefore")] public string NotBefore { get; set; } + [JsonProperty("notAfter")] public string NotAfter { get; set; } + [JsonProperty("maxPathLength")] public int? MaxPathLength { get; set; } + [JsonProperty("keyAlgorithm")] public string KeyAlgorithm { get; set; } + [JsonProperty("dn")] public string DistinguishedName { get; set; } + [JsonProperty("parentCaId")] public string ParentCaId { get; set; } + [JsonProperty("serialNumber")] public string SerialNumber { get; set; } + [JsonProperty("activeCaCertId")] public string ActiveCaCertId { get; set; } + } + internal sealed class InfisicalInternalCaResponseDto { [JsonProperty("id")] public string Id { get; set; } @@ -28,6 +48,7 @@ namespace PSInfisicalAPI.Pki [JsonProperty("activeCaCertId")] public string ActiveCaCertId { get; set; } [JsonProperty("createdAt")] public string CreatedAt { get; set; } [JsonProperty("updatedAt")] public string UpdatedAt { get; set; } + [JsonProperty("configuration")] public InfisicalInternalCaConfigurationDto Configuration { get; set; } } internal sealed class InfisicalInternalCaListResponseDto diff --git a/src/PSInfisicalAPI/Pki/InfisicalCaMapper.cs b/src/PSInfisicalAPI/Pki/InfisicalCaMapper.cs index dc83822..aa1c3dd 100644 --- a/src/PSInfisicalAPI/Pki/InfisicalCaMapper.cs +++ b/src/PSInfisicalAPI/Pki/InfisicalCaMapper.cs @@ -14,34 +14,41 @@ namespace PSInfisicalAPI.Pki return null; } + InfisicalInternalCaConfigurationDto cfg = dto.Configuration; + return new InfisicalCertificateAuthority { Id = dto.Id, ProjectId = !string.IsNullOrEmpty(dto.ProjectId) ? dto.ProjectId : fallbackProjectId, Name = dto.Name, - FriendlyName = dto.FriendlyName, - Type = dto.Type, + FriendlyName = Coalesce(cfg != null ? cfg.FriendlyName : null, dto.FriendlyName), + Type = Coalesce(dto.Type, cfg != null ? cfg.Type : null), Status = dto.Status, EnableDirectIssuance = dto.EnableDirectIssuance, - KeyAlgorithm = dto.KeyAlgorithm, - DistinguishedName = dto.DistinguishedName, - OrganizationName = dto.OrganizationName, - OrganizationUnit = dto.OrganizationUnit, - Country = dto.Country, - State = dto.State, - Locality = dto.Locality, - CommonName = dto.CommonName, - MaxPathLength = dto.MaxPathLength, - NotBefore = dto.NotBefore, - NotAfter = dto.NotAfter, - SerialNumber = dto.SerialNumber, - ParentCaId = dto.ParentCaId, - ActiveCaCertId = dto.ActiveCaCertId, + KeyAlgorithm = Coalesce(cfg != null ? cfg.KeyAlgorithm : null, dto.KeyAlgorithm), + DistinguishedName = Coalesce(cfg != null ? cfg.DistinguishedName : null, dto.DistinguishedName), + OrganizationName = Coalesce(cfg != null ? cfg.OrganizationName : null, dto.OrganizationName), + OrganizationUnit = Coalesce(cfg != null ? cfg.OrganizationUnit : null, dto.OrganizationUnit), + Country = Coalesce(cfg != null ? cfg.Country : null, dto.Country), + State = Coalesce(cfg != null ? cfg.State : null, dto.State), + Locality = Coalesce(cfg != null ? cfg.Locality : null, dto.Locality), + CommonName = Coalesce(cfg != null ? cfg.CommonName : null, dto.CommonName), + MaxPathLength = (cfg != null && cfg.MaxPathLength.HasValue) ? cfg.MaxPathLength : dto.MaxPathLength, + NotBefore = Coalesce(cfg != null ? cfg.NotBefore : null, dto.NotBefore), + NotAfter = Coalesce(cfg != null ? cfg.NotAfter : null, dto.NotAfter), + SerialNumber = Coalesce(cfg != null ? cfg.SerialNumber : null, dto.SerialNumber), + ParentCaId = Coalesce(cfg != null ? cfg.ParentCaId : null, dto.ParentCaId), + ActiveCaCertId = Coalesce(cfg != null ? cfg.ActiveCaCertId : null, dto.ActiveCaCertId), CreatedAtUtc = ParseTimestamp(dto.CreatedAt), UpdatedAtUtc = ParseTimestamp(dto.UpdatedAt) }; } + private static string Coalesce(string primary, string fallback) + { + return !string.IsNullOrEmpty(primary) ? primary : fallback; + } + public static InfisicalCertificateAuthority[] MapMany(IEnumerable items, string fallbackProjectId) { if (items == null) diff --git a/src/PSInfisicalAPI/Pki/InfisicalCertificateApplicationDtos.cs b/src/PSInfisicalAPI/Pki/InfisicalCertificateApplicationDtos.cs new file mode 100644 index 0000000..356c616 --- /dev/null +++ b/src/PSInfisicalAPI/Pki/InfisicalCertificateApplicationDtos.cs @@ -0,0 +1,92 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace PSInfisicalAPI.Pki +{ + internal sealed class InfisicalCertificateApplicationResponseDto + { + [JsonProperty("id")] public string Id { get; set; } + [JsonProperty("projectId")] public string ProjectId { get; set; } + [JsonProperty("name")] public string Name { get; set; } + [JsonProperty("description")] public string Description { get; set; } + [JsonProperty("profileCount")] public int? ProfileCount { get; set; } + [JsonProperty("memberCount")] public int? MemberCount { get; set; } + [JsonProperty("certificateCount")] public int? CertificateCount { get; set; } + [JsonProperty("createdAt")] public string CreatedAt { get; set; } + [JsonProperty("updatedAt")] public string UpdatedAt { get; set; } + } + + internal sealed class InfisicalCertificateApplicationListResponseDto + { + [JsonProperty("applications")] public List Applications { get; set; } + [JsonProperty("total")] public int? Total { get; set; } + } + + internal sealed class InfisicalCertificateApplicationProfileAttachmentDto + { + [JsonProperty("applicationId")] public string ApplicationId { get; set; } + [JsonProperty("profileId")] public string ProfileId { get; set; } + [JsonProperty("profileSlug")] public string ProfileSlug { get; set; } + [JsonProperty("profileDescription")] public string ProfileDescription { get; set; } + [JsonProperty("apiConfigId")] public string ApiConfigId { get; set; } + [JsonProperty("estConfigId")] public string EstConfigId { get; set; } + [JsonProperty("acmeConfigId")] public string AcmeConfigId { get; set; } + [JsonProperty("scepConfigId")] public string ScepConfigId { get; set; } + [JsonProperty("createdAt")] public string CreatedAt { get; set; } + [JsonProperty("updatedAt")] public string UpdatedAt { get; set; } + } + + internal sealed class InfisicalCertificateApplicationProfilesResponseDto + { + [JsonProperty("profiles")] public List Profiles { get; set; } + } + + internal sealed class InfisicalCertificateApplicationEnrollmentResponseDto + { + [JsonProperty("applicationId")] public string ApplicationId { get; set; } + [JsonProperty("profileId")] public string ProfileId { get; set; } + [JsonProperty("api")] public InfisicalCertificateApplicationApiEnrollmentDto Api { get; set; } + [JsonProperty("est")] public InfisicalCertificateApplicationEstEnrollmentDto Est { get; set; } + [JsonProperty("acme")] public InfisicalCertificateApplicationAcmeEnrollmentDto Acme { get; set; } + [JsonProperty("scep")] public InfisicalCertificateApplicationScepEnrollmentDto Scep { get; set; } + [JsonProperty("estConfigured")] public bool? EstConfigured { get; set; } + [JsonProperty("acmeConfigured")] public bool? AcmeConfigured { get; set; } + [JsonProperty("scepConfigured")] public bool? ScepConfigured { get; set; } + } + + internal sealed class InfisicalCertificateApplicationApiEnrollmentDto + { + [JsonProperty("id")] public string Id { get; set; } + [JsonProperty("autoRenew")] public bool? AutoRenew { get; set; } + [JsonProperty("renewBeforeDays")] public int? RenewBeforeDays { get; set; } + } + + internal sealed class InfisicalCertificateApplicationEstEnrollmentDto + { + [JsonProperty("id")] public string Id { get; set; } + [JsonProperty("disableBootstrapCaValidation")] public bool? DisableBootstrapCaValidation { get; set; } + [JsonProperty("estEndpointUrl")] public string EstEndpointUrl { get; set; } + } + + internal sealed class InfisicalCertificateApplicationAcmeEnrollmentDto + { + [JsonProperty("id")] public string Id { get; set; } + [JsonProperty("skipDnsOwnershipVerification")] public bool? SkipDnsOwnershipVerification { get; set; } + [JsonProperty("skipEabBinding")] public bool? SkipEabBinding { get; set; } + [JsonProperty("directoryUrl")] public string DirectoryUrl { get; set; } + } + + internal sealed class InfisicalCertificateApplicationScepEnrollmentDto + { + [JsonProperty("id")] public string Id { get; set; } + [JsonProperty("challengeType")] public string ChallengeType { get; set; } + [JsonProperty("includeCaCertInResponse")] public bool? IncludeCaCertInResponse { get; set; } + [JsonProperty("allowCertBasedRenewal")] public bool? AllowCertBasedRenewal { get; set; } + [JsonProperty("dynamicChallengeExpiryMinutes")] public int? DynamicChallengeExpiryMinutes { get; set; } + [JsonProperty("dynamicChallengeMaxPending")] public int? DynamicChallengeMaxPending { get; set; } + [JsonProperty("scepEndpointUrl")] public string ScepEndpointUrl { get; set; } + [JsonProperty("challengeEndpointUrl")] public string ChallengeEndpointUrl { get; set; } + [JsonProperty("raCertificatePem")] public string RaCertificatePem { get; set; } + [JsonProperty("raCertExpiresAt")] public string RaCertExpiresAt { get; set; } + } +} diff --git a/src/PSInfisicalAPI/Pki/InfisicalCertificateApplicationMapper.cs b/src/PSInfisicalAPI/Pki/InfisicalCertificateApplicationMapper.cs new file mode 100644 index 0000000..7798243 --- /dev/null +++ b/src/PSInfisicalAPI/Pki/InfisicalCertificateApplicationMapper.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using PSInfisicalAPI.Models; + +namespace PSInfisicalAPI.Pki +{ + internal static class InfisicalCertificateApplicationMapper + { + public static InfisicalCertificateApplication Map(InfisicalCertificateApplicationResponseDto dto, string fallbackProjectId) + { + if (dto == null) { return null; } + return new InfisicalCertificateApplication + { + Id = dto.Id, + ProjectId = !string.IsNullOrEmpty(dto.ProjectId) ? dto.ProjectId : fallbackProjectId, + Name = dto.Name, + Description = dto.Description, + ProfileCount = dto.ProfileCount, + MemberCount = dto.MemberCount, + CertificateCount = dto.CertificateCount, + CreatedAtUtc = ParseTimestamp(dto.CreatedAt), + UpdatedAtUtc = ParseTimestamp(dto.UpdatedAt) + }; + } + + public static InfisicalCertificateApplication[] MapMany(IEnumerable items, string fallbackProjectId) + { + if (items == null) { return Array.Empty(); } + List results = new List(); + foreach (InfisicalCertificateApplicationResponseDto dto in items) + { + InfisicalCertificateApplication mapped = Map(dto, fallbackProjectId); + if (mapped != null) { results.Add(mapped); } + } + + return results.ToArray(); + } + + public static InfisicalCertificateApplicationProfileAttachment MapAttachment(InfisicalCertificateApplicationProfileAttachmentDto dto) + { + if (dto == null) { return null; } + return new InfisicalCertificateApplicationProfileAttachment + { + ApplicationId = dto.ApplicationId, + ProfileId = dto.ProfileId, + ProfileSlug = dto.ProfileSlug, + ProfileDescription = dto.ProfileDescription, + ApiConfigId = dto.ApiConfigId, + EstConfigId = dto.EstConfigId, + AcmeConfigId = dto.AcmeConfigId, + ScepConfigId = dto.ScepConfigId, + CreatedAtUtc = ParseTimestamp(dto.CreatedAt), + UpdatedAtUtc = ParseTimestamp(dto.UpdatedAt) + }; + } + + public static InfisicalCertificateApplicationProfileAttachment[] MapAttachments(IEnumerable items) + { + if (items == null) { return Array.Empty(); } + List results = new List(); + foreach (InfisicalCertificateApplicationProfileAttachmentDto dto in items) + { + InfisicalCertificateApplicationProfileAttachment mapped = MapAttachment(dto); + if (mapped != null) { results.Add(mapped); } + } + + return results.ToArray(); + } + + public static InfisicalCertificateApplicationEnrollment MapEnrollment(InfisicalCertificateApplicationEnrollmentResponseDto dto) + { + if (dto == null) { return null; } + return new InfisicalCertificateApplicationEnrollment + { + ApplicationId = dto.ApplicationId, + ProfileId = dto.ProfileId, + Api = MapApi(dto.Api), + Est = MapEst(dto.Est), + Acme = MapAcme(dto.Acme), + Scep = MapScep(dto.Scep), + EstConfigured = dto.EstConfigured.GetValueOrDefault(), + AcmeConfigured = dto.AcmeConfigured.GetValueOrDefault(), + ScepConfigured = dto.ScepConfigured.GetValueOrDefault() + }; + } + + private static InfisicalCertificateApplicationApiEnrollment MapApi(InfisicalCertificateApplicationApiEnrollmentDto dto) + { + if (dto == null) { return null; } + return new InfisicalCertificateApplicationApiEnrollment { Id = dto.Id, AutoRenew = dto.AutoRenew, RenewBeforeDays = dto.RenewBeforeDays }; + } + + private static InfisicalCertificateApplicationEstEnrollment MapEst(InfisicalCertificateApplicationEstEnrollmentDto dto) + { + if (dto == null) { return null; } + return new InfisicalCertificateApplicationEstEnrollment { Id = dto.Id, DisableBootstrapCaValidation = dto.DisableBootstrapCaValidation, EstEndpointUrl = dto.EstEndpointUrl }; + } + + private static InfisicalCertificateApplicationAcmeEnrollment MapAcme(InfisicalCertificateApplicationAcmeEnrollmentDto dto) + { + if (dto == null) { return null; } + return new InfisicalCertificateApplicationAcmeEnrollment { Id = dto.Id, SkipDnsOwnershipVerification = dto.SkipDnsOwnershipVerification, SkipEabBinding = dto.SkipEabBinding, DirectoryUrl = dto.DirectoryUrl }; + } + + private static InfisicalCertificateApplicationScepEnrollment MapScep(InfisicalCertificateApplicationScepEnrollmentDto dto) + { + if (dto == null) { return null; } + return new InfisicalCertificateApplicationScepEnrollment + { + Id = dto.Id, + ChallengeType = dto.ChallengeType, + IncludeCaCertInResponse = dto.IncludeCaCertInResponse, + AllowCertBasedRenewal = dto.AllowCertBasedRenewal, + DynamicChallengeExpiryMinutes = dto.DynamicChallengeExpiryMinutes, + DynamicChallengeMaxPending = dto.DynamicChallengeMaxPending, + ScepEndpointUrl = dto.ScepEndpointUrl, + ChallengeEndpointUrl = dto.ChallengeEndpointUrl, + RaCertificatePem = dto.RaCertificatePem, + RaCertificateThumbprint = ComputeThumbprint(dto.RaCertificatePem), + RaCertExpiresAtUtc = ParseTimestamp(dto.RaCertExpiresAt) + }; + } + + internal static string ComputeThumbprint(string pem) + { + if (string.IsNullOrEmpty(pem)) { return null; } + try + { + byte[] der = Convert.FromBase64String(StripPemArmor(pem)); + using (X509Certificate2 cert = new X509Certificate2(der)) + { + return cert.Thumbprint; + } + } + catch + { + return null; + } + } + + private static string StripPemArmor(string pem) + { + StringBuilder sb = new StringBuilder(pem.Length); + using (StringReader reader = new StringReader(pem)) + { + string line; + while ((line = reader.ReadLine()) != null) + { + string trimmed = line.Trim(); + if (trimmed.Length == 0) { continue; } + if (trimmed.StartsWith("-----", StringComparison.Ordinal)) { continue; } + sb.Append(trimmed); + } + } + + return sb.ToString(); + } + + private static DateTimeOffset? ParseTimestamp(string value) + { + if (string.IsNullOrEmpty(value)) { return null; } + DateTimeOffset parsed; + if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out parsed)) + { + return parsed; + } + + return null; + } + } +} diff --git a/src/PSInfisicalAPI/Pki/InfisicalCertificatePolicyDtos.cs b/src/PSInfisicalAPI/Pki/InfisicalCertificatePolicyDtos.cs new file mode 100644 index 0000000..485ea5a --- /dev/null +++ b/src/PSInfisicalAPI/Pki/InfisicalCertificatePolicyDtos.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace PSInfisicalAPI.Pki +{ + internal sealed class InfisicalCertificatePolicyResponseDto + { + [JsonProperty("id")] public string Id { get; set; } + [JsonProperty("projectId")] public string ProjectId { get; set; } + [JsonProperty("name")] public string Name { get; set; } + [JsonProperty("description")] public string Description { get; set; } + [JsonProperty("subject")] public InfisicalCertificatePolicySubjectDto Subject { get; set; } + [JsonProperty("sans")] public JToken SansRaw { get; set; } + [JsonProperty("keyUsages")] public InfisicalCertificatePolicyUsagesDto KeyUsages { get; set; } + [JsonProperty("extendedKeyUsages")] public InfisicalCertificatePolicyUsagesDto ExtendedKeyUsages { get; set; } + [JsonProperty("algorithms")] public InfisicalCertificatePolicyAlgorithmsDto Algorithms { get; set; } + [JsonProperty("validity")] public InfisicalCertificatePolicyValidityDto Validity { get; set; } + [JsonProperty("createdAt")] public string CreatedAt { get; set; } + [JsonProperty("updatedAt")] public string UpdatedAt { get; set; } + } + + internal sealed class InfisicalCertificatePolicySubjectDto + { + [JsonProperty("type")] public string Type { get; set; } + [JsonProperty("allowed")] public JToken AllowedRaw { get; set; } + } + + internal sealed class InfisicalCertificatePolicySanDto + { + [JsonProperty("type")] public string Type { get; set; } + [JsonProperty("allowed")] public JToken AllowedRaw { get; set; } + [JsonProperty("required")] public JToken RequiredRaw { get; set; } + } + + internal sealed class InfisicalCertificatePolicyUsagesDto + { + [JsonProperty("allowed")] public JToken AllowedRaw { get; set; } + [JsonProperty("required")] public JToken RequiredRaw { get; set; } + } + + internal sealed class InfisicalCertificatePolicyAlgorithmsDto + { + [JsonProperty("signature")] public string Signature { get; set; } + [JsonProperty("keyAlgorithm")] public JToken KeyAlgorithmRaw { get; set; } + } + + internal sealed class InfisicalCertificatePolicyValidityDto + { + [JsonProperty("max")] public string Max { get; set; } + } + + internal sealed class InfisicalCertificatePolicyListResponseDto + { + [JsonProperty("certificatePolicies")] public List CertificatePolicies { get; set; } + [JsonProperty("totalCount")] public int? TotalCount { get; set; } + } +} diff --git a/src/PSInfisicalAPI/Pki/InfisicalCertificatePolicyMapper.cs b/src/PSInfisicalAPI/Pki/InfisicalCertificatePolicyMapper.cs new file mode 100644 index 0000000..69d3f74 --- /dev/null +++ b/src/PSInfisicalAPI/Pki/InfisicalCertificatePolicyMapper.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Newtonsoft.Json.Linq; +using PSInfisicalAPI.Models; + +namespace PSInfisicalAPI.Pki +{ + internal static class InfisicalCertificatePolicyMapper + { + public static InfisicalCertificatePolicy Map(InfisicalCertificatePolicyResponseDto dto, string fallbackProjectId) + { + if (dto == null) + { + return null; + } + + return new InfisicalCertificatePolicy + { + Id = dto.Id, + ProjectId = !string.IsNullOrEmpty(dto.ProjectId) ? dto.ProjectId : fallbackProjectId, + Name = dto.Name, + Description = dto.Description, + Subject = MapSubject(dto.Subject), + Sans = MapSans(dto.SansRaw), + KeyUsages = MapUsages(dto.KeyUsages), + ExtendedKeyUsages = MapUsages(dto.ExtendedKeyUsages), + Algorithms = MapAlgorithms(dto.Algorithms), + Validity = MapValidity(dto.Validity), + CreatedAtUtc = ParseTimestamp(dto.CreatedAt), + UpdatedAtUtc = ParseTimestamp(dto.UpdatedAt) + }; + } + + public static InfisicalCertificatePolicy[] MapMany(IEnumerable items, string fallbackProjectId) + { + if (items == null) + { + return Array.Empty(); + } + + List results = new List(); + foreach (InfisicalCertificatePolicyResponseDto dto in items) + { + InfisicalCertificatePolicy mapped = Map(dto, fallbackProjectId); + if (mapped != null) + { + results.Add(mapped); + } + } + + return results.ToArray(); + } + + private static InfisicalCertificatePolicySubject MapSubject(InfisicalCertificatePolicySubjectDto dto) + { + if (dto == null) { return null; } + return new InfisicalCertificatePolicySubject + { + Type = dto.Type, + Allowed = InfisicalCertificateProfileMapper.FlattenStringOrStringArray(dto.AllowedRaw) + }; + } + + private static InfisicalCertificatePolicySan[] MapSans(JToken token) + { + if (token == null || token.Type == JTokenType.Null) { return null; } + + List results = new List(); + if (token.Type == JTokenType.Array) + { + foreach (JToken child in (JArray)token) + { + InfisicalCertificatePolicySan mapped = MapSanObject(child); + if (mapped != null) { results.Add(mapped); } + } + } + else if (token.Type == JTokenType.Object) + { + InfisicalCertificatePolicySan mapped = MapSanObject(token); + if (mapped != null) { results.Add(mapped); } + } + + return results.Count > 0 ? results.ToArray() : null; + } + + private static InfisicalCertificatePolicySan MapSanObject(JToken token) + { + if (token == null || token.Type != JTokenType.Object) { return null; } + InfisicalCertificatePolicySanDto dto = token.ToObject(); + if (dto == null) { return null; } + return new InfisicalCertificatePolicySan + { + Type = dto.Type, + Allowed = InfisicalCertificateProfileMapper.FlattenStringOrStringArray(dto.AllowedRaw), + Required = InfisicalCertificateProfileMapper.FlattenStringOrStringArray(dto.RequiredRaw) + }; + } + + private static InfisicalCertificatePolicyUsages MapUsages(InfisicalCertificatePolicyUsagesDto dto) + { + if (dto == null) { return null; } + return new InfisicalCertificatePolicyUsages + { + Allowed = InfisicalCertificateProfileMapper.FlattenStringOrStringArray(dto.AllowedRaw), + Required = InfisicalCertificateProfileMapper.FlattenStringOrStringArray(dto.RequiredRaw) + }; + } + + private static InfisicalCertificatePolicyAlgorithms MapAlgorithms(InfisicalCertificatePolicyAlgorithmsDto dto) + { + if (dto == null) { return null; } + return new InfisicalCertificatePolicyAlgorithms + { + Signature = dto.Signature, + KeyAlgorithms = InfisicalCertificateProfileMapper.FlattenStringOrStringArray(dto.KeyAlgorithmRaw) + }; + } + + private static InfisicalCertificatePolicyValidity MapValidity(InfisicalCertificatePolicyValidityDto dto) + { + if (dto == null) { return null; } + return new InfisicalCertificatePolicyValidity { Max = dto.Max }; + } + + private static DateTimeOffset? ParseTimestamp(string value) + { + if (string.IsNullOrEmpty(value)) { return null; } + DateTimeOffset parsed; + if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out parsed)) + { + return parsed; + } + + return null; + } + } +} diff --git a/src/PSInfisicalAPI/Pki/InfisicalCertificateProfileDtos.cs b/src/PSInfisicalAPI/Pki/InfisicalCertificateProfileDtos.cs new file mode 100644 index 0000000..8c9fb32 --- /dev/null +++ b/src/PSInfisicalAPI/Pki/InfisicalCertificateProfileDtos.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace PSInfisicalAPI.Pki +{ + internal sealed class InfisicalCertificateProfileResponseDto + { + [JsonProperty("id")] public string Id { get; set; } + [JsonProperty("projectId")] public string ProjectId { get; set; } + [JsonProperty("caId")] public string CaId { get; set; } + [JsonProperty("certificatePolicyId")] public string CertificatePolicyId { get; set; } + [JsonProperty("slug")] public string Slug { get; set; } + [JsonProperty("description")] public string Description { get; set; } + [JsonProperty("enrollmentType")] public string EnrollmentType { get; set; } + [JsonProperty("issuerType")] public string IssuerType { get; set; } + [JsonProperty("estConfigId")] public string EstConfigId { get; set; } + [JsonProperty("apiConfigId")] public string ApiConfigId { get; set; } + [JsonProperty("acmeConfigId")] public string AcmeConfigId { get; set; } + [JsonProperty("scepConfigId")] public string ScepConfigId { get; set; } + [JsonProperty("createdAt")] public string CreatedAt { get; set; } + [JsonProperty("updatedAt")] public string UpdatedAt { get; set; } + [JsonProperty("defaults")] public InfisicalCertificateProfileDefaultsDto Defaults { get; set; } + [JsonProperty("certificateAuthority")] public InfisicalCertificateAuthoritySummaryDto CertificateAuthority { get; set; } + [JsonProperty("certificatePolicy")] public InfisicalCertificatePolicySummaryDto CertificatePolicy { get; set; } + [JsonProperty("apiConfig")] public InfisicalCertificateProfileApiConfigDto ApiConfig { get; set; } + } + + internal sealed class InfisicalCertificateProfileDefaultsDto + { + [JsonProperty("ttlDays")] public int? TtlDays { get; set; } + [JsonProperty("keyAlgorithm")] public string KeyAlgorithm { get; set; } + [JsonProperty("signatureAlgorithm")] public string SignatureAlgorithm { get; set; } + [JsonProperty("keyUsages")] public JToken KeyUsagesRaw { get; set; } + [JsonProperty("extendedKeyUsages")] public JToken ExtendedKeyUsagesRaw { get; set; } + } + + internal sealed class InfisicalCertificateAuthoritySummaryDto + { + [JsonProperty("id")] public string Id { get; set; } + [JsonProperty("status")] public string Status { get; set; } + [JsonProperty("name")] public string Name { get; set; } + [JsonProperty("isExternal")] public bool? IsExternal { get; set; } + [JsonProperty("externalType")] public string ExternalType { get; set; } + } + + internal sealed class InfisicalCertificatePolicySummaryDto + { + [JsonProperty("id")] public string Id { get; set; } + [JsonProperty("projectId")] public string ProjectId { get; set; } + [JsonProperty("name")] public string Name { get; set; } + } + + internal sealed class InfisicalCertificateProfileApiConfigDto + { + [JsonProperty("id")] public string Id { get; set; } + [JsonProperty("autoRenew")] public bool? AutoRenew { get; set; } + [JsonProperty("renewBeforeDays")] public int? RenewBeforeDays { get; set; } + } + + internal sealed class InfisicalCertificateProfileListResponseDto + { + [JsonProperty("certificateProfiles")] public List CertificateProfiles { get; set; } + [JsonProperty("totalCount")] public int? TotalCount { get; set; } + } +} diff --git a/src/PSInfisicalAPI/Pki/InfisicalCertificateProfileMapper.cs b/src/PSInfisicalAPI/Pki/InfisicalCertificateProfileMapper.cs new file mode 100644 index 0000000..7550ff1 --- /dev/null +++ b/src/PSInfisicalAPI/Pki/InfisicalCertificateProfileMapper.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Newtonsoft.Json.Linq; +using PSInfisicalAPI.Models; + +namespace PSInfisicalAPI.Pki +{ + internal static class InfisicalCertificateProfileMapper + { + public static InfisicalCertificateProfile Map(InfisicalCertificateProfileResponseDto dto, string fallbackProjectId) + { + if (dto == null) + { + return null; + } + + return new InfisicalCertificateProfile + { + Id = dto.Id, + ProjectId = !string.IsNullOrEmpty(dto.ProjectId) ? dto.ProjectId : fallbackProjectId, + CaId = dto.CaId, + CertificatePolicyId = dto.CertificatePolicyId, + Slug = dto.Slug, + Description = dto.Description, + EnrollmentType = dto.EnrollmentType, + IssuerType = dto.IssuerType, + EstConfigId = dto.EstConfigId, + ApiConfigId = dto.ApiConfigId, + AcmeConfigId = dto.AcmeConfigId, + ScepConfigId = dto.ScepConfigId, + CreatedAtUtc = ParseTimestamp(dto.CreatedAt), + UpdatedAtUtc = ParseTimestamp(dto.UpdatedAt), + Defaults = MapDefaults(dto.Defaults), + CertificateAuthority = MapCa(dto.CertificateAuthority), + CertificatePolicy = MapPolicy(dto.CertificatePolicy), + ApiConfig = MapApiConfig(dto.ApiConfig) + }; + } + + public static InfisicalCertificateProfile[] MapMany(IEnumerable items, string fallbackProjectId) + { + if (items == null) + { + return Array.Empty(); + } + + List results = new List(); + foreach (InfisicalCertificateProfileResponseDto dto in items) + { + InfisicalCertificateProfile mapped = Map(dto, fallbackProjectId); + if (mapped != null) + { + results.Add(mapped); + } + } + + return results.ToArray(); + } + + private static InfisicalCertificateProfileDefaults MapDefaults(InfisicalCertificateProfileDefaultsDto dto) + { + if (dto == null) + { + return null; + } + + return new InfisicalCertificateProfileDefaults + { + TtlDays = dto.TtlDays, + KeyAlgorithm = dto.KeyAlgorithm, + SignatureAlgorithm = dto.SignatureAlgorithm, + KeyUsages = FlattenStringOrStringArray(dto.KeyUsagesRaw), + ExtendedKeyUsages = FlattenStringOrStringArray(dto.ExtendedKeyUsagesRaw) + }; + } + + private static InfisicalCertificateAuthoritySummary MapCa(InfisicalCertificateAuthoritySummaryDto dto) + { + if (dto == null) + { + return null; + } + + return new InfisicalCertificateAuthoritySummary + { + Id = dto.Id, + Status = dto.Status, + Name = dto.Name, + IsExternal = dto.IsExternal, + ExternalType = dto.ExternalType + }; + } + + private static InfisicalCertificatePolicySummary MapPolicy(InfisicalCertificatePolicySummaryDto dto) + { + if (dto == null) + { + return null; + } + + return new InfisicalCertificatePolicySummary + { + Id = dto.Id, + ProjectId = dto.ProjectId, + Name = dto.Name + }; + } + + private static InfisicalCertificateProfileApiConfig MapApiConfig(InfisicalCertificateProfileApiConfigDto dto) + { + if (dto == null) + { + return null; + } + + return new InfisicalCertificateProfileApiConfig + { + Id = dto.Id, + AutoRenew = dto.AutoRenew, + RenewBeforeDays = dto.RenewBeforeDays + }; + } + + internal static string[] FlattenStringOrStringArray(JToken token) + { + if (token == null || token.Type == JTokenType.Null) { return null; } + if (token.Type == JTokenType.String) { return new[] { (string)token }; } + if (token.Type == JTokenType.Array) + { + List items = new List(); + foreach (JToken child in (JArray)token) + { + if (child != null && child.Type == JTokenType.String) { items.Add((string)child); } + } + + return items.ToArray(); + } + + return null; + } + + private static DateTimeOffset? ParseTimestamp(string value) + { + if (string.IsNullOrEmpty(value)) { return null; } + DateTimeOffset parsed; + if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out parsed)) + { + return parsed; + } + + return null; + } + } +} diff --git a/src/PSInfisicalAPI/Pki/InfisicalCertificateRequestHelpers.cs b/src/PSInfisicalAPI/Pki/InfisicalCertificateRequestHelpers.cs new file mode 100644 index 0000000..4395c75 --- /dev/null +++ b/src/PSInfisicalAPI/Pki/InfisicalCertificateRequestHelpers.cs @@ -0,0 +1,341 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using PSInfisicalAPI.Logging; +using PSInfisicalAPI.Models; + +namespace PSInfisicalAPI.Pki +{ + internal static class InfisicalCertificateRequestHelpers + { + public static InfisicalCsrSubject MergeSubject(IDictionary subject, string commonName, string country, string state, string locality, string organization, string organizationalUnit, string emailAddress) + { + InfisicalCsrSubject result = new InfisicalCsrSubject(); + if (subject != null) + { + result.CommonName = ReadString(subject, "CN", "CommonName"); + result.Country = ReadString(subject, "C", "Country"); + result.State = ReadString(subject, "ST", "S", "State"); + result.Locality = ReadString(subject, "L", "Locality"); + result.Organization = ReadString(subject, "O", "Organization"); + result.OrganizationalUnit = ReadString(subject, "OU", "OrganizationalUnit"); + result.EmailAddress = ReadString(subject, "E", "EMAIL", "EmailAddress"); + } + + if (!string.IsNullOrEmpty(commonName)) { result.CommonName = commonName; } + if (!string.IsNullOrEmpty(country)) { result.Country = country; } + if (!string.IsNullOrEmpty(state)) { result.State = state; } + if (!string.IsNullOrEmpty(locality)) { result.Locality = locality; } + if (!string.IsNullOrEmpty(organization)) { result.Organization = organization; } + if (!string.IsNullOrEmpty(organizationalUnit)) { result.OrganizationalUnit = organizationalUnit; } + if (!string.IsNullOrEmpty(emailAddress)) { result.EmailAddress = emailAddress; } + + return result; + } + + public static string ResolveLocalFqdn() + { + try + { + string host = System.Net.Dns.GetHostName(); + string domain = null; + try { domain = System.Net.NetworkInformation.IPGlobalProperties.GetIPGlobalProperties().DomainName; } + catch { domain = null; } + + if (!string.IsNullOrEmpty(domain) && !host.EndsWith("." + domain, StringComparison.OrdinalIgnoreCase)) + { + return string.Concat(host, ".", domain); + } + + return host; + } + catch + { + return null; + } + } + + public static void InstallToStore(X509Certificate2 cert, StoreName storeName, StoreLocation storeLocation, bool force, IInfisicalLogger logger, string component) + { + X509Store store = new X509Store(storeName, storeLocation); + try + { + store.Open(OpenFlags.ReadWrite); + X509Certificate2Collection existing = store.Certificates.Find(X509FindType.FindByThumbprint, cert.Thumbprint, false); + string target = string.Concat(storeLocation.ToString(), @"\", storeName.ToString(), " [", cert.Thumbprint, "]"); + if (existing.Count > 0) + { + if (!force) + { + logger.Information(component, string.Concat("Certificate already present in ", target, "; no action taken.")); + return; + } + + store.RemoveRange(existing); + } + + store.Add(cert); + logger.Information(component, string.Concat("Installed certificate to ", target, ".")); + } + finally + { + store.Close(); + } + } + + public static void InstallChain(InfisicalSignedCertificate signed, StoreLocation storeLocation, bool force, IInfisicalLogger logger, string component) + { + List chainCerts = CollectChainCertificates(signed); + InstallChain(chainCerts, storeLocation, force, logger, component); + } + + public static void InstallChain(IEnumerable chainCerts, StoreLocation storeLocation, bool force, IInfisicalLogger logger, string component) + { + if (chainCerts == null) { return; } + foreach (X509Certificate2 chainCert in chainCerts) + { + if (chainCert == null) { continue; } + StoreName targetStore = GetChainCertificateTargetStore(chainCert); + InstallToStore(chainCert, targetStore, storeLocation, force, logger, component); + } + } + + public static StoreName GetChainCertificateTargetStore(X509Certificate2 cert) + { + return IsSelfSigned(cert) ? StoreName.Root : StoreName.CertificateAuthority; + } + + public static X509KeyStorageFlags ResolveKeyStorageFlags(InfisicalPrivateKeyProtection protection, bool persistKey, bool machineKey) + { + X509KeyStorageFlags flags = X509KeyStorageFlags.DefaultKeySet; + switch (protection) + { + case InfisicalPrivateKeyProtection.Exportable: + flags |= X509KeyStorageFlags.Exportable; + break; + case InfisicalPrivateKeyProtection.Ephemeral: + const int ephemeralValue = 32; + if (Enum.GetName(typeof(X509KeyStorageFlags), ephemeralValue) == null) + { + throw new PlatformNotSupportedException("InfisicalPrivateKeyProtection.Ephemeral requires .NET Core 3.0 or later (PowerShell 7+). Use LocalOnly or NonExportable on Windows PowerShell 5.1."); + } + flags |= (X509KeyStorageFlags)ephemeralValue; + break; + } + + if (machineKey) { flags |= X509KeyStorageFlags.MachineKeySet; } + if (persistKey) { flags |= X509KeyStorageFlags.PersistKeySet; } + + return flags; + } + + public static bool ShouldScrubPrivateKeyPem(InfisicalPrivateKeyProtection protection, bool hasExplicitPrivateKeyPath) + { + if (hasExplicitPrivateKeyPath) { return true; } + return protection == InfisicalPrivateKeyProtection.NonExportable + || protection == InfisicalPrivateKeyProtection.Ephemeral; + } + + public static void WritePrivateKeyPem(string privateKeyPem, string path) + { + if (string.IsNullOrEmpty(privateKeyPem)) { throw new ArgumentException("PrivateKeyPem is empty.", nameof(privateKeyPem)); } + if (string.IsNullOrEmpty(path)) { throw new ArgumentException("Path is required.", nameof(path)); } + + string fullPath = System.IO.Path.GetFullPath(path); + string directory = System.IO.Path.GetDirectoryName(fullPath); + if (!string.IsNullOrEmpty(directory) && !System.IO.Directory.Exists(directory)) + { + System.IO.Directory.CreateDirectory(directory); + } + + System.IO.File.WriteAllText(fullPath, privateKeyPem); + } + + public static InfisicalCertificateResult BuildResultFromExistingLocal(X509Certificate2 leaf) + { + return BuildResultFromExistingLocal(leaf, null); + } + + public static InfisicalCertificateResult BuildResultFromExistingLocal(X509Certificate2 leaf, InfisicalCertificateBundle fallbackBundle) + { + if (leaf == null) { throw new ArgumentNullException(nameof(leaf)); } + + InfisicalCertificateResult result = new InfisicalCertificateResult + { + Leaf = leaf, + SerialNumber = leaf.SerialNumber, + CertificatePem = ExportCertificateToPem(leaf) + }; + + List chainElements = BuildLocalChain(leaf); + + if (fallbackBundle != null && !string.IsNullOrEmpty(fallbackBundle.CertificateChainPem)) + { + List bundleChain = PemCertificateBuilder.ReadCertificateChain(fallbackBundle.CertificateChainPem); + HashSet seen = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (X509Certificate2 c in chainElements) { if (c != null) { seen.Add(c.Thumbprint); } } + foreach (X509Certificate2 c in bundleChain) + { + if (c == null) { continue; } + if (seen.Add(c.Thumbprint)) { chainElements.Add(c); } + } + } + + List intermediates = new List(); + X509Certificate2 root = null; + foreach (X509Certificate2 cert in chainElements) + { + if (string.Equals(cert.Thumbprint, leaf.Thumbprint, StringComparison.OrdinalIgnoreCase)) { continue; } + if (IsSelfSigned(cert)) { if (root == null) { root = cert; } } + else { intermediates.Add(cert); } + } + + result.Intermediates = intermediates.ToArray(); + result.Root = root; + + List ordered = new List { leaf }; + ordered.AddRange(intermediates); + if (root != null) { ordered.Add(root); } + result.Chain = ordered.ToArray(); + + if (intermediates.Count > 0 || root != null) + { + StringBuilder sb = new StringBuilder(); + foreach (X509Certificate2 c in intermediates) { sb.Append(ExportCertificateToPem(c)); } + if (root != null) { sb.Append(ExportCertificateToPem(root)); } + result.CertificateChainPem = sb.ToString(); + } + + return result; + } + + public static InfisicalCertificateResult BuildResult(X509Certificate2 leaf, InfisicalSignedCertificate signed) + { + InfisicalCertificateResult result = new InfisicalCertificateResult { Leaf = leaf }; + if (signed != null) + { + result.SerialNumber = signed.SerialNumber; + result.CertificatePem = signed.CertificatePem; + result.CertificateChainPem = signed.CertificateChainPem; + result.PrivateKeyPem = signed.PrivateKeyPem; + result.Status = signed.Status; + result.StatusMessage = signed.StatusMessage; + result.CertificateRequestId = signed.CertificateRequestId; + } + + List chainCerts = signed != null ? CollectChainCertificates(signed) : new List(); + List intermediates = new List(); + X509Certificate2 root = null; + foreach (X509Certificate2 cert in chainCerts) + { + if (IsSelfSigned(cert)) { if (root == null) { root = cert; } } + else { intermediates.Add(cert); } + } + + result.Intermediates = intermediates.ToArray(); + result.Root = root; + + List ordered = new List(); + if (leaf != null) { ordered.Add(leaf); } + ordered.AddRange(intermediates); + if (root != null) { ordered.Add(root); } + result.Chain = ordered.ToArray(); + return result; + } + + private static List CollectChainCertificates(InfisicalSignedCertificate signed) + { + List chainCerts = PemCertificateBuilder.ReadCertificateChain(signed.CertificateChainPem); + if (!string.IsNullOrEmpty(signed.IssuingCaCertificatePem)) + { + foreach (X509Certificate2 issuing in PemCertificateBuilder.ReadCertificateChain(signed.IssuingCaCertificatePem)) + { + chainCerts.Add(issuing); + } + } + + HashSet seen = new HashSet(StringComparer.OrdinalIgnoreCase); + List deduped = new List(); + foreach (X509Certificate2 cert in chainCerts) + { + if (cert == null) { continue; } + if (seen.Add(cert.Thumbprint)) { deduped.Add(cert); } + } + + return deduped; + } + + private static bool IsSelfSigned(X509Certificate2 cert) + { + if (cert == null) { return false; } + return string.Equals(cert.Subject, cert.Issuer, StringComparison.OrdinalIgnoreCase); + } + + private static List BuildLocalChain(X509Certificate2 leaf) + { + List result = new List(); + using (X509Chain chain = new X509Chain()) + { + chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + chain.ChainPolicy.VerificationFlags = + X509VerificationFlags.IgnoreNotTimeValid | + X509VerificationFlags.IgnoreNotTimeNested | + X509VerificationFlags.IgnoreInvalidName | + X509VerificationFlags.IgnoreInvalidPolicy | + X509VerificationFlags.IgnoreEndRevocationUnknown | + X509VerificationFlags.IgnoreCertificateAuthorityRevocationUnknown | + X509VerificationFlags.IgnoreRootRevocationUnknown | + X509VerificationFlags.IgnoreCtlNotTimeValid | + X509VerificationFlags.IgnoreCtlSignerRevocationUnknown | + X509VerificationFlags.IgnoreInvalidBasicConstraints | + X509VerificationFlags.IgnoreWrongUsage; + + try { chain.Build(leaf); } + catch { return result; } + + foreach (X509ChainElement element in chain.ChainElements) + { + if (element != null && element.Certificate != null) + { + result.Add(new X509Certificate2(element.Certificate.RawData)); + } + } + } + + return result; + } + + private static string ExportCertificateToPem(X509Certificate2 cert) + { + byte[] der = cert.Export(X509ContentType.Cert); + StringBuilder sb = new StringBuilder(); + sb.AppendLine("-----BEGIN CERTIFICATE-----"); + sb.AppendLine(Convert.ToBase64String(der, Base64FormattingOptions.InsertLineBreaks)); + sb.AppendLine("-----END CERTIFICATE-----"); + return sb.ToString(); + } + + private static string ReadString(IDictionary source, params string[] keys) + { + foreach (string key in keys) + { + if (source.Contains(key)) + { + object value = source[key]; + if (value != null) + { + string text = value.ToString(); + if (!string.IsNullOrEmpty(text)) + { + return text; + } + } + } + } + + return null; + } + } +} diff --git a/src/PSInfisicalAPI/Pki/InfisicalCsrBuilder.cs b/src/PSInfisicalAPI/Pki/InfisicalCsrBuilder.cs new file mode 100644 index 0000000..2aae813 --- /dev/null +++ b/src/PSInfisicalAPI/Pki/InfisicalCsrBuilder.cs @@ -0,0 +1,201 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using Org.BouncyCastle.Asn1; +using Org.BouncyCastle.Asn1.Pkcs; +using Org.BouncyCastle.Asn1.Sec; +using Org.BouncyCastle.Asn1.X509; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Generators; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.OpenSsl; +using Org.BouncyCastle.Pkcs; +using Org.BouncyCastle.Security; +using BcAttribute = Org.BouncyCastle.Asn1.Cms.Attribute; + +namespace PSInfisicalAPI.Pki +{ + public enum InfisicalKeyAlgorithm + { + Rsa = 0, + Ecdsa = 1, + Ed25519 = 2 + } + + public enum InfisicalEcCurve + { + P256 = 0, + P384 = 1 + } + + public sealed class InfisicalCsrSubject + { + public string CommonName { get; set; } + public string Country { get; set; } + public string State { get; set; } + public string Locality { get; set; } + public string Organization { get; set; } + public string OrganizationalUnit { get; set; } + public string EmailAddress { get; set; } + } + + public sealed class InfisicalCsrOptions + { + public InfisicalKeyAlgorithm KeyAlgorithm { get; set; } = InfisicalKeyAlgorithm.Rsa; + public int RsaKeySize { get; set; } = 2048; + public InfisicalEcCurve EcCurve { get; set; } = InfisicalEcCurve.P256; + } + + public sealed class InfisicalCsrResult + { + public string CsrPem { get; set; } + public string PrivateKeyPem { get; set; } + } + + public static class InfisicalCsrBuilder + { + public static InfisicalCsrResult Build(InfisicalCsrSubject subject, IEnumerable dnsNames, IEnumerable ipAddresses, InfisicalCsrOptions options) + { + if (subject == null) { throw new ArgumentNullException(nameof(subject)); } + if (string.IsNullOrEmpty(subject.CommonName)) { throw new ArgumentException("Subject.CommonName is required.", nameof(subject)); } + if (options == null) { options = new InfisicalCsrOptions(); } + + SecureRandom random = new SecureRandom(); + AsymmetricCipherKeyPair keyPair = GenerateKeyPair(options, random); + string signatureAlgorithm = ResolveSignatureAlgorithm(options); + + X509Name x509Name = BuildX509Name(subject); + Asn1Set attributes = BuildSanAttributes(dnsNames, ipAddresses); + + Pkcs10CertificationRequest pkcs10 = new Pkcs10CertificationRequest(signatureAlgorithm, x509Name, keyPair.Public, attributes, keyPair.Private); + + return new InfisicalCsrResult + { + CsrPem = WritePem(pkcs10), + PrivateKeyPem = WritePem(keyPair.Private) + }; + } + + private static AsymmetricCipherKeyPair GenerateKeyPair(InfisicalCsrOptions options, SecureRandom random) + { + switch (options.KeyAlgorithm) + { + case InfisicalKeyAlgorithm.Rsa: + { + int keySize = options.RsaKeySize; + if (keySize != 2048 && keySize != 3072 && keySize != 4096) + { + throw new ArgumentException("RsaKeySize must be 2048, 3072, or 4096.", nameof(options)); + } + + RsaKeyPairGenerator generator = new RsaKeyPairGenerator(); + generator.Init(new KeyGenerationParameters(random, keySize)); + return generator.GenerateKeyPair(); + } + case InfisicalKeyAlgorithm.Ecdsa: + { + DerObjectIdentifier curveOid = options.EcCurve == InfisicalEcCurve.P384 + ? SecObjectIdentifiers.SecP384r1 + : SecObjectIdentifiers.SecP256r1; + ECKeyPairGenerator generator = new ECKeyPairGenerator("ECDSA"); + generator.Init(new ECKeyGenerationParameters(curveOid, random)); + return generator.GenerateKeyPair(); + } + case InfisicalKeyAlgorithm.Ed25519: + { + Ed25519KeyPairGenerator generator = new Ed25519KeyPairGenerator(); + generator.Init(new Ed25519KeyGenerationParameters(random)); + return generator.GenerateKeyPair(); + } + default: + throw new ArgumentOutOfRangeException(nameof(options), options.KeyAlgorithm, "Unsupported KeyAlgorithm."); + } + } + + private static string ResolveSignatureAlgorithm(InfisicalCsrOptions options) + { + switch (options.KeyAlgorithm) + { + case InfisicalKeyAlgorithm.Rsa: + return "SHA256WITHRSA"; + case InfisicalKeyAlgorithm.Ecdsa: + return options.EcCurve == InfisicalEcCurve.P384 ? "SHA384WITHECDSA" : "SHA256WITHECDSA"; + case InfisicalKeyAlgorithm.Ed25519: + return "Ed25519"; + default: + throw new ArgumentOutOfRangeException(nameof(options), options.KeyAlgorithm, "Unsupported KeyAlgorithm."); + } + } + + private static X509Name BuildX509Name(InfisicalCsrSubject subject) + { + List order = new List(); + Dictionary values = new Dictionary(); + + AppendComponent(order, values, X509Name.C, subject.Country); + AppendComponent(order, values, X509Name.ST, subject.State); + AppendComponent(order, values, X509Name.L, subject.Locality); + AppendComponent(order, values, X509Name.O, subject.Organization); + AppendComponent(order, values, X509Name.OU, subject.OrganizationalUnit); + AppendComponent(order, values, X509Name.CN, subject.CommonName); + AppendComponent(order, values, X509Name.EmailAddress, subject.EmailAddress); + + return new X509Name(order, values); + } + + private static void AppendComponent(List order, Dictionary values, DerObjectIdentifier oid, string value) + { + if (string.IsNullOrEmpty(value)) { return; } + order.Add(oid); + values[oid] = value; + } + + private static Asn1Set BuildSanAttributes(IEnumerable dnsNames, IEnumerable ipAddresses) + { + List generalNames = new List(); + if (dnsNames != null) + { + foreach (string dns in dnsNames) + { + if (string.IsNullOrEmpty(dns)) { continue; } + generalNames.Add(new GeneralName(GeneralName.DnsName, dns)); + } + } + + if (ipAddresses != null) + { + foreach (string ip in ipAddresses) + { + if (string.IsNullOrEmpty(ip)) { continue; } + IPAddress parsed; + if (!IPAddress.TryParse(ip, out parsed)) { continue; } + generalNames.Add(new GeneralName(GeneralName.IPAddress, ip)); + } + } + + if (generalNames.Count == 0) { return null; } + + GeneralNames sanValue = new GeneralNames(generalNames.ToArray()); + X509Extensions extensions = new X509Extensions( + new Dictionary + { + { X509Extensions.SubjectAlternativeName, new X509Extension(false, new DerOctetString(sanValue)) } + }); + + BcAttribute extensionRequest = new BcAttribute(PkcsObjectIdentifiers.Pkcs9AtExtensionRequest, new DerSet(extensions)); + return new DerSet(extensionRequest); + } + + private static string WritePem(object obj) + { + using (StringWriter sw = new StringWriter()) + { + PemWriter pemWriter = new PemWriter(sw); + pemWriter.WriteObject(obj); + pemWriter.Writer.Flush(); + return sw.ToString(); + } + } + } +} diff --git a/src/PSInfisicalAPI/Pki/InfisicalLocalCertificateLookup.cs b/src/PSInfisicalAPI/Pki/InfisicalLocalCertificateLookup.cs new file mode 100644 index 0000000..366ed98 --- /dev/null +++ b/src/PSInfisicalAPI/Pki/InfisicalLocalCertificateLookup.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; + +namespace PSInfisicalAPI.Pki +{ + internal static class InfisicalLocalCertificateLookup + { + public static X509Certificate2 FindMatch(StoreName storeName, StoreLocation storeLocation, string commonName, IEnumerable candidateSerialNumbers) + { + HashSet serialSet = NormalizeSerials(candidateSerialNumbers); + string subjectFilter = !string.IsNullOrEmpty(commonName) ? string.Concat("CN=", commonName) : null; + + X509Store store = new X509Store(storeName, storeLocation); + try + { + store.Open(OpenFlags.ReadOnly); + + X509Certificate2 bestMatch = null; + foreach (X509Certificate2 candidate in store.Certificates) + { + if (subjectFilter != null && candidate.Subject.IndexOf(subjectFilter, StringComparison.OrdinalIgnoreCase) < 0) + { + continue; + } + + if (serialSet.Count > 0) + { + string normalizedSerial = NormalizeSerial(candidate.SerialNumber); + if (!serialSet.Contains(normalizedSerial)) + { + continue; + } + } + + if (bestMatch == null || candidate.NotAfter > bestMatch.NotAfter) + { + bestMatch = candidate; + } + } + + return bestMatch; + } + finally + { + store.Close(); + } + } + + public static bool IsRenewable(X509Certificate2 cert, int renewalThresholdDays) + { + if (cert == null) { return true; } + DateTime threshold = DateTime.UtcNow.AddDays(renewalThresholdDays); + return cert.NotAfter.ToUniversalTime() <= threshold; + } + + private static HashSet NormalizeSerials(IEnumerable serials) + { + HashSet set = new HashSet(StringComparer.OrdinalIgnoreCase); + if (serials == null) { return set; } + foreach (string serial in serials) + { + string normalized = NormalizeSerial(serial); + if (!string.IsNullOrEmpty(normalized)) + { + set.Add(normalized); + } + } + + return set; + } + + private static string NormalizeSerial(string value) + { + if (string.IsNullOrEmpty(value)) { return null; } + string trimmed = value.Trim(); + if (trimmed.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) + { + trimmed = trimmed.Substring(2); + } + + return trimmed.Replace(":", string.Empty).Replace(" ", string.Empty).TrimStart('0'); + } + } +} diff --git a/src/PSInfisicalAPI/Pki/InfisicalPkiClient.cs b/src/PSInfisicalAPI/Pki/InfisicalPkiClient.cs index 06b38a3..2735cd6 100644 --- a/src/PSInfisicalAPI/Pki/InfisicalPkiClient.cs +++ b/src/PSInfisicalAPI/Pki/InfisicalPkiClient.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using Newtonsoft.Json.Linq; using PSInfisicalAPI.Connections; using PSInfisicalAPI.Endpoints; using PSInfisicalAPI.Errors; @@ -8,6 +9,7 @@ using PSInfisicalAPI.Http; using PSInfisicalAPI.Logging; using PSInfisicalAPI.Models; using PSInfisicalAPI.Serialization; +using System.Linq; namespace PSInfisicalAPI.Pki { @@ -30,23 +32,22 @@ namespace PSInfisicalAPI.Pki public InfisicalCertificateAuthority[] ListInternalCertificateAuthorities(InfisicalConnection connection, string projectId) { if (connection == null) { throw new ArgumentNullException(nameof(connection)); } - string resolvedProjectId = FirstNonEmpty(projectId, connection.ProjectId); List> query = null; - if (!string.IsNullOrEmpty(resolvedProjectId)) + if (!string.IsNullOrEmpty(projectId)) { - query = new List> { new KeyValuePair("projectId", resolvedProjectId) }; + query = new List> { new KeyValuePair("projectId", projectId) }; } try { _logger.Information(Component, "Attempting to list Infisical internal certificate authorities. Please Wait..."); InfisicalHttpResponse response = _invoker.InvokeWithCandidateFallback(connection, InfisicalEndpointNames.ListInternalCertificateAuthorities, "ListInternalCertificateAuthorities", null, query, null); - InfisicalInternalCaListResponseDto dto = _serializer.Deserialize(response.Body); + string body = response.Body; response.Clear(); - List source = dto != null ? (dto.CertificateAuthorities ?? dto.Cas) : null; - InfisicalCertificateAuthority[] mapped = InfisicalCaMapper.MapMany(source, resolvedProjectId); + List source = ParseCaListBody(body); + InfisicalCertificateAuthority[] mapped = InfisicalCaMapper.MapMany(source, projectId); _logger.Information(Component, "Infisical internal certificate authority list retrieval was successful."); return mapped; } @@ -63,21 +64,21 @@ namespace PSInfisicalAPI.Pki if (string.IsNullOrEmpty(caId)) { throw new InfisicalConfigurationException("CaId is required."); } Dictionary pathParameters = new Dictionary { { "caId", caId } }; + List> query = null; + if (!string.IsNullOrEmpty(projectId)) + { + query = new List> { new KeyValuePair("projectId", projectId) }; + } try { _logger.Information(Component, string.Concat("Attempting to retrieve Infisical internal certificate authority '", caId, "'. Please Wait...")); - InfisicalHttpResponse response = _invoker.InvokeWithCandidateFallback(connection, InfisicalEndpointNames.RetrieveInternalCertificateAuthority, "RetrieveInternalCertificateAuthority", pathParameters, null, null); - InfisicalInternalCaSingleResponseDto dto = _serializer.Deserialize(response.Body); + InfisicalHttpResponse response = _invoker.InvokeWithCandidateFallback(connection, InfisicalEndpointNames.RetrieveInternalCertificateAuthority, "RetrieveInternalCertificateAuthority", pathParameters, query, null); + string body = response.Body; response.Clear(); - InfisicalInternalCaResponseDto inner = dto != null ? (dto.CertificateAuthority ?? dto.Ca) : null; - if (inner == null) - { - inner = _serializer.Deserialize(response.Body); - } - - InfisicalCertificateAuthority mapped = InfisicalCaMapper.Map(inner, FirstNonEmpty(projectId, connection.ProjectId)); + InfisicalInternalCaResponseDto inner = ParseCaSingleBody(body); + InfisicalCertificateAuthority mapped = InfisicalCaMapper.Map(inner, projectId); _logger.Information(Component, "Infisical internal certificate authority retrieval was successful."); return mapped; } @@ -88,14 +89,104 @@ namespace PSInfisicalAPI.Pki } } + public InfisicalCertificateAuthority[] ListAllCertificateAuthorities(InfisicalConnection connection, string projectId) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + if (string.IsNullOrEmpty(projectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + + List> query = new List> + { + new KeyValuePair("projectId", projectId) + }; + + try + { + _logger.Information(Component, "Attempting to list Infisical certificate authorities. Please Wait..."); + InfisicalHttpResponse response = _invoker.InvokeWithCandidateFallback(connection, InfisicalEndpointNames.ListCertificateAuthorities, "ListCertificateAuthorities", null, query, null); + string body = response.Body; + response.Clear(); + + List source = ParseCaListBody(body); + InfisicalCertificateAuthority[] mapped = InfisicalCaMapper.MapMany(source, projectId); + _logger.Information(Component, "Infisical certificate authority list retrieval was successful."); + return mapped; + } + catch (Exception) + { + _logger.Error(Component, "Infisical certificate authority list retrieval failed."); + throw; + } + } + + public InfisicalCertificate RetrieveCertificate(InfisicalConnection connection, string identifier) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + if (string.IsNullOrEmpty(identifier)) { throw new InfisicalConfigurationException("Identifier (serial number or id) is required."); } + + Dictionary pathParameters = new Dictionary { { "serialNumber", identifier } }; + + try + { + _logger.Information(Component, string.Concat("Attempting to retrieve Infisical certificate '", identifier, "'. Please Wait...")); + InfisicalHttpResponse response = _invoker.InvokeWithCandidateFallback(connection, InfisicalEndpointNames.RetrieveCertificate, "RetrieveCertificate", pathParameters, null, null); + string body = response.Body; + response.Clear(); + + InfisicalCertificateResponseDto inner = ParseCertificateSingleBody(body); + InfisicalCertificate mapped = InfisicalCertificateMapper.Map(inner, null); + _logger.Information(Component, "Infisical certificate retrieval was successful."); + return mapped; + } + catch (Exception) + { + _logger.Error(Component, "Infisical certificate retrieval failed."); + throw; + } + } + + private List ParseCaListBody(string body) + { + if (string.IsNullOrEmpty(body)) { return null; } + JToken token = JToken.Parse(body); + if (token.Type == JTokenType.Array) + { + return token.ToObject>(); + } + + InfisicalInternalCaListResponseDto wrapper = token.ToObject(); + return wrapper != null ? (wrapper.CertificateAuthorities ?? wrapper.Cas) : null; + } + + private InfisicalInternalCaResponseDto ParseCaSingleBody(string body) + { + if (string.IsNullOrEmpty(body)) { return null; } + JToken token = JToken.Parse(body); + if (token.Type != JTokenType.Object) { return null; } + JObject obj = (JObject)token; + + if (obj["certificateAuthority"] is JObject ca1) { return ca1.ToObject(); } + if (obj["ca"] is JObject ca2) { return ca2.ToObject(); } + return obj.ToObject(); + } + + private InfisicalCertificateResponseDto ParseCertificateSingleBody(string body) + { + if (string.IsNullOrEmpty(body)) { return null; } + JToken token = JToken.Parse(body); + if (token.Type != JTokenType.Object) { return null; } + JObject obj = (JObject)token; + + if (obj["certificate"] is JObject cert) { return cert.ToObject(); } + return obj.ToObject(); + } + public InfisicalCertificateSearchResult SearchCertificates(InfisicalConnection connection, InfisicalCertificateSearchQuery query) { if (connection == null) { throw new ArgumentNullException(nameof(connection)); } if (query == null) { throw new ArgumentNullException(nameof(query)); } - string resolvedProjectId = FirstNonEmpty(query.ProjectId, connection.ProjectId); - if (string.IsNullOrEmpty(resolvedProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + if (string.IsNullOrEmpty(query.ProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } - Dictionary pathParameters = new Dictionary { { "projectId", resolvedProjectId } }; + Dictionary pathParameters = new Dictionary { { "projectId", query.ProjectId } }; InfisicalCertificateSearchRequestDto request = BuildSearchRequest(query); string body = _serializer.Serialize(request); @@ -106,7 +197,7 @@ namespace PSInfisicalAPI.Pki InfisicalCertificateSearchResponseDto dto = _serializer.Deserialize(response.Body); response.Clear(); - InfisicalCertificate[] mapped = InfisicalCertificateMapper.MapMany(dto != null ? dto.Certificates : null, resolvedProjectId); + InfisicalCertificate[] mapped = InfisicalCertificateMapper.MapMany(dto != null ? dto.Certificates : null, query.ProjectId); int total = dto != null ? dto.TotalCount : mapped.Length; _logger.Information(Component, "Infisical certificate search was successful."); return new InfisicalCertificateSearchResult { Certificates = mapped, TotalCount = total }; @@ -118,6 +209,401 @@ namespace PSInfisicalAPI.Pki } } + public InfisicalSignedCertificate SignCertificateBySubscriber(InfisicalConnection connection, string subscriberName, string projectId, string csrPem) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + if (string.IsNullOrEmpty(subscriberName)) { throw new InfisicalConfigurationException("SubscriberName is required."); } + if (string.IsNullOrEmpty(csrPem)) { throw new InfisicalConfigurationException("CSR is required."); } + if (string.IsNullOrEmpty(projectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + + Dictionary pathParameters = new Dictionary { { "subscriberName", subscriberName } }; + InfisicalSignCertificateBySubscriberRequestDto request = new InfisicalSignCertificateBySubscriberRequestDto + { + ProjectId = projectId, + Csr = csrPem + }; + string body = _serializer.Serialize(request); + + try + { + _logger.Information(Component, string.Concat("Attempting to sign certificate via subscriber '", subscriberName, "'. Please Wait...")); + InfisicalHttpResponse response = _invoker.InvokeWithCandidateFallback(connection, InfisicalEndpointNames.SignCertificateBySubscriber, "SignCertificateBySubscriber", pathParameters, null, body); + InfisicalSignCertificateResponseDto dto = _serializer.Deserialize(response.Body); + response.Clear(); + + InfisicalSignedCertificate signed = MapSigned(dto); + _logger.Information(Component, "Infisical certificate signing (subscriber) was successful."); + return signed; + } + catch (Exception) + { + _logger.Error(Component, "Infisical certificate signing (subscriber) failed."); + throw; + } + } + + public InfisicalSignedCertificate SignCertificateByCa(InfisicalConnection connection, string caId, string csrPem, string commonName, string altNames, string ttl, string notBefore, string notAfter, string friendlyName, string pkiCollectionId, IEnumerable keyUsages, IEnumerable extendedKeyUsages) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + if (string.IsNullOrEmpty(caId)) { throw new InfisicalConfigurationException("CaId is required."); } + if (string.IsNullOrEmpty(csrPem)) { throw new InfisicalConfigurationException("CSR is required."); } + if (string.IsNullOrEmpty(ttl) && string.IsNullOrEmpty(notAfter)) { throw new InfisicalConfigurationException("Either Ttl or NotAfter must be provided."); } + + Dictionary pathParameters = new Dictionary { { "caId", caId } }; + InfisicalSignCertificateByCaRequestDto request = new InfisicalSignCertificateByCaRequestDto + { + Csr = csrPem, + CommonName = commonName, + AltNames = altNames, + Ttl = ttl, + NotBefore = notBefore, + NotAfter = notAfter, + FriendlyName = friendlyName, + PkiCollectionId = pkiCollectionId, + KeyUsages = keyUsages != null ? keyUsages.ToList() : null, + ExtendedKeyUsages = extendedKeyUsages != null ? extendedKeyUsages.ToList() : null + }; + string body = _serializer.Serialize(request); + + try + { + _logger.Information(Component, string.Concat("Attempting to sign certificate via CA '", caId, "'. Please Wait...")); + InfisicalHttpResponse response = _invoker.InvokeWithCandidateFallback(connection, InfisicalEndpointNames.SignCertificateByCa, "SignCertificateByCa", pathParameters, null, body); + InfisicalSignCertificateResponseDto dto = _serializer.Deserialize(response.Body); + response.Clear(); + + InfisicalSignedCertificate signed = MapSigned(dto); + _logger.Information(Component, "Infisical certificate signing (CA) was successful."); + return signed; + } + catch (Exception) + { + _logger.Error(Component, "Infisical certificate signing (CA) failed."); + throw; + } + } + + private static InfisicalSignedCertificate MapSigned(InfisicalSignCertificateResponseDto dto) + { + if (dto == null) { return null; } + return new InfisicalSignedCertificate + { + SerialNumber = dto.SerialNumber, + CertificatePem = dto.Certificate, + CertificateChainPem = dto.CertificateChain, + IssuingCaCertificatePem = dto.IssuingCaCertificate + }; + } + + public InfisicalSignedCertificate IssueCertificateByProfile(InfisicalConnection connection, string profileId, string csrPem, string commonName, string organization, string organizationalUnit, string country, string state, string locality, string ttl, string notBefore, string notAfter, IEnumerable keyUsages, IEnumerable extendedKeyUsages) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + if (string.IsNullOrEmpty(profileId)) { throw new InfisicalConfigurationException("CertificateProfileId is required."); } + if (string.IsNullOrEmpty(csrPem)) { throw new InfisicalConfigurationException("CSR is required."); } + + InfisicalIssueCertificateAttributesDto attributes = new InfisicalIssueCertificateAttributesDto + { + CommonName = commonName, + Organization = organization, + OrganizationalUnit = organizationalUnit, + Country = country, + State = state, + Locality = locality, + Ttl = ttl, + NotBefore = notBefore, + NotAfter = notAfter, + KeyUsages = keyUsages != null ? new List(keyUsages) : null, + ExtendedKeyUsages = extendedKeyUsages != null ? new List(extendedKeyUsages) : null + }; + + InfisicalIssueCertificateByProfileRequestDto request = new InfisicalIssueCertificateByProfileRequestDto + { + ProfileId = profileId, + Csr = csrPem, + Attributes = attributes + }; + string body = _serializer.Serialize(request); + + try + { + _logger.Information(Component, string.Concat("Attempting to issue certificate via profile '", profileId, "'. Please Wait...")); + InfisicalHttpResponse response = _invoker.InvokeWithCandidateFallback(connection, InfisicalEndpointNames.IssueCertificateByProfile, "IssueCertificateByProfile", null, null, body); + InfisicalIssueCertificateResponseDto dto = _serializer.Deserialize(response.Body); + response.Clear(); + + if (dto == null || dto.Certificate == null || string.IsNullOrEmpty(dto.Certificate.Certificate)) + { + string status = dto != null ? dto.Status : "unknown"; + string message = dto != null ? dto.Message : null; + string requestId = dto != null ? dto.CertificateRequestId : null; + _logger.Warning(Component, string.Concat("Profile issuance did not return a certificate (status='", status ?? "unknown", "'", string.IsNullOrEmpty(message) ? "" : string.Concat(", message='", message, "'"), string.IsNullOrEmpty(requestId) ? "" : string.Concat(", certificateRequestId='", requestId, "'"), "). The profile may require manual approval or additional validation; returning a status-only result.")); + return new InfisicalSignedCertificate + { + Status = status, + StatusMessage = message, + CertificateRequestId = requestId + }; + } + + InfisicalSignedCertificate signed = new InfisicalSignedCertificate + { + SerialNumber = dto.Certificate.SerialNumber, + CertificatePem = dto.Certificate.Certificate, + CertificateChainPem = dto.Certificate.CertificateChain, + IssuingCaCertificatePem = dto.Certificate.IssuingCaCertificate, + Status = dto.Status, + StatusMessage = dto.Message, + CertificateRequestId = dto.CertificateRequestId + }; + _logger.Information(Component, "Infisical certificate issuance (profile) was successful."); + return signed; + } + catch (Exception) + { + _logger.Error(Component, "Infisical certificate issuance (profile) failed."); + throw; + } + } + + public InfisicalPkiSubscriber[] ListPkiSubscribers(InfisicalConnection connection, string projectId) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + if (string.IsNullOrEmpty(projectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + + Dictionary pathParameters = new Dictionary { { "projectId", projectId } }; + + try + { + _logger.Information(Component, "Attempting to list Infisical PKI subscribers. Please Wait..."); + InfisicalHttpResponse response = _invoker.InvokeWithCandidateFallback(connection, InfisicalEndpointNames.ListPkiSubscribers, "ListPkiSubscribers", pathParameters, null, null); + string body = response.Body; + response.Clear(); + + List source = ParsePkiSubscriberListBody(body); + InfisicalPkiSubscriber[] mapped = InfisicalPkiSubscriberMapper.MapMany(source, projectId); + _logger.Information(Component, "Infisical PKI subscriber list retrieval was successful."); + return mapped; + } + catch (Exception) + { + _logger.Error(Component, "Infisical PKI subscriber list retrieval failed."); + throw; + } + } + + public InfisicalPkiSubscriber GetPkiSubscriber(InfisicalConnection connection, string subscriberName, string projectId) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + if (string.IsNullOrEmpty(subscriberName)) { throw new InfisicalConfigurationException("SubscriberName is required."); } + if (string.IsNullOrEmpty(projectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + + Dictionary pathParameters = new Dictionary { { "subscriberName", subscriberName } }; + List> query = new List> { new KeyValuePair("projectId", projectId) }; + + try + { + _logger.Information(Component, string.Concat("Attempting to retrieve Infisical PKI subscriber '", subscriberName, "'. Please Wait...")); + InfisicalHttpResponse response = _invoker.InvokeWithCandidateFallback(connection, InfisicalEndpointNames.GetPkiSubscriber, "GetPkiSubscriber", pathParameters, query, null); + InfisicalPkiSubscriberResponseDto dto = _serializer.Deserialize(response.Body); + response.Clear(); + + InfisicalPkiSubscriber mapped = InfisicalPkiSubscriberMapper.Map(dto, projectId); + _logger.Information(Component, "Infisical PKI subscriber retrieval was successful."); + return mapped; + } + catch (Exception) + { + _logger.Error(Component, "Infisical PKI subscriber retrieval failed."); + throw; + } + } + + private List ParsePkiSubscriberListBody(string body) + { + if (string.IsNullOrEmpty(body)) { return null; } + JToken token = JToken.Parse(body); + if (token.Type == JTokenType.Array) + { + return token.ToObject>(); + } + + InfisicalPkiSubscriberListResponseDto wrapper = token.ToObject(); + return wrapper != null ? wrapper.Subscribers : null; + } + + public InfisicalCertificateProfile[] ListCertificateProfiles(InfisicalConnection connection, string projectId, int? limit, int? offset, bool? includeConfigs) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + if (string.IsNullOrEmpty(projectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + + List> query = new List> + { + new KeyValuePair("projectId", projectId) + }; + 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")); } + + try + { + _logger.Information(Component, "Attempting to list Infisical certificate profiles. Please Wait..."); + InfisicalHttpResponse response = _invoker.InvokeWithCandidateFallback(connection, InfisicalEndpointNames.ListCertificateProfiles, "ListCertificateProfiles", null, query, null); + string body = response.Body; + response.Clear(); + + List source = ParseCertificateProfileListBody(body); + InfisicalCertificateProfile[] mapped = InfisicalCertificateProfileMapper.MapMany(source, projectId); + _logger.Information(Component, "Infisical certificate profile list retrieval was successful."); + return mapped; + } + catch (Exception) + { + _logger.Error(Component, "Infisical certificate profile list retrieval failed."); + throw; + } + } + + public InfisicalCertificateProfile GetCertificateProfile(InfisicalConnection connection, string certificateProfileId, string projectId) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + if (string.IsNullOrEmpty(certificateProfileId)) { throw new InfisicalConfigurationException("CertificateProfileId is required."); } + + Dictionary pathParameters = new Dictionary { { "certificateProfileId", certificateProfileId } }; + List> query = null; + if (!string.IsNullOrEmpty(projectId)) + { + query = new List> { new KeyValuePair("projectId", projectId) }; + } + + try + { + _logger.Information(Component, string.Concat("Attempting to retrieve Infisical certificate profile '", certificateProfileId, "'. Please Wait...")); + InfisicalHttpResponse response = _invoker.InvokeWithCandidateFallback(connection, InfisicalEndpointNames.GetCertificateProfile, "GetCertificateProfile", pathParameters, query, null); + string body = response.Body; + response.Clear(); + + InfisicalCertificateProfileResponseDto inner = ParseCertificateProfileSingleBody(body); + InfisicalCertificateProfile mapped = InfisicalCertificateProfileMapper.Map(inner, projectId); + _logger.Information(Component, "Infisical certificate profile retrieval was successful."); + return mapped; + } + catch (Exception) + { + _logger.Error(Component, "Infisical certificate profile retrieval failed."); + throw; + } + } + + private List ParseCertificateProfileListBody(string body) + { + if (string.IsNullOrEmpty(body)) { return null; } + JToken token = JToken.Parse(body); + if (token.Type == JTokenType.Array) + { + return token.ToObject>(); + } + + InfisicalCertificateProfileListResponseDto wrapper = token.ToObject(); + return wrapper != null ? wrapper.CertificateProfiles : null; + } + + private InfisicalCertificateProfileResponseDto ParseCertificateProfileSingleBody(string body) + { + if (string.IsNullOrEmpty(body)) { return null; } + JToken token = JToken.Parse(body); + if (token.Type != JTokenType.Object) { return null; } + JObject obj = (JObject)token; + + if (obj["certificateProfile"] is JObject inner) { return inner.ToObject(); } + return obj.ToObject(); + } + + public InfisicalCertificatePolicy[] ListCertificatePolicies(InfisicalConnection connection, string projectId, int? limit, int? offset) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + if (string.IsNullOrEmpty(projectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + + List> query = new List> + { + new KeyValuePair("projectId", projectId) + }; + 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))); } + + try + { + _logger.Information(Component, "Attempting to list Infisical certificate policies. Please Wait..."); + InfisicalHttpResponse response = _invoker.InvokeWithCandidateFallback(connection, InfisicalEndpointNames.ListCertificatePolicies, "ListCertificatePolicies", null, query, null); + string body = response.Body; + response.Clear(); + + List source = ParseCertificatePolicyListBody(body); + InfisicalCertificatePolicy[] mapped = InfisicalCertificatePolicyMapper.MapMany(source, projectId); + _logger.Information(Component, "Infisical certificate policy list retrieval was successful."); + return mapped; + } + catch (Exception) + { + _logger.Error(Component, "Infisical certificate policy list retrieval failed."); + throw; + } + } + + public InfisicalCertificatePolicy GetCertificatePolicy(InfisicalConnection connection, string certificatePolicyId, string projectId) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + if (string.IsNullOrEmpty(certificatePolicyId)) { throw new InfisicalConfigurationException("CertificatePolicyId is required."); } + + Dictionary pathParameters = new Dictionary { { "certificatePolicyId", certificatePolicyId } }; + List> query = null; + if (!string.IsNullOrEmpty(projectId)) + { + query = new List> { new KeyValuePair("projectId", projectId) }; + } + + try + { + _logger.Information(Component, string.Concat("Attempting to retrieve Infisical certificate policy '", certificatePolicyId, "'. Please Wait...")); + InfisicalHttpResponse response = _invoker.InvokeWithCandidateFallback(connection, InfisicalEndpointNames.GetCertificatePolicy, "GetCertificatePolicy", pathParameters, query, null); + string body = response.Body; + response.Clear(); + + InfisicalCertificatePolicyResponseDto inner = ParseCertificatePolicySingleBody(body); + InfisicalCertificatePolicy mapped = InfisicalCertificatePolicyMapper.Map(inner, projectId); + _logger.Information(Component, "Infisical certificate policy retrieval was successful."); + return mapped; + } + catch (Exception) + { + _logger.Error(Component, "Infisical certificate policy retrieval failed."); + throw; + } + } + + private List ParseCertificatePolicyListBody(string body) + { + if (string.IsNullOrEmpty(body)) { return null; } + JToken token = JToken.Parse(body); + if (token.Type == JTokenType.Array) + { + return token.ToObject>(); + } + + InfisicalCertificatePolicyListResponseDto wrapper = token.ToObject(); + return wrapper != null ? wrapper.CertificatePolicies : null; + } + + private InfisicalCertificatePolicyResponseDto ParseCertificatePolicySingleBody(string body) + { + if (string.IsNullOrEmpty(body)) { return null; } + JToken token = JToken.Parse(body); + if (token.Type != JTokenType.Object) { return null; } + JObject obj = (JObject)token; + + if (obj["certificatePolicy"] is JObject inner) { return inner.ToObject(); } + return obj.ToObject(); + } + public InfisicalCertificateBundle GetCertificateBundle(InfisicalConnection connection, string serialNumber) { if (connection == null) { throw new ArgumentNullException(nameof(connection)); } @@ -143,6 +629,228 @@ namespace PSInfisicalAPI.Pki } } + public InfisicalCertificateApplication[] ListCertificateApplications(InfisicalConnection connection, string projectId, int? limit, int? offset) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + if (string.IsNullOrEmpty(projectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + + List> query = new List>(); + 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))); } + + Dictionary headers = BuildProjectHeader(projectId); + + try + { + _logger.Information(Component, "Attempting to list Infisical certificate applications. Please Wait..."); + InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.ListCertificateApplications, "ListCertificateApplications", null, query, null, headers); + string body = response.Body; + response.Clear(); + + List source = ParseApplicationListBody(body); + InfisicalCertificateApplication[] mapped = InfisicalCertificateApplicationMapper.MapMany(source, projectId); + _logger.Information(Component, "Infisical certificate application list retrieval was successful."); + return mapped; + } + catch (Exception) + { + _logger.Error(Component, "Infisical certificate application list retrieval failed."); + throw; + } + } + + public InfisicalCertificateApplication GetCertificateApplication(InfisicalConnection connection, string applicationId, string projectId) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + if (string.IsNullOrEmpty(applicationId)) { throw new InfisicalConfigurationException("ApplicationId is required."); } + + Dictionary pathParameters = new Dictionary { { "applicationId", applicationId } }; + Dictionary headers = !string.IsNullOrEmpty(projectId) ? BuildProjectHeader(projectId) : null; + + try + { + _logger.Information(Component, string.Concat("Attempting to retrieve Infisical certificate application '", applicationId, "'. Please Wait...")); + InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.GetCertificateApplication, "GetCertificateApplication", pathParameters, null, null, headers); + string body = response.Body; + response.Clear(); + + InfisicalCertificateApplicationResponseDto inner = ParseApplicationSingleBody(body); + InfisicalCertificateApplication mapped = InfisicalCertificateApplicationMapper.Map(inner, projectId); + _logger.Information(Component, "Infisical certificate application retrieval was successful."); + return mapped; + } + catch (Exception) + { + _logger.Error(Component, "Infisical certificate application retrieval failed."); + throw; + } + } + + public InfisicalCertificateApplication GetCertificateApplicationByName(InfisicalConnection connection, string name, string projectId) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + if (string.IsNullOrEmpty(name)) { throw new InfisicalConfigurationException("ApplicationName is required."); } + if (string.IsNullOrEmpty(projectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + + Dictionary pathParameters = new Dictionary { { "name", name } }; + Dictionary headers = BuildProjectHeader(projectId); + + try + { + _logger.Information(Component, string.Concat("Attempting to retrieve Infisical certificate application '", name, "' by name. Please Wait...")); + InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.GetCertificateApplicationByName, "GetCertificateApplicationByName", pathParameters, null, null, headers); + string body = response.Body; + response.Clear(); + + InfisicalCertificateApplicationResponseDto inner = ParseApplicationSingleBody(body); + InfisicalCertificateApplication mapped = InfisicalCertificateApplicationMapper.Map(inner, projectId); + _logger.Information(Component, "Infisical certificate application (by name) retrieval was successful."); + return mapped; + } + catch (Exception) + { + _logger.Error(Component, "Infisical certificate application (by name) retrieval failed."); + throw; + } + } + + public InfisicalCertificateApplicationProfileAttachment[] ListCertificateApplicationProfiles(InfisicalConnection connection, string applicationId, string projectId) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + if (string.IsNullOrEmpty(applicationId)) { throw new InfisicalConfigurationException("ApplicationId is required."); } + + Dictionary pathParameters = new Dictionary { { "applicationId", applicationId } }; + Dictionary headers = !string.IsNullOrEmpty(projectId) ? BuildProjectHeader(projectId) : null; + + try + { + _logger.Information(Component, string.Concat("Attempting to list profile attachments for Infisical certificate application '", applicationId, "'. Please Wait...")); + InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.ListCertificateApplicationProfiles, "ListCertificateApplicationProfiles", pathParameters, null, null, headers); + string body = response.Body; + response.Clear(); + + List source = ParseApplicationProfilesBody(body); + InfisicalCertificateApplicationProfileAttachment[] mapped = InfisicalCertificateApplicationMapper.MapAttachments(source); + _logger.Information(Component, "Infisical certificate application profile attachment listing was successful."); + return mapped; + } + catch (Exception) + { + _logger.Error(Component, "Infisical certificate application profile attachment listing failed."); + throw; + } + } + + public InfisicalCertificateApplicationEnrollment GetCertificateApplicationEnrollment(InfisicalConnection connection, string applicationId, string profileId, string projectId) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + if (string.IsNullOrEmpty(applicationId)) { throw new InfisicalConfigurationException("ApplicationId is required."); } + if (string.IsNullOrEmpty(profileId)) { throw new InfisicalConfigurationException("ProfileId is required."); } + + Dictionary pathParameters = new Dictionary + { + { "applicationId", applicationId }, + { "profileId", profileId } + }; + Dictionary headers = !string.IsNullOrEmpty(projectId) ? BuildProjectHeader(projectId) : null; + + try + { + _logger.Information(Component, string.Concat("Attempting to retrieve enrollment for application '", applicationId, "' / profile '", profileId, "'. Please Wait...")); + InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.GetCertificateApplicationEnrollment, "GetCertificateApplicationEnrollment", pathParameters, null, null, headers); + InfisicalCertificateApplicationEnrollmentResponseDto dto = _serializer.Deserialize(response.Body); + response.Clear(); + + InfisicalCertificateApplicationEnrollment mapped = InfisicalCertificateApplicationMapper.MapEnrollment(dto); + _logger.Information(Component, "Infisical certificate application enrollment retrieval was successful."); + return mapped; + } + catch (Exception) + { + _logger.Error(Component, "Infisical certificate application enrollment retrieval failed."); + throw; + } + } + + public string GenerateScepDynamicChallenge(InfisicalConnection connection, string applicationId, string profileId) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + if (string.IsNullOrEmpty(applicationId)) { throw new InfisicalConfigurationException("ApplicationId is required."); } + if (string.IsNullOrEmpty(profileId)) { throw new InfisicalConfigurationException("ProfileId is required."); } + + Dictionary pathParameters = new Dictionary + { + { "applicationId", applicationId }, + { "profileId", profileId } + }; + Dictionary headers = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "Accept", "text/plain" } + }; + + try + { + _logger.Information(Component, string.Concat("Attempting to generate SCEP dynamic challenge for application '", applicationId, "' / profile '", profileId, "'. Please Wait...")); + InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.GenerateScepDynamicChallenge, "GenerateScepDynamicChallenge", pathParameters, null, string.Empty, headers); + string body = response.Body != null ? response.Body.Trim() : null; + response.Clear(); + + if (string.IsNullOrEmpty(body)) { throw new InfisicalApiException("SCEP dynamic challenge response was empty."); } + _logger.Information(Component, "Infisical SCEP dynamic challenge generation was successful."); + return body; + } + catch (Exception) + { + _logger.Error(Component, "Infisical SCEP dynamic challenge generation failed."); + throw; + } + } + + private static Dictionary BuildProjectHeader(string projectId) + { + return new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "x-infisical-project-id", projectId } + }; + } + + private List ParseApplicationListBody(string body) + { + if (string.IsNullOrEmpty(body)) { return null; } + JToken token = JToken.Parse(body); + if (token.Type == JTokenType.Array) + { + return token.ToObject>(); + } + + InfisicalCertificateApplicationListResponseDto wrapper = token.ToObject(); + return wrapper != null ? wrapper.Applications : null; + } + + private InfisicalCertificateApplicationResponseDto ParseApplicationSingleBody(string body) + { + if (string.IsNullOrEmpty(body)) { return null; } + JToken token = JToken.Parse(body); + if (token.Type != JTokenType.Object) { return null; } + JObject obj = (JObject)token; + + if (obj["application"] is JObject inner) { return inner.ToObject(); } + return obj.ToObject(); + } + + private List ParseApplicationProfilesBody(string body) + { + if (string.IsNullOrEmpty(body)) { return null; } + JToken token = JToken.Parse(body); + if (token.Type == JTokenType.Array) + { + return token.ToObject>(); + } + + InfisicalCertificateApplicationProfilesResponseDto wrapper = token.ToObject(); + return wrapper != null ? wrapper.Profiles : null; + } + internal static InfisicalCertificateSearchRequestDto BuildSearchRequest(InfisicalCertificateSearchQuery query) { return new InfisicalCertificateSearchRequestDto diff --git a/src/PSInfisicalAPI/Pki/InfisicalPkiSubscriberDtos.cs b/src/PSInfisicalAPI/Pki/InfisicalPkiSubscriberDtos.cs new file mode 100644 index 0000000..b424535 --- /dev/null +++ b/src/PSInfisicalAPI/Pki/InfisicalPkiSubscriberDtos.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace PSInfisicalAPI.Pki +{ + internal sealed class InfisicalPkiSubscriberResponseDto + { + [JsonProperty("id")] public string Id { get; set; } + [JsonProperty("projectId")] public string ProjectId { get; set; } + [JsonProperty("caId")] public string CaId { get; set; } + [JsonProperty("name")] public string Name { get; set; } + [JsonProperty("commonName")] public string CommonName { get; set; } + [JsonProperty("status")] public string Status { get; set; } + [JsonProperty("ttl")] public string Ttl { get; set; } + [JsonProperty("subjectAlternativeNames")] public List SubjectAlternativeNames { get; set; } + [JsonProperty("keyUsages")] public List KeyUsages { get; set; } + [JsonProperty("extendedKeyUsages")] public List ExtendedKeyUsages { get; set; } + [JsonProperty("enableAutoRenewal")] public bool? EnableAutoRenewal { get; set; } + [JsonProperty("autoRenewalPeriodInDays")] public int? AutoRenewalPeriodInDays { get; set; } + [JsonProperty("lastOperationStatus")] public string LastOperationStatus { get; set; } + [JsonProperty("lastOperationMessage")] public string LastOperationMessage { get; set; } + [JsonProperty("lastOperationAt")] public string LastOperationAt { get; set; } + [JsonProperty("createdAt")] public string CreatedAt { get; set; } + [JsonProperty("updatedAt")] public string UpdatedAt { get; set; } + [JsonProperty("properties")] public InfisicalPkiSubscriberPropertiesDto Properties { get; set; } + } + + internal sealed class InfisicalPkiSubscriberPropertiesDto + { + [JsonProperty("azureTemplateType")] public string AzureTemplateType { get; set; } + [JsonProperty("organization")] public string Organization { get; set; } + [JsonProperty("organizationalUnit")] public string OrganizationalUnit { get; set; } + [JsonProperty("country")] public string Country { get; set; } + [JsonProperty("state")] public string State { get; set; } + [JsonProperty("locality")] public string Locality { get; set; } + [JsonProperty("emailAddress")] public string EmailAddress { get; set; } + } + + internal sealed class InfisicalPkiSubscriberListResponseDto + { + [JsonProperty("subscribers")] public List Subscribers { get; set; } + } +} diff --git a/src/PSInfisicalAPI/Pki/InfisicalPkiSubscriberMapper.cs b/src/PSInfisicalAPI/Pki/InfisicalPkiSubscriberMapper.cs new file mode 100644 index 0000000..1e0284c --- /dev/null +++ b/src/PSInfisicalAPI/Pki/InfisicalPkiSubscriberMapper.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using PSInfisicalAPI.Models; + +namespace PSInfisicalAPI.Pki +{ + internal static class InfisicalPkiSubscriberMapper + { + public static InfisicalPkiSubscriber Map(InfisicalPkiSubscriberResponseDto dto, string fallbackProjectId) + { + if (dto == null) + { + return null; + } + + return new InfisicalPkiSubscriber + { + Id = dto.Id, + ProjectId = !string.IsNullOrEmpty(dto.ProjectId) ? dto.ProjectId : fallbackProjectId, + CaId = dto.CaId, + Name = dto.Name, + CommonName = dto.CommonName, + Status = dto.Status, + Ttl = dto.Ttl, + SubjectAlternativeNames = dto.SubjectAlternativeNames != null ? dto.SubjectAlternativeNames.ToArray() : null, + KeyUsages = dto.KeyUsages != null ? dto.KeyUsages.ToArray() : null, + ExtendedKeyUsages = dto.ExtendedKeyUsages != null ? dto.ExtendedKeyUsages.ToArray() : null, + EnableAutoRenewal = dto.EnableAutoRenewal, + AutoRenewalPeriodInDays = dto.AutoRenewalPeriodInDays, + LastOperationStatus = dto.LastOperationStatus, + LastOperationMessage = dto.LastOperationMessage, + LastOperationAtUtc = ParseTimestamp(dto.LastOperationAt), + CreatedAtUtc = ParseTimestamp(dto.CreatedAt), + UpdatedAtUtc = ParseTimestamp(dto.UpdatedAt), + Properties = MapProperties(dto.Properties) + }; + } + + public static InfisicalPkiSubscriber[] MapMany(IEnumerable items, string fallbackProjectId) + { + if (items == null) + { + return Array.Empty(); + } + + List results = new List(); + foreach (InfisicalPkiSubscriberResponseDto dto in items) + { + InfisicalPkiSubscriber mapped = Map(dto, fallbackProjectId); + if (mapped != null) + { + results.Add(mapped); + } + } + + return results.ToArray(); + } + + private static InfisicalPkiSubscriberProperties MapProperties(InfisicalPkiSubscriberPropertiesDto dto) + { + if (dto == null) + { + return null; + } + + return new InfisicalPkiSubscriberProperties + { + AzureTemplateType = dto.AzureTemplateType, + Organization = dto.Organization, + OrganizationalUnit = dto.OrganizationalUnit, + Country = dto.Country, + State = dto.State, + Locality = dto.Locality, + EmailAddress = dto.EmailAddress + }; + } + + private static DateTimeOffset? ParseTimestamp(string value) + { + if (string.IsNullOrEmpty(value)) + { + return null; + } + + DateTimeOffset parsed; + if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out parsed)) + { + return parsed; + } + + return null; + } + } +} diff --git a/src/PSInfisicalAPI/Pki/InfisicalPrivateKeyProtection.cs b/src/PSInfisicalAPI/Pki/InfisicalPrivateKeyProtection.cs new file mode 100644 index 0000000..0a793b9 --- /dev/null +++ b/src/PSInfisicalAPI/Pki/InfisicalPrivateKeyProtection.cs @@ -0,0 +1,10 @@ +namespace PSInfisicalAPI.Pki +{ + public enum InfisicalPrivateKeyProtection + { + Exportable = 0, + LocalOnly = 1, + NonExportable = 2, + Ephemeral = 3 + } +} diff --git a/src/PSInfisicalAPI/Pki/InfisicalSignCertificateDtos.cs b/src/PSInfisicalAPI/Pki/InfisicalSignCertificateDtos.cs new file mode 100644 index 0000000..71d3647 --- /dev/null +++ b/src/PSInfisicalAPI/Pki/InfisicalSignCertificateDtos.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace PSInfisicalAPI.Pki +{ + internal sealed class InfisicalSignCertificateBySubscriberRequestDto + { + [JsonProperty("projectId")] public string ProjectId { get; set; } + [JsonProperty("csr")] public string Csr { get; set; } + } + + internal sealed class InfisicalSignCertificateByCaRequestDto + { + [JsonProperty("csr")] public string Csr { get; set; } + [JsonProperty("commonName", NullValueHandling = NullValueHandling.Ignore)] public string CommonName { get; set; } + [JsonProperty("altNames", NullValueHandling = NullValueHandling.Ignore)] public string AltNames { get; set; } + [JsonProperty("ttl", NullValueHandling = NullValueHandling.Ignore)] public string Ttl { get; set; } + [JsonProperty("notBefore", NullValueHandling = NullValueHandling.Ignore)] public string NotBefore { get; set; } + [JsonProperty("notAfter", NullValueHandling = NullValueHandling.Ignore)] public string NotAfter { get; set; } + [JsonProperty("friendlyName", NullValueHandling = NullValueHandling.Ignore)] public string FriendlyName { get; set; } + [JsonProperty("pkiCollectionId", NullValueHandling = NullValueHandling.Ignore)] public string PkiCollectionId { get; set; } + [JsonProperty("keyUsages", NullValueHandling = NullValueHandling.Ignore)] public List KeyUsages { get; set; } + [JsonProperty("extendedKeyUsages", NullValueHandling = NullValueHandling.Ignore)] public List ExtendedKeyUsages { get; set; } + } + + internal sealed class InfisicalSignCertificateResponseDto + { + [JsonProperty("certificate")] public string Certificate { get; set; } + [JsonProperty("certificateChain")] public string CertificateChain { get; set; } + [JsonProperty("issuingCaCertificate")] public string IssuingCaCertificate { get; set; } + [JsonProperty("serialNumber")] public string SerialNumber { get; set; } + } + + internal sealed class InfisicalIssueCertificateByProfileRequestDto + { + [JsonProperty("profileId")] public string ProfileId { get; set; } + [JsonProperty("csr", NullValueHandling = NullValueHandling.Ignore)] public string Csr { get; set; } + [JsonProperty("attributes", NullValueHandling = NullValueHandling.Ignore)] public InfisicalIssueCertificateAttributesDto Attributes { get; set; } + } + + internal sealed class InfisicalIssueCertificateAttributesDto + { + [JsonProperty("commonName", NullValueHandling = NullValueHandling.Ignore)] public string CommonName { get; set; } + [JsonProperty("organization", NullValueHandling = NullValueHandling.Ignore)] public string Organization { get; set; } + [JsonProperty("organizationalUnit", NullValueHandling = NullValueHandling.Ignore)] public string OrganizationalUnit { get; set; } + [JsonProperty("country", NullValueHandling = NullValueHandling.Ignore)] public string Country { get; set; } + [JsonProperty("state", NullValueHandling = NullValueHandling.Ignore)] public string State { get; set; } + [JsonProperty("locality", NullValueHandling = NullValueHandling.Ignore)] public string Locality { get; set; } + [JsonProperty("ttl", NullValueHandling = NullValueHandling.Ignore)] public string Ttl { get; set; } + [JsonProperty("notBefore", NullValueHandling = NullValueHandling.Ignore)] public string NotBefore { get; set; } + [JsonProperty("notAfter", NullValueHandling = NullValueHandling.Ignore)] public string NotAfter { get; set; } + [JsonProperty("keyUsages", NullValueHandling = NullValueHandling.Ignore)] public List KeyUsages { get; set; } + [JsonProperty("extendedKeyUsages", NullValueHandling = NullValueHandling.Ignore)] public List ExtendedKeyUsages { get; set; } + } + + internal sealed class InfisicalIssueCertificateResponseDto + { + [JsonProperty("certificate")] public InfisicalIssueCertificateInnerDto Certificate { get; set; } + [JsonProperty("certificateRequestId")] public string CertificateRequestId { get; set; } + [JsonProperty("status")] public string Status { get; set; } + [JsonProperty("message")] public string Message { get; set; } + } + + internal sealed class InfisicalIssueCertificateInnerDto + { + [JsonProperty("certificate")] public string Certificate { get; set; } + [JsonProperty("certificateChain")] public string CertificateChain { get; set; } + [JsonProperty("issuingCaCertificate")] public string IssuingCaCertificate { get; set; } + [JsonProperty("serialNumber")] public string SerialNumber { get; set; } + [JsonProperty("certificateId")] public string CertificateId { get; set; } + [JsonProperty("privateKey")] public string PrivateKey { get; set; } + } +} diff --git a/src/PSInfisicalAPI/Projects/InfisicalProjectClient.cs b/src/PSInfisicalAPI/Projects/InfisicalProjectClient.cs index a0c5e28..900f29a 100644 --- a/src/PSInfisicalAPI/Projects/InfisicalProjectClient.cs +++ b/src/PSInfisicalAPI/Projects/InfisicalProjectClient.cs @@ -27,13 +27,25 @@ namespace PSInfisicalAPI.Projects } public InfisicalProject[] List(InfisicalConnection connection) + { + return List(connection, null, false); + } + + public InfisicalProject[] List(InfisicalConnection connection, string type, bool includeRoles) { if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + List> queryParameters = new List>(); + queryParameters.Add(new KeyValuePair("includeRoles", includeRoles ? "true" : "false")); + if (!string.IsNullOrEmpty(type)) + { + queryParameters.Add(new KeyValuePair("type", type)); + } + try { _logger.Information(Component, "Attempting to list Infisical projects. Please Wait..."); - InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.ListProjects, "ListProjects", null, null, null); + InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.ListProjects, "ListProjects", null, queryParameters, null); InfisicalProjectListResponseDto dto = _serializer.Deserialize(response.Body); response.Clear(); diff --git a/src/PSInfisicalAPI/Secrets/InfisicalSecretsClient.cs b/src/PSInfisicalAPI/Secrets/InfisicalSecretsClient.cs index 8e4161b..f13bc53 100644 --- a/src/PSInfisicalAPI/Secrets/InfisicalSecretsClient.cs +++ b/src/PSInfisicalAPI/Secrets/InfisicalSecretsClient.cs @@ -32,13 +32,11 @@ namespace PSInfisicalAPI.Secrets if (connection == null) { throw new ArgumentNullException(nameof(connection)); } if (query == null) { throw new ArgumentNullException(nameof(query)); } - string resolvedProjectId = FirstNonEmpty(query.ProjectId, connection.ProjectId); - List> queryParameters = new List>(); - AddIfNotNull(queryParameters, "workspaceId", resolvedProjectId); - AddIfNotNull(queryParameters, "projectId", resolvedProjectId); - AddIfNotNull(queryParameters, "environment", FirstNonEmpty(query.Environment, connection.Environment)); - AddIfNotNull(queryParameters, "secretPath", FirstNonEmpty(query.SecretPath, connection.DefaultSecretPath, "/")); + AddIfNotNull(queryParameters, "workspaceId", query.ProjectId); + AddIfNotNull(queryParameters, "projectId", query.ProjectId); + AddIfNotNull(queryParameters, "environment", query.Environment); + AddIfNotNull(queryParameters, "secretPath", FirstNonEmpty(query.SecretPath, "/")); queryParameters.Add(new KeyValuePair("recursive", query.Recursive ? "true" : "false")); if (query.IncludeImports.HasValue) { queryParameters.Add(new KeyValuePair("includeImports", query.IncludeImports.Value ? "true" : "false")); } if (query.IncludePersonalOverrides) { queryParameters.Add(new KeyValuePair("includePersonalOverrides", "true")); } @@ -96,13 +94,11 @@ namespace PSInfisicalAPI.Secrets Dictionary pathParameters = new Dictionary { { "secretName", query.SecretName } }; - string resolvedProjectId = FirstNonEmpty(query.ProjectId, connection.ProjectId); - List> queryParameters = new List>(); - AddIfNotNull(queryParameters, "workspaceId", resolvedProjectId); - AddIfNotNull(queryParameters, "projectId", resolvedProjectId); - AddIfNotNull(queryParameters, "environment", FirstNonEmpty(query.Environment, connection.Environment)); - AddIfNotNull(queryParameters, "secretPath", FirstNonEmpty(query.SecretPath, connection.DefaultSecretPath, "/")); + AddIfNotNull(queryParameters, "workspaceId", query.ProjectId); + AddIfNotNull(queryParameters, "projectId", query.ProjectId); + AddIfNotNull(queryParameters, "environment", query.Environment); + AddIfNotNull(queryParameters, "secretPath", FirstNonEmpty(query.SecretPath, "/")); AddIfNotNull(queryParameters, "type", string.IsNullOrEmpty(query.Type) ? "shared" : query.Type.ToLowerInvariant()); if (query.Version.HasValue) { queryParameters.Add(new KeyValuePair("version", query.Version.Value.ToString(CultureInfo.InvariantCulture))); } if (query.ViewSecretValue.HasValue) { queryParameters.Add(new KeyValuePair("viewSecretValue", query.ViewSecretValue.Value ? "true" : "false")); } @@ -143,17 +139,15 @@ namespace PSInfisicalAPI.Secrets if (string.IsNullOrEmpty(request.SecretName)) { throw new InfisicalConfigurationException("SecretName is required."); } if (request.SecretValue == null) { throw new InfisicalConfigurationException("SecretValue is required."); } - string resolvedProjectId = FirstNonEmpty(request.ProjectId, connection.ProjectId); - string resolvedEnvironment = FirstNonEmpty(request.Environment, connection.Environment); - if (string.IsNullOrEmpty(resolvedProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } - if (string.IsNullOrEmpty(resolvedEnvironment)) { throw new InfisicalConfigurationException("Environment is required."); } + if (string.IsNullOrEmpty(request.ProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + if (string.IsNullOrEmpty(request.Environment)) { throw new InfisicalConfigurationException("Environment is required."); } Dictionary pathParameters = new Dictionary { { "secretName", request.SecretName } }; InfisicalSecretCreateRequestDto dtoRequest = new InfisicalSecretCreateRequestDto { - WorkspaceId = resolvedProjectId, - Environment = resolvedEnvironment, - SecretPath = FirstNonEmpty(request.SecretPath, connection.DefaultSecretPath, "/"), + WorkspaceId = request.ProjectId, + Environment = request.Environment, + SecretPath = FirstNonEmpty(request.SecretPath, "/"), Type = string.IsNullOrEmpty(request.Type) ? "shared" : request.Type.ToLowerInvariant(), SecretValue = request.SecretValue, SecretComment = request.SecretComment, @@ -186,17 +180,15 @@ namespace PSInfisicalAPI.Secrets if (request == null) { throw new ArgumentNullException(nameof(request)); } if (string.IsNullOrEmpty(request.SecretName)) { throw new InfisicalConfigurationException("SecretName is required."); } - string resolvedProjectId = FirstNonEmpty(request.ProjectId, connection.ProjectId); - string resolvedEnvironment = FirstNonEmpty(request.Environment, connection.Environment); - if (string.IsNullOrEmpty(resolvedProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } - if (string.IsNullOrEmpty(resolvedEnvironment)) { throw new InfisicalConfigurationException("Environment is required."); } + if (string.IsNullOrEmpty(request.ProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + if (string.IsNullOrEmpty(request.Environment)) { throw new InfisicalConfigurationException("Environment is required."); } Dictionary pathParameters = new Dictionary { { "secretName", request.SecretName } }; InfisicalSecretUpdateRequestDto dtoRequest = new InfisicalSecretUpdateRequestDto { - WorkspaceId = resolvedProjectId, - Environment = resolvedEnvironment, - SecretPath = FirstNonEmpty(request.SecretPath, connection.DefaultSecretPath, "/"), + WorkspaceId = request.ProjectId, + Environment = request.Environment, + SecretPath = FirstNonEmpty(request.SecretPath, "/"), Type = string.IsNullOrEmpty(request.Type) ? "shared" : request.Type.ToLowerInvariant(), SecretValue = request.SecretValue, SecretComment = request.SecretComment, @@ -230,10 +222,8 @@ namespace PSInfisicalAPI.Secrets if (request == null) { throw new ArgumentNullException(nameof(request)); } if (request.Secrets == null || request.Secrets.Length == 0) { throw new InfisicalConfigurationException("At least one secret is required."); } - string resolvedProjectId = FirstNonEmpty(request.ProjectId, connection.ProjectId); - string resolvedEnvironment = FirstNonEmpty(request.Environment, connection.Environment); - if (string.IsNullOrEmpty(resolvedProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } - if (string.IsNullOrEmpty(resolvedEnvironment)) { throw new InfisicalConfigurationException("Environment is required."); } + if (string.IsNullOrEmpty(request.ProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + if (string.IsNullOrEmpty(request.Environment)) { throw new InfisicalConfigurationException("Environment is required."); } List items = new List(request.Secrets.Length); foreach (InfisicalBulkCreateSecretItem item in request.Secrets) @@ -253,10 +243,10 @@ namespace PSInfisicalAPI.Secrets InfisicalSecretBatchCreateRequestDto dtoRequest = new InfisicalSecretBatchCreateRequestDto { - WorkspaceId = resolvedProjectId, - ProjectId = resolvedProjectId, - Environment = resolvedEnvironment, - SecretPath = FirstNonEmpty(request.SecretPath, connection.DefaultSecretPath, "/"), + WorkspaceId = request.ProjectId, + ProjectId = request.ProjectId, + Environment = request.Environment, + SecretPath = FirstNonEmpty(request.SecretPath, "/"), Secrets = items }; string body = _serializer.Serialize(dtoRequest); @@ -285,10 +275,8 @@ namespace PSInfisicalAPI.Secrets if (request == null) { throw new ArgumentNullException(nameof(request)); } if (request.Secrets == null || request.Secrets.Length == 0) { throw new InfisicalConfigurationException("At least one secret is required."); } - string resolvedProjectId = FirstNonEmpty(request.ProjectId, connection.ProjectId); - string resolvedEnvironment = FirstNonEmpty(request.Environment, connection.Environment); - if (string.IsNullOrEmpty(resolvedProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } - if (string.IsNullOrEmpty(resolvedEnvironment)) { throw new InfisicalConfigurationException("Environment is required."); } + if (string.IsNullOrEmpty(request.ProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + if (string.IsNullOrEmpty(request.Environment)) { throw new InfisicalConfigurationException("Environment is required."); } List items = new List(request.Secrets.Length); foreach (InfisicalBulkUpdateSecretItem item in request.Secrets) @@ -309,10 +297,10 @@ namespace PSInfisicalAPI.Secrets InfisicalSecretBatchUpdateRequestDto dtoRequest = new InfisicalSecretBatchUpdateRequestDto { - WorkspaceId = resolvedProjectId, - ProjectId = resolvedProjectId, - Environment = resolvedEnvironment, - SecretPath = FirstNonEmpty(request.SecretPath, connection.DefaultSecretPath, "/"), + WorkspaceId = request.ProjectId, + ProjectId = request.ProjectId, + Environment = request.Environment, + SecretPath = FirstNonEmpty(request.SecretPath, "/"), Mode = request.Mode, Secrets = items }; @@ -342,10 +330,8 @@ namespace PSInfisicalAPI.Secrets if (request == null) { throw new ArgumentNullException(nameof(request)); } if (request.SecretNames == null || request.SecretNames.Length == 0) { throw new InfisicalConfigurationException("At least one secret name is required."); } - string resolvedProjectId = FirstNonEmpty(request.ProjectId, connection.ProjectId); - string resolvedEnvironment = FirstNonEmpty(request.Environment, connection.Environment); - if (string.IsNullOrEmpty(resolvedProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } - if (string.IsNullOrEmpty(resolvedEnvironment)) { throw new InfisicalConfigurationException("Environment is required."); } + if (string.IsNullOrEmpty(request.ProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + if (string.IsNullOrEmpty(request.Environment)) { throw new InfisicalConfigurationException("Environment is required."); } List items = new List(request.SecretNames.Length); foreach (string name in request.SecretNames) @@ -356,10 +342,10 @@ namespace PSInfisicalAPI.Secrets InfisicalSecretBatchDeleteRequestDto dtoRequest = new InfisicalSecretBatchDeleteRequestDto { - WorkspaceId = resolvedProjectId, - ProjectId = resolvedProjectId, - Environment = resolvedEnvironment, - SecretPath = FirstNonEmpty(request.SecretPath, connection.DefaultSecretPath, "/"), + WorkspaceId = request.ProjectId, + ProjectId = request.ProjectId, + Environment = request.Environment, + SecretPath = FirstNonEmpty(request.SecretPath, "/"), Secrets = items }; string body = _serializer.Serialize(dtoRequest); @@ -384,13 +370,11 @@ namespace PSInfisicalAPI.Secrets if (request == null) { throw new ArgumentNullException(nameof(request)); } if (request.SecretIds == null || request.SecretIds.Length == 0) { throw new InfisicalConfigurationException("At least one SecretId is required."); } - string resolvedProjectId = FirstNonEmpty(request.ProjectId, connection.ProjectId); - string resolvedSourceEnv = FirstNonEmpty(request.SourceEnvironment, connection.Environment); - if (string.IsNullOrEmpty(resolvedProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } - if (string.IsNullOrEmpty(resolvedSourceEnv)) { throw new InfisicalConfigurationException("SourceEnvironment is required."); } + if (string.IsNullOrEmpty(request.ProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + if (string.IsNullOrEmpty(request.SourceEnvironment)) { throw new InfisicalConfigurationException("SourceEnvironment is required."); } if (string.IsNullOrEmpty(request.DestinationEnvironment)) { throw new InfisicalConfigurationException("DestinationEnvironment is required."); } - string resolvedSourcePath = FirstNonEmpty(request.SourceSecretPath, connection.DefaultSecretPath, "/"); + string resolvedSourcePath = FirstNonEmpty(request.SourceSecretPath, "/"); string resolvedDestPath = FirstNonEmpty(request.DestinationSecretPath, resolvedSourcePath); InfisicalSecretDuplicateAttributesDto attributes = null; @@ -407,8 +391,8 @@ namespace PSInfisicalAPI.Secrets InfisicalSecretDuplicateRequestDto dtoRequest = new InfisicalSecretDuplicateRequestDto { - ProjectId = resolvedProjectId, - SourceEnvironment = resolvedSourceEnv, + ProjectId = request.ProjectId, + SourceEnvironment = request.SourceEnvironment, DestinationEnvironment = request.DestinationEnvironment, SourceSecretPath = resolvedSourcePath, DestinationSecretPath = resolvedDestPath, @@ -454,17 +438,15 @@ namespace PSInfisicalAPI.Secrets if (request == null) { throw new ArgumentNullException(nameof(request)); } if (string.IsNullOrEmpty(request.SecretName)) { throw new InfisicalConfigurationException("SecretName is required."); } - string resolvedProjectId = FirstNonEmpty(request.ProjectId, connection.ProjectId); - string resolvedEnvironment = FirstNonEmpty(request.Environment, connection.Environment); - if (string.IsNullOrEmpty(resolvedProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } - if (string.IsNullOrEmpty(resolvedEnvironment)) { throw new InfisicalConfigurationException("Environment is required."); } + if (string.IsNullOrEmpty(request.ProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + if (string.IsNullOrEmpty(request.Environment)) { throw new InfisicalConfigurationException("Environment is required."); } Dictionary pathParameters = new Dictionary { { "secretName", request.SecretName } }; InfisicalSecretDeleteRequestDto dtoRequest = new InfisicalSecretDeleteRequestDto { - WorkspaceId = resolvedProjectId, - Environment = resolvedEnvironment, - SecretPath = FirstNonEmpty(request.SecretPath, connection.DefaultSecretPath, "/"), + WorkspaceId = request.ProjectId, + Environment = request.Environment, + SecretPath = FirstNonEmpty(request.SecretPath, "/"), Type = string.IsNullOrEmpty(request.Type) ? "shared" : request.Type.ToLowerInvariant() }; string body = _serializer.Serialize(dtoRequest); @@ -625,15 +607,14 @@ namespace PSInfisicalAPI.Secrets private static InfisicalApiException BuildApiException(InfisicalHttpResponse response, InfisicalEndpointDefinition definition) { - InfisicalApiException exception = new InfisicalApiException(string.Concat( - "Infisical API returned ", - response.StatusCode.ToString(CultureInfo.InvariantCulture), - " (", response.ReasonPhrase ?? string.Empty, ").")); + string message = InfisicalApiErrorEnvelope.BuildExceptionMessage(response.StatusCode, response.ReasonPhrase, response.Body); + InfisicalApiException exception = new InfisicalApiException(message); exception.StatusCode = response.StatusCode; exception.ReasonPhrase = response.ReasonPhrase; exception.EndpointName = definition.Name; exception.RequestMethod = definition.Method; exception.SanitizedBody = response.Body; + InfisicalApiErrorEnvelope.Enrich(exception, response.Body); return exception; } diff --git a/src/PSInfisicalAPI/Tags/InfisicalTagClient.cs b/src/PSInfisicalAPI/Tags/InfisicalTagClient.cs index 9430ae2..2eec1bf 100644 --- a/src/PSInfisicalAPI/Tags/InfisicalTagClient.cs +++ b/src/PSInfisicalAPI/Tags/InfisicalTagClient.cs @@ -29,10 +29,9 @@ namespace PSInfisicalAPI.Tags public InfisicalTag[] List(InfisicalConnection connection, string projectId) { if (connection == null) { throw new ArgumentNullException(nameof(connection)); } - string resolvedProjectId = FirstNonEmpty(projectId, connection.ProjectId); - if (string.IsNullOrEmpty(resolvedProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + if (string.IsNullOrEmpty(projectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } - Dictionary pathParameters = new Dictionary { { "projectId", resolvedProjectId } }; + Dictionary pathParameters = new Dictionary { { "projectId", projectId } }; try { @@ -42,7 +41,7 @@ namespace PSInfisicalAPI.Tags response.Clear(); List source = dto != null ? (dto.WorkspaceTags ?? dto.Tags) : null; - InfisicalTag[] mapped = InfisicalTagMapper.MapMany(source, resolvedProjectId); + InfisicalTag[] mapped = InfisicalTagMapper.MapMany(source, projectId); _logger.Information(Component, "Infisical tag list retrieval was successful."); return mapped; } @@ -56,11 +55,10 @@ namespace PSInfisicalAPI.Tags public InfisicalTag Retrieve(InfisicalConnection connection, string projectId, string tagSlugOrId) { if (connection == null) { throw new ArgumentNullException(nameof(connection)); } - string resolvedProjectId = FirstNonEmpty(projectId, connection.ProjectId); - if (string.IsNullOrEmpty(resolvedProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + if (string.IsNullOrEmpty(projectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } if (string.IsNullOrEmpty(tagSlugOrId)) { throw new InfisicalConfigurationException("Tag slug or id is required."); } - InfisicalTag[] all = List(connection, resolvedProjectId); + InfisicalTag[] all = List(connection, projectId); foreach (InfisicalTag tag in all) { if (string.Equals(tag.Id, tagSlugOrId, StringComparison.OrdinalIgnoreCase) || @@ -76,11 +74,10 @@ namespace PSInfisicalAPI.Tags public InfisicalTag Create(InfisicalConnection connection, string projectId, string slug, string name, string color) { if (connection == null) { throw new ArgumentNullException(nameof(connection)); } - string resolvedProjectId = FirstNonEmpty(projectId, connection.ProjectId); - if (string.IsNullOrEmpty(resolvedProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + if (string.IsNullOrEmpty(projectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } if (string.IsNullOrEmpty(slug)) { throw new InfisicalConfigurationException("Slug is required."); } - Dictionary pathParameters = new Dictionary { { "projectId", resolvedProjectId } }; + Dictionary pathParameters = new Dictionary { { "projectId", projectId } }; InfisicalTagCreateRequestDto request = new InfisicalTagCreateRequestDto { Slug = slug, Name = name, Color = color }; string body = _serializer.Serialize(request); @@ -92,7 +89,7 @@ namespace PSInfisicalAPI.Tags response.Clear(); InfisicalTagResponseDto inner = dto != null ? (dto.WorkspaceTag ?? dto.Tag) : null; - InfisicalTag mapped = InfisicalTagMapper.Map(inner, resolvedProjectId); + InfisicalTag mapped = InfisicalTagMapper.Map(inner, projectId); _logger.Information(Component, "Infisical tag creation was successful."); return mapped; } @@ -106,11 +103,10 @@ namespace PSInfisicalAPI.Tags public InfisicalTag Update(InfisicalConnection connection, string projectId, string tagId, string slug, string name, string color) { if (connection == null) { throw new ArgumentNullException(nameof(connection)); } - string resolvedProjectId = FirstNonEmpty(projectId, connection.ProjectId); - if (string.IsNullOrEmpty(resolvedProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + if (string.IsNullOrEmpty(projectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } if (string.IsNullOrEmpty(tagId)) { throw new InfisicalConfigurationException("TagId is required."); } - Dictionary pathParameters = new Dictionary { { "projectId", resolvedProjectId }, { "tagId", tagId } }; + Dictionary pathParameters = new Dictionary { { "projectId", projectId }, { "tagId", tagId } }; InfisicalTagUpdateRequestDto request = new InfisicalTagUpdateRequestDto { Slug = slug, Name = name, Color = color }; string body = _serializer.Serialize(request); @@ -122,7 +118,7 @@ namespace PSInfisicalAPI.Tags response.Clear(); InfisicalTagResponseDto inner = dto != null ? (dto.WorkspaceTag ?? dto.Tag) : null; - InfisicalTag mapped = InfisicalTagMapper.Map(inner, resolvedProjectId); + InfisicalTag mapped = InfisicalTagMapper.Map(inner, projectId); _logger.Information(Component, "Infisical tag update was successful."); return mapped; } @@ -136,11 +132,10 @@ namespace PSInfisicalAPI.Tags public void Delete(InfisicalConnection connection, string projectId, string tagId) { if (connection == null) { throw new ArgumentNullException(nameof(connection)); } - string resolvedProjectId = FirstNonEmpty(projectId, connection.ProjectId); - if (string.IsNullOrEmpty(resolvedProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + if (string.IsNullOrEmpty(projectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } if (string.IsNullOrEmpty(tagId)) { throw new InfisicalConfigurationException("TagId is required."); } - Dictionary pathParameters = new Dictionary { { "projectId", resolvedProjectId }, { "tagId", tagId } }; + Dictionary pathParameters = new Dictionary { { "projectId", projectId }, { "tagId", tagId } }; try { @@ -156,11 +151,5 @@ namespace PSInfisicalAPI.Tags } } - private static string FirstNonEmpty(params string[] values) - { - if (values == null) { return null; } - foreach (string value in values) { if (!string.IsNullOrEmpty(value)) { return value; } } - return null; - } } }