Update Aka issuer to use v2 API

Signed-off-by: Edward Lynes <elynes@akamai.com>
This commit is contained in:
Edward Lynes
2021-05-14 13:31:13 -04:00
parent e941307bdc
commit ecc552a7de
8 changed files with 580 additions and 716 deletions
+1
View File
@@ -15,6 +15,7 @@ require (
github.com/Azure/go-autorest/autorest/to v0.4.0
github.com/Azure/go-autorest/autorest/validation v0.3.0 // indirect
github.com/Venafi/vcert/v4 v4.13.1
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.1.0
github.com/aws/aws-sdk-go v1.34.30
github.com/cloudflare/cloudflare-go v0.13.2
github.com/cpu/goacmedns v0.0.3
+11
View File
@@ -73,6 +73,8 @@ github.com/Venafi/vcert/v4 v4.13.1 h1:T+ZAygGBhncKGUFCtBth2rXrokJkGv8cOeEOgx9XLz
github.com/Venafi/vcert/v4 v4.13.1/go.mod h1:Z3sJFoAurFNXPpoSUSHq46aIeHLiGQEMDhprfxlpofQ=
github.com/agnivade/levenshtein v1.0.1 h1:3oJU7J3FGFmyhn8KHjmVaZCN5hxTr7GxgRue+sxIXdQ=
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.1.0 h1:NNsy0ugDFZbu6S11NlsR4Kmko9hJ00gv9U2JHbwpf3g=
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.1.0/go.mod h1:kX6YddBkXqqywAe8c9LyvgTCyFuZCTMF4cRPQhc3Fy8=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
@@ -379,6 +381,7 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.9.5 h1:UImYN5qQ8tuGpGE16ZmjvcTtTw24zw1QAp/SlnNrZhI=
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
github.com/hashicorp/consul/api v1.1.0 h1:BNQPM9ytxj6jbjjdRPioQ94T6YXriSopn0i8COv6SRA=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1 h1:LnuDWGNsoajlhGyHJvuWW6FVqRl8JOTPqS6CPTsYjhY=
@@ -555,6 +558,7 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223 h1:F9x/1yl3T2
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
@@ -582,6 +586,8 @@ github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQ
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pavel-v-chernykh/keystore-go v2.1.0+incompatible h1:Jd6xfriVlJ6hWPvYOE0Ni0QWcNTLRehfGPFxr3eSL80=
github.com/pavel-v-chernykh/keystore-go v2.1.0+incompatible/go.mod h1:xlUlxe/2ItGlQyMTstqeDv9r3U4obH7xYd26TbDQutY=
github.com/pavel-v-chernykh/keystore-go/v4 v4.1.0 h1:xKxUVGoB9VJU+lgQLPN0KURjw+XCVVSpHfQEeyxk3zo=
@@ -707,6 +713,9 @@ github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k=
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
github.com/vektah/gqlparser v1.1.2 h1:ZsyLGn7/7jDNI+y4SEhI4yAxRChlv15pUHMjijT+e68=
github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1 h1:j2hhcujLRHAg872RWAV5yaUrEjHEObwDv3aImCaNLek=
@@ -966,9 +975,11 @@ gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.52.0 h1:j+Lt/M1oPPejkniCg1TkWE2J3Eh1oZTsHSXzMTzUXn4=
gopkg.in/ini.v1 v1.52.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
+9
View File
@@ -59,6 +59,15 @@ def go_repositories():
version = "v1.0.1",
)
go_repository(
name = "com_github_akamai_AkamaiOPEN-edgegrid-golang",
build_file_generation = "on",
build_file_proto_mode = "disable",
importpath = "github.com/akamai/AkamaiOPEN-edgegrid-golang",
sum = "h1:NNsy0ugDFZbu6S11NlsR4Kmko9hJ00gv9U2JHbwpf3g=",
version = "v1.1.0",
)
go_repository(
name = "com_github_alecthomas_template",
build_file_generation = "on",
+5 -9
View File
@@ -2,16 +2,14 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = [
"akamai.go",
"edgegridauth.go",
],
srcs = ["akamai.go"],
importpath = "github.com/jetstack/cert-manager/pkg/issuer/acme/dns/akamai",
visibility = ["//visibility:public"],
deps = [
"//pkg/issuer/acme/dns/util:go_default_library",
"//pkg/logs:go_default_library",
"//pkg/util:go_default_library",
"@com_github_akamai_AkamaiOPEN-edgegrid-golang//configdns-v2:go_default_library",
"@com_github_akamai_AkamaiOPEN-edgegrid-golang//edgegrid:go_default_library",
"@com_github_go_logr_logr//:go_default_library",
"@com_github_pkg_errors//:go_default_library",
],
@@ -19,13 +17,11 @@ go_library(
go_test(
name = "go_default_test",
srcs = [
"akamai_test.go",
"edgegridauth_test.go",
],
srcs = ["akamai_test.go"],
embed = [":go_default_library"],
deps = [
"//pkg/issuer/acme/dns/util:go_default_library",
"@com_github_akamai_AkamaiOPEN-edgegrid-golang//configdns-v2:go_default_library",
"@com_github_stretchr_testify//assert:go_default_library",
],
)
+195 -202
View File
@@ -15,51 +15,76 @@ limitations under the License.
*/
// Package akamai implements a DNS provider for solving the DNS-01
// challenge using Akamai FastDNS.
// See https://developer.akamai.com/api/luna/config-dns/overview.html
// challenge using Akamai Edge DNS.
// See https://developer.akamai.com/api/cloud_security/edge_dns_zone_management/v2.html
package akamai
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"time"
dns "github.com/akamai/AkamaiOPEN-edgegrid-golang/configdns-v2"
"github.com/akamai/AkamaiOPEN-edgegrid-golang/edgegrid"
"github.com/go-logr/logr"
"github.com/pkg/errors"
"github.com/jetstack/cert-manager/pkg/issuer/acme/dns/util"
logf "github.com/jetstack/cert-manager/pkg/logs"
pkgutil "github.com/jetstack/cert-manager/pkg/util"
)
// Interface defined to enable mocking and required functions
type OpenEdgegridDNSService interface {
GetRecord(zone string, name string, record_type string) (*dns.RecordBody, error)
RecordSave(rec *dns.RecordBody, zone string) error
RecordUpdate(rec *dns.RecordBody, zone string) error
RecordDelete(rec *dns.RecordBody, zone string) error
}
type OpenDNSConfig struct {
config edgegrid.Config
}
// DNSProvider is an implementation of the acme.ChallengeProvider interface
type DNSProvider struct {
dns01Nameservers []string
// serviceConsumerDomain as issued by Akamai Luna Control Center.
// The ServiceConsumerDomain is the base URL.
serviceConsumerDomain string
auth *EdgeGridAuth
transport http.RoundTripper
dns01Nameservers []string
serviceConsumerDomain string
dnsclient OpenEdgegridDNSService
TTL int
findHostedDomainByFqdn func(string, []string) (string, error)
isNotFound func(error) bool
log logr.Logger
}
// NewDNSProvider returns a DNSProvider instance configured for Akamai.
func NewDNSProvider(serviceConsumerDomain, clientToken, clientSecret, accessToken string, dns01Nameservers []string) (*DNSProvider, error) {
return &DNSProvider{
dns01Nameservers,
serviceConsumerDomain,
NewEdgeGridAuth(clientToken, clientSecret, accessToken),
http.DefaultTransport,
findHostedDomainByFqdn,
logf.Log.WithName("akamai-dns"),
}, nil
// required Aka OpenEdgegrid creds + non empty dnsservers list
if serviceConsumerDomain == "" || clientToken == "" || clientSecret == "" || accessToken == "" || len(dns01Nameservers) < 1 {
return nil, fmt.Errorf("edgedns: Provider creation failed. Missing required arguments.")
}
dnsp := &DNSProvider{
dns01Nameservers: dns01Nameservers,
serviceConsumerDomain: serviceConsumerDomain,
dnsclient: &OpenDNSConfig{},
findHostedDomainByFqdn: findHostedDomainByFqdn,
isNotFound: isNotFound,
log: logf.Log.WithName("akamai-dns"),
TTL: 300,
}
dnsp.dnsclient.(*OpenDNSConfig).config = edgegrid.Config{
Host: serviceConsumerDomain,
ClientToken: clientToken,
ClientSecret: clientSecret,
AccessToken: accessToken,
MaxBody: 131072,
}
dns.Init(dnsp.dnsclient.(*OpenDNSConfig).config)
return dnsp, nil
}
func findHostedDomainByFqdn(fqdn string, ns []string) (string, error) {
@@ -71,218 +96,186 @@ func findHostedDomainByFqdn(fqdn string, ns []string) (string, error) {
return util.UnFqdn(zone), nil
}
// Present creates a TXT record to fulfil the dns-01 challenge
// Present creates/updates a TXT record to fulfill the dns-01 challenge.
func (a *DNSProvider) Present(domain, fqdn, value string) error {
return a.setTxtRecord(fqdn, &dns01Record{value, 60})
}
// CleanUp removes the TXT record matching the specified parameters
func (a *DNSProvider) CleanUp(domain, fqdn, value string) error {
return a.setTxtRecord(fqdn, nil)
}
logf.V(logf.DebugLevel).Infof("entering Present. domain: %s, fqdn: %s, value: %s", domain, fqdn, value)
type dns01Record struct {
value string
ttl int
}
func (a *DNSProvider) setTxtRecord(fqdn string, dns01Record *dns01Record) error {
hostedDomain, err := a.findHostedDomainByFqdn(fqdn, a.dns01Nameservers)
if err != nil {
return errors.Wrapf(err, "failed to determine hosted domain for %q", fqdn)
}
zoneData, err := a.loadZoneData(hostedDomain)
if err != nil {
return errors.Wrapf(err, "failed to load zone data for %q", hostedDomain)
return errors.Wrapf(err, "edgedns: failed to determine hosted domain for %q", fqdn)
}
hostedDomain = util.UnFqdn(hostedDomain)
logf.V(logf.DebugLevel).Infof("hostedDomain: %s", hostedDomain)
recordName, err := makeTxtRecordName(fqdn, hostedDomain)
if err != nil {
return errors.Wrapf(err, "failed to create TXT record name")
return errors.Wrapf(err, "edgedns: failed to create TXT record name")
}
logf.V(logf.DebugLevel).Infof("recordName: %s", recordName)
record, err := a.dnsclient.GetRecord(hostedDomain, recordName, "TXT")
if err != nil && !a.isNotFound(err) {
return errors.Wrapf(err, "edgedns: failed to retrieve TXT record")
}
if updated, err := zoneData.setTxtRecord(recordName, dns01Record); !updated || err != nil {
if err != nil {
return errors.Wrapf(err, "failed to set TXT record in %q", hostedDomain)
if err == nil && record == nil {
return fmt.Errorf("edgedns: unknown error")
}
if record != nil {
logf.V(logf.InfoLevel).Infof("edgedns: TXT record already exists. Updating target")
if containsValue(record.Target, value) {
// have a record and have entry already
return nil
}
return errors.Errorf("no %q TXT record found in %q", recordName, hostedDomain)
record.Target = append(record.Target, `"`+value+`"`)
record.TTL = a.TTL
err = a.dnsclient.RecordUpdate(record, hostedDomain)
if err != nil {
return errors.Wrapf(err, "edgedns: failed to update TXT record")
}
return nil
}
newSerial, err := zoneData.incSoaSerial()
record = &dns.RecordBody{
Name: recordName,
RecordType: "TXT",
TTL: a.TTL,
Target: []string{`"` + value + `"`},
}
err = a.dnsclient.RecordSave(record, hostedDomain)
if err != nil {
return errors.Wrapf(err, "failed to increment SOA serial for %q", hostedDomain)
return errors.Wrapf(err, "edgedns: failed to create TXT record")
}
if err := a.saveZoneData(hostedDomain, zoneData); err != nil {
return errors.Wrapf(err, "failed to save zone data for %q", hostedDomain)
}
logf.V(logf.DebugLevel).Infof("Updated Akamai TXT record for %q on %q using SOA serial of %d", recordName, hostedDomain, newSerial)
return nil
}
// CleanUp removes/updates the TXT record matching the specified parameters.
func (a *DNSProvider) CleanUp(domain, fqdn, value string) error {
logf.V(logf.DebugLevel).Infof("entering CleanUp. domain: %s, fqdn: %s, value: %s", domain, fqdn, value)
hostedDomain, err := a.findHostedDomainByFqdn(fqdn, a.dns01Nameservers)
if err != nil {
return errors.Wrapf(err, "edgedns: failed to determine hosted domain for %q", fqdn)
}
hostedDomain = util.UnFqdn(hostedDomain)
logf.V(logf.DebugLevel).Infof("hostedDomain: %s", hostedDomain)
recordName, err := makeTxtRecordName(fqdn, hostedDomain)
if err != nil {
return errors.Wrapf(err, "edgedns: failed to create TXT record name")
}
logf.V(logf.DebugLevel).Infof("recordName: %s", recordName)
existingRec, err := a.dnsclient.GetRecord(hostedDomain, recordName, "TXT")
if err != nil {
if a.isNotFound(err) {
return nil
}
return errors.Wrapf(err, "edgedns: failed to retrieve TXT record")
}
if existingRec == nil {
return fmt.Errorf("edgedns: unknown failure")
}
if len(existingRec.Target) == 0 {
return fmt.Errorf("edgedns: TXT record is invalid")
}
if !containsValue(existingRec.Target, value) {
return nil
}
var newRData []string
for _, val := range existingRec.Target {
tval := strings.Trim(val, `"`)
if tval == value {
continue
}
newRData = append(newRData, val)
}
if len(newRData) > 0 {
existingRec.Target = newRData
logf.V(logf.DebugLevel).Infof("updating Akamai TXT record: %s, data: %s", existingRec.Name, newRData)
err = a.dnsclient.RecordUpdate(existingRec, hostedDomain)
if err != nil {
return errors.Wrapf(err, "edgedns: TXT record update failed")
}
return nil
}
logf.V(logf.DebugLevel).Infof("deleting Akamai TXT record %s", existingRec.Name)
err = a.dnsclient.RecordDelete(existingRec, hostedDomain)
if err != nil {
return errors.Wrapf(err, "edgedns: TXT record delete failed")
}
return nil
}
func containsValue(values []string, value string) bool {
for _, val := range values {
if strings.Trim(val, `"`) == value {
return true
}
}
return false
}
func isNotFound(err error) bool {
if err == nil {
return false
}
_, ok := err.(*dns.RecordError)
if ok {
return true
}
return false
}
func makeTxtRecordName(fqdn, hostedDomain string) (string, error) {
if !strings.HasSuffix(fqdn, "."+hostedDomain+".") {
recName := util.UnFqdn(fqdn)
if !strings.HasSuffix(recName, hostedDomain) {
return "", errors.Errorf("fqdn %q is not part of %q", fqdn, hostedDomain)
}
return fqdn[0 : len(fqdn)-len(hostedDomain)-2], nil
return recName, nil
}
func (a *DNSProvider) urlForDomain(domain string) string {
return fmt.Sprintf("https://%s/config-dns/v1/zones/%s", a.serviceConsumerDomain, domain)
func (o OpenDNSConfig) GetRecord(zone string, name string, record_type string) (*dns.RecordBody, error) {
dns.Config = o.config
return dns.GetRecord(zone, name, record_type)
}
func (a *DNSProvider) loadZoneData(domain string) (zoneData, error) {
url := a.urlForDomain(domain)
req, err := http.NewRequest(http.MethodGet, url, http.NoBody)
if err != nil {
return nil, errors.Wrap(err, "failed to create HTTP request")
}
func (o OpenDNSConfig) RecordSave(rec *dns.RecordBody, zone string) error {
responsePayload, err := a.makeRequest(req)
if err != nil {
return nil, err
}
var zoneData map[string]interface{}
err = json.NewDecoder(bytes.NewReader(responsePayload)).Decode(&zoneData)
if err != nil {
return nil, errors.Wrap(err, "failed to decode Akamai OPEN API response")
}
return zoneData, nil
return rec.Save(zone)
}
func (a *DNSProvider) saveZoneData(domain string, data zoneData) error {
body, err := json.Marshal(data)
if err != nil {
return errors.Wrap(err, "failed to encode zone data")
}
func (o OpenDNSConfig) RecordUpdate(rec *dns.RecordBody, zone string) error {
url := a.urlForDomain(domain)
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
if err != nil {
return errors.Wrap(err, "failed to create HTTP request")
}
req.Header.Set("Content-Type", "application/json")
if _, err := a.makeRequest(req); err != nil {
return err
}
return nil
return rec.Update(zone)
}
func (a *DNSProvider) makeRequest(req *http.Request) ([]byte, error) {
req.Header.Set("User-Agent", pkgutil.CertManagerUserAgent)
func (o OpenDNSConfig) RecordDelete(rec *dns.RecordBody, zone string) error {
if err := a.auth.SignRequest(req); err != nil {
return nil, errors.Wrap(err, "failed to sign HTTP request")
}
client := http.Client{
Transport: a.transport,
Timeout: 30 * time.Second,
}
resp, err := client.Do(req)
if err != nil {
return nil, errors.Wrap(err, "error querying Akamai OPEN API")
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNoContent {
return nil, nil
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected response from Akamai OPEN API: status_code: %d, status: %s", resp.StatusCode, resp.Status)
}
responsePayload, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "failed to read response payload")
}
return responsePayload, nil
}
type zoneData map[string]interface{}
func (z zoneData) setTxtRecord(name string, dns01Record *dns01Record) (bool, error) {
zone, ok := z["zone"].(map[string]interface{})
if !ok {
return false, errors.New("failed to retrieve zone from zone data")
}
var txtRecords []interface{}
if txtNode, ok := zone["txt"]; ok {
if txtRecords, ok = txtNode.([]interface{}); !ok {
return false, errors.New("failed to retrieve TXT records from zone data")
}
}
if dns01Record == nil {
if txtRecords = deleteRecord(txtRecords, name); txtRecords == nil {
return false, nil
}
} else {
txtRecords = updateRecord(txtRecords, name, map[string]interface{}{
"name": name,
"ttl": dns01Record.ttl,
"active": true,
"target": dns01Record.value,
})
}
if len(txtRecords) < 1 {
delete(zone, "txt")
} else {
zone["txt"] = txtRecords
}
return true, nil
}
func (z zoneData) incSoaSerial() (uint64, error) {
soa, ok := z["zone"].(map[string]interface{})["soa"].(map[string]interface{})
if !ok {
return 0, errors.New("failed to retrieve SOA record from zone data")
}
serial, ok := soa["serial"].(float64)
if !ok {
return 0, errors.New("failed to retrieve SOA serial from zone data")
}
newSerial := uint64(serial) + 1
soa["serial"] = newSerial
return newSerial, nil
}
func deleteRecord(records []interface{}, name string) []interface{} {
for pos := range records {
if recordName, ok := records[pos].(map[string]interface{})["name"]; ok && recordName == name {
return append(records[:pos], records[pos+1:]...)
}
}
return nil
}
func updateRecord(records []interface{}, name string, record map[string]interface{}) []interface{} {
for pos := range records {
if records[pos].(map[string]interface{})["name"] == name {
records[pos] = record
return records
}
}
return append(records, record)
return rec.Delete(zone)
}
+359 -128
View File
@@ -17,164 +17,395 @@ limitations under the License.
package akamai
import (
"bytes"
"encoding/json"
"io/ioutil"
"net/http"
"strings"
"testing"
"fmt"
"reflect"
dns "github.com/akamai/AkamaiOPEN-edgegrid-golang/configdns-v2"
"github.com/jetstack/cert-manager/pkg/issuer/acme/dns/util"
"github.com/stretchr/testify/assert"
)
const sampleZoneData = `{
"token": "a184671d5307a388180fbf7f11dbdf46",
"zone": {
"name": "example.com",
"soa": {
"contact": "hostmaster.akamai.com.",
"expire": 604800,
"minimum": 180,
"originserver": "use4.akamai.com.",
"refresh": 900,
"retry": 300,
"serial": 1271354824,
"ttl": 900
},
"ns": [
{
"active": true,
"name": "",
"target": "use4.akam.net.",
"ttl": 3600
},
{
"active": true,
"name": "",
"target": "use3.akam.net.",
"ttl": 3600
}
]
}
}`
func testRecordBodyData() *dns.RecordBody {
const sampleZoneDataWithTxt = `{
"token": "a184671d5307a388180fbf7f11dbdf46",
"zone": {
"name": "example.com",
"soa": {
"contact": "hostmaster.akamai.com.",
"expire": 604800,
"minimum": 180,
"originserver": "use4.akamai.com.",
"refresh": 900,
"retry": 300,
"serial": 1271354825,
"ttl": 900
},
"ns": [
{
"active": true,
"name": "",
"target": "use4.akam.net.",
"ttl": 3600
},
{
"active": true,
"name": "",
"target": "use3.akam.net.",
"ttl": 3600
}
],
"txt": [
{
"active": true,
"name" :"_acme-challenge.test",
"target": "dns01-key",
"ttl": 60
}
]
}
}`
type httpResponder func(req *http.Request) (*http.Response, error)
func (r httpResponder) RoundTrip(req *http.Request) (*http.Response, error) {
return r(req)
return &dns.RecordBody{
Name: "_acme-challenge.test.example.com",
RecordType: "TXT",
Target: []string{`"` + "dns01-key" + `"`},
TTL: 300,
}
}
func TestPresent(t *testing.T) {
func testRecordBodyDataExist() *dns.RecordBody {
return &dns.RecordBody{
Name: "_acme-challenge.test.example.com",
RecordType: "TXT",
Target: []string{`"` + "dns01-key" + `"`, `"` + "dns01-key-stub" + `"`},
TTL: 300,
}
}
// OpenEdggrid DNS Stub
type StubOpenDNSConfig struct {
FuncOutput map[string]interface{}
FuncErrors map[string]error
}
//
func findStubHostedDomainByFqdn(fqdn string, ns []string) (string, error) {
return "test.example.com", nil
}
func stubIsNotFoundTrue(err error) bool {
return true
}
func stubIsNotFoundFalse(err error) bool {
return false
}
// TestNewDNSProvider performs sanity check on provider init
func TestNewDNSProvider(t *testing.T) {
akamai, err := NewDNSProvider("akamai.example.com", "token", "secret", "access-token", util.RecursiveNameservers)
assert.NoError(t, err)
// samplee couple important fields
assert.Equal(t, akamai.serviceConsumerDomain, "akamai.example.com")
assert.Equal(t, fmt.Sprintf("%T", akamai.dnsclient), "*akamai.OpenDNSConfig")
}
// TestPresentBasicFlow tests basic flow, e.g. no record exists.
func TestPresentBasicFlow(t *testing.T) {
akamai, err := NewDNSProvider("akamai.example.com", "token", "secret", "access-token", util.RecursiveNameservers)
assert.NoError(t, err)
var response []byte
mockTransport(t, akamai, "example.com", sampleZoneData, &response)
akamai.findHostedDomainByFqdn = findStubHostedDomainByFqdn
akamai.isNotFound = stubIsNotFoundTrue
akamai.dnsclient = &StubOpenDNSConfig{FuncOutput: map[string]interface{}{}, FuncErrors: map[string]error{}}
akamai.dnsclient.(*StubOpenDNSConfig).FuncOutput["GetRecord"] = nil
akamai.dnsclient.(*StubOpenDNSConfig).FuncOutput["RecordSave"] = testRecordBodyData()
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordUpdate"] = fmt.Errorf("Update not expected")
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordDelete"] = fmt.Errorf("Delete not expected")
assert.NoError(t, akamai.Present("test.example.com", "_acme-challenge.test.example.com.", "dns01-key"))
var expected, actual map[string]interface{}
assert.NoError(t, json.Unmarshal([]byte(sampleZoneDataWithTxt), &expected))
assert.NoError(t, json.Unmarshal(response, &actual))
assert.EqualValues(t, expected, actual)
}
func TestCleanUp(t *testing.T) {
// TestPresentExists tests flow with existing record.
func TestPresentExists(t *testing.T) {
akamai, err := NewDNSProvider("akamai.example.com", "token", "secret", "access-token", util.RecursiveNameservers)
assert.NoError(t, err)
var response []byte
mockTransport(t, akamai, "example.com", sampleZoneDataWithTxt, &response)
akamai.findHostedDomainByFqdn = findStubHostedDomainByFqdn
akamai.isNotFound = stubIsNotFoundFalse // ignored for this flow ...
akamai.dnsclient = &StubOpenDNSConfig{FuncOutput: map[string]interface{}{}, FuncErrors: map[string]error{}}
akamai.dnsclient.(*StubOpenDNSConfig).FuncOutput["GetRecord"] = testRecordBodyData()
akamai.dnsclient.(*StubOpenDNSConfig).FuncOutput["RecordUpdate"] = testRecordBodyDataExist()
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordSave"] = fmt.Errorf("Save not expected")
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordDelete"] = fmt.Errorf("Delete not expected")
assert.NoError(t, akamai.Present("test.example.com", "_acme-challenge.test.example.com.", "dns01-key-stub"))
}
// TestPresentValueExists tests flow with existing record.
func TestPresentValueExists(t *testing.T) {
akamai, err := NewDNSProvider("akamai.example.com", "token", "secret", "access-token", util.RecursiveNameservers)
assert.NoError(t, err)
akamai.findHostedDomainByFqdn = findStubHostedDomainByFqdn
akamai.isNotFound = stubIsNotFoundFalse // ignored for this flow ...
akamai.dnsclient = &StubOpenDNSConfig{FuncOutput: map[string]interface{}{}, FuncErrors: map[string]error{}}
akamai.dnsclient.(*StubOpenDNSConfig).FuncOutput["GetRecord"] = testRecordBodyData()
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordSave"] = fmt.Errorf("Save not expected")
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordUpdate"] = fmt.Errorf("Update not expected")
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordDelete"] = fmt.Errorf("Delete not expected")
assert.NoError(t, akamai.Present("test.example.com", "_acme-challenge.test.example.com.", "dns01-key"))
}
func TestPresentFailGetRecord(t *testing.T) {
akamai, err := NewDNSProvider("akamai.example.com", "token", "secret", "access-token", util.RecursiveNameservers)
assert.NoError(t, err)
akamai.findHostedDomainByFqdn = findStubHostedDomainByFqdn
akamai.isNotFound = stubIsNotFoundFalse
akamai.dnsclient = &StubOpenDNSConfig{FuncOutput: map[string]interface{}{}, FuncErrors: map[string]error{}}
akamai.dnsclient.(*StubOpenDNSConfig).FuncOutput["GetRecord"] = nil
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["GetRecord"] = fmt.Errorf("Failed Get Test")
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordSave"] = fmt.Errorf("Save not expected")
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordUpdate"] = fmt.Errorf("Update not expected")
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordDelete"] = fmt.Errorf("Delete not expected")
assert.Error(t, akamai.Present("test.example.com", "_acme-challenge.test.example.com.", "dns01-key"))
}
func TestPresentFailSaveRecord(t *testing.T) {
akamai, err := NewDNSProvider("akamai.example.com", "token", "secret", "access-token", util.RecursiveNameservers)
assert.NoError(t, err)
akamai.findHostedDomainByFqdn = findStubHostedDomainByFqdn
akamai.isNotFound = stubIsNotFoundTrue
akamai.dnsclient = &StubOpenDNSConfig{FuncOutput: map[string]interface{}{}, FuncErrors: map[string]error{}}
akamai.dnsclient.(*StubOpenDNSConfig).FuncOutput["GetRecord"] = nil
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordSave"] = fmt.Errorf("Save fail")
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordUpdate"] = fmt.Errorf("Update not expected")
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordDelete"] = fmt.Errorf("Delete not expected")
assert.Error(t, akamai.Present("test.example.com", "_acme-challenge.test.example.com.", "dns01-key"))
}
func TestPresentFailUpdateRecord(t *testing.T) {
akamai, err := NewDNSProvider("akamai.example.com", "token", "secret", "access-token", util.RecursiveNameservers)
assert.NoError(t, err)
akamai.findHostedDomainByFqdn = findStubHostedDomainByFqdn
akamai.isNotFound = stubIsNotFoundFalse // ignored for this flow ...
akamai.dnsclient = &StubOpenDNSConfig{FuncOutput: map[string]interface{}{}, FuncErrors: map[string]error{}}
akamai.dnsclient.(*StubOpenDNSConfig).FuncOutput["GetRecord"] = testRecordBodyData()
akamai.dnsclient.(*StubOpenDNSConfig).FuncOutput["RecordUpdate"] = testRecordBodyDataExist()
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordSave"] = fmt.Errorf("Save not expected")
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordUpdate"] = fmt.Errorf("Update failed")
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordDelete"] = fmt.Errorf("Delete not expected")
assert.Error(t, akamai.Present("test.example.com", "_acme-challenge.test.example.com.", "dns01-key-stub"))
}
// TestCleanUpBasicFlow tests flow with existing record.
func TestCleanUpBasicFlow(t *testing.T) {
akamai, err := NewDNSProvider("akamai.example.com", "token", "secret", "access-token", util.RecursiveNameservers)
assert.NoError(t, err)
akamai.findHostedDomainByFqdn = findStubHostedDomainByFqdn
akamai.isNotFound = stubIsNotFoundFalse // ignored for this flow ...
akamai.dnsclient = &StubOpenDNSConfig{FuncOutput: map[string]interface{}{}, FuncErrors: map[string]error{}}
akamai.dnsclient.(*StubOpenDNSConfig).FuncOutput["GetRecord"] = testRecordBodyData()
akamai.dnsclient.(*StubOpenDNSConfig).FuncOutput["RecordDelete"] = testRecordBodyData()
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordSave"] = fmt.Errorf("Save not expected")
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordUpdate"] = fmt.Errorf("Update not expected")
assert.NoError(t, akamai.CleanUp("test.example.com", "_acme-challenge.test.example.com.", "dns01-key"))
var expected, actual map[string]interface{}
assert.NoError(t, json.Unmarshal([]byte(sampleZoneData), &expected))
expected["zone"].(map[string]interface{})["soa"].(map[string]interface{})["serial"] = 1271354826.
assert.NoError(t, json.Unmarshal(response, &actual))
assert.EqualValues(t, expected, actual)
}
func mockTransport(t *testing.T, akamai *DNSProvider, domain, data string, response *[]byte) {
akamai.transport = httpResponder(func(req *http.Request) (*http.Response, error) {
defer req.Body.Close()
// TestPresentExists tests flow with existing record.
func TestCleanUpExists(t *testing.T) {
akamai, err := NewDNSProvider("akamai.example.com", "token", "secret", "access-token", util.RecursiveNameservers)
assert.NoError(t, err)
if req.URL.String() != "https://akamai.example.com/config-dns/v1/zones/"+domain {
return &http.Response{
StatusCode: http.StatusNotFound,
Body: http.NoBody,
}, nil
}
akamai.findHostedDomainByFqdn = findStubHostedDomainByFqdn
akamai.isNotFound = stubIsNotFoundFalse // ignored for this flow ...
akamai.dnsclient = &StubOpenDNSConfig{FuncOutput: map[string]interface{}{}, FuncErrors: map[string]error{}}
akamai.dnsclient.(*StubOpenDNSConfig).FuncOutput["GetRecord"] = testRecordBodyData()
akamai.dnsclient.(*StubOpenDNSConfig).FuncOutput["RecordUpdate"] = testRecordBodyDataExist()
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordSave"] = fmt.Errorf("Save not expected")
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordDelete"] = fmt.Errorf("Delete not expected")
if req.Method == http.MethodGet {
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte(data))),
}, nil
}
assert.NoError(t, akamai.CleanUp("test.example.com", "_acme-challenge.test.example.com.", "dns01-key-stub"))
if req.Method == http.MethodPost {
if req.Header.Get("Content-Type") != "application/json" {
t.Fatalf("unsupported Content Type: %v", req.Header.Get("Content-Type"))
}
}
var err error
*response, err = ioutil.ReadAll(req.Body)
assert.NoError(t, err)
// TestCleanUpExistsNoValue tests flow with existing record.
func TestCleanUpExistsNoValue(t *testing.T) {
akamai, err := NewDNSProvider("akamai.example.com", "token", "secret", "access-token", util.RecursiveNameservers)
assert.NoError(t, err)
return &http.Response{
StatusCode: http.StatusNoContent,
Body: http.NoBody,
}, nil
}
akamai.findHostedDomainByFqdn = findStubHostedDomainByFqdn
akamai.isNotFound = stubIsNotFoundFalse // ignored for this flow ...
akamai.dnsclient = &StubOpenDNSConfig{FuncOutput: map[string]interface{}{}, FuncErrors: map[string]error{}}
akamai.dnsclient.(*StubOpenDNSConfig).FuncOutput["GetRecord"] = testRecordBodyData()
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordSave"] = fmt.Errorf("Save not expected")
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordUpdate"] = fmt.Errorf("Update not expected")
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordDelete"] = fmt.Errorf("Delete not expected")
t.Fatalf("unexpected method: %v", req.Method)
return nil, nil
})
akamai.findHostedDomainByFqdn = func(fqdn string, _ []string) (string, error) {
if !strings.HasSuffix(fqdn, domain+".") {
t.Fatalf("unexpected fqdn: %s", fqdn)
}
return domain, nil
assert.NoError(t, akamai.CleanUp("test.example.com", "_acme-challenge.test.example.com.", "dns01-key-stub"))
}
// TestCleanUpNoRecord tests flow with no existing record.
func TestCleanUpNoRecord(t *testing.T) {
akamai, err := NewDNSProvider("akamai.example.com", "token", "secret", "access-token", util.RecursiveNameservers)
assert.NoError(t, err)
akamai.findHostedDomainByFqdn = findStubHostedDomainByFqdn
akamai.isNotFound = stubIsNotFoundTrue // ignored for this flow ...
akamai.dnsclient = &StubOpenDNSConfig{FuncOutput: map[string]interface{}{}, FuncErrors: map[string]error{}}
akamai.dnsclient.(*StubOpenDNSConfig).FuncOutput["GetRecord"] = nil
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordSave"] = fmt.Errorf("Save not expected")
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordUpdate"] = fmt.Errorf("Update not expected")
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordDelete"] = fmt.Errorf("Delete not expected")
assert.NoError(t, akamai.CleanUp("test.example.com", "_acme-challenge.test.example.com.", "dns01"))
}
func TestCleanUpFailGetRecord(t *testing.T) {
akamai, err := NewDNSProvider("akamai.example.com", "token", "secret", "access-token", util.RecursiveNameservers)
assert.NoError(t, err)
akamai.findHostedDomainByFqdn = findStubHostedDomainByFqdn
akamai.isNotFound = stubIsNotFoundFalse
akamai.dnsclient = &StubOpenDNSConfig{FuncOutput: map[string]interface{}{}, FuncErrors: map[string]error{}}
akamai.dnsclient.(*StubOpenDNSConfig).FuncOutput["GetRecord"] = nil
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["GetRecord"] = fmt.Errorf("Failed Get Record")
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordSave"] = fmt.Errorf("Save not expected")
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordUpdate"] = fmt.Errorf("Update not expected")
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordDelete"] = fmt.Errorf("Delete not expected")
assert.Error(t, akamai.CleanUp("test.example.com", "_acme-challenge.test.example.com.", "dns01-key"))
}
func TestCleanUpFailUpdateRecord(t *testing.T) {
akamai, err := NewDNSProvider("akamai.example.com", "token", "secret", "access-token", util.RecursiveNameservers)
assert.NoError(t, err)
akamai.findHostedDomainByFqdn = findStubHostedDomainByFqdn
akamai.isNotFound = stubIsNotFoundFalse // ignored for this flow ...
akamai.dnsclient = &StubOpenDNSConfig{FuncOutput: map[string]interface{}{}, FuncErrors: map[string]error{}}
akamai.dnsclient.(*StubOpenDNSConfig).FuncOutput["GetRecord"] = testRecordBodyDataExist()
akamai.dnsclient.(*StubOpenDNSConfig).FuncOutput["RecordUpdate"] = testRecordBodyData()
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordSave"] = fmt.Errorf("Save not expected")
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordUpdate"] = fmt.Errorf("Update failed")
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordDelete"] = fmt.Errorf("Delete not expected")
assert.Error(t, akamai.CleanUp("test.example.com", "_acme-challenge.test.example.com.", "dns01-key-stub"))
}
func TestCleanUpFailDeleteRecord(t *testing.T) {
akamai, err := NewDNSProvider("akamai.example.com", "token", "secret", "access-token", util.RecursiveNameservers)
assert.NoError(t, err)
akamai.findHostedDomainByFqdn = findStubHostedDomainByFqdn
akamai.isNotFound = stubIsNotFoundFalse // ignored for this flow ...
akamai.dnsclient = &StubOpenDNSConfig{FuncOutput: map[string]interface{}{}, FuncErrors: map[string]error{}}
akamai.dnsclient.(*StubOpenDNSConfig).FuncOutput["GetRecord"] = testRecordBodyData()
akamai.dnsclient.(*StubOpenDNSConfig).FuncOutput["RecordDelete"] = testRecordBodyData()
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordSave"] = fmt.Errorf("Save not expected")
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordUpdate"] = fmt.Errorf("Update not expected")
akamai.dnsclient.(*StubOpenDNSConfig).FuncErrors["RecordDelete"] = fmt.Errorf("Delete failed")
assert.Error(t, akamai.CleanUp("test.example.com", "_acme-challenge.test.example.com.", "dns01-key"))
}
// Stub Get Record
func (o StubOpenDNSConfig) GetRecord(zone string, name string, record_type string) (*dns.RecordBody, error) {
var rec *dns.RecordBody
err, ok := o.FuncErrors["GetRecord"]
if ok {
return nil, err
}
exp, ok := o.FuncOutput["GetRecord"]
if ok {
if exp == nil {
return nil, fmt.Errorf("GetRecord: Unexpected nil")
}
rec = exp.(*dns.RecordBody)
// comare passed with expected
if name != rec.Name {
return nil, fmt.Errorf("GetRecord: expected/actual Name don't match")
}
if record_type != rec.RecordType {
return nil, fmt.Errorf("GetRecord: expected/actual Record Type don't match")
}
}
return rec, nil
}
func (o StubOpenDNSConfig) RecordSave(rec *dns.RecordBody, zone string) error {
exp, ok := o.FuncOutput["RecordSave"]
if ok {
// comare passed with expected
if rec.Name != exp.(*dns.RecordBody).Name {
return fmt.Errorf("RecordSave: expected/actual Name don't match")
}
if rec.RecordType != exp.(*dns.RecordBody).RecordType {
return fmt.Errorf("RecordSave: expected/actual Record Type don't match")
}
if !reflect.DeepEqual(rec.Target, exp.(*dns.RecordBody).Target) {
return fmt.Errorf("RecordSave: expected/actual Target don't match")
}
if rec.TTL != exp.(*dns.RecordBody).TTL {
return fmt.Errorf("RecordSave: expected/actual TTL don't match")
}
}
err, ok := o.FuncErrors["RecordSave"]
if ok {
return err
}
return nil
}
func (o StubOpenDNSConfig) RecordUpdate(rec *dns.RecordBody, zone string) error {
exp, ok := o.FuncOutput["RecordUpdate"]
if ok {
// comare passed with expected
if rec.Name != exp.(*dns.RecordBody).Name {
return fmt.Errorf("RecordUpdate: expected/actual Name don't match")
}
if rec.RecordType != exp.(*dns.RecordBody).RecordType {
return fmt.Errorf("RecordUpdate: expected/actual Record Type don't match")
}
if !reflect.DeepEqual(rec.Target, exp.(*dns.RecordBody).Target) {
return fmt.Errorf("RecordUpdate: expected/actual Target don't match")
}
if rec.TTL != exp.(*dns.RecordBody).TTL {
return fmt.Errorf("RecordUpdate: expected/actual TTL don't match")
}
}
err, ok := o.FuncErrors["RecordUpdate"]
if ok {
return err
}
return nil
}
func (o StubOpenDNSConfig) RecordDelete(rec *dns.RecordBody, zone string) error {
exp, ok := o.FuncOutput["RecordDelete"]
if ok {
// comare passed with expected
if rec.Name != exp.(*dns.RecordBody).Name {
return fmt.Errorf("RecordDelete: expected/actual Name don't match")
}
if rec.RecordType != exp.(*dns.RecordBody).RecordType {
return fmt.Errorf("RecordDelete: expected/actual Record Type don't match")
}
if !reflect.DeepEqual(rec.Target, exp.(*dns.RecordBody).Target) {
return fmt.Errorf("RecordDelete: expected/actual Target don't match")
}
if rec.TTL != exp.(*dns.RecordBody).TTL {
return fmt.Errorf("RecordDelete: expected/actual TTL don't match")
}
}
err, ok := o.FuncErrors["RecordDelete"]
if ok {
return err
}
return nil
}
-280
View File
@@ -1,280 +0,0 @@
/*
Copyright 2020 The cert-manager Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package akamai
import (
"bytes"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"sort"
"strings"
"time"
"unicode"
)
// EdgeGridAuth holds all values required to perform Akamai API Client Authentication.
// See https://developer.akamai.com/introduction/Client_Auth.html.
type EdgeGridAuth struct {
ClientToken string
ClientSecret string
AccessToken string
HeadersToSign []string
MaxBody int
now func() time.Time
createNonce func() (string, error)
}
type signingData struct {
timestamp string
authHeader string
dataToSign string
}
// edgeGridAuthTimeFormat is used for timestamps in request signatures.
const edgeGridAuthTimeFormat = "20060102T15:04:05-0700" // yyyyMMddTHH:mm:ss+0000
const NoMaxBody = -1
// NewEdgeGridAuth returns a new request signer for Akamai EdgeGrid
func NewEdgeGridAuth(clientToken, clientSecret, accessToken string, headersToSign ...string) *EdgeGridAuth {
return &EdgeGridAuth{
ClientToken: clientToken,
ClientSecret: clientSecret,
AccessToken: accessToken,
HeadersToSign: headersToSign,
MaxBody: NoMaxBody,
now: time.Now,
createNonce: createRandomNonce,
}
}
// SignRequest calculates the signature for Akamai Open API and adds it as the Authorization header.
// The Authorization header starts with the signing algorithm moniker (name of the algorithm) used to sign the request.
// The moniker below identifies EdgeGrid V1, hash message authentication code, SHA256 as the hash standard.
// This moniker is then followed by a space and an ordered list of name value pairs with each field separated by a semicolon.
func (e *EdgeGridAuth) SignRequest(req *http.Request) error {
signingData, err := e.signingData(req)
if err != nil {
return err
}
req.Header.Set("Authorization", fmt.Sprintf(
"%ssignature=%s",
signingData.authHeader,
e.calculateRequestSignature(signingData)))
return nil
}
func (e *EdgeGridAuth) calculateRequestSignature(signingData *signingData) string {
return computeSignature(
signingData.dataToSign,
e.signingKey(signingData.timestamp))
}
func (e *EdgeGridAuth) signingData(req *http.Request) (*signingData, error) {
nonce, err := e.createNonce()
if err != nil {
return nil, err
}
timestamp := e.now().UTC().Format(edgeGridAuthTimeFormat)
authHeader := fmt.Sprintf("EG1-HMAC-SHA256 client_token=%s;access_token=%s;timestamp=%s;nonce=%s;",
e.ClientToken,
e.AccessToken,
timestamp,
nonce)
return &signingData{
timestamp: timestamp,
authHeader: authHeader,
dataToSign: e.dataToSign(req, authHeader),
}, nil
}
// dataToSign includes the information from the HTTP request that is relevant to ensuring that the request is authentic.
// This data set comprised of the request data combined with the authorization header value (excluding the signature field,
// but including the ; right before the signature field).
func (e *EdgeGridAuth) dataToSign(req *http.Request, authHeader string) string {
var buffer bytes.Buffer
buffer.WriteString(req.Method)
buffer.WriteRune('\t')
buffer.WriteString(req.URL.Scheme)
buffer.WriteRune('\t')
buffer.WriteString(req.URL.Host)
buffer.WriteRune('\t')
buffer.WriteString(relativeURL(req.URL))
buffer.WriteRune('\t')
buffer.WriteString(e.canonicalizedHeaders(req))
buffer.WriteRune('\t')
buffer.WriteString(e.computeBodyHash(req))
buffer.WriteRune('\t')
buffer.WriteString(authHeader)
return buffer.String()
}
// signingKey is derived from the client secret.
// The signing key is computed as the base64 encoding of the SHA256 HMAC of the timestamp string
// (the field value included in the HTTP authorization header described above) with the client secret as the key.
func (e *EdgeGridAuth) signingKey(timestamp string) string {
return computeSignature(timestamp, e.ClientSecret)
}
// relativeURL is the part of the URL that starts from the root path and includes the query string, with the handling of following special cases:
// If the path is null or empty, set it to / (forward-slash).
// If the path does not start with /, add / to the beginning.
func relativeURL(url *url.URL) string {
relativeURL := url.Path
if relativeURL == "" {
return "/"
}
if relativeURL[0] != '/' {
relativeURL = "/" + relativeURL
}
if url.RawQuery != "" {
relativeURL += "?"
relativeURL += url.RawQuery
}
return relativeURL
}
// computeBodyHash returns the base64-encoded SHA256 hash of the POST body.
// For any other request methods, this field is empty. But the tac separator (\t) must be included.
// The size of the POST body must be less than or equal to the value specified by the service.
// Any request that does not meet this criteria SHOULD be rejected during the signing process,
// as the request will be rejected by EdgeGrid.
func (e *EdgeGridAuth) computeBodyHash(req *http.Request) string {
if req.Body != nil {
bodyBytes, _ := ioutil.ReadAll(req.Body)
req.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
if req.Method == http.MethodPost && len(bodyBytes) > 0 {
dataToHash := bodyBytes
if e.MaxBody != NoMaxBody && len(dataToHash) > e.MaxBody {
dataToHash = dataToHash[0:e.MaxBody]
}
sha256Sum := sha256.Sum256(dataToHash)
return base64.StdEncoding.EncodeToString(sha256Sum[:])
}
}
return ""
}
// canonicalizedHeaders returns the request headers as a canonicalized string.
//
// The protocol does not support multiple request headers with the same header name.
// Such requests SHOULD be rejected during the signing process. Otherwise, EdgeGrid
// will not produce the intended results by rejecting such requests or removing all
// (but one) duplicated headers.
//
// Header names are case-insensitive per rfc2616.
//
// For each entry in the list of headers designated by the service provider to include
// in the signature in the specified order, the canonicalization of the request header
// is done as follows:
//
// Get the first header value for the name.
// Trim the leading and trailing white spaces.
// Replace all repeated white spaces with a single space.
// Concatenate the name:value pairs with the tab (\t) separator (name field is all in lower case).
// Terminate the headers with another tab (\t) separator.
//
// NOTE: The canonicalized data is used for creating the signature only, as this step
// might alter the header value. If a header in the list is not present in the request,
// or the header value is empty, nothing for that header, neither the name nor the tab
// separator, may be included.
func (e *EdgeGridAuth) canonicalizedHeaders(req *http.Request) string {
if len(e.HeadersToSign) < 1 {
return ""
}
var headerNamesToSign []string
for headerName := range req.Header {
for _, sign := range e.HeadersToSign {
if strings.EqualFold(sign, headerName) {
headerNamesToSign = append(headerNamesToSign, headerName)
break
}
}
}
if len(headerNamesToSign) < 1 {
return ""
}
sort.Strings(headerNamesToSign)
var buffer bytes.Buffer
for _, headerName := range headerNamesToSign {
for _, c := range headerName {
buffer.WriteRune(unicode.ToLower(c))
}
buffer.WriteRune(':')
white := false
empty := true
for _, c := range req.Header.Get(headerName) {
if unicode.IsSpace(c) {
white = true
} else {
if white && !empty {
buffer.WriteRune(' ')
}
buffer.WriteRune(unicode.ToLower(c))
empty = false
white = false
}
}
buffer.WriteRune('\t')
}
return buffer.String()
}
// calculateSignature is the base64-encoding of the SHA256 HMAC of the data to sign with the signing key.
func computeSignature(message string, secret string) string {
key := []byte(secret)
h := hmac.New(sha256.New, key)
h.Write([]byte(message))
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}
func createRandomNonce() (string, error) {
bytes := make([]byte, 18)
_, err := rand.Read(bytes)
if err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(bytes), nil
}
@@ -1,97 +0,0 @@
/*
Copyright 2020 The cert-manager Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package akamai
import (
"net/http"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestDataToSign(t *testing.T) {
req, err := http.NewRequest(
http.MethodGet,
"https://akaa-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net/diagnostic-tools/v1/locations",
http.NoBody)
assert.NoError(t, err)
auth := NewEdgeGridAuth("ClientToken", "ClientSecret", "AccessToken")
auth.now = func() time.Time {
return time.Unix(1396461906, 0) // 20140402T18:05:06Z
}
auth.createNonce = func() (string, error) {
return "185f94eb-537c-4c01-b8cc-2fa5a06aee7f", nil
}
data, err := auth.signingData(req)
assert.NoError(t, err)
expected := "GET" +
"\thttps" +
"\takaa-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna.akamaiapis.net" +
"\t/diagnostic-tools/v1/locations" +
"\t" + // headers
"\t" + // content hash
"\tEG1-HMAC-SHA256 " +
"client_token=ClientToken;" +
"access_token=AccessToken;" +
"timestamp=20140402T18:05:06+0000;" +
"nonce=185f94eb-537c-4c01-b8cc-2fa5a06aee7f;"
assert.EqualValues(t, expected, data.dataToSign)
}
func TestDataToSignWithHeaders(t *testing.T) {
req, err := http.NewRequest(
http.MethodGet,
"http://akaa-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna-dev.akamaiapis.net/sample-api/v1/property/?fields=x&format=json&cpcode=1234",
http.NoBody)
assert.NoError(t, err)
req.Header.Set("x-a", "va")
req.Header.Set("x-c", "\" xc \"")
req.Header.Set("x-b", " w b")
auth := NewEdgeGridAuth(
"ClientToken", "ClientSecret", "AccessToken",
"x-c", "x-b", "x-a")
auth.now = func() time.Time {
return time.Unix(1376917283, 0) // 20130819T13:01:23Z
}
auth.createNonce = func() (string, error) {
return "ac392096-8aa1-44fd-8c3b-f797d35a6736", nil
}
data, err := auth.signingData(req)
assert.NoError(t, err)
expected := "GET" +
"\thttp" +
"\takaa-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx.luna-dev.akamaiapis.net" +
"\t/sample-api/v1/property/?fields=x&format=json&cpcode=1234" +
"\tx-a:va\tx-b:w b\tx-c:\" xc \"\t" +
"\t" + // content hash
"\tEG1-HMAC-SHA256 " +
"client_token=ClientToken;" +
"access_token=AccessToken;" +
"timestamp=20130819T13:01:23+0000;" +
"nonce=ac392096-8aa1-44fd-8c3b-f797d35a6736;"
assert.EqualValues(t, expected, data.dataToSign)
}