From ecc552a7de9fba1817b080bcb681ae0fbbae0f4d Mon Sep 17 00:00:00 2001 From: Edward Lynes Date: Fri, 14 May 2021 13:31:13 -0400 Subject: [PATCH] Update Aka issuer to use v2 API Signed-off-by: Edward Lynes --- go.mod | 1 + go.sum | 11 + hack/build/repos.bzl | 9 + pkg/issuer/acme/dns/akamai/BUILD.bazel | 14 +- pkg/issuer/acme/dns/akamai/akamai.go | 397 +++++++------- pkg/issuer/acme/dns/akamai/akamai_test.go | 487 +++++++++++++----- pkg/issuer/acme/dns/akamai/edgegridauth.go | 280 ---------- .../acme/dns/akamai/edgegridauth_test.go | 97 ---- 8 files changed, 580 insertions(+), 716 deletions(-) delete mode 100644 pkg/issuer/acme/dns/akamai/edgegridauth.go delete mode 100644 pkg/issuer/acme/dns/akamai/edgegridauth_test.go diff --git a/go.mod b/go.mod index fe44e9463..4dc277a3e 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index a1f852946..96ab355f7 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/hack/build/repos.bzl b/hack/build/repos.bzl index 4c97fb539..3bfdeeb2c 100644 --- a/hack/build/repos.bzl +++ b/hack/build/repos.bzl @@ -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", diff --git a/pkg/issuer/acme/dns/akamai/BUILD.bazel b/pkg/issuer/acme/dns/akamai/BUILD.bazel index 092950672..2a42ce5aa 100644 --- a/pkg/issuer/acme/dns/akamai/BUILD.bazel +++ b/pkg/issuer/acme/dns/akamai/BUILD.bazel @@ -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", ], ) diff --git a/pkg/issuer/acme/dns/akamai/akamai.go b/pkg/issuer/acme/dns/akamai/akamai.go index b061e5fc1..d3bc44311 100644 --- a/pkg/issuer/acme/dns/akamai/akamai.go +++ b/pkg/issuer/acme/dns/akamai/akamai.go @@ -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) } diff --git a/pkg/issuer/acme/dns/akamai/akamai_test.go b/pkg/issuer/acme/dns/akamai/akamai_test.go index bea79cff2..e121095ee 100644 --- a/pkg/issuer/acme/dns/akamai/akamai_test.go +++ b/pkg/issuer/acme/dns/akamai/akamai_test.go @@ -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 } diff --git a/pkg/issuer/acme/dns/akamai/edgegridauth.go b/pkg/issuer/acme/dns/akamai/edgegridauth.go deleted file mode 100644 index 9b0edc265..000000000 --- a/pkg/issuer/acme/dns/akamai/edgegridauth.go +++ /dev/null @@ -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, SHA–256 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 SHA–256 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 SHA–256 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 SHA–256 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 -} diff --git a/pkg/issuer/acme/dns/akamai/edgegridauth_test.go b/pkg/issuer/acme/dns/akamai/edgegridauth_test.go deleted file mode 100644 index 7f49e3209..000000000 --- a/pkg/issuer/acme/dns/akamai/edgegridauth_test.go +++ /dev/null @@ -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) -}