diff --git a/AzureKeyVault/AzureClient.cs b/AzureKeyVault/AzureClient.cs index e470341..1647001 100644 --- a/AzureKeyVault/AzureClient.cs +++ b/AzureKeyVault/AzureClient.cs @@ -22,6 +22,8 @@ using Keyfactor.Orchestrators.Common.Enums; using Keyfactor.Orchestrators.Extensions; using Microsoft.Extensions.Logging; +using Microsoft.VisualBasic; +using Newtonsoft.Json; namespace Keyfactor.Extensions.Orchestrator.AzureKeyVault { @@ -38,8 +40,6 @@ private Uri AzureCloudEndpoint case "china": logger.LogTrace(AzureAuthorityHosts.AzureChina.ToString()); return AzureAuthorityHosts.AzureChina; - //case "germany": - // return AzureAuthorityHosts.AzureGermany; // germany is no longer a valid azure authority host as of 2021 case "government": logger.LogTrace(AzureAuthorityHosts.AzureGovernment.ToString()); return AzureAuthorityHosts.AzureGovernment; @@ -304,7 +304,23 @@ public virtual async Task> GetCertificatesAsyn { var cert = await CertClient.GetCertificateAsync(certificate.Name); logger.LogTrace($"got certificate details"); + logger.LogTrace($"cert properties: {JsonConvert.SerializeObject(cert.Value?.Properties)}"); + var itemEntryParams = new Dictionary(); + if (cert.Value?.Properties?.Tags != null && cert.Value.Properties.Tags.Count > 0) { // set tags entry parameter to value + itemEntryParams.Add(EntryParameters.TAGS, JsonConvert.SerializeObject(cert.Value.Properties.Tags)); + } + + if (cert.Value.Policy != null) // set nonexportable entry parameter to value + { + var exportable = cert.Value.Policy?.Exportable; + itemEntryParams.Add(EntryParameters.NON_EXPORTABLE, !exportable); + } + + itemEntryParams.Add(EntryParameters.PRESERVE_TAGS, null); // we can never know this; it's only evaluated on enrollment; set to null + + logger.LogTrace($"evaluated entry parameters to be returned: {JsonConvert.SerializeObject(itemEntryParams)}"); + inventoryItems.Add(new CurrentInventoryItem() { Alias = cert.Value.Name, @@ -312,7 +328,7 @@ public virtual async Task> GetCertificatesAsyn ItemStatus = OrchestratorInventoryItemStatus.Unknown, UseChainLevel = true, Certificates = new List() { Convert.ToBase64String(cert.Value.Cer) }, - Parameters = cert.Value.Properties.Tags as Dictionary + Parameters = itemEntryParams }); } catch (Exception ex) diff --git a/AzureKeyVault/Jobs/Management.cs b/AzureKeyVault/Jobs/Management.cs index 3884503..ed0d055 100644 --- a/AzureKeyVault/Jobs/Management.cs +++ b/AzureKeyVault/Jobs/Management.cs @@ -50,7 +50,7 @@ public JobResult ProcessJob(ManagementJobConfiguration config) logger.LogTrace("parsing entry parameters.. "); tagsJSON = config.JobProperties[EntryParameters.TAGS] as string ?? string.Empty; - preserveTags = config.JobProperties[EntryParameters.PRESERVE_TAGS] as bool? ?? false; + preserveTags = config.JobProperties[EntryParameters.PRESERVE_TAGS] as bool? ?? true; nonExportable = config.JobProperties[EntryParameters.NON_EXPORTABLE] as bool? ?? false; switch (config.OperationType) diff --git a/CHANGELOG.md b/CHANGELOG.md index 520816e..d04cbc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,6 @@ - +- 3.2.2 + - Updated screenshots in README + - Returning entry parameters along with inventory - 3.2.1 - Documentation updates and improvements - Updated NuGet packages diff --git a/docsource/images/AKV-entry-parameters-store-type-dialog.png b/docsource/images/AKV-entry-parameters-store-type-dialog.png index 25d2c06..4051ad6 100644 Binary files a/docsource/images/AKV-entry-parameters-store-type-dialog.png and b/docsource/images/AKV-entry-parameters-store-type-dialog.png differ diff --git a/scripts/store_types/bash/curl_create_store_types.sh b/scripts/store_types/bash/curl_create_store_types.sh new file mode 100755 index 0000000..a743ab3 --- /dev/null +++ b/scripts/store_types/bash/curl_create_store_types.sh @@ -0,0 +1,185 @@ +#!/usr/bin/env bash + +# Creates all 1 store types via the Keyfactor Command REST API using curl. +# +# Authentication (first matching method is used): +# OAuth access token: KEYFACTOR_AUTH_ACCESS_TOKEN +# OAuth client creds: KEYFACTOR_AUTH_CLIENT_ID + KEYFACTOR_AUTH_CLIENT_SECRET +# + KEYFACTOR_AUTH_TOKEN_URL +# Basic auth (AD): KEYFACTOR_USERNAME + KEYFACTOR_PASSWORD + KEYFACTOR_DOMAIN +# +# Always required: +# KEYFACTOR_HOSTNAME Command hostname (e.g. my-command.example.com) +# +# Auto-generated by doctool generate-store-type-scripts — do not edit by hand. + +if [ -z "${KEYFACTOR_HOSTNAME}" ]; then + echo "ERROR: KEYFACTOR_HOSTNAME is required" + exit 1 +fi + +BASE_URL="https://${KEYFACTOR_HOSTNAME}/keyfactorapi" + +# --------------------------------------------------------------------------- +# Resolve auth +# --------------------------------------------------------------------------- +if [ -n "${KEYFACTOR_AUTH_ACCESS_TOKEN}" ]; then + BEARER_TOKEN="${KEYFACTOR_AUTH_ACCESS_TOKEN}" +elif [ -n "${KEYFACTOR_AUTH_CLIENT_ID}" ] && [ -n "${KEYFACTOR_AUTH_CLIENT_SECRET}" ] && [ -n "${KEYFACTOR_AUTH_TOKEN_URL}" ]; then + echo "Fetching OAuth token..." + BEARER_TOKEN=$(curl -s -X POST "${KEYFACTOR_AUTH_TOKEN_URL}" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + --data-urlencode "grant_type=client_credentials" \ + --data-urlencode "client_id=${KEYFACTOR_AUTH_CLIENT_ID}" \ + --data-urlencode "client_secret=${KEYFACTOR_AUTH_CLIENT_SECRET}" | jq -r '.access_token') + if [ -z "${BEARER_TOKEN}" ] || [ "${BEARER_TOKEN}" = "null" ]; then + echo "ERROR: Failed to fetch OAuth token from ${KEYFACTOR_AUTH_TOKEN_URL}" + exit 1 + fi +elif [ -n "${KEYFACTOR_USERNAME}" ] && [ -n "${KEYFACTOR_PASSWORD}" ] && [ -n "${KEYFACTOR_DOMAIN}" ]; then + BEARER_TOKEN="" +else + echo "ERROR: Authentication required. Set one of:" + echo " KEYFACTOR_AUTH_ACCESS_TOKEN" + echo " KEYFACTOR_AUTH_CLIENT_ID + KEYFACTOR_AUTH_CLIENT_SECRET + KEYFACTOR_AUTH_TOKEN_URL" + echo " KEYFACTOR_USERNAME + KEYFACTOR_PASSWORD + KEYFACTOR_DOMAIN" + exit 1 +fi + +if [ -n "${BEARER_TOKEN}" ]; then + CURL_AUTH=("-H" "Authorization: Bearer ${BEARER_TOKEN}") +else + CURL_AUTH=("-u" "${KEYFACTOR_USERNAME}@${KEYFACTOR_DOMAIN}:${KEYFACTOR_PASSWORD}") +fi + +create_store_type() { + local name="$1" + local body="$2" + echo "Creating ${name} store type..." + response=$(curl -s -o /dev/null -w "%{http_code}" \ + -X POST "${BASE_URL}/certificatestoretypes" \ + -H "Content-Type: application/json" \ + -H "x-keyfactor-requested-with: APIClient" \ + "${CURL_AUTH[@]}" \ + -d "${body}") + if [ "$response" = "200" ] || [ "$response" = "201" ]; then + echo " OK (HTTP ${response})" + else + echo " FAILED (HTTP ${response})" + fi +} + +# --------------------------------------------------------------------------- +# AKV — The GUID of the tenant ID of the Azure Keyvault instance; for example, '12345678-1234-1234-1234-123456789abc'. +# --------------------------------------------------------------------------- +create_store_type "AKV" '{ + "BlueprintAllowed": false, + "Capability": "AKV", + "CustomAliasAllowed": "Optional", + "EntryParameters": [ + { + "Name": "CertificateTags", + "DisplayName": "Certificate Tags", + "Description": "If desired, tags can be applied to the KeyVault entries. Provide them as a JSON string of key-value pairs ie: '{'tag-name': 'tag-content', 'other-tag-name': 'other-tag-content'}'", + "Type": "string", + "DefaultValue": "", + "RequiredWhen": { + "HasPrivateKey": false, + "OnAdd": false, + "OnRemove": false, + "OnReenrollment": false + } + }, + { + "Name": "PreserveExistingTags", + "DisplayName": "Preserve Existing Tags", + "Description": "If true, this will perform a union of any tags provided with enrollment with the tags on the existing cert with the same alias and apply the result to the new certificate.", + "Type": "Bool", + "DefaultValue": "False", + "RequiredWhen": { + "HasPrivateKey": false, + "OnAdd": false, + "OnRemove": false, + "OnReenrollment": false + } + }, + { + "Name": "NonExportable", + "DisplayName": "Non Exportable Private Key", + "Description": "If true, this will mark the certificate as having a non-exportable private key when importing into Azure KeyVault", + "Type": "Bool", + "DefaultValue": "False", + "RequiredWhen": { + "HasPrivateKey": false, + "OnAdd": false, + "OnRemove": false, + "OnReenrollment": false + } + } + ], + "JobProperties": [], + "LocalStore": false, + "Name": "Azure Keyvault", + "PasswordOptions": { + "EntrySupported": false, + "StoreRequired": false, + "Style": "Default" + }, + "PowerShell": false, + "PrivateKeyAllowed": "Optional", + "Properties": [ + { + "Name": "TenantId", + "DisplayName": "Tenant Id", + "Type": "String", + "DependsOn": "", + "Required": false + }, + { + "Name": "SkuType", + "DisplayName": "SKU Type", + "Type": "MultipleChoice", + "DependsOn": "", + "DefaultValue": "standard,premium", + "Required": false + }, + { + "Name": "VaultRegion", + "DisplayName": "Vault Region", + "Type": "MultipleChoice", + "DependsOn": "", + "DefaultValue": "eastus,eastus2,westus2,westus3,westus", + "Required": false + }, + { + "Name": "AzureCloud", + "DisplayName": "Azure Cloud", + "Type": "MultipleChoice", + "DependsOn": "", + "DefaultValue": "public,china,government", + "Required": false + }, + { + "Name": "PrivateEndpoint", + "DisplayName": "Private KeyVault Endpoint", + "Type": "String", + "DependsOn": "", + "Required": false + } + ], + "ServerRequired": true, + "ShortName": "AKV", + "StorePathDescription": "A string formatted as '{subscription id}:{resource group name}:{vault name}'; for example, '12345678-1234-1234-1234-123456789abc:myResourceGroup:myVault'.", + "StorePathType": "", + "StorePathValue": "", + "SupportedOperations": { + "Add": true, + "Create": true, + "Discovery": true, + "Enrollment": false, + "Remove": true + } +}' + + +echo "Completed." diff --git a/scripts/store_types/bash/kfutil_create_store_types.sh b/scripts/store_types/bash/kfutil_create_store_types.sh new file mode 100755 index 0000000..51c1748 --- /dev/null +++ b/scripts/store_types/bash/kfutil_create_store_types.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +# Creates all 1 store types using kfutil. +# kfutil reads definitions from the Keyfactor integration catalog. +# +# Auth environment variables (first matching method is used): +# OAuth access token: KEYFACTOR_AUTH_ACCESS_TOKEN +# OAuth client creds: KEYFACTOR_AUTH_CLIENT_ID + KEYFACTOR_AUTH_CLIENT_SECRET +# + KEYFACTOR_AUTH_TOKEN_URL +# Basic auth (AD): KEYFACTOR_HOSTNAME + KEYFACTOR_USERNAME + KEYFACTOR_PASSWORD +# + KEYFACTOR_DOMAIN +# +# Auto-generated by doctool generate-store-type-scripts — do not edit by hand. + +if ! command -v kfutil &> /dev/null; then + echo "kfutil could not be found. Please install kfutil" + echo "See https://github.com/Keyfactor/kfutil#quickstart" + exit 1 +fi + +if [ -z "$KEYFACTOR_HOSTNAME" ]; then + echo "KEYFACTOR_HOSTNAME not set — launching kfutil login" + kfutil login +fi + +kfutil store-types create --name "AKV" + +echo "Done. All store types created." diff --git a/scripts/store_types/powershell/kfutil_create_store_types.ps1 b/scripts/store_types/powershell/kfutil_create_store_types.ps1 new file mode 100644 index 0000000..51a3153 --- /dev/null +++ b/scripts/store_types/powershell/kfutil_create_store_types.ps1 @@ -0,0 +1,29 @@ +# Creates all 1 store types using kfutil. +# kfutil reads definitions from the Keyfactor integration catalog. +# +# Auth environment variables (first matching method is used): +# OAuth access token: KEYFACTOR_AUTH_ACCESS_TOKEN +# OAuth client creds: KEYFACTOR_AUTH_CLIENT_ID + KEYFACTOR_AUTH_CLIENT_SECRET +# + KEYFACTOR_AUTH_TOKEN_URL +# Basic auth (AD): KEYFACTOR_HOSTNAME + KEYFACTOR_USERNAME + KEYFACTOR_PASSWORD +# + KEYFACTOR_DOMAIN +# +# Auto-generated by doctool generate-store-type-scripts — do not edit by hand. + +# Uncomment if kfutil is not in your PATH +# Set-Alias -Name kfutil -Value 'C:\Program Files\Keyfactor\kfutil\kfutil.exe' + +if ($null -eq (Get-Command "kfutil" -ErrorAction SilentlyContinue)) { + Write-Host "kfutil could not be found. Please install kfutil" + Write-Host "See https://github.com/Keyfactor/kfutil#quickstart" + exit 1 +} + +if (-not $env:KEYFACTOR_HOSTNAME) { + Write-Host "KEYFACTOR_HOSTNAME not set — launching kfutil login" + & kfutil login +} + +& kfutil store-types create --name "AKV" + +Write-Host "Done. All store types created." diff --git a/scripts/store_types/powershell/restmethod_create_store_types.ps1 b/scripts/store_types/powershell/restmethod_create_store_types.ps1 new file mode 100644 index 0000000..2eeb0b3 --- /dev/null +++ b/scripts/store_types/powershell/restmethod_create_store_types.ps1 @@ -0,0 +1,179 @@ +# Creates all 1 store types via the Keyfactor Command REST API +# using PowerShell Invoke-RestMethod. +# +# Authentication (first matching method is used): +# OAuth access token: KEYFACTOR_AUTH_ACCESS_TOKEN +# OAuth client creds: KEYFACTOR_AUTH_CLIENT_ID + KEYFACTOR_AUTH_CLIENT_SECRET +# + KEYFACTOR_AUTH_TOKEN_URL +# Basic auth (AD): KEYFACTOR_USERNAME + KEYFACTOR_PASSWORD + KEYFACTOR_DOMAIN +# +# Always required: +# KEYFACTOR_HOSTNAME Command hostname (e.g. my-command.example.com) +# +# Auto-generated by doctool generate-store-type-scripts — do not edit by hand. + +if (-not $env:KEYFACTOR_HOSTNAME) { + Write-Error "KEYFACTOR_HOSTNAME is required" + exit 1 +} + +$uri = "https://$($env:KEYFACTOR_HOSTNAME)/keyfactorapi/certificatestoretypes" +$headers = @{ + 'Content-Type' = "application/json" + 'x-keyfactor-requested-with' = "APIClient" +} + +# --------------------------------------------------------------------------- +# Resolve auth +# --------------------------------------------------------------------------- +if ($env:KEYFACTOR_AUTH_ACCESS_TOKEN) { + $headers['Authorization'] = "Bearer $($env:KEYFACTOR_AUTH_ACCESS_TOKEN)" +} elseif ($env:KEYFACTOR_AUTH_CLIENT_ID -and $env:KEYFACTOR_AUTH_CLIENT_SECRET -and $env:KEYFACTOR_AUTH_TOKEN_URL) { + Write-Host "Fetching OAuth token..." + $tokenBody = @{ + grant_type = 'client_credentials' + client_id = $env:KEYFACTOR_AUTH_CLIENT_ID + client_secret = $env:KEYFACTOR_AUTH_CLIENT_SECRET + } + $tokenResp = Invoke-RestMethod -Method Post -Uri $env:KEYFACTOR_AUTH_TOKEN_URL -Body $tokenBody + $headers['Authorization'] = "Bearer $($tokenResp.access_token)" +} elseif ($env:KEYFACTOR_USERNAME -and $env:KEYFACTOR_PASSWORD -and $env:KEYFACTOR_DOMAIN) { + $cred = [System.Convert]::ToBase64String( + [System.Text.Encoding]::ASCII.GetBytes( + "$($env:KEYFACTOR_USERNAME)@$($env:KEYFACTOR_DOMAIN):$($env:KEYFACTOR_PASSWORD)")) + $headers['Authorization'] = "Basic $cred" +} else { + Write-Error ("Authentication required. Set one of:`n" + + " KEYFACTOR_AUTH_ACCESS_TOKEN`n" + + " KEYFACTOR_AUTH_CLIENT_ID + KEYFACTOR_AUTH_CLIENT_SECRET + KEYFACTOR_AUTH_TOKEN_URL`n" + + " KEYFACTOR_USERNAME + KEYFACTOR_PASSWORD + KEYFACTOR_DOMAIN") + exit 1 +} + +function New-StoreType { + param([string]$Name, [string]$Body) + Write-Host "Creating $Name store type..." + try { + Invoke-RestMethod -Method Post -Uri $uri -Headers $headers -Body $Body -ContentType "application/json" | Out-Null + Write-Host " OK" + } catch { + Write-Warning " FAILED: $($_.Exception.Message)" + } +} + +# --------------------------------------------------------------------------- +# AKV — The GUID of the tenant ID of the Azure Keyvault instance; for example, '12345678-1234-1234-1234-123456789abc'. +# --------------------------------------------------------------------------- +New-StoreType "AKV" @' +{ + "BlueprintAllowed": false, + "Capability": "AKV", + "CustomAliasAllowed": "Optional", + "EntryParameters": [ + { + "Name": "CertificateTags", + "DisplayName": "Certificate Tags", + "Description": "If desired, tags can be applied to the KeyVault entries. Provide them as a JSON string of key-value pairs ie: '{'tag-name': 'tag-content', 'other-tag-name': 'other-tag-content'}'", + "Type": "string", + "DefaultValue": "", + "RequiredWhen": { + "HasPrivateKey": false, + "OnAdd": false, + "OnRemove": false, + "OnReenrollment": false + } + }, + { + "Name": "PreserveExistingTags", + "DisplayName": "Preserve Existing Tags", + "Description": "If true, this will perform a union of any tags provided with enrollment with the tags on the existing cert with the same alias and apply the result to the new certificate.", + "Type": "Bool", + "DefaultValue": "False", + "RequiredWhen": { + "HasPrivateKey": false, + "OnAdd": false, + "OnRemove": false, + "OnReenrollment": false + } + }, + { + "Name": "NonExportable", + "DisplayName": "Non Exportable Private Key", + "Description": "If true, this will mark the certificate as having a non-exportable private key when importing into Azure KeyVault", + "Type": "Bool", + "DefaultValue": "False", + "RequiredWhen": { + "HasPrivateKey": false, + "OnAdd": false, + "OnRemove": false, + "OnReenrollment": false + } + } + ], + "JobProperties": [], + "LocalStore": false, + "Name": "Azure Keyvault", + "PasswordOptions": { + "EntrySupported": false, + "StoreRequired": false, + "Style": "Default" + }, + "PowerShell": false, + "PrivateKeyAllowed": "Optional", + "Properties": [ + { + "Name": "TenantId", + "DisplayName": "Tenant Id", + "Type": "String", + "DependsOn": "", + "Required": false + }, + { + "Name": "SkuType", + "DisplayName": "SKU Type", + "Type": "MultipleChoice", + "DependsOn": "", + "DefaultValue": "standard,premium", + "Required": false + }, + { + "Name": "VaultRegion", + "DisplayName": "Vault Region", + "Type": "MultipleChoice", + "DependsOn": "", + "DefaultValue": "eastus,eastus2,westus2,westus3,westus", + "Required": false + }, + { + "Name": "AzureCloud", + "DisplayName": "Azure Cloud", + "Type": "MultipleChoice", + "DependsOn": "", + "DefaultValue": "public,china,government", + "Required": false + }, + { + "Name": "PrivateEndpoint", + "DisplayName": "Private KeyVault Endpoint", + "Type": "String", + "DependsOn": "", + "Required": false + } + ], + "ServerRequired": true, + "ShortName": "AKV", + "StorePathDescription": "A string formatted as '{subscription id}:{resource group name}:{vault name}'; for example, '12345678-1234-1234-1234-123456789abc:myResourceGroup:myVault'.", + "StorePathType": "", + "StorePathValue": "", + "SupportedOperations": { + "Add": true, + "Create": true, + "Discovery": true, + "Enrollment": false, + "Remove": true + } +} +'@ + + +Write-Host "Completed."