Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Access CA certificate support #995

Merged
merged 2 commits into from Mar 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions cloudflare/provider.go
Expand Up @@ -105,6 +105,7 @@ func Provider() terraform.ResourceProvider {

ResourcesMap: map[string]*schema.Resource{
"cloudflare_access_application": resourceCloudflareAccessApplication(),
"cloudflare_access_ca_certificate": resourceCloudflareAccessCACertificate(),
"cloudflare_access_policy": resourceCloudflareAccessPolicy(),
"cloudflare_access_group": resourceCloudflareAccessGroup(),
"cloudflare_access_rule": resourceCloudflareAccessRule(),
Expand Down
157 changes: 157 additions & 0 deletions cloudflare/resource_cloudflare_access_ca_certificate.go
@@ -0,0 +1,157 @@
package cloudflare

import (
"context"
"fmt"
"log"
"strings"

cloudflare "github.com/cloudflare/cloudflare-go"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
)

func resourceCloudflareAccessCACertificate() *schema.Resource {
return &schema.Resource{
Create: resourceCloudflareAccessCACertificateCreate,
Read: resourceCloudflareAccessCACertificateRead,
Update: resourceCloudflareAccessCACertificateUpdate,
Delete: resourceCloudflareAccessCACertificateDelete,
Importer: &schema.ResourceImporter{
State: resourceCloudflareAccessCACertificateImport,
},

Schema: map[string]*schema.Schema{
"account_id": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ConflictsWith: []string{"zone_id"},
},
"zone_id": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ConflictsWith: []string{"account_id"},
},
"application_id": {
Type: schema.TypeString,
Required: true,
},
"aud": {
Type: schema.TypeString,
Computed: true,
},
"public_key": {
Type: schema.TypeString,
Computed: true,
},
},
}
}

func resourceCloudflareAccessCACertificateCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudflare.API)

identifier, err := initIdentifier(d)
if err != nil {
return err
}

var accessCACert cloudflare.AccessCACertificate
if identifier.Type == AccountType {
accessCACert, err = client.CreateAccessCACertificate(context.Background(), identifier.Value, d.Get("application_id").(string))
} else {
accessCACert, err = client.CreateZoneLevelAccessCACertificate(context.Background(), identifier.Value, d.Get("application_id").(string))
}
if err != nil {
return fmt.Errorf("error creating Access CA Certificate for %s %q: %s", identifier.Type, identifier.Value, err)
}

d.SetId(accessCACert.ID)

return resourceCloudflareAccessCACertificateRead(d, meta)
}

func resourceCloudflareAccessCACertificateRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudflare.API)
applicationID := d.Get("application_id").(string)
identifier, err := initIdentifier(d)
if err != nil {
return err
}

var accessCACert cloudflare.AccessCACertificate
if identifier.Type == AccountType {
accessCACert, err = client.AccessCACertificate(context.Background(), identifier.Value, applicationID)
} else {
accessCACert, err = client.ZoneLevelAccessCACertificate(context.Background(), identifier.Value, applicationID)
}

if err != nil {
if strings.Contains(err.Error(), "HTTP status 404") {
log.Printf("[INFO] Access CA Certificate %s no longer exists", d.Id())
d.SetId("")
return nil
}
return fmt.Errorf("error finding Access CA Certificate %q: %s", d.Id(), err)
}

d.Set("aud", accessCACert.Aud)
d.Set("public_key", accessCACert.PublicKey)

return nil
}

func resourceCloudflareAccessCACertificateUpdate(d *schema.ResourceData, meta interface{}) error {
return nil
}

func resourceCloudflareAccessCACertificateDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudflare.API)
applicationID := d.Get("application_id").(string)

log.Printf("[DEBUG] Deleting Cloudflare CA Certificate using ID: %s", d.Id())

identifier, err := initIdentifier(d)
if err != nil {
return err
}

if identifier.Type == AccountType {
err = client.DeleteAccessCACertificate(context.Background(), identifier.Value, applicationID)
} else {
err = client.DeleteZoneLevelAccessCACertificate(context.Background(), identifier.Value, applicationID)
}

if err != nil {
return err
}

d.SetId("")

return nil
}

func resourceCloudflareAccessCACertificateImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
attributes := strings.SplitN(d.Id(), "/", 3)

if len(attributes) != 3 {
return nil, fmt.Errorf("invalid id (\"%s\") specified, should be in format \"account/accountID/accessCACertificateID\" or \"zone/zoneID/accessCACertificateID\"", d.Id())
}

identifierType, identifierID, accessCACertificateID := attributes[0], attributes[1], attributes[2]

if AccessIdentifierType(identifierType) != AccountType && AccessIdentifierType(identifierType) != ZoneType {
return nil, fmt.Errorf("invalid id (\"%s\") specified, should be in format \"account/accountID/accessCACertificateID\" or \"zone/zoneID/accessCACertificateID\"", d.Id())
}

log.Printf("[DEBUG] Importing Cloudflare Access CA Certificate: id %s for %s %s", accessCACertificateID, identifierType, identifierID)

//lintignore:R001
d.Set(fmt.Sprintf("%s_id", identifierType), identifierID)
d.SetId(accessCACertificateID)

resourceCloudflareAccessCACertificateRead(d, meta)

return []*schema.ResourceData{d}, nil
}
102 changes: 102 additions & 0 deletions cloudflare/resource_cloudflare_access_ca_certificate_test.go
@@ -0,0 +1,102 @@
package cloudflare

import (
"context"
"fmt"
"os"
"testing"

"github.com/cloudflare/cloudflare-go"
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/terraform"
)

func TestAccCloudflareAccessCACertificate_AccountLevel(t *testing.T) {
domain := os.Getenv("CLOUDFLARE_DOMAIN")
accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID")
rnd := generateRandomResourceName()
name := fmt.Sprintf("cloudflare_access_ca_certificate.%s", rnd)

resource.Test(t, resource.TestCase{
PreCheck: func() {
testAccessAccPreCheck(t)
testAccPreCheckAccount(t)
},
Providers: testAccProviders,
CheckDestroy: testAccCheckCloudflareAccessCACertificateDestroy,
Steps: []resource.TestStep{
{
Config: testAccCloudflareAccessCACertificateBasic(rnd, domain, AccessIdentifier{Type: AccountType, Value: accountID}),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(name, "account_id", accountID),
resource.TestCheckResourceAttrSet(name, "application_id"),
resource.TestCheckResourceAttrSet(name, "aud"),
resource.TestCheckResourceAttrSet(name, "public_key"),
),
},
},
})
}

func TestAccCloudflareAccessCACertificate_ZoneLevel(t *testing.T) {
domain := os.Getenv("CLOUDFLARE_DOMAIN")
zoneID := os.Getenv("CLOUDFLARE_ZONE_ID")
rnd := generateRandomResourceName()
name := fmt.Sprintf("cloudflare_access_ca_certificate.%s", rnd)

resource.Test(t, resource.TestCase{
PreCheck: func() {
testAccessAccPreCheck(t)
testAccPreCheckAccount(t)
},
Providers: testAccProviders,
CheckDestroy: testAccCheckCloudflareAccessCACertificateDestroy,
Steps: []resource.TestStep{
{
Config: testAccCloudflareAccessCACertificateBasic(rnd, domain, AccessIdentifier{Type: ZoneType, Value: zoneID}),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(name, "zone_id", zoneID),
resource.TestCheckResourceAttrSet(name, "application_id"),
resource.TestCheckResourceAttrSet(name, "aud"),
resource.TestCheckResourceAttrSet(name, "public_key"),
),
},
},
})
}

func testAccCloudflareAccessCACertificateBasic(resourceName, domain string, identifier AccessIdentifier) string {
return fmt.Sprintf(`
resource "cloudflare_access_application" "%[1]s" {
name = "%[1]s"
%[3]s_id = "%[4]s"
domain = "%[1]s.%[2]s"
}

resource "cloudflare_access_ca_certificate" "%[1]s" {
%[3]s_id = "%[4]s"
application_id = cloudflare_access_application.%[1]s.id
}`, resourceName, domain, identifier.Type, identifier.Value)
}

