diff --git a/CHANGELOG.md b/CHANGELOG.md
index 538967c..55870a4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,11 +6,18 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loos
## Unreleased
+- Added Organization CRUD cmdlets: `Get-InfisicalOrganization`, `New-InfisicalOrganization`, `Update-InfisicalOrganization`, `Remove-InfisicalOrganization`. `Get` lists every organization the active session can see (List parameter set, default) and returns a single record when `-OrganizationId` is supplied (Single parameter set). `New`/`Update`/`Remove` honor `-WhatIf`/`-Confirm`; `Remove` defaults to High `ConfirmImpact` and supports `-PassThru`. No project context required. Backed by new `InfisicalOrganization` model, DTO, mapper, and client wired into `InfisicalEndpointRegistry` (`ListOrganizations`, `RetrieveOrganization`, `CreateOrganization`, `UpdateOrganization`, `DeleteOrganization`).
+- Added Sub-Organization CRUD cmdlets: `Get-InfisicalSubOrganization`, `New-InfisicalSubOrganization`, `Update-InfisicalSubOrganization`, `Remove-InfisicalSubOrganization`, targeting the `/api/v1/sub-organizations` Beta endpoints. `Get` lists by default and accepts optional `-Limit`, `-Offset`, `-Search`, `-OrderBy`, `-OrderDirection`, and `-IsAccessible` query parameters; supplying `-SubOrganizationId` returns a single record. `New`/`Update`/`Remove` honor `-WhatIf`/`-Confirm`; `Remove` defaults to High `ConfirmImpact` and supports `-PassThru`. No project context required. Backed by new `InfisicalSubOrganization` model, DTO, mapper, and client wired into `InfisicalEndpointRegistry` (`ListSubOrganizations`, `RetrieveSubOrganization`, `CreateSubOrganization`, `UpdateSubOrganization`, `DeleteSubOrganization`).
+- Added `Get-InfisicalSANList` cmdlet: emits a deduplicated SAN candidate set containing the local device name, the device name suffixed with each non-empty DNS suffix found across operational adapters and the system primary domain, every IPv4 unicast address falling within RFC 1918 (10/8, 172.16/12, 192.168/16) or CGNAT (100.64/10), and the IPv4/IPv6 loopback addresses (127.0.0.1, ::1). Intended to feed `Request-InfisicalCertificate -DnsName` directly.
+- `Get-InfisicalSANList`: added optional `-InclusionExpression` and `-ExclusionExpression` case-insensitive regex filters. Applied in fetch -> include -> exclude -> output order after the deduplicated set is built; both default to unset (no filtering).
+- `Get-InfisicalSANList`: output is a single strongly-typed `System.String[]` array emitted non-enumerated (`OutputType(string[])`), so variable assignment yields `string[]` rather than `object[]`. This lets `[System.Collections.Generic.List[string]]::AddRange()` consume the result directly and lets the array bind straight to `string[]` parameters such as `Request-InfisicalCertificate -DnsName`.
+- `build.ps1` `CmdletsToExport` and `Test-ModuleImports` expected list now contain 51 cmdlets. `docs/DesignSpec.md` updated with `§16.7` (Organizations) and `§16.8` (Sub-Organizations); full MAML help added for all 9 new cmdlets in `Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml`.
+
## 2026.06.06.2229
- Build produced from commit 207e7429e448.
-## Unreleased (carried forward)
+## Unreleased (carried forward)
- `Start-InfisicalProcess`: switched stdout/stderr capture to event-based `OutputDataReceived`/`ErrorDataReceived` with `BeginOutputReadLine`/`BeginErrorReadLine` (removed `Task`/`ReadToEndAsync`/`GetAwaiter().GetResult()` to eliminate PowerShell `SynchronizationContext` deadlock risk). Restored the original `do { log; sleep } while (!HasExited)` polling pattern using `Thread.Sleep(pollInterval)` so verbose "has been running for X" / "Checking again in Y" messages fire at the configured cadence even when no `-ExecutionTimeout` is supplied.
- `Start-InfisicalProcess`: TimeSpan values in verbose logs and on the result now use a friendly format ("`7 seconds, and 364 milliseconds`", "`1 minute, and 30 seconds`", "`N/A`" when zero) matching the legacy `Start-ProcessWithOutput` `GetTimeSpanMessage` scriptblock. Added `DurationFriendly` property to `InfisicalProcessResult` and a "The command execution took X" verbose line at completion.
diff --git a/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml b/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml
index 7b31ff4..24f3026 100644
--- a/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml
+++ b/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml
@@ -1701,4 +1701,322 @@ $StartInfisicalProcessResult = Start-InfisicalProcess @StartInfisicalProcessPara
+
+
+ Get-InfisicalOrganization
+ Lists or retrieves Infisical organizations accessible to the current identity.
+ Get
+ InfisicalOrganization
+
+
+ Default (List parameter set) returns every organization the active session can see; visibility is governed by Infisical's role assignments. When -OrganizationId is supplied (Single parameter set) the cmdlet returns one organization. Does not require a project context.
+
+
+ Notes
+
+ The List-mode result is an array of InfisicalOrganization objects; pipe into Where-Object or Select-Object to filter by Slug, Name, or Id. The cmdlet accepts pipeline input by property name on -OrganizationId.
+
+
+
+
+ EXAMPLE 1
+ Get-InfisicalOrganization
+ Lists every organization the current session can see.
+
+
+ EXAMPLE 2
+ Get-InfisicalOrganization -OrganizationId $OrganizationId
+ Retrieves the canonical record for a single organization by id.
+
+
+
+
+
+
+ New-InfisicalOrganization
+ Creates a new Infisical organization.
+ New
+ InfisicalOrganization
+
+
+ Creates a new organization with the supplied name and optional slug. Honors -WhatIf and -Confirm. Requires server-side permission to create organizations.
+
+
+ Notes
+
+ Slug must be unique server-side; if omitted, the server derives one from the name.
+
+
+
+
+ EXAMPLE 1
+ New-InfisicalOrganization -Name 'Acme Corporation'
+ Creates a new organization named 'Acme Corporation' with a server-derived slug.
+
+
+ EXAMPLE 2
+ $NewInfisicalOrganizationParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
+$NewInfisicalOrganizationParameters.Name = 'Acme Corporation'
+$NewInfisicalOrganizationParameters.Slug = 'acme-corp'
+$NewInfisicalOrganizationParameters.Verbose = $True
+
+$NewInfisicalOrganizationResult = New-InfisicalOrganization @NewInfisicalOrganizationParameters
+ Creates an organization with an explicit slug.
+
+
+
+
+
+
+ Update-InfisicalOrganization
+ Updates mutable attributes on an existing Infisical organization.
+ Update
+ InfisicalOrganization
+
+
+ Updates the name or slug of an organization. -OrganizationId is required. Only bound parameters are transmitted. Honors -WhatIf and -Confirm.
+
+
+ Notes
+
+ Renaming or re-slugging an organization may affect billing exports and identity URLs; coordinate with downstream consumers.
+
+
+
+
+ EXAMPLE 1
+ Update-InfisicalOrganization -OrganizationId $OrganizationId -Name 'Acme Corp.'
+ Renames the supplied organization.
+
+
+ EXAMPLE 2
+ $UpdateInfisicalOrganizationParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
+$UpdateInfisicalOrganizationParameters.OrganizationId = $OrganizationId
+$UpdateInfisicalOrganizationParameters.Name = 'Acme Corp.'
+$UpdateInfisicalOrganizationParameters.Slug = 'acme-corp'
+$UpdateInfisicalOrganizationParameters.Verbose = $True
+
+$UpdateInfisicalOrganizationResult = Update-InfisicalOrganization @UpdateInfisicalOrganizationParameters
+ Renames the organization and updates its slug.
+
+
+
+
+
+
+ Remove-InfisicalOrganization
+ Deletes an Infisical organization.
+ Remove
+ InfisicalOrganization
+
+
+ Deletes an organization by id. -OrganizationId is required. High ConfirmImpact prompts unless -Confirm:$False is supplied. -PassThru emits the removed organization id.
+
+
+ Notes
+
+ This is irreversible and removes all projects, sub-organizations, secrets, and identities owned by the organization. Honors -WhatIf and -Confirm.
+
+
+
+
+ EXAMPLE 1
+ Remove-InfisicalOrganization -OrganizationId $OrganizationId -Confirm:$False
+ Deletes the supplied organization without prompting.
+
+
+ EXAMPLE 2
+ $RemoveInfisicalOrganizationParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
+$RemoveInfisicalOrganizationParameters.OrganizationId = $OrganizationId
+$RemoveInfisicalOrganizationParameters.PassThru = $True
+$RemoveInfisicalOrganizationParameters.Confirm = $False
+$RemoveInfisicalOrganizationParameters.Verbose = $True
+
+$RemoveInfisicalOrganizationResult = Remove-InfisicalOrganization @RemoveInfisicalOrganizationParameters
+ Removes the organization without confirmation and emits the removed organization id for logging.
+
+
+
+
+
+
+ Get-InfisicalSubOrganization
+ Lists or retrieves Infisical sub-organizations accessible to the current identity.
+ Get
+ InfisicalSubOrganization
+
+
+ Default (List parameter set) returns every sub-organization the active session can see. Optional -Limit, -Offset, -Search, -OrderBy, -OrderDirection, and -IsAccessible are forwarded to the server as query parameters. When -SubOrganizationId is supplied (Single parameter set) the cmdlet returns one sub-organization. Does not require a project context.
+
+
+ Notes
+
+ Sub-organizations are a beta Infisical feature. The List result is an array of InfisicalSubOrganization objects; pipe into Where-Object or Select-Object to filter further. The cmdlet accepts pipeline input by property name on -SubOrganizationId.
+
+
+
+
+ EXAMPLE 1
+ Get-InfisicalSubOrganization
+ Lists every sub-organization the current session can see.
+
+
+ EXAMPLE 2
+ Get-InfisicalSubOrganization -SubOrganizationId $SubOrganizationId
+ Retrieves the canonical record for a single sub-organization by id.
+
+
+ EXAMPLE 3
+ Get-InfisicalSubOrganization -Search 'platform' -OrderBy 'name' -OrderDirection 'asc' -Limit 25 -IsAccessible
+ Lists up to 25 sub-organizations matching 'platform', sorted ascending by name, restricted to those the current identity has access to.
+
+
+
+
+
+
+ New-InfisicalSubOrganization
+ Creates a new Infisical sub-organization.
+ New
+ InfisicalSubOrganization
+
+
+ Creates a sub-organization with the supplied name and slug. Both are required by the server. Honors -WhatIf and -Confirm.
+
+
+ Notes
+
+ Slug must be unique within the parent organization. Sub-organizations are a beta Infisical feature.
+
+
+
+
+ EXAMPLE 1
+ New-InfisicalSubOrganization -Name 'Platform Engineering' -Slug 'platform-eng'
+ Creates a new sub-organization named 'Platform Engineering' with slug 'platform-eng'.
+
+
+ EXAMPLE 2
+ $NewInfisicalSubOrganizationParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
+$NewInfisicalSubOrganizationParameters.Name = 'Platform Engineering'
+$NewInfisicalSubOrganizationParameters.Slug = 'platform-eng'
+$NewInfisicalSubOrganizationParameters.Verbose = $True
+
+$NewInfisicalSubOrganizationResult = New-InfisicalSubOrganization @NewInfisicalSubOrganizationParameters
+ Splatted invocation that creates a sub-organization and logs the request via the verbose stream.
+
+
+
+
+
+
+ Update-InfisicalSubOrganization
+ Updates mutable attributes on an existing Infisical sub-organization.
+ Update
+ InfisicalSubOrganization
+
+
+ Updates the name or slug of a sub-organization. -SubOrganizationId is required. Only bound parameters are transmitted. Honors -WhatIf and -Confirm.
+
+
+ Notes
+
+ Sub-organizations are a beta Infisical feature; coordinate slug changes with downstream consumers that pin the slug in scripts or configuration files.
+
+
+
+
+ EXAMPLE 1
+ Update-InfisicalSubOrganization -SubOrganizationId $SubOrganizationId -Name 'Platform (v2)'
+ Renames the supplied sub-organization.
+
+
+ EXAMPLE 2
+ $UpdateInfisicalSubOrganizationParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
+$UpdateInfisicalSubOrganizationParameters.SubOrganizationId = $SubOrganizationId
+$UpdateInfisicalSubOrganizationParameters.Name = 'Platform (v2)'
+$UpdateInfisicalSubOrganizationParameters.Slug = 'platform-v2'
+$UpdateInfisicalSubOrganizationParameters.Verbose = $True
+
+$UpdateInfisicalSubOrganizationResult = Update-InfisicalSubOrganization @UpdateInfisicalSubOrganizationParameters
+ Renames the sub-organization and updates its slug in a single call.
+
+
+
+
+
+
+ Remove-InfisicalSubOrganization
+ Deletes an Infisical sub-organization.
+ Remove
+ InfisicalSubOrganization
+
+
+ Deletes a sub-organization by id. -SubOrganizationId is required. High ConfirmImpact prompts unless -Confirm:$False is supplied. -PassThru emits the removed sub-organization id.
+
+
+ Notes
+
+ This is destructive and removes all projects, secrets, and identities scoped to the sub-organization. Honors -WhatIf and -Confirm.
+
+
+
+
+ EXAMPLE 1
+ Remove-InfisicalSubOrganization -SubOrganizationId $SubOrganizationId -Confirm:$False
+ Deletes the supplied sub-organization without prompting.
+
+
+ EXAMPLE 2
+ $RemoveInfisicalSubOrganizationParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
+$RemoveInfisicalSubOrganizationParameters.SubOrganizationId = $SubOrganizationId
+$RemoveInfisicalSubOrganizationParameters.PassThru = $True
+$RemoveInfisicalSubOrganizationParameters.Confirm = $False
+$RemoveInfisicalSubOrganizationParameters.Verbose = $True
+
+$RemoveInfisicalSubOrganizationResult = Remove-InfisicalSubOrganization @RemoveInfisicalSubOrganizationParameters
+ Removes the sub-organization without confirmation and emits the removed id for logging.
+
+
+
+
+
+
+ Get-InfisicalSANList
+ Builds a deduplicated list of Subject Alternative Name candidates for the local device.
+ Get
+ InfisicalSANList
+
+
+ Returns, in order: the local device name; the device name suffixed with each non-empty DNS suffix found on any operational (non-loopback) network adapter and the system primary domain; every IPv4 unicast address whose first octets fall within RFC 1918 (10/8, 172.16/12, 192.168/16) or CGNAT (100.64/10); and the IPv4 and IPv6 loopback addresses (127.0.0.1, ::1). Optional -InclusionExpression and -ExclusionExpression regex filters are applied in that order after collection, before output. Suitable as a one-shot SAN provider for Request-InfisicalCertificate -DnsName.
+
+
+ Notes
+
+ Output is a single strongly-typed System.String[] array (emitted non-enumerated) so it round-trips into [System.Collections.Generic.List[string]]::AddRange() and binds directly to string[] parameters such as Request-InfisicalCertificate -DnsName. The device name comes first so it can be reused as a CommonName. Routable public IPv4 addresses, link-local addresses, and IPv6 unicast addresses other than loopback are intentionally excluded. -InclusionExpression and -ExclusionExpression are case-insensitive .NET regular expressions; inclusion is applied first, then exclusion.
+
+
+
+
+ EXAMPLE 1
+ Get-InfisicalSANList
+ Returns the SAN candidate list for the current device.
+
+
+ EXAMPLE 2
+ $Sans = Get-InfisicalSANList
+Request-InfisicalCertificate -ProjectId $ProjectId -CertificateAuthorityId $CaId -CommonName $Sans[0] -DnsName $Sans -Ttl '90d'
+ Captures the SAN list, then uses the device name as the CommonName and the full list as DnsName when requesting a certificate.
+
+
+ EXAMPLE 3
+ $GetInfisicalSANListParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
+$GetInfisicalSANListParameters.InclusionExpression = '\.gracesolution\.prv$|^10\.|^172\.'
+$GetInfisicalSANListParameters.ExclusionExpression = '^127\.|^::1$'
+$Sans = Get-InfisicalSANList @GetInfisicalSANListParameters
+ Keeps only entries ending in the corporate DNS suffix or sitting in the 10/8 or 172/12 ranges, then drops loopback. Filters are case-insensitive and applied in fetch -> include -> exclude -> output order.
+
+
+
+
diff --git a/README.md b/README.md
index 8c85102..ba2e9b3 100644
--- a/README.md
+++ b/README.md
@@ -26,7 +26,7 @@ Import-Module -Name .\Module\PSInfisicalAPI
## Cmdlets
-The module exports 42 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.
+The module exports 51 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
@@ -47,6 +47,24 @@ The module exports 42 cmdlets. Discovery cmdlets (`Get-Infisical*`) use a `List`
| `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. |
+### Organizations
+
+| Cmdlet | Purpose |
+| ------------------------------ | -------------------------------------------------------------------------------------------------- |
+| `Get-InfisicalOrganization` | Lists or retrieves Infisical organizations accessible to the current identity. |
+| `New-InfisicalOrganization` | Creates a new Infisical organization. |
+| `Update-InfisicalOrganization` | Updates the name or slug of an existing Infisical organization. |
+| `Remove-InfisicalOrganization` | Deletes an Infisical organization. |
+
+### Sub-Organizations
+
+| Cmdlet | Purpose |
+| --------------------------------- | -------------------------------------------------------------------------------------------------- |
+| `Get-InfisicalSubOrganization` | Lists or retrieves Infisical sub-organizations, with optional search, paging, and ordering filters. |
+| `New-InfisicalSubOrganization` | Creates a new Infisical sub-organization. |
+| `Update-InfisicalSubOrganization` | Updates the name or slug of an existing Infisical sub-organization. |
+| `Remove-InfisicalSubOrganization` | Deletes an Infisical sub-organization. |
+
### Projects
| Cmdlet | Purpose |
@@ -98,6 +116,7 @@ The module exports 42 cmdlets. Discovery cmdlets (`Get-Infisical*`) use a `List`
| `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. |
+| `Get-InfisicalSANList` | Builds a SAN candidate list (device name, `.` per adapter DNS suffix, RFC 1918 + CGNAT IPv4 addresses, IPv4/IPv6 loopback) for `Request-InfisicalCertificate -DnsName`. |
### Process
@@ -125,6 +144,40 @@ Get-InfisicalSecret -SecretPath '/'
Disconnect-Infisical
```
+## End-to-end: request and install a chained certificate
+
+Connects, selects a project by name, sources SANs from `Get-InfisicalSANList`, picks the first available internal CA, requests a certificate, installs it (and its chain) into the current-user store, and disconnects. Each call uses a splatted `OrderedDictionary` constructed with `OrdinalIgnoreCase` so parameter names round-trip case-insensitively.
+
+```powershell
+$ConnectInfisicalParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
+ $ConnectInfisicalParameters.BaseUri = 'https://app.infisical.com'
+ $ConnectInfisicalParameters.OrganizationId = '00000000-0000-0000-0000-000000000000'
+ $ConnectInfisicalParameters.ClientId = 'machine-identity-client-id'
+ $ConnectInfisicalParameters.ClientSecret = ConvertTo-SecureString -String 'ClientSecret' -AsPlainText -Force
+ $ConnectInfisicalParameters.PassThru = $True
+ $ConnectInfisicalParameters.Verbose = $True
+
+$Connection = Connect-Infisical @ConnectInfisicalParameters
+
+$Project = Get-InfisicalProject | Where-Object {($_.Name -eq 'Platform')} | Select-Object -First 1
+$Ca = Get-InfisicalCertificateAuthority -ProjectId ($Project.Id) | Select-Object -First 1
+$SanList = Get-InfisicalSANList
+
+$RequestInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
+ $RequestInfisicalCertificateParameters.ProjectId = $Project.Id
+ $RequestInfisicalCertificateParameters.CertificateAuthorityId = $Ca.Id
+ $RequestInfisicalCertificateParameters.CommonName = "CN=$($Env:ComputerName.ToUpper())"
+ $RequestInfisicalCertificateParameters.DnsName = New-Object -TypeName 'System.Collections.Generic.List[System.String]'
+ $RequestInfisicalCertificateParameters.DnsName.AddRange($SanList)
+ $RequestInfisicalCertificateParameters.DnsName.Add('myrecord.mydomain.com')
+ $RequestInfisicalCertificateParameters.Ttl = '90d'
+ $RequestInfisicalCertificateParameters.Install = $True
+ $RequestInfisicalCertificateParameters.InstallChain = $True
+ $Certificate = Request-InfisicalCertificate @RequestInfisicalCertificateParameters
+
+$Null = Disconnect-Infisical -Verbose
+```
+
## Automatic environment-variable discovery
When `Connect-Infisical` is invoked with one or more parameters missing (or set to whitespace/empty), the cmdlet searches environment variables and uses the first value it finds. This makes invocation as simple as `Connect-Infisical` when variables are set up in advance.
diff --git a/build.ps1 b/build.ps1
index 26fdb71..66299a1 100644
--- a/build.ps1
+++ b/build.ps1
@@ -129,6 +129,14 @@ function Write-Manifest {
'New-InfisicalTag',
'Update-InfisicalTag',
'Remove-InfisicalTag',
+ 'Get-InfisicalOrganization',
+ 'New-InfisicalOrganization',
+ 'Update-InfisicalOrganization',
+ 'Remove-InfisicalOrganization',
+ 'Get-InfisicalSubOrganization',
+ 'New-InfisicalSubOrganization',
+ 'Update-InfisicalSubOrganization',
+ 'Remove-InfisicalSubOrganization',
'Get-InfisicalCertificateAuthority',
'Get-InfisicalPkiSubscriber',
'Get-InfisicalCertificateProfile',
@@ -145,7 +153,8 @@ function Write-Manifest {
'Get-InfisicalScepMdmProfile',
'Export-InfisicalScepMdmProfile',
'Write-InfisicalScepMdmProfileToWmi',
- 'Start-InfisicalProcess'
+ 'Start-InfisicalProcess',
+ 'Get-InfisicalSANList'
)
AliasesToExport = @()
VariablesToExport = @()
@@ -210,7 +219,7 @@ 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','Request-InfisicalCertificate','ConvertTo-InfisicalCertificate','Install-InfisicalCertificate','Uninstall-InfisicalCertificate','Export-InfisicalCertificate','Get-InfisicalCertificateApplication','Get-InfisicalCertificateApplicationEnrollment','New-InfisicalScepDynamicChallenge','Get-InfisicalScepMdmProfile','Export-InfisicalScepMdmProfile','Write-InfisicalScepMdmProfileToWmi','Start-InfisicalProcess')
+`$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-InfisicalOrganization','New-InfisicalOrganization','Update-InfisicalOrganization','Remove-InfisicalOrganization','Get-InfisicalSubOrganization','New-InfisicalSubOrganization','Update-InfisicalSubOrganization','Remove-InfisicalSubOrganization','Get-InfisicalCertificateAuthority','Get-InfisicalPkiSubscriber','Get-InfisicalCertificateProfile','Get-InfisicalCertificatePolicy','Get-InfisicalCertificate','Request-InfisicalCertificate','ConvertTo-InfisicalCertificate','Install-InfisicalCertificate','Uninstall-InfisicalCertificate','Export-InfisicalCertificate','Get-InfisicalCertificateApplication','Get-InfisicalCertificateApplicationEnrollment','New-InfisicalScepDynamicChallenge','Get-InfisicalScepMdmProfile','Export-InfisicalScepMdmProfile','Write-InfisicalScepMdmProfileToWmi','Start-InfisicalProcess','Get-InfisicalSANList')
foreach (`$expected in `$expectedCmds) {
if (-not (Get-Command -Name `$expected -Module PSInfisicalAPI -ErrorAction SilentlyContinue)) {
throw "Cmdlet not found: `$expected"
diff --git a/docs/DesignSpec.md b/docs/DesignSpec.md
index b0000ad..d63f3af 100644
--- a/docs/DesignSpec.md
+++ b/docs/DesignSpec.md
@@ -39,6 +39,14 @@ Get-InfisicalTag
New-InfisicalTag
Update-InfisicalTag
Remove-InfisicalTag
+Get-InfisicalOrganization
+New-InfisicalOrganization
+Update-InfisicalOrganization
+Remove-InfisicalOrganization
+Get-InfisicalSubOrganization
+New-InfisicalSubOrganization
+Update-InfisicalSubOrganization
+Remove-InfisicalSubOrganization
```
Infisical’s public API is REST-based and provides programmatic access for managing secrets and related resources. Current Infisical documentation shows the list-secrets endpoint under `/api/v4/secrets`, the single-secret retrieval endpoint under `/api/v4/secrets/{secretName}`, and Universal Auth login under `/api/v1/auth/universal-auth/login`. The implementation must centralize API endpoint definitions because Infisical uses different API versions across resource families. ([Infisical Blog][1])
@@ -1533,6 +1541,78 @@ Output: `InfisicalProcessResult` with `ExitCode`, `ExitCodeAsHex`, `ExitCodeAsIn
---
+# 16.7 Organization Cmdlets
+
+Organizations are the top-level tenancy boundary in Infisical. They are not scoped under a project; the active connection's `OrganizationId` is used as the default identifier when an explicit one is not supplied.
+
+Cmdlet signatures:
+
+```powershell
+Get-InfisicalOrganization [[-OrganizationId] ] # default = List
+New-InfisicalOrganization [-Name] [-Slug ] [-WhatIf] [-Confirm]
+Update-InfisicalOrganization [-OrganizationId] [-Name ] [-Slug ] [-WhatIf] [-Confirm]
+Remove-InfisicalOrganization [-OrganizationId] [-PassThru] [-WhatIf] [-Confirm]
+```
+
+Parameter sets:
+
+| Cmdlet | Default set | Single set | Notes |
+|---|---|---|---|
+| `Get-InfisicalOrganization` | `List` (no `-Id`) | `Single` (`-OrganizationId`/`-Id`) | No `-ProjectId`. |
+| `New-InfisicalOrganization` | n/a | `-Name` mandatory, `-Slug` optional | ShouldProcess. |
+| `Update-InfisicalOrganization` | n/a | `-OrganizationId` mandatory | ShouldProcess; only bound parameters are sent. |
+| `Remove-InfisicalOrganization` | n/a | `-OrganizationId` mandatory | `ConfirmImpact.High`; `-PassThru` emits removed id. |
+
+Endpoints:
+
+| Operation | Method | Template | Version |
+|---|---|---|---|
+| List | `GET` | `/api/v2/organizations` | v2 |
+| Retrieve | `GET` | `/api/v1/organization/{organizationId}` | v1 |
+| Create | `POST` | `/api/v2/organizations` | v2 |
+| Update | `PATCH` | `/api/v1/organization/{organizationId}` | v1 |
+| Delete | `DELETE` | `/api/v1/organization/{organizationId}` | v1 |
+
+Output: `InfisicalOrganization` with `Id`, `Name`, `Slug`, `CustomerId`, `AuthEnforced`, `ScimEnabled`, `CreatedAtUtc`, `UpdatedAtUtc`.
+
+---
+
+# 16.8 Sub-Organization Cmdlets
+
+Sub-organizations partition an organization into isolated child tenants. They are not scoped under a project; the active connection is used for the parent organization context.
+
+Cmdlet signatures:
+
+```powershell
+Get-InfisicalSubOrganization [[-SubOrganizationId] ] [-Limit ] [-Offset ] [-Search ] [-OrderBy ] [-OrderDirection ] [-IsAccessible]
+New-InfisicalSubOrganization [-Name] [-Slug] [-WhatIf] [-Confirm]
+Update-InfisicalSubOrganization [-SubOrganizationId] [-Name ] [-Slug ] [-WhatIf] [-Confirm]
+Remove-InfisicalSubOrganization [-SubOrganizationId] [-PassThru] [-WhatIf] [-Confirm]
+```
+
+Parameter sets:
+
+| Cmdlet | Default set | Single set | Notes |
+|---|---|---|---|
+| `Get-InfisicalSubOrganization` | `List` (no `-Id`) | `Single` (`-SubOrganizationId`/`-Id`) | List supports server-side `-Limit`, `-Offset`, `-Search`, `-OrderBy`, `-OrderDirection`, `-IsAccessible`. |
+| `New-InfisicalSubOrganization` | n/a | `-Name` + `-Slug` mandatory | ShouldProcess. |
+| `Update-InfisicalSubOrganization` | n/a | `-SubOrganizationId` mandatory | ShouldProcess; only bound parameters are sent. |
+| `Remove-InfisicalSubOrganization` | n/a | `-SubOrganizationId` mandatory | `ConfirmImpact.High`; `-PassThru` emits removed id. |
+
+Endpoints (beta):
+
+| Operation | Method | Template | Version |
+|---|---|---|---|
+| List | `GET` | `/api/v1/sub-organizations` | v1 |
+| Retrieve | `GET` | `/api/v1/sub-organizations/{subOrgId}` | v1 |
+| Create | `POST` | `/api/v1/sub-organizations` | v1 |
+| Update | `PATCH` | `/api/v1/sub-organizations/{subOrgId}` | v1 |
+| Delete | `DELETE` | `/api/v1/sub-organizations/{subOrgId}` | v1 |
+
+Output: `InfisicalSubOrganization` with `Id`, `Name`, `Slug`, `OrganizationId`, `IsAccessible`, `CreatedAtUtc`, `UpdatedAtUtc`.
+
+---
+
# 17. SecureString Utility
Required utility:
diff --git a/src/PSInfisicalAPI.Tests/EndpointRegistryTests.cs b/src/PSInfisicalAPI.Tests/EndpointRegistryTests.cs
index 190a3ad..f45456b 100644
--- a/src/PSInfisicalAPI.Tests/EndpointRegistryTests.cs
+++ b/src/PSInfisicalAPI.Tests/EndpointRegistryTests.cs
@@ -75,6 +75,16 @@ namespace PSInfisicalAPI.Tests
[InlineData(InfisicalEndpointNames.BulkUpdateSecret, "PATCH", "/api/v4/secrets/batch")]
[InlineData(InfisicalEndpointNames.BulkDeleteSecret, "DELETE", "/api/v4/secrets/batch")]
[InlineData(InfisicalEndpointNames.DuplicateSecret, "POST", "/api/v4/secrets/duplicate")]
+ [InlineData(InfisicalEndpointNames.ListOrganizations, "GET", "/api/v2/organizations")]
+ [InlineData(InfisicalEndpointNames.RetrieveOrganization, "GET", "/api/v1/organization/{organizationId}")]
+ [InlineData(InfisicalEndpointNames.CreateOrganization, "POST", "/api/v2/organizations")]
+ [InlineData(InfisicalEndpointNames.UpdateOrganization, "PATCH", "/api/v1/organization/{organizationId}")]
+ [InlineData(InfisicalEndpointNames.DeleteOrganization, "DELETE", "/api/v1/organization/{organizationId}")]
+ [InlineData(InfisicalEndpointNames.ListSubOrganizations, "GET", "/api/v1/sub-organizations")]
+ [InlineData(InfisicalEndpointNames.RetrieveSubOrganization, "GET", "/api/v1/sub-organizations/{subOrgId}")]
+ [InlineData(InfisicalEndpointNames.CreateSubOrganization, "POST", "/api/v1/sub-organizations")]
+ [InlineData(InfisicalEndpointNames.UpdateSubOrganization, "PATCH", "/api/v1/sub-organizations/{subOrgId}")]
+ [InlineData(InfisicalEndpointNames.DeleteSubOrganization, "DELETE", "/api/v1/sub-organizations/{subOrgId}")]
public void Registered_Endpoints_Have_Expected_Shape(string name, string method, string template)
{
InfisicalEndpointDefinition definition = InfisicalEndpointRegistry.Get(name);
diff --git a/src/PSInfisicalAPI.Tests/OrganizationMapperTests.cs b/src/PSInfisicalAPI.Tests/OrganizationMapperTests.cs
new file mode 100644
index 0000000..2663094
--- /dev/null
+++ b/src/PSInfisicalAPI.Tests/OrganizationMapperTests.cs
@@ -0,0 +1,72 @@
+using System.Reflection;
+using PSInfisicalAPI.Models;
+using Xunit;
+
+namespace PSInfisicalAPI.Tests
+{
+ public class OrganizationMapperTests
+ {
+ private static readonly System.Type MapperType = typeof(PSInfisicalAPI.Connections.InfisicalConnection).Assembly
+ .GetType("PSInfisicalAPI.Organizations.InfisicalOrganizationMapper", true);
+
+ private static readonly System.Type DtoType = typeof(PSInfisicalAPI.Connections.InfisicalConnection).Assembly
+ .GetType("PSInfisicalAPI.Organizations.InfisicalOrganizationResponseDto", true);
+
+ private static InfisicalOrganization InvokeMap(object dto)
+ {
+ MethodInfo map = MapperType.GetMethod("Map", BindingFlags.Public | BindingFlags.Static);
+ return (InfisicalOrganization)map.Invoke(null, new[] { dto });
+ }
+
+ [Fact]
+ public void Map_Null_Dto_Returns_Null()
+ {
+ Assert.Null(InvokeMap(null));
+ }
+
+ [Fact]
+ public void Map_Populates_Core_Fields()
+ {
+ object dto = System.Activator.CreateInstance(DtoType);
+ DtoType.GetProperty("Id").SetValue(dto, "org-001");
+ DtoType.GetProperty("Name").SetValue(dto, "Acme");
+ DtoType.GetProperty("Slug").SetValue(dto, "acme");
+ DtoType.GetProperty("CustomerId").SetValue(dto, "cust-9");
+ DtoType.GetProperty("AuthEnforced").SetValue(dto, true);
+ DtoType.GetProperty("ScimEnabled").SetValue(dto, true);
+ DtoType.GetProperty("CreatedAt").SetValue(dto, "2026-01-15T12:34:56Z");
+ DtoType.GetProperty("UpdatedAt").SetValue(dto, "2026-02-20T09:00:00Z");
+
+ InfisicalOrganization organization = InvokeMap(dto);
+
+ Assert.Equal("org-001", organization.Id);
+ Assert.Equal("Acme", organization.Name);
+ Assert.Equal("acme", organization.Slug);
+ Assert.Equal("cust-9", organization.CustomerId);
+ Assert.True(organization.AuthEnforced);
+ Assert.True(organization.ScimEnabled);
+ Assert.NotNull(organization.CreatedAtUtc);
+ Assert.NotNull(organization.UpdatedAtUtc);
+ }
+
+ [Fact]
+ public void Map_Falls_Back_To_InternalId()
+ {
+ object dto = System.Activator.CreateInstance(DtoType);
+ DtoType.GetProperty("InternalId").SetValue(dto, "internal-id-1");
+
+ InfisicalOrganization organization = InvokeMap(dto);
+
+ Assert.Equal("internal-id-1", organization.Id);
+ }
+
+ [Fact]
+ public void MapMany_Null_Returns_Empty()
+ {
+ MethodInfo mapMany = MapperType.GetMethod("MapMany", BindingFlags.Public | BindingFlags.Static);
+ InfisicalOrganization[] result = (InfisicalOrganization[])mapMany.Invoke(null, new object[] { null });
+ Assert.NotNull(result);
+ Assert.Empty(result);
+ }
+ }
+}
diff --git a/src/PSInfisicalAPI.Tests/SubOrganizationMapperTests.cs b/src/PSInfisicalAPI.Tests/SubOrganizationMapperTests.cs
new file mode 100644
index 0000000..c6426c0
--- /dev/null
+++ b/src/PSInfisicalAPI.Tests/SubOrganizationMapperTests.cs
@@ -0,0 +1,72 @@
+using System.Reflection;
+using PSInfisicalAPI.Models;
+using Xunit;
+
+namespace PSInfisicalAPI.Tests
+{
+ public class SubOrganizationMapperTests
+ {
+ private static readonly System.Type MapperType = typeof(PSInfisicalAPI.Connections.InfisicalConnection).Assembly
+ .GetType("PSInfisicalAPI.SubOrganizations.InfisicalSubOrganizationMapper", true);
+
+ private static readonly System.Type DtoType = typeof(PSInfisicalAPI.Connections.InfisicalConnection).Assembly
+ .GetType("PSInfisicalAPI.SubOrganizations.InfisicalSubOrganizationResponseDto", true);
+
+ private static InfisicalSubOrganization InvokeMap(object dto)
+ {
+ MethodInfo map = MapperType.GetMethod("Map", BindingFlags.Public | BindingFlags.Static);
+ return (InfisicalSubOrganization)map.Invoke(null, new[] { dto });
+ }
+
+ [Fact]
+ public void Map_Null_Dto_Returns_Null()
+ {
+ Assert.Null(InvokeMap(null));
+ }
+
+ [Fact]
+ public void Map_Populates_Core_Fields()
+ {
+ object dto = System.Activator.CreateInstance(DtoType);
+ DtoType.GetProperty("Id").SetValue(dto, "sub-001");
+ DtoType.GetProperty("Name").SetValue(dto, "Platform Engineering");
+ DtoType.GetProperty("Slug").SetValue(dto, "platform-eng");
+ DtoType.GetProperty("OrganizationId").SetValue(dto, "org-001");
+ DtoType.GetProperty("IsAccessible").SetValue(dto, true);
+ DtoType.GetProperty("CreatedAt").SetValue(dto, "2026-01-15T12:34:56Z");
+ DtoType.GetProperty("UpdatedAt").SetValue(dto, "2026-02-20T09:00:00Z");
+
+ InfisicalSubOrganization subOrganization = InvokeMap(dto);
+
+ Assert.Equal("sub-001", subOrganization.Id);
+ Assert.Equal("Platform Engineering", subOrganization.Name);
+ Assert.Equal("platform-eng", subOrganization.Slug);
+ Assert.Equal("org-001", subOrganization.OrganizationId);
+ Assert.True(subOrganization.IsAccessible);
+ Assert.NotNull(subOrganization.CreatedAtUtc);
+ Assert.NotNull(subOrganization.UpdatedAtUtc);
+ }
+
+ [Fact]
+ public void Map_Falls_Back_To_InternalId_And_OrgId()
+ {
+ object dto = System.Activator.CreateInstance(DtoType);
+ DtoType.GetProperty("InternalId").SetValue(dto, "internal-id-1");
+ DtoType.GetProperty("OrgId").SetValue(dto, "org-fallback");
+
+ InfisicalSubOrganization subOrganization = InvokeMap(dto);
+
+ Assert.Equal("internal-id-1", subOrganization.Id);
+ Assert.Equal("org-fallback", subOrganization.OrganizationId);
+ }
+
+ [Fact]
+ public void MapMany_Null_Returns_Empty()
+ {
+ MethodInfo mapMany = MapperType.GetMethod("MapMany", BindingFlags.Public | BindingFlags.Static);
+ InfisicalSubOrganization[] result = (InfisicalSubOrganization[])mapMany.Invoke(null, new object[] { null });
+ Assert.NotNull(result);
+ Assert.Empty(result);
+ }
+ }
+}
diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalOrganizationCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalOrganizationCmdlet.cs
new file mode 100644
index 0000000..783dc0a
--- /dev/null
+++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalOrganizationCmdlet.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Management.Automation;
+using PSInfisicalAPI.Connections;
+using PSInfisicalAPI.Models;
+using PSInfisicalAPI.Organizations;
+
+namespace PSInfisicalAPI.Cmdlets
+{
+ [Cmdlet(VerbsCommon.Get, "InfisicalOrganization", DefaultParameterSetName = "List")]
+ [OutputType(typeof(InfisicalOrganization))]
+ public sealed class GetInfisicalOrganizationCmdlet : InfisicalCmdletBase
+ {
+ [Parameter(ParameterSetName = "Single", Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)]
+ [Alias("Id")]
+ public string OrganizationId { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ try
+ {
+ InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
+ InfisicalOrganizationClient client = new InfisicalOrganizationClient(HttpClient, Logger);
+
+ if (string.Equals(ParameterSetName, "Single", StringComparison.Ordinal))
+ {
+ InfisicalOrganization organization = client.Retrieve(connection, OrganizationId);
+ if (organization != null)
+ {
+ WriteObject(organization);
+ }
+
+ return;
+ }
+
+ InfisicalOrganization[] organizations = client.List(connection);
+ foreach (InfisicalOrganization organization in organizations)
+ {
+ WriteObject(organization);
+ }
+ }
+ catch (Exception exception)
+ {
+ ThrowTerminatingForException("GetInfisicalOrganizationCmdlet", "GetOrganization", exception);
+ }
+ }
+ }
+}
diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalSANListCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalSANListCmdlet.cs
new file mode 100644
index 0000000..998fca8
--- /dev/null
+++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalSANListCmdlet.cs
@@ -0,0 +1,108 @@
+using System;
+using System.Collections.Generic;
+using System.Management.Automation;
+using System.Net;
+using System.Net.NetworkInformation;
+using System.Net.Sockets;
+using System.Text.RegularExpressions;
+
+namespace PSInfisicalAPI.Cmdlets
+{
+ [Cmdlet(VerbsCommon.Get, "InfisicalSANList")]
+ [OutputType(typeof(string[]))]
+ public sealed class GetInfisicalSANListCmdlet : InfisicalCmdletBase
+ {
+ private const string Component = "GetInfisicalSANListCmdlet";
+
+ [Parameter]
+ [ValidateNotNullOrEmpty]
+ public string InclusionExpression { get; set; }
+
+ [Parameter]
+ [ValidateNotNullOrEmpty]
+ public string ExclusionExpression { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ try
+ {
+ Regex includeRegex = !string.IsNullOrEmpty(InclusionExpression) ? new Regex(InclusionExpression, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant) : null;
+ Regex excludeRegex = !string.IsNullOrEmpty(ExclusionExpression) ? new Regex(ExclusionExpression, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant) : null;
+
+ List sans = new List();
+ HashSet seen = new HashSet(StringComparer.OrdinalIgnoreCase);
+
+ string deviceName = Dns.GetHostName();
+ AddUnique(sans, seen, deviceName);
+
+ HashSet suffixes = new HashSet(StringComparer.OrdinalIgnoreCase);
+ string globalDomain = IPGlobalProperties.GetIPGlobalProperties().DomainName;
+ if (!string.IsNullOrEmpty(globalDomain)) { suffixes.Add(globalDomain); }
+
+ NetworkInterface[] adapters = NetworkInterface.GetAllNetworkInterfaces();
+ foreach (NetworkInterface adapter in adapters)
+ {
+ if (adapter.OperationalStatus != OperationalStatus.Up) { continue; }
+ if (adapter.NetworkInterfaceType == NetworkInterfaceType.Loopback) { continue; }
+
+ IPInterfaceProperties props = adapter.GetIPProperties();
+ if (!string.IsNullOrEmpty(props.DnsSuffix)) { suffixes.Add(props.DnsSuffix); }
+
+ foreach (UnicastIPAddressInformation unicast in props.UnicastAddresses)
+ {
+ IPAddress ip = unicast.Address;
+ if (ip.AddressFamily == AddressFamily.InterNetwork && IsRfc1918OrCgnat(ip))
+ {
+ AddUnique(sans, seen, ip.ToString());
+ }
+ }
+ }
+
+ foreach (string suffix in suffixes)
+ {
+ string trimmed = suffix.Trim().TrimStart('.');
+ if (!string.IsNullOrEmpty(trimmed))
+ {
+ AddUnique(sans, seen, string.Concat(deviceName, ".", trimmed));
+ }
+ }
+
+ AddUnique(sans, seen, "127.0.0.1");
+ AddUnique(sans, seen, "::1");
+
+ List filtered = new List(sans.Count);
+ foreach (string san in sans)
+ {
+ if (includeRegex != null && !includeRegex.IsMatch(san)) { continue; }
+ if (excludeRegex != null && excludeRegex.IsMatch(san)) { continue; }
+ filtered.Add(san);
+ }
+
+ WriteObject(filtered.ToArray(), false);
+ }
+ catch (Exception exception)
+ {
+ ThrowTerminatingForException(Component, "GetSANList", exception);
+ }
+ }
+
+ private static bool IsRfc1918OrCgnat(IPAddress ip)
+ {
+ byte[] bytes = ip.GetAddressBytes();
+ if (bytes.Length != 4) { return false; }
+
+ if (bytes[0] == 10) { return true; }
+ if (bytes[0] == 172 && bytes[1] >= 16 && bytes[1] <= 31) { return true; }
+ if (bytes[0] == 192 && bytes[1] == 168) { return true; }
+ if (bytes[0] == 100 && bytes[1] >= 64 && bytes[1] <= 127) { return true; }
+
+ return false;
+ }
+
+ private static void AddUnique(List list, HashSet seen, string value)
+ {
+ if (string.IsNullOrEmpty(value)) { return; }
+ if (seen.Add(value)) { list.Add(value); }
+ }
+ }
+}
diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalSubOrganizationCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalSubOrganizationCmdlet.cs
new file mode 100644
index 0000000..b967590
--- /dev/null
+++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalSubOrganizationCmdlet.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Management.Automation;
+using PSInfisicalAPI.Connections;
+using PSInfisicalAPI.Models;
+using PSInfisicalAPI.SubOrganizations;
+
+namespace PSInfisicalAPI.Cmdlets
+{
+ [Cmdlet(VerbsCommon.Get, "InfisicalSubOrganization", DefaultParameterSetName = "List")]
+ [OutputType(typeof(InfisicalSubOrganization))]
+ public sealed class GetInfisicalSubOrganizationCmdlet : InfisicalCmdletBase
+ {
+ [Parameter(ParameterSetName = "Single", Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)]
+ [Alias("Id")]
+ public string SubOrganizationId { get; set; }
+
+ [Parameter(ParameterSetName = "List")] public int? Limit { get; set; }
+ [Parameter(ParameterSetName = "List")] public int? Offset { get; set; }
+ [Parameter(ParameterSetName = "List")] public string Search { get; set; }
+ [Parameter(ParameterSetName = "List")] public string OrderBy { get; set; }
+
+ [Parameter(ParameterSetName = "List")]
+ [ValidateSet("asc", "desc")]
+ public string OrderDirection { get; set; }
+
+ [Parameter(ParameterSetName = "List")] public SwitchParameter IsAccessible { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ try
+ {
+ InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
+ InfisicalSubOrganizationClient client = new InfisicalSubOrganizationClient(HttpClient, Logger);
+
+ if (string.Equals(ParameterSetName, "Single", StringComparison.Ordinal))
+ {
+ InfisicalSubOrganization subOrganization = client.Retrieve(connection, SubOrganizationId);
+ if (subOrganization != null)
+ {
+ WriteObject(subOrganization);
+ }
+
+ return;
+ }
+
+ bool? isAccessible = MyInvocation.BoundParameters.ContainsKey("IsAccessible") ? (bool?)IsAccessible.IsPresent : null;
+ InfisicalSubOrganization[] subOrganizations = client.List(connection, Limit, Offset, Search, OrderBy, OrderDirection, isAccessible);
+ foreach (InfisicalSubOrganization subOrganization in subOrganizations)
+ {
+ WriteObject(subOrganization);
+ }
+ }
+ catch (Exception exception)
+ {
+ ThrowTerminatingForException("GetInfisicalSubOrganizationCmdlet", "GetSubOrganization", exception);
+ }
+ }
+ }
+}
diff --git a/src/PSInfisicalAPI/Cmdlets/NewInfisicalOrganizationCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/NewInfisicalOrganizationCmdlet.cs
new file mode 100644
index 0000000..2d67d01
--- /dev/null
+++ b/src/PSInfisicalAPI/Cmdlets/NewInfisicalOrganizationCmdlet.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Management.Automation;
+using PSInfisicalAPI.Connections;
+using PSInfisicalAPI.Models;
+using PSInfisicalAPI.Organizations;
+
+namespace PSInfisicalAPI.Cmdlets
+{
+ [Cmdlet(VerbsCommon.New, "InfisicalOrganization", SupportsShouldProcess = true)]
+ [OutputType(typeof(InfisicalOrganization))]
+ public sealed class NewInfisicalOrganizationCmdlet : InfisicalCmdletBase
+ {
+ [Parameter(Mandatory = true, Position = 0)] public string Name { get; set; }
+ [Parameter] public string Slug { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ try
+ {
+ if (!ShouldProcess(Name, "Create Infisical organization"))
+ {
+ return;
+ }
+
+ InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
+ InfisicalOrganizationClient client = new InfisicalOrganizationClient(HttpClient, Logger);
+ InfisicalOrganization organization = client.Create(connection, Name, Slug);
+ if (organization != null)
+ {
+ WriteObject(organization);
+ }
+ }
+ catch (Exception exception)
+ {
+ ThrowTerminatingForException("NewInfisicalOrganizationCmdlet", "CreateOrganization", exception);
+ }
+ }
+ }
+}
diff --git a/src/PSInfisicalAPI/Cmdlets/NewInfisicalSubOrganizationCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/NewInfisicalSubOrganizationCmdlet.cs
new file mode 100644
index 0000000..f01c580
--- /dev/null
+++ b/src/PSInfisicalAPI/Cmdlets/NewInfisicalSubOrganizationCmdlet.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Management.Automation;
+using PSInfisicalAPI.Connections;
+using PSInfisicalAPI.Models;
+using PSInfisicalAPI.SubOrganizations;
+
+namespace PSInfisicalAPI.Cmdlets
+{
+ [Cmdlet(VerbsCommon.New, "InfisicalSubOrganization", SupportsShouldProcess = true)]
+ [OutputType(typeof(InfisicalSubOrganization))]
+ public sealed class NewInfisicalSubOrganizationCmdlet : InfisicalCmdletBase
+ {
+ [Parameter(Mandatory = true, Position = 0)] public string Name { get; set; }
+ [Parameter(Mandatory = true, Position = 1)] public string Slug { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ try
+ {
+ if (!ShouldProcess(Name, "Create Infisical sub-organization"))
+ {
+ return;
+ }
+
+ InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
+ InfisicalSubOrganizationClient client = new InfisicalSubOrganizationClient(HttpClient, Logger);
+ InfisicalSubOrganization subOrganization = client.Create(connection, Name, Slug);
+ if (subOrganization != null)
+ {
+ WriteObject(subOrganization);
+ }
+ }
+ catch (Exception exception)
+ {
+ ThrowTerminatingForException("NewInfisicalSubOrganizationCmdlet", "CreateSubOrganization", exception);
+ }
+ }
+ }
+}
diff --git a/src/PSInfisicalAPI/Cmdlets/RemoveInfisicalOrganizationCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/RemoveInfisicalOrganizationCmdlet.cs
new file mode 100644
index 0000000..5adbd6e
--- /dev/null
+++ b/src/PSInfisicalAPI/Cmdlets/RemoveInfisicalOrganizationCmdlet.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Management.Automation;
+using PSInfisicalAPI.Connections;
+using PSInfisicalAPI.Organizations;
+
+namespace PSInfisicalAPI.Cmdlets
+{
+ [Cmdlet(VerbsCommon.Remove, "InfisicalOrganization", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]
+ public sealed class RemoveInfisicalOrganizationCmdlet : InfisicalCmdletBase
+ {
+ [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)]
+ [Alias("Id")]
+ public string OrganizationId { get; set; }
+
+ [Parameter] public SwitchParameter PassThru { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ try
+ {
+ InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
+
+ if (!ShouldProcess(OrganizationId, "Remove Infisical organization"))
+ {
+ return;
+ }
+
+ InfisicalOrganizationClient client = new InfisicalOrganizationClient(HttpClient, Logger);
+ client.Delete(connection, OrganizationId);
+
+ if (PassThru.IsPresent)
+ {
+ WriteObject(OrganizationId);
+ }
+ }
+ catch (Exception exception)
+ {
+ ThrowTerminatingForException("RemoveInfisicalOrganizationCmdlet", "DeleteOrganization", exception);
+ }
+ }
+ }
+}
diff --git a/src/PSInfisicalAPI/Cmdlets/RemoveInfisicalSubOrganizationCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/RemoveInfisicalSubOrganizationCmdlet.cs
new file mode 100644
index 0000000..28ae190
--- /dev/null
+++ b/src/PSInfisicalAPI/Cmdlets/RemoveInfisicalSubOrganizationCmdlet.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Management.Automation;
+using PSInfisicalAPI.Connections;
+using PSInfisicalAPI.SubOrganizations;
+
+namespace PSInfisicalAPI.Cmdlets
+{
+ [Cmdlet(VerbsCommon.Remove, "InfisicalSubOrganization", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]
+ public sealed class RemoveInfisicalSubOrganizationCmdlet : InfisicalCmdletBase
+ {
+ [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)]
+ [Alias("Id")]
+ public string SubOrganizationId { get; set; }
+
+ [Parameter] public SwitchParameter PassThru { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ try
+ {
+ InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
+
+ if (!ShouldProcess(SubOrganizationId, "Remove Infisical sub-organization"))
+ {
+ return;
+ }
+
+ InfisicalSubOrganizationClient client = new InfisicalSubOrganizationClient(HttpClient, Logger);
+ client.Delete(connection, SubOrganizationId);
+
+ if (PassThru.IsPresent)
+ {
+ WriteObject(SubOrganizationId);
+ }
+ }
+ catch (Exception exception)
+ {
+ ThrowTerminatingForException("RemoveInfisicalSubOrganizationCmdlet", "DeleteSubOrganization", exception);
+ }
+ }
+ }
+}
diff --git a/src/PSInfisicalAPI/Cmdlets/UpdateInfisicalOrganizationCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/UpdateInfisicalOrganizationCmdlet.cs
new file mode 100644
index 0000000..c8aeb60
--- /dev/null
+++ b/src/PSInfisicalAPI/Cmdlets/UpdateInfisicalOrganizationCmdlet.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Management.Automation;
+using PSInfisicalAPI.Connections;
+using PSInfisicalAPI.Models;
+using PSInfisicalAPI.Organizations;
+
+namespace PSInfisicalAPI.Cmdlets
+{
+ [Cmdlet(VerbsData.Update, "InfisicalOrganization", SupportsShouldProcess = true)]
+ [OutputType(typeof(InfisicalOrganization))]
+ public sealed class UpdateInfisicalOrganizationCmdlet : InfisicalCmdletBase
+ {
+ [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)]
+ [Alias("Id")]
+ public string OrganizationId { get; set; }
+
+ [Parameter] public string Name { get; set; }
+ [Parameter] public string Slug { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ try
+ {
+ InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
+
+ if (!ShouldProcess(OrganizationId, "Update Infisical organization"))
+ {
+ return;
+ }
+
+ InfisicalOrganizationClient client = new InfisicalOrganizationClient(HttpClient, Logger);
+ InfisicalOrganization organization = client.Update(connection, OrganizationId, Name, Slug);
+ if (organization != null)
+ {
+ WriteObject(organization);
+ }
+ }
+ catch (Exception exception)
+ {
+ ThrowTerminatingForException("UpdateInfisicalOrganizationCmdlet", "UpdateOrganization", exception);
+ }
+ }
+ }
+}
diff --git a/src/PSInfisicalAPI/Cmdlets/UpdateInfisicalSubOrganizationCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/UpdateInfisicalSubOrganizationCmdlet.cs
new file mode 100644
index 0000000..3c463aa
--- /dev/null
+++ b/src/PSInfisicalAPI/Cmdlets/UpdateInfisicalSubOrganizationCmdlet.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Management.Automation;
+using PSInfisicalAPI.Connections;
+using PSInfisicalAPI.Models;
+using PSInfisicalAPI.SubOrganizations;
+
+namespace PSInfisicalAPI.Cmdlets
+{
+ [Cmdlet(VerbsData.Update, "InfisicalSubOrganization", SupportsShouldProcess = true)]
+ [OutputType(typeof(InfisicalSubOrganization))]
+ public sealed class UpdateInfisicalSubOrganizationCmdlet : InfisicalCmdletBase
+ {
+ [Parameter(Mandatory = true, ValueFromPipelineByPropertyName = true, Position = 0)]
+ [Alias("Id")]
+ public string SubOrganizationId { get; set; }
+
+ [Parameter] public string Name { get; set; }
+ [Parameter] public string Slug { get; set; }
+
+ protected override void ProcessRecord()
+ {
+ try
+ {
+ InfisicalConnection connection = InfisicalSessionManager.RequireCurrent();
+
+ if (!ShouldProcess(SubOrganizationId, "Update Infisical sub-organization"))
+ {
+ return;
+ }
+
+ InfisicalSubOrganizationClient client = new InfisicalSubOrganizationClient(HttpClient, Logger);
+ InfisicalSubOrganization subOrganization = client.Update(connection, SubOrganizationId, Name, Slug);
+ if (subOrganization != null)
+ {
+ WriteObject(subOrganization);
+ }
+ }
+ catch (Exception exception)
+ {
+ ThrowTerminatingForException("UpdateInfisicalSubOrganizationCmdlet", "UpdateSubOrganization", exception);
+ }
+ }
+ }
+}
diff --git a/src/PSInfisicalAPI/Endpoints/InfisicalEndpointNames.cs b/src/PSInfisicalAPI/Endpoints/InfisicalEndpointNames.cs
index 678514f..64278e4 100644
--- a/src/PSInfisicalAPI/Endpoints/InfisicalEndpointNames.cs
+++ b/src/PSInfisicalAPI/Endpoints/InfisicalEndpointNames.cs
@@ -44,6 +44,18 @@ namespace PSInfisicalAPI.Endpoints
public const string UpdateTag = "UpdateTag";
public const string DeleteTag = "DeleteTag";
+ public const string ListOrganizations = "ListOrganizations";
+ public const string RetrieveOrganization = "RetrieveOrganization";
+ public const string CreateOrganization = "CreateOrganization";
+ public const string UpdateOrganization = "UpdateOrganization";
+ public const string DeleteOrganization = "DeleteOrganization";
+
+ public const string ListSubOrganizations = "ListSubOrganizations";
+ public const string RetrieveSubOrganization = "RetrieveSubOrganization";
+ public const string CreateSubOrganization = "CreateSubOrganization";
+ public const string UpdateSubOrganization = "UpdateSubOrganization";
+ public const string DeleteSubOrganization = "DeleteSubOrganization";
+
public const string ListInternalCertificateAuthorities = "ListInternalCertificateAuthorities";
public const string RetrieveInternalCertificateAuthority = "RetrieveInternalCertificateAuthority";
public const string SearchCertificates = "SearchCertificates";
diff --git a/src/PSInfisicalAPI/Endpoints/InfisicalEndpointRegistry.cs b/src/PSInfisicalAPI/Endpoints/InfisicalEndpointRegistry.cs
index e3f5ac4..5e7554e 100644
--- a/src/PSInfisicalAPI/Endpoints/InfisicalEndpointRegistry.cs
+++ b/src/PSInfisicalAPI/Endpoints/InfisicalEndpointRegistry.cs
@@ -16,6 +16,8 @@ namespace PSInfisicalAPI.Endpoints
RegisterEnvironments(Candidates);
RegisterFolders(Candidates);
RegisterTags(Candidates);
+ RegisterOrganizations(Candidates);
+ RegisterSubOrganizations(Candidates);
RegisterPki(Candidates);
}
@@ -496,6 +498,112 @@ namespace PSInfisicalAPI.Endpoints
});
}
+ private static void RegisterOrganizations(Dictionary> map)
+ {
+ Add(map, new InfisicalEndpointDefinition
+ {
+ Name = InfisicalEndpointNames.ListOrganizations,
+ Resource = "Organizations",
+ Version = "v2",
+ Method = "GET",
+ Template = "/api/v2/organizations",
+ RequiresAuthorization = true
+ });
+
+ Add(map, new InfisicalEndpointDefinition
+ {
+ Name = InfisicalEndpointNames.RetrieveOrganization,
+ Resource = "Organizations",
+ Version = "v1",
+ Method = "GET",
+ Template = "/api/v1/organization/{organizationId}",
+ RequiresAuthorization = true
+ });
+
+ Add(map, new InfisicalEndpointDefinition
+ {
+ Name = InfisicalEndpointNames.CreateOrganization,
+ Resource = "Organizations",
+ Version = "v2",
+ Method = "POST",
+ Template = "/api/v2/organizations",
+ RequiresAuthorization = true
+ });
+
+ Add(map, new InfisicalEndpointDefinition
+ {
+ Name = InfisicalEndpointNames.UpdateOrganization,
+ Resource = "Organizations",
+ Version = "v1",
+ Method = "PATCH",
+ Template = "/api/v1/organization/{organizationId}",
+ RequiresAuthorization = true
+ });
+
+ Add(map, new InfisicalEndpointDefinition
+ {
+ Name = InfisicalEndpointNames.DeleteOrganization,
+ Resource = "Organizations",
+ Version = "v1",
+ Method = "DELETE",
+ Template = "/api/v1/organization/{organizationId}",
+ RequiresAuthorization = true
+ });
+ }
+
+ private static void RegisterSubOrganizations(Dictionary> map)
+ {
+ Add(map, new InfisicalEndpointDefinition
+ {
+ Name = InfisicalEndpointNames.ListSubOrganizations,
+ Resource = "SubOrganizations",
+ Version = "v1",
+ Method = "GET",
+ Template = "/api/v1/sub-organizations",
+ RequiresAuthorization = true
+ });
+
+ Add(map, new InfisicalEndpointDefinition
+ {
+ Name = InfisicalEndpointNames.RetrieveSubOrganization,
+ Resource = "SubOrganizations",
+ Version = "v1",
+ Method = "GET",
+ Template = "/api/v1/sub-organizations/{subOrgId}",
+ RequiresAuthorization = true
+ });
+
+ Add(map, new InfisicalEndpointDefinition
+ {
+ Name = InfisicalEndpointNames.CreateSubOrganization,
+ Resource = "SubOrganizations",
+ Version = "v1",
+ Method = "POST",
+ Template = "/api/v1/sub-organizations",
+ RequiresAuthorization = true
+ });
+
+ Add(map, new InfisicalEndpointDefinition
+ {
+ Name = InfisicalEndpointNames.UpdateSubOrganization,
+ Resource = "SubOrganizations",
+ Version = "v1",
+ Method = "PATCH",
+ Template = "/api/v1/sub-organizations/{subOrgId}",
+ RequiresAuthorization = true
+ });
+
+ Add(map, new InfisicalEndpointDefinition
+ {
+ Name = InfisicalEndpointNames.DeleteSubOrganization,
+ Resource = "SubOrganizations",
+ Version = "v1",
+ Method = "DELETE",
+ Template = "/api/v1/sub-organizations/{subOrgId}",
+ RequiresAuthorization = true
+ });
+ }
+
private static void RegisterPki(Dictionary> map)
{
Add(map, new InfisicalEndpointDefinition
diff --git a/src/PSInfisicalAPI/Models/InfisicalOrganization.cs b/src/PSInfisicalAPI/Models/InfisicalOrganization.cs
new file mode 100644
index 0000000..bace43e
--- /dev/null
+++ b/src/PSInfisicalAPI/Models/InfisicalOrganization.cs
@@ -0,0 +1,21 @@
+using System;
+
+namespace PSInfisicalAPI.Models
+{
+ public sealed class InfisicalOrganization
+ {
+ public string Id { get; set; }
+ public string Name { get; set; }
+ public string Slug { get; set; }
+ public string CustomerId { get; set; }
+ public bool AuthEnforced { get; set; }
+ public bool ScimEnabled { get; set; }
+ public DateTimeOffset? CreatedAtUtc { get; set; }
+ public DateTimeOffset? UpdatedAtUtc { get; set; }
+
+ public override string ToString()
+ {
+ return string.IsNullOrEmpty(Slug) ? (Name ?? Id) : Slug;
+ }
+ }
+}
diff --git a/src/PSInfisicalAPI/Models/InfisicalSubOrganization.cs b/src/PSInfisicalAPI/Models/InfisicalSubOrganization.cs
new file mode 100644
index 0000000..ba9c318
--- /dev/null
+++ b/src/PSInfisicalAPI/Models/InfisicalSubOrganization.cs
@@ -0,0 +1,20 @@
+using System;
+
+namespace PSInfisicalAPI.Models
+{
+ public sealed class InfisicalSubOrganization
+ {
+ public string Id { get; set; }
+ public string Name { get; set; }
+ public string Slug { get; set; }
+ public string OrganizationId { get; set; }
+ public bool IsAccessible { get; set; }
+ public DateTimeOffset? CreatedAtUtc { get; set; }
+ public DateTimeOffset? UpdatedAtUtc { get; set; }
+
+ public override string ToString()
+ {
+ return string.IsNullOrEmpty(Slug) ? (Name ?? Id) : Slug;
+ }
+ }
+}
diff --git a/src/PSInfisicalAPI/Organizations/InfisicalOrganizationClient.cs b/src/PSInfisicalAPI/Organizations/InfisicalOrganizationClient.cs
new file mode 100644
index 0000000..18f7d67
--- /dev/null
+++ b/src/PSInfisicalAPI/Organizations/InfisicalOrganizationClient.cs
@@ -0,0 +1,150 @@
+using System;
+using System.Collections.Generic;
+using PSInfisicalAPI.Connections;
+using PSInfisicalAPI.Endpoints;
+using PSInfisicalAPI.Errors;
+using PSInfisicalAPI.Http;
+using PSInfisicalAPI.Logging;
+using PSInfisicalAPI.Models;
+using PSInfisicalAPI.Serialization;
+
+namespace PSInfisicalAPI.Organizations
+{
+ public sealed class InfisicalOrganizationClient
+ {
+ private const string Component = "OrganizationClient";
+
+ private readonly IInfisicalLogger _logger;
+ private readonly JsonInfisicalSerializer _serializer;
+ private readonly InfisicalApiInvoker _invoker;
+
+ public InfisicalOrganizationClient(IInfisicalHttpClient httpClient, IInfisicalLogger logger)
+ {
+ if (httpClient == null) { throw new ArgumentNullException(nameof(httpClient)); }
+ _logger = logger ?? NullInfisicalLogger.Instance;
+ _serializer = new JsonInfisicalSerializer();
+ _invoker = new InfisicalApiInvoker(httpClient);
+ }
+
+ public InfisicalOrganization[] List(InfisicalConnection connection)
+ {
+ if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
+
+ try
+ {
+ _logger.Information(Component, "Attempting to list Infisical organizations. Please Wait...");
+ InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.ListOrganizations, "ListOrganizations", null, null, null);
+ InfisicalOrganizationListResponseDto dto = _serializer.Deserialize(response.Body);
+ response.Clear();
+
+ InfisicalOrganization[] mapped = InfisicalOrganizationMapper.MapMany(dto != null ? dto.Organizations : null);
+ _logger.Information(Component, "Infisical organization list retrieval was successful.");
+ return mapped;
+ }
+ catch (Exception)
+ {
+ _logger.Error(Component, "Infisical organization list retrieval failed.");
+ throw;
+ }
+ }
+
+ public InfisicalOrganization Retrieve(InfisicalConnection connection, string organizationId)
+ {
+ if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
+ if (string.IsNullOrEmpty(organizationId)) { throw new InfisicalConfigurationException("OrganizationId is required."); }
+
+ Dictionary pathParameters = new Dictionary { { "organizationId", organizationId } };
+
+ try
+ {
+ _logger.Information(Component, string.Concat("Attempting to retrieve Infisical organization '", organizationId, "'. Please Wait..."));
+ InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.RetrieveOrganization, "RetrieveOrganization", pathParameters, null, null);
+ InfisicalOrganizationSingleResponseDto dto = _serializer.Deserialize(response.Body);
+ response.Clear();
+
+ InfisicalOrganization mapped = InfisicalOrganizationMapper.Map(dto != null ? dto.Organization : null);
+ _logger.Information(Component, "Infisical organization retrieval was successful.");
+ return mapped;
+ }
+ catch (Exception)
+ {
+ _logger.Error(Component, "Infisical organization retrieval failed.");
+ throw;
+ }
+ }
+
+ public InfisicalOrganization Create(InfisicalConnection connection, string name, string slug)
+ {
+ if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
+ if (string.IsNullOrEmpty(name)) { throw new InfisicalConfigurationException("Name is required."); }
+
+ InfisicalOrganizationCreateRequestDto request = new InfisicalOrganizationCreateRequestDto { Name = name, Slug = slug };
+ string body = _serializer.Serialize(request);
+
+ try
+ {
+ _logger.Information(Component, string.Concat("Attempting to create Infisical organization '", name, "'. Please Wait..."));
+ InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.CreateOrganization, "CreateOrganization", null, null, body);
+ InfisicalOrganizationSingleResponseDto dto = _serializer.Deserialize(response.Body);
+ response.Clear();
+
+ InfisicalOrganization mapped = InfisicalOrganizationMapper.Map(dto != null ? dto.Organization : null);
+ _logger.Information(Component, "Infisical organization creation was successful.");
+ return mapped;
+ }
+ catch (Exception)
+ {
+ _logger.Error(Component, "Infisical organization creation failed.");
+ throw;
+ }
+ }
+
+ public InfisicalOrganization Update(InfisicalConnection connection, string organizationId, string name, string slug)
+ {
+ if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
+ if (string.IsNullOrEmpty(organizationId)) { throw new InfisicalConfigurationException("OrganizationId is required."); }
+
+ Dictionary pathParameters = new Dictionary { { "organizationId", organizationId } };
+ InfisicalOrganizationUpdateRequestDto request = new InfisicalOrganizationUpdateRequestDto { Name = name, Slug = slug };
+ string body = _serializer.Serialize(request);
+
+ try
+ {
+ _logger.Information(Component, string.Concat("Attempting to update Infisical organization '", organizationId, "'. Please Wait..."));
+ InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.UpdateOrganization, "UpdateOrganization", pathParameters, null, body);
+ InfisicalOrganizationSingleResponseDto dto = _serializer.Deserialize(response.Body);
+ response.Clear();
+
+ InfisicalOrganization mapped = InfisicalOrganizationMapper.Map(dto != null ? dto.Organization : null);
+ _logger.Information(Component, "Infisical organization update was successful.");
+ return mapped;
+ }
+ catch (Exception)
+ {
+ _logger.Error(Component, "Infisical organization update failed.");
+ throw;
+ }
+ }
+
+ public void Delete(InfisicalConnection connection, string organizationId)
+ {
+ if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
+ if (string.IsNullOrEmpty(organizationId)) { throw new InfisicalConfigurationException("OrganizationId is required."); }
+
+ Dictionary pathParameters = new Dictionary { { "organizationId", organizationId } };
+
+ try
+ {
+ _logger.Information(Component, string.Concat("Attempting to delete Infisical organization '", organizationId, "'. Please Wait..."));
+ InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.DeleteOrganization, "DeleteOrganization", pathParameters, null, null);
+ response.Clear();
+ _logger.Information(Component, "Infisical organization deletion was successful.");
+ }
+ catch (Exception)
+ {
+ _logger.Error(Component, "Infisical organization deletion failed.");
+ throw;
+ }
+ }
+ }
+}
diff --git a/src/PSInfisicalAPI/Organizations/InfisicalOrganizationDtos.cs b/src/PSInfisicalAPI/Organizations/InfisicalOrganizationDtos.cs
new file mode 100644
index 0000000..9122bb9
--- /dev/null
+++ b/src/PSInfisicalAPI/Organizations/InfisicalOrganizationDtos.cs
@@ -0,0 +1,40 @@
+using System.Collections.Generic;
+using Newtonsoft.Json;
+
+namespace PSInfisicalAPI.Organizations
+{
+ internal sealed class InfisicalOrganizationResponseDto
+ {
+ [JsonProperty("id")] public string Id { get; set; }
+ [JsonProperty("_id")] public string InternalId { get; set; }
+ [JsonProperty("name")] public string Name { get; set; }
+ [JsonProperty("slug")] public string Slug { get; set; }
+ [JsonProperty("customerId")] public string CustomerId { get; set; }
+ [JsonProperty("authEnforced")] public bool AuthEnforced { get; set; }
+ [JsonProperty("scimEnabled")] public bool ScimEnabled { get; set; }
+ [JsonProperty("createdAt")] public string CreatedAt { get; set; }
+ [JsonProperty("updatedAt")] public string UpdatedAt { get; set; }
+ }
+
+ internal sealed class InfisicalOrganizationListResponseDto
+ {
+ [JsonProperty("organizations")] public List Organizations { get; set; }
+ }
+
+ internal sealed class InfisicalOrganizationSingleResponseDto
+ {
+ [JsonProperty("organization")] public InfisicalOrganizationResponseDto Organization { get; set; }
+ }
+
+ internal sealed class InfisicalOrganizationCreateRequestDto
+ {
+ [JsonProperty("name")] public string Name { get; set; }
+ [JsonProperty("slug", NullValueHandling = NullValueHandling.Ignore)] public string Slug { get; set; }
+ }
+
+ internal sealed class InfisicalOrganizationUpdateRequestDto
+ {
+ [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name { get; set; }
+ [JsonProperty("slug", NullValueHandling = NullValueHandling.Ignore)] public string Slug { get; set; }
+ }
+}
diff --git a/src/PSInfisicalAPI/Organizations/InfisicalOrganizationMapper.cs b/src/PSInfisicalAPI/Organizations/InfisicalOrganizationMapper.cs
new file mode 100644
index 0000000..01f986b
--- /dev/null
+++ b/src/PSInfisicalAPI/Organizations/InfisicalOrganizationMapper.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using PSInfisicalAPI.Models;
+
+namespace PSInfisicalAPI.Organizations
+{
+ internal static class InfisicalOrganizationMapper
+ {
+ public static InfisicalOrganization Map(InfisicalOrganizationResponseDto dto)
+ {
+ if (dto == null)
+ {
+ return null;
+ }
+
+ return new InfisicalOrganization
+ {
+ Id = !string.IsNullOrEmpty(dto.Id) ? dto.Id : dto.InternalId,
+ Name = dto.Name,
+ Slug = dto.Slug,
+ CustomerId = dto.CustomerId,
+ AuthEnforced = dto.AuthEnforced,
+ ScimEnabled = dto.ScimEnabled,
+ CreatedAtUtc = ParseTimestamp(dto.CreatedAt),
+ UpdatedAtUtc = ParseTimestamp(dto.UpdatedAt)
+ };
+ }
+
+ public static InfisicalOrganization[] MapMany(IEnumerable items)
+ {
+ if (items == null)
+ {
+ return Array.Empty();
+ }
+
+ List results = new List();
+ foreach (InfisicalOrganizationResponseDto dto in items)
+ {
+ InfisicalOrganization mapped = Map(dto);
+ if (mapped != null)
+ {
+ results.Add(mapped);
+ }
+ }
+
+ return results.ToArray();
+ }
+
+ 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/SubOrganizations/InfisicalSubOrganizationClient.cs b/src/PSInfisicalAPI/SubOrganizations/InfisicalSubOrganizationClient.cs
new file mode 100644
index 0000000..97b861c
--- /dev/null
+++ b/src/PSInfisicalAPI/SubOrganizations/InfisicalSubOrganizationClient.cs
@@ -0,0 +1,160 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using PSInfisicalAPI.Connections;
+using PSInfisicalAPI.Endpoints;
+using PSInfisicalAPI.Errors;
+using PSInfisicalAPI.Http;
+using PSInfisicalAPI.Logging;
+using PSInfisicalAPI.Models;
+using PSInfisicalAPI.Serialization;
+
+namespace PSInfisicalAPI.SubOrganizations
+{
+ public sealed class InfisicalSubOrganizationClient
+ {
+ private const string Component = "SubOrganizationClient";
+
+ private readonly IInfisicalLogger _logger;
+ private readonly JsonInfisicalSerializer _serializer;
+ private readonly InfisicalApiInvoker _invoker;
+
+ public InfisicalSubOrganizationClient(IInfisicalHttpClient httpClient, IInfisicalLogger logger)
+ {
+ if (httpClient == null) { throw new ArgumentNullException(nameof(httpClient)); }
+ _logger = logger ?? NullInfisicalLogger.Instance;
+ _serializer = new JsonInfisicalSerializer();
+ _invoker = new InfisicalApiInvoker(httpClient);
+ }
+
+ public InfisicalSubOrganization[] List(InfisicalConnection connection, int? limit, int? offset, string search, string orderBy, string orderDirection, bool? isAccessible)
+ {
+ if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
+
+ List> queryParameters = new List>();
+ if (limit.HasValue) { queryParameters.Add(new KeyValuePair("limit", limit.Value.ToString(CultureInfo.InvariantCulture))); }
+ if (offset.HasValue) { queryParameters.Add(new KeyValuePair("offset", offset.Value.ToString(CultureInfo.InvariantCulture))); }
+ if (!string.IsNullOrEmpty(search)) { queryParameters.Add(new KeyValuePair("search", search)); }
+ if (!string.IsNullOrEmpty(orderBy)) { queryParameters.Add(new KeyValuePair("orderBy", orderBy)); }
+ if (!string.IsNullOrEmpty(orderDirection)) { queryParameters.Add(new KeyValuePair("orderDirection", orderDirection)); }
+ if (isAccessible.HasValue) { queryParameters.Add(new KeyValuePair("isAccessible", isAccessible.Value ? "true" : "false")); }
+
+ try
+ {
+ _logger.Information(Component, "Attempting to list Infisical sub-organizations. Please Wait...");
+ InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.ListSubOrganizations, "ListSubOrganizations", null, queryParameters, null);
+ InfisicalSubOrganizationListResponseDto dto = _serializer.Deserialize(response.Body);
+ response.Clear();
+
+ InfisicalSubOrganization[] mapped = InfisicalSubOrganizationMapper.MapMany(dto != null ? dto.SubOrganizations : null);
+ _logger.Information(Component, "Infisical sub-organization list retrieval was successful.");
+ return mapped;
+ }
+ catch (Exception)
+ {
+ _logger.Error(Component, "Infisical sub-organization list retrieval failed.");
+ throw;
+ }
+ }
+
+ public InfisicalSubOrganization Retrieve(InfisicalConnection connection, string subOrganizationId)
+ {
+ if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
+ if (string.IsNullOrEmpty(subOrganizationId)) { throw new InfisicalConfigurationException("SubOrganizationId is required."); }
+
+ Dictionary pathParameters = new Dictionary { { "subOrgId", subOrganizationId } };
+
+ try
+ {
+ _logger.Information(Component, string.Concat("Attempting to retrieve Infisical sub-organization '", subOrganizationId, "'. Please Wait..."));
+ InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.RetrieveSubOrganization, "RetrieveSubOrganization", pathParameters, null, null);
+ InfisicalSubOrganizationSingleResponseDto dto = _serializer.Deserialize(response.Body);
+ response.Clear();
+
+ InfisicalSubOrganization mapped = InfisicalSubOrganizationMapper.Map(dto != null ? dto.SubOrganization : null);
+ _logger.Information(Component, "Infisical sub-organization retrieval was successful.");
+ return mapped;
+ }
+ catch (Exception)
+ {
+ _logger.Error(Component, "Infisical sub-organization retrieval failed.");
+ throw;
+ }
+ }
+
+ public InfisicalSubOrganization Create(InfisicalConnection connection, string name, string slug)
+ {
+ if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
+ if (string.IsNullOrEmpty(name)) { throw new InfisicalConfigurationException("Name is required."); }
+ if (string.IsNullOrEmpty(slug)) { throw new InfisicalConfigurationException("Slug is required."); }
+
+ InfisicalSubOrganizationCreateRequestDto request = new InfisicalSubOrganizationCreateRequestDto { Name = name, Slug = slug };
+ string body = _serializer.Serialize(request);
+
+ try
+ {
+ _logger.Information(Component, string.Concat("Attempting to create Infisical sub-organization '", name, "'. Please Wait..."));
+ InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.CreateSubOrganization, "CreateSubOrganization", null, null, body);
+ InfisicalSubOrganizationSingleResponseDto dto = _serializer.Deserialize(response.Body);
+ response.Clear();
+
+ InfisicalSubOrganization mapped = InfisicalSubOrganizationMapper.Map(dto != null ? dto.SubOrganization : null);
+ _logger.Information(Component, "Infisical sub-organization creation was successful.");
+ return mapped;
+ }
+ catch (Exception)
+ {
+ _logger.Error(Component, "Infisical sub-organization creation failed.");
+ throw;
+ }
+ }
+
+ public InfisicalSubOrganization Update(InfisicalConnection connection, string subOrganizationId, string name, string slug)
+ {
+ if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
+ if (string.IsNullOrEmpty(subOrganizationId)) { throw new InfisicalConfigurationException("SubOrganizationId is required."); }
+
+ Dictionary pathParameters = new Dictionary { { "subOrgId", subOrganizationId } };
+ InfisicalSubOrganizationUpdateRequestDto request = new InfisicalSubOrganizationUpdateRequestDto { Name = name, Slug = slug };
+ string body = _serializer.Serialize(request);
+
+ try
+ {
+ _logger.Information(Component, string.Concat("Attempting to update Infisical sub-organization '", subOrganizationId, "'. Please Wait..."));
+ InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.UpdateSubOrganization, "UpdateSubOrganization", pathParameters, null, body);
+ InfisicalSubOrganizationSingleResponseDto dto = _serializer.Deserialize(response.Body);
+ response.Clear();
+
+ InfisicalSubOrganization mapped = InfisicalSubOrganizationMapper.Map(dto != null ? dto.SubOrganization : null);
+ _logger.Information(Component, "Infisical sub-organization update was successful.");
+ return mapped;
+ }
+ catch (Exception)
+ {
+ _logger.Error(Component, "Infisical sub-organization update failed.");
+ throw;
+ }
+ }
+
+ public void Delete(InfisicalConnection connection, string subOrganizationId)
+ {
+ if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
+ if (string.IsNullOrEmpty(subOrganizationId)) { throw new InfisicalConfigurationException("SubOrganizationId is required."); }
+
+ Dictionary pathParameters = new Dictionary { { "subOrgId", subOrganizationId } };
+
+ try
+ {
+ _logger.Information(Component, string.Concat("Attempting to delete Infisical sub-organization '", subOrganizationId, "'. Please Wait..."));
+ InfisicalHttpResponse response = _invoker.Invoke(connection, InfisicalEndpointNames.DeleteSubOrganization, "DeleteSubOrganization", pathParameters, null, null);
+ response.Clear();
+ _logger.Information(Component, "Infisical sub-organization deletion was successful.");
+ }
+ catch (Exception)
+ {
+ _logger.Error(Component, "Infisical sub-organization deletion failed.");
+ throw;
+ }
+ }
+ }
+}
diff --git a/src/PSInfisicalAPI/SubOrganizations/InfisicalSubOrganizationDtos.cs b/src/PSInfisicalAPI/SubOrganizations/InfisicalSubOrganizationDtos.cs
new file mode 100644
index 0000000..917e0e3
--- /dev/null
+++ b/src/PSInfisicalAPI/SubOrganizations/InfisicalSubOrganizationDtos.cs
@@ -0,0 +1,40 @@
+using System.Collections.Generic;
+using Newtonsoft.Json;
+
+namespace PSInfisicalAPI.SubOrganizations
+{
+ internal sealed class InfisicalSubOrganizationResponseDto
+ {
+ [JsonProperty("id")] public string Id { get; set; }
+ [JsonProperty("_id")] public string InternalId { get; set; }
+ [JsonProperty("name")] public string Name { get; set; }
+ [JsonProperty("slug")] public string Slug { get; set; }
+ [JsonProperty("organizationId")] public string OrganizationId { get; set; }
+ [JsonProperty("orgId")] public string OrgId { get; set; }
+ [JsonProperty("isAccessible")] public bool IsAccessible { get; set; }
+ [JsonProperty("createdAt")] public string CreatedAt { get; set; }
+ [JsonProperty("updatedAt")] public string UpdatedAt { get; set; }
+ }
+
+ internal sealed class InfisicalSubOrganizationListResponseDto
+ {
+ [JsonProperty("subOrganizations")] public List SubOrganizations { get; set; }
+ }
+
+ internal sealed class InfisicalSubOrganizationSingleResponseDto
+ {
+ [JsonProperty("subOrganization")] public InfisicalSubOrganizationResponseDto SubOrganization { get; set; }
+ }
+
+ internal sealed class InfisicalSubOrganizationCreateRequestDto
+ {
+ [JsonProperty("name")] public string Name { get; set; }
+ [JsonProperty("slug")] public string Slug { get; set; }
+ }
+
+ internal sealed class InfisicalSubOrganizationUpdateRequestDto
+ {
+ [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name { get; set; }
+ [JsonProperty("slug", NullValueHandling = NullValueHandling.Ignore)] public string Slug { get; set; }
+ }
+}
diff --git a/src/PSInfisicalAPI/SubOrganizations/InfisicalSubOrganizationMapper.cs b/src/PSInfisicalAPI/SubOrganizations/InfisicalSubOrganizationMapper.cs
new file mode 100644
index 0000000..2a8604d
--- /dev/null
+++ b/src/PSInfisicalAPI/SubOrganizations/InfisicalSubOrganizationMapper.cs
@@ -0,0 +1,65 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using PSInfisicalAPI.Models;
+
+namespace PSInfisicalAPI.SubOrganizations
+{
+ internal static class InfisicalSubOrganizationMapper
+ {
+ public static InfisicalSubOrganization Map(InfisicalSubOrganizationResponseDto dto)
+ {
+ if (dto == null)
+ {
+ return null;
+ }
+
+ return new InfisicalSubOrganization
+ {
+ Id = !string.IsNullOrEmpty(dto.Id) ? dto.Id : dto.InternalId,
+ Name = dto.Name,
+ Slug = dto.Slug,
+ OrganizationId = !string.IsNullOrEmpty(dto.OrganizationId) ? dto.OrganizationId : dto.OrgId,
+ IsAccessible = dto.IsAccessible,
+ CreatedAtUtc = ParseTimestamp(dto.CreatedAt),
+ UpdatedAtUtc = ParseTimestamp(dto.UpdatedAt)
+ };
+ }
+
+ public static InfisicalSubOrganization[] MapMany(IEnumerable items)
+ {
+ if (items == null)
+ {
+ return Array.Empty();
+ }
+
+ List results = new List();
+ foreach (InfisicalSubOrganizationResponseDto dto in items)
+ {
+ InfisicalSubOrganization mapped = Map(dto);
+ if (mapped != null)
+ {
+ results.Add(mapped);
+ }
+ }
+
+ return results.ToArray();
+ }
+
+ 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;
+ }
+ }
+}