Skip to content

Commit

Permalink
Gandi plugin now supports Personal Access Tokens (PAT) auth in additi…
Browse files Browse the repository at this point in the history
…on to legacy API Keys (#554)
  • Loading branch information
rmbolger committed Jun 24, 2024
1 parent 614070c commit dd5fd74
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 58 deletions.
154 changes: 100 additions & 54 deletions Posh-ACME/Plugins/Gandi.ps1
Original file line number Diff line number Diff line change
@@ -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)]
Expand All @@ -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) {
Expand All @@ -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 }
}

<#
Expand Down Expand Up @@ -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)]
Expand All @@ -109,49 +128,67 @@ 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) {
if ($rec.rrset_values.Count -gt 1) {
# 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."
}
Expand Down Expand Up @@ -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
Expand All @@ -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"
}
32 changes: 28 additions & 4 deletions docs/Plugins/Gandi.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

0 comments on commit dd5fd74

Please sign in to comment.