func testAccCheckCloudflareAccessCACertificateDestroy(s *terraform.State) error {
client := testAccProvider.Meta().(*cloudflare.API)

for _, rs := range s.RootModule().Resources {
if rs.Type != "cloudflare_access_ca_certificate" {
continue
}

_, err := client.AccessCACertificate(context.Background(), rs.Primary.Attributes["account_id"], rs.Primary.ID)
if err == nil {
return fmt.Errorf("Access CA certificate still exists")
}

_, err = client.ZoneLevelAccessCACertificate(context.Background(), rs.Primary.Attributes["zone_id"], rs.Primary.ID)
if err == nil {
return fmt.Errorf("Access CA certificate still exists")
}
}

return nil
}
2 changes: 1 addition & 1 deletion cloudflare/utils.go
Expand Up @@ -200,7 +200,7 @@ func initIdentifier(d *schema.ResourceData) (*AccessIdentifier, error) {
accountID := d.Get("account_id").(string)
zoneID := d.Get("zone_id").(string)
if accountID == "" && zoneID == "" {
return nil, fmt.Errorf("error creating Access Application: zone_id or account_id required")
return nil, fmt.Errorf("error creating Access resource: zone_id or account_id required")
}

if accountID != "" {
Expand Down
3 changes: 3 additions & 0 deletions website/cloudflare.erb
Expand Up @@ -49,6 +49,9 @@
<li<%= sidebar_current("docs-cloudflare-resource-access-application") %>>
<a href="/docs/providers/cloudflare/r/access_application.html">cloudflare_access_application</a>
</li>
<li<%= sidebar_current("docs-cloudflare-resource-access-ca-certificate") %>>
<a href="/docs/providers/cloudflare/r/access_ca_certificate.html">cloudflare_access_ca_certificate</a>
</li>
<li<%= sidebar_current("docs-cloudflare-resource-access-identity-provider") %>>
<a href="/docs/providers/cloudflare/r/access_identity_provider.html">cloudflare_access_identity_provider</a>
</li>
Expand Down
56 changes: 56 additions & 0 deletions website/docs/r/access_ca_certificate.html.markdown
@@ -0,0 +1,56 @@
---
layout: "cloudflare"
page_title: "Cloudflare: cloudflare_access_ca_certificate"
sidebar_current: "docs-cloudflare-resource-access-ca-certificate"
description: |-
Provides a Cloudflare Access CA certificate resource.
---

# cloudflare_access_ca_certificate

Cloudflare Access can replace traditional SSH key models with short-lived
certificates issued to your users based on the token generated by their Access
login.

## Example Usage

```hcl
# account level
resource "cloudflare_access_ca_certificate" "example" {
account_id = "1d5fdc9e88c8a8c4518b068cd94331fe"
application_id = "6cd6cea3-3ef2-4542-9aea-85a0bbcd5414"
}

# zone level
resource "cloudflare_access_ca_certificate" "another_example" {
zone_id = "b6bc7eb6027c792a6bca3dc91fd2d7e0"
application_id = "fe2be0ff-7f13-4350-8c8e-a9b9795fe3c2"
}
```

## Argument Reference

The following arguments are supported:

-> **Note:** It's required that an `account_id` or `zone_id` is provided and in most cases using either is fine. However, if you're using a scoped access token, you must provide the argument that matches the token's scope. For example, an access token that is scoped to the "example.com" zone needs to use the `zone_id` argument.

* `account_id` - (Optional) The account to which the Access CA certificate should be added. Conflicts with `zone_id`.
* `zone_id` - (Optional) The DNS zone to which the Access CA certificate should be added. Conflicts with `account_id`.
* `application_id` - (Required) The Access Application ID to associate with the CA certificate.

## Attributes Reference

The following additional attributes are exported:

* `id` - ID of the CA certificate
* `aud` - Application Audience (AUD) Tag of the CA certificate
* `public_key` - Cryptographic public key of the generated CA certificate

## Import

Access CA certificates can be imported using a composite ID formed of identifer
("account" or "zone"), identifier ID and the CA certificate ID.

```
$ terraform import cloudflare_access_ca_certificate.example account/1d5fdc9e88c8a8c4518b068cd94331fe/edc1e4e24567217764b4322669c44df985dddffdf03ac781
```