From 77cb03ec9845e1b824159fecdcd358e8b57358e0 Mon Sep 17 00:00:00 2001 From: GraceSolutions Date: Sat, 6 Jun 2026 20:17:49 -0400 Subject: [PATCH] feat: add Organization/Sub-Organization CRUD cmdlets and Get-InfisicalSANList Adds 8 cmdlets for Organization and Sub-Organization CRUD (Get/New/Update/Remove for each), targeting /api/v2/organizations and /api/v1/sub-organizations. Get cmdlets default to List parameter set and switch to Single when -OrganizationId or -SubOrganizationId is supplied. New/Update/Remove honor -WhatIf/-Confirm; Remove defaults to High ConfirmImpact and supports -PassThru. No project context required. Adds Get-InfisicalSANList: 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 or CGNAT, and the IPv4/IPv6 loopback addresses. Supports optional case-insensitive -InclusionExpression and -ExclusionExpression regex filters applied in fetch -> include -> exclude -> output order. Output is a single strongly-typed System.String[] array emitted non-enumerated so List.AddRange consumes it directly. Registers 10 new endpoints, adds InfisicalOrganization/InfisicalSubOrganization models with DTOs, mappers, and clients, full MAML help for all 9 new cmdlets, mapper unit tests, EndpointRegistry inline-data coverage, and docs/DesignSpec.md sections 16.7 and 16.8. build.ps1 CmdletsToExport and Test-ModuleImports expected list now contain 51 cmdlets. README updated with Organization/Sub-Organization tables, the new Get-InfisicalSANList entry, and an end-to-end certificate request example using splatted OrderedDictionary blocks. --- CHANGELOG.md | 9 +- .../en-US/PSInfisicalAPI.dll-Help.xml | 318 ++++++++++++++++++ README.md | 55 ++- build.ps1 | 13 +- docs/DesignSpec.md | 80 +++++ .../EndpointRegistryTests.cs | 10 + .../OrganizationMapperTests.cs | 72 ++++ .../SubOrganizationMapperTests.cs | 72 ++++ .../Cmdlets/GetInfisicalOrganizationCmdlet.cs | 47 +++ .../Cmdlets/GetInfisicalSANListCmdlet.cs | 108 ++++++ .../GetInfisicalSubOrganizationCmdlet.cs | 59 ++++ .../Cmdlets/NewInfisicalOrganizationCmdlet.cs | 39 +++ .../NewInfisicalSubOrganizationCmdlet.cs | 39 +++ .../RemoveInfisicalOrganizationCmdlet.cs | 42 +++ .../RemoveInfisicalSubOrganizationCmdlet.cs | 42 +++ .../UpdateInfisicalOrganizationCmdlet.cs | 44 +++ .../UpdateInfisicalSubOrganizationCmdlet.cs | 44 +++ .../Endpoints/InfisicalEndpointNames.cs | 12 + .../Endpoints/InfisicalEndpointRegistry.cs | 108 ++++++ .../Models/InfisicalOrganization.cs | 21 ++ .../Models/InfisicalSubOrganization.cs | 20 ++ .../InfisicalOrganizationClient.cs | 150 +++++++++ .../InfisicalOrganizationDtos.cs | 40 +++ .../InfisicalOrganizationMapper.cs | 66 ++++ .../InfisicalSubOrganizationClient.cs | 160 +++++++++ .../InfisicalSubOrganizationDtos.cs | 40 +++ .../InfisicalSubOrganizationMapper.cs | 65 ++++ 27 files changed, 1771 insertions(+), 4 deletions(-) create mode 100644 src/PSInfisicalAPI.Tests/OrganizationMapperTests.cs create mode 100644 src/PSInfisicalAPI.Tests/SubOrganizationMapperTests.cs create mode 100644 src/PSInfisicalAPI/Cmdlets/GetInfisicalOrganizationCmdlet.cs create mode 100644 src/PSInfisicalAPI/Cmdlets/GetInfisicalSANListCmdlet.cs create mode 100644 src/PSInfisicalAPI/Cmdlets/GetInfisicalSubOrganizationCmdlet.cs create mode 100644 src/PSInfisicalAPI/Cmdlets/NewInfisicalOrganizationCmdlet.cs create mode 100644 src/PSInfisicalAPI/Cmdlets/NewInfisicalSubOrganizationCmdlet.cs create mode 100644 src/PSInfisicalAPI/Cmdlets/RemoveInfisicalOrganizationCmdlet.cs create mode 100644 src/PSInfisicalAPI/Cmdlets/RemoveInfisicalSubOrganizationCmdlet.cs create mode 100644 src/PSInfisicalAPI/Cmdlets/UpdateInfisicalOrganizationCmdlet.cs create mode 100644 src/PSInfisicalAPI/Cmdlets/UpdateInfisicalSubOrganizationCmdlet.cs create mode 100644 src/PSInfisicalAPI/Models/InfisicalOrganization.cs create mode 100644 src/PSInfisicalAPI/Models/InfisicalSubOrganization.cs create mode 100644 src/PSInfisicalAPI/Organizations/InfisicalOrganizationClient.cs create mode 100644 src/PSInfisicalAPI/Organizations/InfisicalOrganizationDtos.cs create mode 100644 src/PSInfisicalAPI/Organizations/InfisicalOrganizationMapper.cs create mode 100644 src/PSInfisicalAPI/SubOrganizations/InfisicalSubOrganizationClient.cs create mode 100644 src/PSInfisicalAPI/SubOrganizations/InfisicalSubOrganizationDtos.cs create mode 100644 src/PSInfisicalAPI/SubOrganizations/InfisicalSubOrganizationMapper.cs 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; + } + } +}