From 2d2fe1b13518b3f019680df8678ead69067d025b Mon Sep 17 00:00:00 2001 From: Artem Lifshits Date: Wed, 15 Jan 2025 15:11:08 +0100 Subject: [PATCH 1/2] apigw certs --- acceptance/openstack/apigw/v2/cert_test.go | 153 ++++++++++++++++++ acceptance/openstack/common.go | 55 +++++++ openstack/apigw/v2/cert/Bind.go | 35 ++++ openstack/apigw/v2/cert/BindCertToDomain.go | 36 +++++ openstack/apigw/v2/cert/Create.go | 68 ++++++++ openstack/apigw/v2/cert/Delete.go | 8 + openstack/apigw/v2/cert/Get.go | 18 +++ openstack/apigw/v2/cert/List.go | 80 +++++++++ openstack/apigw/v2/cert/Unbind.go | 21 +++ .../apigw/v2/cert/UnbindCertFromDomain.go | 19 +++ openstack/apigw/v2/cert/Update.go | 43 +++++ 11 files changed, 536 insertions(+) create mode 100644 acceptance/openstack/apigw/v2/cert_test.go create mode 100644 openstack/apigw/v2/cert/Bind.go create mode 100644 openstack/apigw/v2/cert/BindCertToDomain.go create mode 100644 openstack/apigw/v2/cert/Create.go create mode 100644 openstack/apigw/v2/cert/Delete.go create mode 100644 openstack/apigw/v2/cert/Get.go create mode 100644 openstack/apigw/v2/cert/List.go create mode 100644 openstack/apigw/v2/cert/Unbind.go create mode 100644 openstack/apigw/v2/cert/UnbindCertFromDomain.go create mode 100644 openstack/apigw/v2/cert/Update.go diff --git a/acceptance/openstack/apigw/v2/cert_test.go b/acceptance/openstack/apigw/v2/cert_test.go new file mode 100644 index 000000000..3855fc43b --- /dev/null +++ b/acceptance/openstack/apigw/v2/cert_test.go @@ -0,0 +1,153 @@ +package v2 + +import ( + "os" + "testing" + + golangsdk "github.com/opentelekomcloud/gophertelekomcloud" + "github.com/opentelekomcloud/gophertelekomcloud/acceptance/clients" + "github.com/opentelekomcloud/gophertelekomcloud/acceptance/openstack" + "github.com/opentelekomcloud/gophertelekomcloud/acceptance/tools" + "github.com/opentelekomcloud/gophertelekomcloud/openstack/apigw/v2/cert" + "github.com/opentelekomcloud/gophertelekomcloud/openstack/apigw/v2/domain" + "github.com/opentelekomcloud/gophertelekomcloud/openstack/apigw/v2/group" + "github.com/opentelekomcloud/gophertelekomcloud/openstack/dns/v2/zones" + th "github.com/opentelekomcloud/gophertelekomcloud/testhelper" +) + +func TestCertificateLifecycle(t *testing.T) { + gatewayID := os.Getenv("GATEWAY_ID") + if gatewayID == "" { + t.Skip("`GATEWAY_ID` needs to be defined") + } + + client, err := clients.NewAPIGWClient() + th.AssertNoErr(t, err) + + t.Logf("Attempting to create API Gateway Group") + grp := CreateGroup(client, t, gatewayID) + t.Cleanup(func() { + t.Logf("Attempting to delete API Gateway Group") + th.AssertNoErr(t, group.Delete(client, gatewayID, grp.ID)) + }) + + t.Logf("Attempting to create Public DNS zone with A record") + clientNetwork, err := clients.NewDNSV2Client() + th.AssertNoErr(t, err) + rs := CreateDns(clientNetwork, t) + t.Cleanup(func() { + t.Logf("Attempting to delete Public DNS zone") + _, err := zones.Delete(clientNetwork, rs.ZoneID).Extract() + th.AssertNoErr(t, err) + }) + + createOpts := domain.CreateOpts{ + GatewayID: gatewayID, + GroupID: grp.ID, + UrlDomain: rs.Name, + } + t.Logf("Attempting to create API Gateway Domain") + dom, err := domain.Create(client, createOpts) + th.AssertNoErr(t, err) + t.Cleanup(func() { + t.Logf("Attempting to delete API Gateway Domain") + th.AssertNoErr(t, domain.Delete(client, domain.DeleteOpts{ + GatewayID: gatewayID, + GroupID: grp.ID, + DomainID: dom.ID, + })) + }) + + createResp := CreateTestCertificate(client, t, gatewayID, dom.UrlDomain) + + t.Cleanup(func() { + t.Logf("Attempting to delete certificate: %s", createResp.ID) + th.AssertNoErr(t, cert.Delete(client, createResp.ID)) + }) + + newCert, newPk, err := openstack.GenerateTestCertKeyPair(dom.UrlDomain) + + updateOpts := cert.UpdateOpts{ + Name: createResp.Name + "_updated", + CertContent: newCert, + PrivateKey: newPk, + Type: "instance", + InstanceID: gatewayID, + } + + t.Logf("Attempting to update certificate: %s", createResp.ID) + updateResp, err := cert.Update(client, createResp.ID, updateOpts) + th.AssertNoErr(t, err) + th.AssertEquals(t, updateOpts.Name, updateResp.Name) + + t.Logf("Attempting to get certificate: %s", createResp.ID) + getResp, err := cert.Get(client, createResp.ID) + th.AssertNoErr(t, err) + tools.PrintResource(t, getResp) + + bindOpts := cert.BindOpts{ + InstanceID: gatewayID, + GroupID: grp.ID, + DomainID: dom.ID, + CertificateIDs: []string{ + createResp.ID, + }, + } + + t.Logf("Attempting to bind domain with certificates: %s", createResp.ID) + err = cert.Bind(client, bindOpts) + th.AssertNoErr(t, err) + + t.Logf("Attempting to unbind domain certificate: %s", createResp.ID) + err = cert.Unbind(client, bindOpts) + th.AssertNoErr(t, err) + + bindCertOpts := cert.AttachDomainOpts{ + CertificateID: createResp.ID, + Domains: []cert.AttachDomainInfo{ + { + Domain: dom.UrlDomain, + }, + }, + } + + t.Logf("Attempting to bind cert to domain: %s", createResp.ID) + err = cert.BindCertToDomain(client, bindCertOpts) + th.AssertNoErr(t, err) + + t.Logf("Attempting to unbind cert from domain: %s", createResp.ID) + err = cert.UnbindCertFromDomain(client, bindCertOpts) + th.AssertNoErr(t, err) +} + +func TestCertificateList(t *testing.T) { + client, err := clients.NewAPIGWClient() + gatewayID := os.Getenv("GATEWAY_ID") + if gatewayID == "" { + t.Skip("`GATEWAY_ID` needs to be defined") + } + th.AssertNoErr(t, err) + t.Log("Attempting to list certificates") + allPages, err := cert.List(client, cert.ListOpts{ + InstanceId: gatewayID, + }) + th.AssertNoErr(t, err) + tools.PrintResource(t, allPages) +} + +func CreateTestCertificate(client *golangsdk.ServiceClient, t *testing.T, gatewayID, domainName string) *cert.CertificateResp { + certificate, privateKey, err := openstack.GenerateTestCertKeyPair(domainName) + + opts := cert.CreateOpts{ + Name: tools.RandomString("cert_", 5), + CertContent: certificate, + PrivateKey: privateKey, + Type: "instance", + InstanceID: gatewayID, + } + + t.Logf("Attempting to create certificate: %s", opts.Name) + createResp, err := cert.Create(client, opts) + th.AssertNoErr(t, err) + return createResp +} diff --git a/acceptance/openstack/common.go b/acceptance/openstack/common.go index 87f1f750c..2bc1c582a 100644 --- a/acceptance/openstack/common.go +++ b/acceptance/openstack/common.go @@ -3,9 +3,17 @@ package openstack import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" "fmt" + "math/big" "net" "testing" + "time" "github.com/opentelekomcloud/gophertelekomcloud" "github.com/opentelekomcloud/gophertelekomcloud/acceptance/clients" @@ -290,3 +298,50 @@ func CreateServer(t *testing.T, client *golangsdk.ServiceClient, ecsName, imageN return server } + +// GenerateTestCertKeyPair generates a test certificate and private key pair +func GenerateTestCertKeyPair(domain string) (string, string, error) { + pk, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return "", "", err + } + + template := x509.Certificate{ + SerialNumber: big.NewInt(time.Now().Unix()), + Subject: pkix.Name{ + Organization: []string{"Test Organization"}, + CommonName: domain, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(1, 0, 0), // Valid for 1 year + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + DNSNames: []string{domain}, + } + + certBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &pk.PublicKey, pk) + if err != nil { + return "", "", err + } + + certBuffer := new(bytes.Buffer) + err = pem.Encode(certBuffer, &pem.Block{ + Type: "CERTIFICATE", + Bytes: certBytes, + }) + if err != nil { + return "", "", err + } + + keyBuffer := new(bytes.Buffer) + err = pem.Encode(keyBuffer, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(pk), + }) + if err != nil { + return "", "", err + } + + return certBuffer.String(), keyBuffer.String(), nil +} diff --git a/openstack/apigw/v2/cert/Bind.go b/openstack/apigw/v2/cert/Bind.go new file mode 100644 index 000000000..b68a9e971 --- /dev/null +++ b/openstack/apigw/v2/cert/Bind.go @@ -0,0 +1,35 @@ +package cert + +import ( + golangsdk "github.com/opentelekomcloud/gophertelekomcloud" + "github.com/opentelekomcloud/gophertelekomcloud/internal/build" +) + +// BindOpts contains the options for binding SSL certificates to a domain +type BindOpts struct { + // Gateway ID + InstanceID string `json:"-"` + // API group ID + GroupID string `json:"-"` + // Domain ID + DomainID string `json:"-"` + // Certificate IDs to attach + CertificateIDs []string `json:"certificate_ids" required:"true"` + // Whether to enable client certificate verification + VerifiedClientCertificateEnabled *bool `json:"verified_client_certificate_enabled,omitempty"` +} + +// Bind SSL certificates to a domain +func Bind(client *golangsdk.ServiceClient, opts BindOpts) error { + b, err := build.RequestBody(opts, "") + if err != nil { + return err + } + + // Build URL: /v2/{project_id}/apigw/instances/{instance_id}/api-groups/{group_id}/domains/{domain_id}/certificates/attach + _, err = client.Post(client.ServiceURL("apigw", "instances", opts.InstanceID, + "api-groups", opts.GroupID, "domains", opts.DomainID, "certificates", "attach"), b, nil, &golangsdk.RequestOpts{ + OkCodes: []int{200, 204}, + }) + return err +} diff --git a/openstack/apigw/v2/cert/BindCertToDomain.go b/openstack/apigw/v2/cert/BindCertToDomain.go new file mode 100644 index 000000000..80cd4566a --- /dev/null +++ b/openstack/apigw/v2/cert/BindCertToDomain.go @@ -0,0 +1,36 @@ +package cert + +import ( + golangsdk "github.com/opentelekomcloud/gophertelekomcloud" + "github.com/opentelekomcloud/gophertelekomcloud/internal/build" +) + +// AttachDomainInfo represents the information for a domain to attach +type AttachDomainInfo struct { + // Domain name + Domain string `json:"domain" required:"true"` + // Gateway IDs + InstanceIDs []string `json:"instance_ids,omitempty"` + // Whether to enable client certificate verification + VerifiedClientCertificateEnabled *bool `json:"verified_client_certificate_enabled,omitempty"` +} + +// AttachDomainOpts contains the options for binding a certificate to domain names +type AttachDomainOpts struct { + CertificateID string `json:"-"` + // Domain names the certificate is bound to + Domains []AttachDomainInfo `json:"domains" required:"true"` +} + +// BindCertToDomain binds an SSL certificate to domain names +func BindCertToDomain(client *golangsdk.ServiceClient, opts AttachDomainOpts) error { + b, err := build.RequestBody(opts, "") + if err != nil { + return err + } + + _, err = client.Post(client.ServiceURL("apigw", "certificates", opts.CertificateID, "domains", "attach"), b, nil, &golangsdk.RequestOpts{ + OkCodes: []int{200, 204}, + }) + return err +} diff --git a/openstack/apigw/v2/cert/Create.go b/openstack/apigw/v2/cert/Create.go new file mode 100644 index 000000000..614ab8918 --- /dev/null +++ b/openstack/apigw/v2/cert/Create.go @@ -0,0 +1,68 @@ +package cert + +import ( + golangsdk "github.com/opentelekomcloud/gophertelekomcloud" + "github.com/opentelekomcloud/gophertelekomcloud/internal/build" + "github.com/opentelekomcloud/gophertelekomcloud/internal/extract" +) + +// CreateOpts contains the options for creating a new SSL certificate +type CreateOpts struct { + // Certificate name. It can contain 4 to 50 characters, starting with a letter. + // Only letters, digits, and underscores (_) are allowed. + Name string `json:"name" required:"true"` + // Certificate content + CertContent string `json:"cert_content" required:"true"` + // Certificate private key + PrivateKey string `json:"private_key" required:"true"` + // Certificate scope (instance or global) + Type string `json:"type,omitempty"` + // Gateway ID. Required if type is set to instance + InstanceID string `json:"instance_id,omitempty"` + // Trusted root certificate (CA) + TrustedRootCA string `json:"trusted_root_ca,omitempty"` +} + +// Create creates a new SSL certificate +func Create(client *golangsdk.ServiceClient, opts CreateOpts) (*CertificateResp, error) { + b, err := build.RequestBody(opts, "") + if err != nil { + return nil, err + } + + raw, err := client.Post(client.ServiceURL("apigw", "certificates"), b, nil, &golangsdk.RequestOpts{ + OkCodes: []int{200}, + }) + if err != nil { + return nil, err + } + + var res CertificateResp + err = extract.Into(raw.Body, &res) + return &res, err +} + +// CertificateResp represents the response from certificate creation +type CertificateResp struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + InstanceID string `json:"instance_id"` + ProjectID string `json:"project_id"` + CommonName string `json:"common_name"` + San []string `json:"san"` + NotAfter string `json:"not_after"` + NotBefore string `json:"not_before"` + SignatureAlgorithm string `json:"signature_algorithm"` + CreateTime string `json:"create_time"` + UpdateTime string `json:"update_time"` + HasTrustedRootCA bool `json:"is_has_trusted_root_ca"` + Version int `json:"version"` + Organization []string `json:"organization"` + OrganizationalUnit []string `json:"organizational_unit"` + Locality []string `json:"locality"` + State []string `json:"state"` + Country []string `json:"country"` + SerialNumber string `json:"serial_number"` + Issuer []string `json:"issuer"` +} diff --git a/openstack/apigw/v2/cert/Delete.go b/openstack/apigw/v2/cert/Delete.go new file mode 100644 index 000000000..c16a5b553 --- /dev/null +++ b/openstack/apigw/v2/cert/Delete.go @@ -0,0 +1,8 @@ +package cert + +import golangsdk "github.com/opentelekomcloud/gophertelekomcloud" + +func Delete(client *golangsdk.ServiceClient, certID string) (err error) { + _, err = client.Delete(client.ServiceURL("apigw", "certificates", certID), nil) + return +} diff --git a/openstack/apigw/v2/cert/Get.go b/openstack/apigw/v2/cert/Get.go new file mode 100644 index 000000000..a141f7c29 --- /dev/null +++ b/openstack/apigw/v2/cert/Get.go @@ -0,0 +1,18 @@ +package cert + +import ( + golangsdk "github.com/opentelekomcloud/gophertelekomcloud" + "github.com/opentelekomcloud/gophertelekomcloud/internal/extract" +) + +// Get retrieves details of a specific SSL certificate +func Get(client *golangsdk.ServiceClient, certificateID string) (*CertificateResp, error) { + raw, err := client.Get(client.ServiceURL("apigw", "certificates", certificateID), nil, nil) + if err != nil { + return nil, err + } + + var res CertificateResp + err = extract.Into(raw.Body, &res) + return &res, err +} diff --git a/openstack/apigw/v2/cert/List.go b/openstack/apigw/v2/cert/List.go new file mode 100644 index 000000000..84c86c072 --- /dev/null +++ b/openstack/apigw/v2/cert/List.go @@ -0,0 +1,80 @@ +package cert + +import ( + "bytes" + + golangsdk "github.com/opentelekomcloud/gophertelekomcloud" + "github.com/opentelekomcloud/gophertelekomcloud/internal/extract" + "github.com/opentelekomcloud/gophertelekomcloud/pagination" +) + +type ListOpts struct { + InstanceId string `q:"instance_id"` + // Offset from which the query starts + Offset *int64 `q:"offset"` + // Number of items displayed on each page + Limit *int `q:"limit"` + // Certificate name + Name string `q:"name"` + // Certificate domain name + CommonName string `q:"common_name"` + // Certificate signature algorithm + SignatureAlgorithm string `q:"signature_algorithm"` + // Certificate scope (instance or global) + Type string `q:"type"` +} + +// List retrieves a list of SSL certificates +func List(client *golangsdk.ServiceClient, opts ListOpts) ([]CertBase, error) { + url, err := golangsdk.NewURLBuilder(). + WithEndpoints("apigw", "certificates"). + WithQueryParams(&opts).Build() + if err != nil { + return nil, err + } + + pages, err := pagination.Pager{ + Client: client, + InitialURL: client.ServiceURL(url.String()), + CreatePage: func(r pagination.NewPageResult) pagination.NewPage { + return CertificatePage{NewSinglePageBase: pagination.NewSinglePageBase{NewPageResult: r}} + }, + }.NewAllPages() + + if err != nil { + return nil, err + } + return ExtractCertificates(pages) +} + +// ExtractCertificates extracts certificates from the response +func ExtractCertificates(r pagination.NewPage) ([]CertBase, error) { + var s struct { + Size int `json:"size"` + Total int64 `json:"total"` + Certs []CertBase `json:"certs"` + } + err := extract.Into(bytes.NewReader((r.(CertificatePage)).Body), &s) + return s.Certs, err +} + +// CertBase represents the basic content of an SSL certificate +type CertBase struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + InstanceID string `json:"instance_id"` + ProjectID string `json:"project_id"` + CommonName string `json:"common_name"` + San []string `json:"san"` + NotAfter string `json:"not_after"` + SignatureAlgorithm string `json:"signature_algorithm"` + CreateTime string `json:"create_time"` + UpdateTime string `json:"update_time"` + HasTrustedRootCA bool `json:"is_has_trusted_root_ca"` +} + +// CertificatePage represents a single page of certificates +type CertificatePage struct { + pagination.NewSinglePageBase +} diff --git a/openstack/apigw/v2/cert/Unbind.go b/openstack/apigw/v2/cert/Unbind.go new file mode 100644 index 000000000..1e86e9ce7 --- /dev/null +++ b/openstack/apigw/v2/cert/Unbind.go @@ -0,0 +1,21 @@ +package cert + +import ( + golangsdk "github.com/opentelekomcloud/gophertelekomcloud" + "github.com/opentelekomcloud/gophertelekomcloud/internal/build" +) + +// Unbind SSL certificates from a domain +func Unbind(client *golangsdk.ServiceClient, opts BindOpts) error { + b, err := build.RequestBody(opts, "") + if err != nil { + return err + } + + // Build URL: /v2/{project_id}/apigw/instances/{instance_id}/api-groups/{group_id}/domains/{domain_id}/certificates/detach + _, err = client.Post(client.ServiceURL("apigw", "instances", opts.InstanceID, + "api-groups", opts.GroupID, "domains", opts.DomainID, "certificates", "detach"), b, nil, &golangsdk.RequestOpts{ + OkCodes: []int{200, 204}, + }) + return err +} diff --git a/openstack/apigw/v2/cert/UnbindCertFromDomain.go b/openstack/apigw/v2/cert/UnbindCertFromDomain.go new file mode 100644 index 000000000..808de370b --- /dev/null +++ b/openstack/apigw/v2/cert/UnbindCertFromDomain.go @@ -0,0 +1,19 @@ +package cert + +import ( + golangsdk "github.com/opentelekomcloud/gophertelekomcloud" + "github.com/opentelekomcloud/gophertelekomcloud/internal/build" +) + +// UnbindCertFromDomain unbinds an SSL certificate from domain names +func UnbindCertFromDomain(client *golangsdk.ServiceClient, opts AttachDomainOpts) error { + b, err := build.RequestBody(opts, "") + if err != nil { + return err + } + + _, err = client.Post(client.ServiceURL("apigw", "certificates", opts.CertificateID, "domains", "detach"), b, nil, &golangsdk.RequestOpts{ + OkCodes: []int{204}, + }) + return err +} diff --git a/openstack/apigw/v2/cert/Update.go b/openstack/apigw/v2/cert/Update.go new file mode 100644 index 000000000..0c0350a4e --- /dev/null +++ b/openstack/apigw/v2/cert/Update.go @@ -0,0 +1,43 @@ +package cert + +import ( + golangsdk "github.com/opentelekomcloud/gophertelekomcloud" + "github.com/opentelekomcloud/gophertelekomcloud/internal/build" + "github.com/opentelekomcloud/gophertelekomcloud/internal/extract" +) + +// UpdateOpts contains the options for updating an SSL certificate +type UpdateOpts struct { + // Certificate name. It can contain 4 to 50 characters, starting with a letter. + // Only letters, digits, and underscores (_) are allowed. + Name string `json:"name" required:"true"` + // Certificate content + CertContent string `json:"cert_content" required:"true"` + // Certificate private key + PrivateKey string `json:"private_key" required:"true"` + // Certificate scope + Type string `json:"type,omitempty"` + // Gateway ID. Required if type is set to instance + InstanceID string `json:"instance_id,omitempty"` + // Trusted root certificate (CA) + TrustedRootCA string `json:"trusted_root_ca,omitempty"` +} + +// Update modifies an existing SSL certificate +func Update(client *golangsdk.ServiceClient, certificateID string, opts UpdateOpts) (*CertificateResp, error) { + b, err := build.RequestBody(opts, "") + if err != nil { + return nil, err + } + + raw, err := client.Put(client.ServiceURL("apigw", "certificates", certificateID), b, nil, &golangsdk.RequestOpts{ + OkCodes: []int{200}, + }) + if err != nil { + return nil, err + } + + var res CertificateResp + err = extract.Into(raw.Body, &res) + return &res, err +} From 26717336d9e5fe46bdc87c94b28383425918b20b Mon Sep 17 00:00:00 2001 From: Artem Lifshits Date: Wed, 15 Jan 2025 15:15:46 +0100 Subject: [PATCH 2/2] fix lint --- acceptance/openstack/apigw/v2/cert_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/acceptance/openstack/apigw/v2/cert_test.go b/acceptance/openstack/apigw/v2/cert_test.go index 3855fc43b..df92bfed0 100644 --- a/acceptance/openstack/apigw/v2/cert_test.go +++ b/acceptance/openstack/apigw/v2/cert_test.go @@ -65,7 +65,7 @@ func TestCertificateLifecycle(t *testing.T) { th.AssertNoErr(t, cert.Delete(client, createResp.ID)) }) - newCert, newPk, err := openstack.GenerateTestCertKeyPair(dom.UrlDomain) + newCert, newPk, _ := openstack.GenerateTestCertKeyPair(dom.UrlDomain) updateOpts := cert.UpdateOpts{ Name: createResp.Name + "_updated", @@ -136,7 +136,7 @@ func TestCertificateList(t *testing.T) { } func CreateTestCertificate(client *golangsdk.ServiceClient, t *testing.T, gatewayID, domainName string) *cert.CertificateResp { - certificate, privateKey, err := openstack.GenerateTestCertKeyPair(domainName) + certificate, privateKey, _ := openstack.GenerateTestCertKeyPair(domainName) opts := cert.CreateOpts{ Name: tools.RandomString("cert_", 5),