From dd5fd7494918fff5e697e19ab919dcd559be9af2 Mon Sep 17 00:00:00 2001 From: Ryan Bolger Date: Mon, 24 Jun 2024 00:01:09 -0700 Subject: [PATCH] Gandi plugin now supports Personal Access Tokens (PAT) auth in addition to legacy API Keys (#554) --- Posh-ACME/Plugins/Gandi.ps1 | 154 +++++++++++++++++++++++------------- docs/Plugins/Gandi.md | 32 +++++++- 2 files changed, 128 insertions(+), 58 deletions(-) diff --git a/Posh-ACME/Plugins/Gandi.ps1 b/Posh-ACME/Plugins/Gandi.ps1 index 65d17601..3f9e29af 100644 --- a/Posh-ACME/Plugins/Gandi.ps1 +++ b/Posh-ACME/Plugins/Gandi.ps1 @@ -1,12 +1,14 @@ function Get-CurrentPluginType { 'dns-01' } function Add-DnsTxt { - [CmdletBinding(DefaultParameterSetName='Secure')] + [CmdletBinding(DefaultParameterSetName='PAT')] param( [Parameter(Mandatory,Position=0)] [string]$RecordName, [Parameter(Mandatory,Position=1)] [string]$TxtValue, + [Parameter(ParameterSetName='PAT')] + [securestring]$GandiPAT, [Parameter(ParameterSetName='Secure',Mandatory,Position=2)] [securestring]$GandiToken, [Parameter(ParameterSetName='DeprecatedInsecure',Mandatory,Position=2)] @@ -15,27 +17,38 @@ function Add-DnsTxt { $ExtraParams ) - # un-secure the password so we can add it to the auth header - if ('Secure' -eq $PSCmdlet.ParameterSetName) { - $GandiTokenInsecure = [pscredential]::new('a',$GandiToken).GetNetworkCredential().Password + # Build the appropriate auth header depending on what type of token was used. + $RestHeaders = @{Accept = 'application/json'} + if ('PAT' -eq $PSCmdlet.ParameterSetName) { + $pat = [pscredential]::new('a',$GandiPAT).GetNetworkCredential().Password + $RestHeaders.Authorization = "Bearer $pat" } - $restParams = @{ - Headers = @{ - 'X-Api-Key' = $GandiTokenInsecure - Accept = 'application/json' + else { + if ('Secure' -eq $PSCmdlet.ParameterSetName) { + $GandiTokenInsecure = [pscredential]::new('a',$GandiToken).GetNetworkCredential().Password } - ContentType = 'application/json' + $RestHeaders.'X-Api-Key' = $GandiTokenInsecure } # get the zone name for our record - $zoneName = Find-GandiZone $RecordName $restParams + $zoneName = Find-GandiZone $RecordName $RestHeaders Write-Debug "Found zone $zoneName" # find the matching TXT record if it exists $recShort = ($RecordName -ireplace [regex]::Escape($zoneName), [string]::Empty).TrimEnd('.') + if (-not $recShort) { $recShort = '@' } $recUrl = "https://dns.api.gandi.net/api/v5/domains/$zoneName/records/$recShort/TXT" try { - $rec = Invoke-RestMethod $recUrl @restParams @script:UseBasic -EA Stop + $queryParams = @{ + Uri = $recUrl + Headers = $RestHeaders + ContentType = 'application/json' + Verbose = $false + ErrorAction = 'Stop' + } + Write-Debug "GET $($queryParams.Uri)" + $rec = Invoke-RestMethod @queryParams @script:UseBasic + Write-Debug "Response:`n$($rec | ConvertTo-Json)" } catch {} if ($rec -and "`"$TxtValue`"" -in $rec.rrset_values) { @@ -44,24 +57,28 @@ function Add-DnsTxt { if (-not $rec) { # add new record Write-Verbose "Adding a TXT record for $RecordName with value $TxtValue" - $body = @{rrset_values=@("`"$TxtValue`"")} - Write-Debug "Sending body:`n$(($body | ConvertTo-Json))" - $bodyJson = $body | ConvertTo-Json -Compress - try { - Invoke-RestMethod $recUrl -Method Post -Body $bodyJson ` - @restParams @script:UseBasic -EA Stop | Out-Null - } catch { throw } + $queryParams = @{ + Method = 'POST' + Body = (@{rrset_values=@("`"$TxtValue`"")} | ConvertTo-Json -Compress) + } } else { # update the existing record Write-Verbose "Updating a TXT record for $RecordName with value $TxtValue" - $body = @{rrset_values=(@($rec.rrset_values) + @("`"$TxtValue`""))} - Write-Debug "Sending body:`n$(($body | ConvertTo-Json))" - $bodyJson = $body | ConvertTo-Json -Compress - try { - Invoke-RestMethod $recUrl -Method Put -Body $bodyJson ` - @restParams @script:UseBasic -EA Stop | Out-Null - } catch { throw } + $queryParams = @{ + Method = 'PUT' + Body = (@{rrset_values=(@($rec.rrset_values) + @("`"$TxtValue`""))} | ConvertTo-Json -Compress) + } } + + $queryParams.Uri = $recUrl + $queryParams.Headers = $RestHeaders + $queryParams.ContentType = 'application/json' + $queryParams.Verbose = $false + $queryParams.ErrorAction = 'Stop' + try { + Write-Debug "$($queryParams.Method) $($queryParams.Uri)`n$($queryParams.Body)" + Invoke-RestMethod @queryParams @script:UseBasic | Out-Null + } catch { throw } } <# @@ -95,12 +112,14 @@ function Add-DnsTxt { } function Remove-DnsTxt { - [CmdletBinding()] + [CmdletBinding(DefaultParameterSetName='PAT')] param( [Parameter(Mandatory,Position=0)] [string]$RecordName, [Parameter(Mandatory,Position=1)] [string]$TxtValue, + [Parameter(ParameterSetName='PAT')] + [securestring]$GandiPAT, [Parameter(ParameterSetName='Secure',Mandatory,Position=2)] [securestring]$GandiToken, [Parameter(ParameterSetName='DeprecatedInsecure',Mandatory,Position=2)] @@ -109,27 +128,38 @@ function Remove-DnsTxt { $ExtraParams ) - # un-secure the password so we can add it to the auth header - if ('Secure' -eq $PSCmdlet.ParameterSetName) { - $GandiTokenInsecure = [pscredential]::new('a',$GandiToken).GetNetworkCredential().Password + # Build the appropriate auth header depending on what type of token was used. + $RestHeaders = @{Accept = 'application/json'} + if ('PAT' -eq $PSCmdlet.ParameterSetName) { + $pat = [pscredential]::new('a',$GandiPAT).GetNetworkCredential().Password + $RestHeaders.Authorization = "Bearer $pat" } - $restParams = @{ - Headers = @{ - 'X-Api-Key' = $GandiTokenInsecure - Accept = 'application/json' + else { + if ('Secure' -eq $PSCmdlet.ParameterSetName) { + $GandiTokenInsecure = [pscredential]::new('a',$GandiToken).GetNetworkCredential().Password } - ContentType = 'application/json' + $RestHeaders.'X-Api-Key' = $GandiTokenInsecure } # get the zone name for our record - $zoneName = Find-GandiZone $RecordName $restParams + $zoneName = Find-GandiZone $RecordName $RestHeaders Write-Debug "Found zone $zoneName" # find the matching TXT record if it exists $recShort = ($RecordName -ireplace [regex]::Escape($zoneName), [string]::Empty).TrimEnd('.') + if (-not $recShort) { $recShort = '@' } $recUrl = "https://dns.api.gandi.net/api/v5/domains/$zoneName/records/$recShort/TXT" try { - $rec = Invoke-RestMethod $recUrl @restParams @script:UseBasic -EA Stop + $queryParams = @{ + Uri = $recUrl + Headers = $RestHeaders + ContentType = 'application/json' + Verbose = $false + ErrorAction = 'Stop' + } + Write-Debug "GET $($queryParams.Uri)" + $rec = Invoke-RestMethod @queryParams @script:UseBasic + Write-Debug "Response:`n$($rec | ConvertTo-Json)" } catch {} if ($rec -and "`"$TxtValue`"" -in $rec.rrset_values) { @@ -137,21 +167,28 @@ function Remove-DnsTxt { # remove just the value we care about Write-Verbose "Removing $TxtValue from TXT record for $RecordName" $otherVals = $rec.rrset_values | Where-Object { $_ -ne "`"$TxtValue`"" } - $body = @{rrset_values=@($otherVals)} - Write-Debug "Sending body:`n$(($body | ConvertTo-Json))" - $bodyJson = $body | ConvertTo-Json -Compress - try { - Invoke-RestMethod $recUrl -Method Put -Body $bodyJson ` - @restParams @script:UseBasic -EA Stop | Out-Null - } catch { throw } + $queryParams = @{ + Method = 'PUT' + Body = (@{rrset_values=@($otherVals)} | ConvertTo-Json -Compress) + } } else { # delete the whole record because this value is the last one Write-Verbose "Removing TXT record for $RecordName" - try { - Invoke-RestMethod $recUrl -Method Delete ` - @restParams @script:UseBasic -EA Stop | Out-Null - } catch { throw } + $queryParams = @{ + Method = 'DELETE' + } } + + $queryParams.Uri = $recUrl + $queryParams.Headers = $RestHeaders + $queryParams.ContentType = 'application/json' + $queryParams.Verbose = $false + $queryParams.ErrorAction = 'Stop' + try { + Write-Debug "$($queryParams.Method) $($queryParams.Uri)`n$($queryParams.Body)" + Invoke-RestMethod @queryParams @script:UseBasic | Out-Null + } catch { throw } + } else { Write-Debug "Record $RecordName with value $TxtValue doesn't exist. Nothing to do." } @@ -218,7 +255,7 @@ function Find-GandiZone { [Parameter(Mandatory,Position=0)] [string]$RecordName, [Parameter(Mandatory,Position=1)] - [hashtable]$RestParams + [hashtable]$RestHeaders ) # setup a module variable to cache the record to zone mapping @@ -242,15 +279,24 @@ function Find-GandiZone { $pieces = $RecordName.Split('.') for ($i=0; $i -lt ($pieces.Count-1); $i++) { $zoneTest = $pieces[$i..($pieces.Count-1)] -join '.' - Write-Debug "Checking $zoneTest" try { - Invoke-RestMethod "https://dns.api.gandi.net/api/v5/domains/$zoneTest" ` - @RestParams @script:UseBasic -EA Stop | Out-Null + $queryParams = @{ + Uri = "https://dns.api.gandi.net/api/v5/domains/$zoneTest" + Headers = $RestHeaders + Verbose = $false + ErrorAction = 'Stop' + } + Write-Debug "GET $($queryParams.Uri)" + $resp = Invoke-RestMethod @queryParams @script:UseBasic + Write-Debug "Response:`n$($resp | ConvertTo-Json -Dep 10)" $script:GandiRecordZones.$RecordName = $zoneTest return $zoneTest - } catch {} + } catch { + if (404 -ne $_.Exception.Response.StatusCode) { + throw + } + } } - return $null - + throw "Unable to find zone matching $RecordName" } diff --git a/docs/Plugins/Gandi.md b/docs/Plugins/Gandi.md index eec226ac..9e9b1dc5 100644 --- a/docs/Plugins/Gandi.md +++ b/docs/Plugins/Gandi.md @@ -6,18 +6,42 @@ This plugin works against the [Gandi](https://www.gandi.net) DNS provider. It is ## Setup -First, login to your [account page](https://account.gandi.net) and go to the `Security` section. There will be an option to generate or regenerate the "API Key for LiveDNS". Do that and make a record the new value. +First, login to your [account page](https://account.gandi.net) and go to the `Authentication Options` section. Down at the bottom in the `Developer access` section, there are options for `Personal Access Token (PAT)` and `API Key (Deprecated)`. + +It is no longer recommended to use the API Key option since it has been deprecated. But it should continue to work with the plugin as long as they allow it to. The main benefit over the PAT option is that it doesn't expire. + +In the PAT section, click the link for `See my personal access tokens`. Then click the `Create a token` button. + +- Select the appropriate organization +- Give it a cosmetic name +- Set the expiration time (1 year is currently the max) +- Choose whether to limit the PAT to a specific set of domains +- Select the option for `Manage domain name technical configurations` which will force the selection of `See and renew domain names`. +- Click `Create`. + +Record the token value. ## Using the Plugin -The API key is used with the `GandiToken` SecureString parameter. +The Personal Access Token (PAT) is used with the `GandiPAT` SecureString parameter. If you are using the Legacy API Key option, use the `GandiToken` SecureString parameter instead. !!! warning - The `GandiTokenInsecure` parameter is deprecated and will be removed in the next major module version. If you are using it, please migrate to the Secure parameter set. + The `GandiTokenInsecure` parameter is deprecated and will be removed in the next major module version. If you are using it, please migrate to one of the Secure parameter sets. + +### Example for Personal Access Token + +```powershell +$pArgs = @{ + GandiPAT = (Read-Host "Personal Access Token" -AsSecureString) +} +New-PACertificate example.com -Plugin Gandi -PluginArgs $pArgs +``` + +### Example for Legacy API Key ```powershell $pArgs = @{ - GandiToken = (Read-Host "Gandi Token" -AsSecureString) + GandiToken = (Read-Host "Legacy API Key" -AsSecureString) } New-PACertificate example.com -Plugin Gandi -PluginArgs $pArgs ```