Skip to content

Commit

Permalink
GIN-341: add teams certificates to Gateway and to Gateway account con…
Browse files Browse the repository at this point in the history
…figuration
  • Loading branch information
alyssamw committed Aug 21, 2024
1 parent 751fdf3 commit d730849
Show file tree
Hide file tree
Showing 8 changed files with 393 additions and 6 deletions.
3 changes: 3 additions & 0 deletions .changelog/3547.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:new-resource
resource/teams-certificate: add teams certificates to Gateway and Gateway account configurations
```
1 change: 1 addition & 0 deletions internal/sdkv2provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ func New(version string) func() *schema.Provider {
"cloudflare_bot_management": resourceCloudflareBotManagement(),
"cloudflare_teams_account": resourceCloudflareTeamsAccount(),
"cloudflare_zero_trust_gateway_settings": resourceCloudflareZeroTrustGatewaySettings(),
"cloudflare_zero_trust_gateway_certificate": resourceCloudflareTeamsCertificate(),
"cloudflare_teams_list": resourceCloudflareTeamsList(),
"cloudflare_zero_trust_list": resourceCloudflareZeroTrustList(),
"cloudflare_teams_location": resourceCloudflareTeamsLocation(),
Expand Down
32 changes: 31 additions & 1 deletion internal/sdkv2provider/resource_cloudflare_teams_accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,12 @@ func resourceCloudflareTeamsAccountRead(ctx context.Context, d *schema.ResourceD
}
}

if configuration.Settings.Certificate != nil {
if err := d.Set("certificate", flattenCertificateConfig(configuration.Settings.Certificate)); err != nil {
return diag.FromErr(fmt.Errorf("error parsing account custom certificate config: %w", err))
}
}

logSettings, err := client.TeamsAccountLoggingConfiguration(ctx, accountID)
if err != nil {
return diag.FromErr(fmt.Errorf("error finding Teams Account log settings %q: %w", d.Id(), err))
Expand Down Expand Up @@ -183,6 +189,7 @@ func resourceCloudflareTeamsAccountUpdate(ctx context.Context, d *schema.Resourc
antivirusConfig := inflateAntivirusConfig(d.Get("antivirus"))
extendedEmailMatchingConfig := inflateExtendedEmailMatchingConfig(d.Get("extended_email_matching"))
customCertificateConfig := inflateCustomCertificateConfig(d.Get("custom_certificate"))
certificateConfig := inflateCertificateConfig(d.Get("certificate"))
loggingConfig := inflateLoggingSettings(d.Get("logging"))
deviceConfig := inflateDeviceSettings(d.Get("proxy"))
payloadLogSettings := inflatePayloadLogSettings(d.Get("payload_log"))
Expand All @@ -194,9 +201,14 @@ func resourceCloudflareTeamsAccountUpdate(ctx context.Context, d *schema.Resourc
FIPS: fipsConfig,
BodyScanning: bodyScanningConfig,
ExtendedEmailMatching: extendedEmailMatchingConfig,
CustomCertificate: customCertificateConfig,
},
}
if customCertificateConfig != nil {
updatedTeamsAccount.Settings.CustomCertificate = customCertificateConfig
}
if certificateConfig != nil {
updatedTeamsAccount.Settings.Certificate = certificateConfig
}

//nolint:staticcheck
tlsDecrypt, ok := d.GetOkExists("tls_decrypt_enabled")
Expand Down Expand Up @@ -548,3 +560,21 @@ func inflateCustomCertificateConfig(config interface{}) *cloudflare.TeamsCustomC
ID: configMap["id"].(string),
}
}

func flattenCertificateConfig(config *cloudflare.TeamsCertificateSetting) []interface{} {
return []interface{}{map[string]interface{}{
"id": *&config.ID,
}}
}

func inflateCertificateConfig(config interface{}) *cloudflare.TeamsCertificateSetting {
list := config.([]interface{})
if len(list) != 1 {
return nil
}

configMap := list[0].(map[string]interface{})
return &cloudflare.TeamsCertificateSetting{
ID: configMap["id"].(string),
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func TestAccCloudflareTeamsAccounts_ConfigurationBasic(t *testing.T) {
Config: testAccCloudflareTeamsAccountBasic(rnd, accountID),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(name, consts.AccountIDSchemaKey, accountID),
resource.TestCheckResourceAttr(name, "custom_certificate.0.enabled", "false"),
resource.TestCheckResourceAttr(name, "certificate.id", "fake-id"),
resource.TestCheckResourceAttr(name, "tls_decrypt_enabled", "true"),
resource.TestCheckResourceAttr(name, "protocol_detection_enabled", "true"),
resource.TestCheckResourceAttr(name, "activity_log_enabled", "true"),
Expand Down Expand Up @@ -138,8 +138,8 @@ resource "cloudflare_zero_trust_gateway_settings" "%[1]s" {
extended_email_matching {
enabled = true
}
custom_certificate {
enabled = false
certificate {
id = "fake-id"
}
}
`, rnd, accountID)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package sdkv2provider

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

"github.com/cloudflare/cloudflare-go"
"github.com/cloudflare/terraform-provider-cloudflare/internal/consts"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/terraform"
)

func TestAccCloudflareTeamsCertificate_Basic(t *testing.T) {
// Temporarily unset CLOUDFLARE_API_TOKEN if it is set as the Access
// service does not yet support the API tokens and it results in
// misleading state error messages.
if os.Getenv("CLOUDFLARE_API_TOKEN") != "" {
t.Setenv("CLOUDFLARE_API_TOKEN", "")
}

rnd := generateRandomResourceName()
name := fmt.Sprintf("cloudflare_zero_trust_gateway_certificate.%s", rnd)

resource.Test(t, resource.TestCase{
PreCheck: func() {
testAccPreCheck(t)
},
ProviderFactories: providerFactories,
CheckDestroy: testAccCheckCloudflareTeamsCertificateDestroy,
Steps: []resource.TestStep{
{
Config: testAccCloudflareTeamsCertificateCustomBasic(rnd, accountID),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(name, consts.AccountIDSchemaKey, accountID),
resource.TestCheckResourceAttr(name, "id", rnd),
resource.TestCheckResourceAttr(name, "binding_status", "inactive"),
resource.TestCheckResourceAttr(name, "gateway_managed", "false"),
),
},
{
Config: testAccCloudflareTeamsCertificateManagedBasic(rnd, accountID),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(name, consts.AccountIDSchemaKey, accountID),
resource.TestCheckResourceAttr(name, "id", rnd),
resource.TestCheckResourceAttr(name, "binding_status", "inactive"),
resource.TestCheckResourceAttr(name, "custom", "false"),
),
},
},
})
}

func testAccCloudflareTeamsCertificateCustomBasic(rnd, accountID string) string {
return fmt.Sprintf(`
resource "cloudflare_zero_trust_gateway_certificate" "%[1]s" {
account_id = "%[2]s"
custom = true
id = "%[1]s"
}
`, rnd, accountID)
}

func testAccCloudflareTeamsCertificateManagedBasic(rnd, accountID string) string {
return fmt.Sprintf(`
resource "cloudflare_zero_trust_gateway_certificate" "%[1]s" {
account_id = "%[2]s"
gateway_managed = true
}
`, rnd, accountID)
}

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

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

accountID = rs.Primary.Attributes[consts.AccountIDSchemaKey]
_, err := client.TeamsCertificate(context.Background(), accountID, rs.Primary.ID)
if err == nil {
return fmt.Errorf("Teams Certificate still exists")
}
}

return nil
}
163 changes: 163 additions & 0 deletions internal/sdkv2provider/resource_cloudflare_teams_certificates.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package sdkv2provider

import (
"context"
"errors"
"fmt"
"strings"
"time"

"github.com/MakeNowJust/heredoc/v2"
cloudflare "github.com/cloudflare/cloudflare-go"
"github.com/cloudflare/terraform-provider-cloudflare/internal/consts"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

func resourceCloudflareTeamsCertificate() *schema.Resource {
return &schema.Resource{
Schema: resourceCloudflareTeamsCertificateSchema(),
CreateContext: resourceCloudflareTeamsCertificateCreate,
UpdateContext: resourceCloudflareTeamsCertificateUpdate,
ReadContext: resourceCloudflareTeamsCertificateRead,
DeleteContext: resourceCloudflareTeamsCertificateDelete,
Importer: &schema.ResourceImporter{
StateContext: resourceCloudflareTeamsCertificateImport,
},
Description: heredoc.Doc(`
Provides a Cloudflare Teams Gateway Certificate resource. A Teams Certificate can
be specified for Gateway TLS interception and block pages.
`),
}
}

func resourceCloudflareTeamsCertificateCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*cloudflare.API)
accountID := d.Get(consts.AccountIDSchemaKey).(string)

if d.Get("gateway_managed").(bool) {
newTeamsCertificate := cloudflare.TeamsCertificateCreateRequest{
ValidityPeriodDays: d.Get("validity_period_days").(int),
}

tflog.Debug(ctx, fmt.Sprintf("Creating Cloudflare Teams Certificate from struct: %+v", newTeamsCertificate))

certificate, err := client.TeamsGenerateCertificate(ctx, accountID, newTeamsCertificate)
if err != nil {
return diag.FromErr(fmt.Errorf("error creating Teams Certificate for account %q: %w", accountID, err))
}
d.SetId(certificate.ID)
}

if d.Get("activate").(bool) {
certificate, err := client.TeamsActivateCertificate(ctx, accountID, d.Get("id").(string))
if err != nil {
return diag.FromErr(fmt.Errorf("error activating Teams Certificate with id %q for account %q: %w", d.Get("id"), accountID, err))
}
d.Set("binding_status", certificate.BindingStatus)
d.Set("qs_pack_id", certificate.QsPackId)
}

return resourceCloudflareTeamsCertificateRead(ctx, d, meta)
}

func resourceCloudflareTeamsCertificateUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*cloudflare.API)
accountID := d.Get(consts.AccountIDSchemaKey).(string)

if d.Get("activate").(bool) {
certificate, err := client.TeamsActivateCertificate(ctx, accountID, d.Get("id").(string))
if err != nil {
return diag.FromErr(fmt.Errorf("error activating Teams Certificate with id %q for account %q: %w", d.Get("id"), accountID, err))
}
d.Set("binding_status", certificate.BindingStatus)
d.Set("qs_pack_id", certificate.QsPackId)
} else {
certificate, err := client.TeamsDeactivateCertificate(ctx, accountID, d.Get("id").(string))
if err != nil {
return diag.FromErr(fmt.Errorf("error deactivating Teams Certificate with id %q for account %q: %w", d.Get("id"), accountID, err))
}
d.Set("binding_status", certificate.BindingStatus)
d.Set("qs_pack_id", certificate.QsPackId)
}

return resourceCloudflareTeamsCertificateRead(ctx, d, meta)
}

func resourceCloudflareTeamsCertificateRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*cloudflare.API)
accountID := d.Get(consts.AccountIDSchemaKey).(string)

Certificate, err := client.TeamsCertificate(ctx, accountID, d.Id())

if err != nil {
var notFoundError *cloudflare.NotFoundError
if errors.As(err, &notFoundError) {
tflog.Info(ctx, fmt.Sprintf("Teams Certificate %s no longer exists", d.Id()))
d.SetId("")
return nil
}
return diag.FromErr(fmt.Errorf("error finding Teams Certificate %q: %w", d.Id(), err))
}

d.Set("id", Certificate.ID)
d.Set("enabled", Certificate.Enabled)
d.Set("binding_status", Certificate.BindingStatus)
d.Set("qs_pack_id", Certificate.QsPackId)
certType := Certificate.Type
if certType == "gateway_managed" {
d.Set("gateway_managed", true)
} else if certType == "custom" {
d.Set("custom", true)
} else {
return diag.FromErr(fmt.Errorf("error reading Teams Certificate type %q: %w", certType, err))
}
if Certificate.UploadedOn != nil {
d.Set("uploaded_on", Certificate.UploadedOn.Format(time.RFC3339Nano))
}
if Certificate.CreatedAt != nil {
d.Set("created_at", Certificate.CreatedAt.Format(time.RFC3339Nano))
}
if Certificate.ExpiresOn != nil {
d.Set("expires_on", Certificate.ExpiresOn.Format(time.RFC3339Nano))
}

return nil
}

func resourceCloudflareTeamsCertificateDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*cloudflare.API)
certID := d.Id()
accountID := d.Get(consts.AccountIDSchemaKey).(string)

tflog.Debug(ctx, fmt.Sprintf("Deleting Cloudflare Teams Certificate using ID: %s", certID))

err := client.TeamsDeleteCertificate(ctx, accountID, certID)
if err != nil {
return diag.FromErr(fmt.Errorf("error deleting Teams Certificate for account %q: %w", accountID, err))
}

resourceCloudflareTeamsCertificateRead(ctx, d, meta)

return nil
}

func resourceCloudflareTeamsCertificateImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
attributes := strings.SplitN(d.Id(), "/", 2)

if len(attributes) != 2 {
return nil, fmt.Errorf("invalid id (\"%s\") specified, should be in format \"accountID/teamsCertificateID\"", d.Id())
}

accountID, teamsCertificateID := attributes[0], attributes[1]

tflog.Debug(ctx, fmt.Sprintf("Importing Cloudflare Teams Certificate: id %s for account %s", teamsCertificateID, accountID))

d.Set(consts.AccountIDSchemaKey, accountID)
d.SetId(teamsCertificateID)

resourceCloudflareTeamsCertificateRead(ctx, d, meta)

return []*schema.ResourceData{d}, nil
}
23 changes: 21 additions & 2 deletions internal/sdkv2provider/schema_cloudflare_teams_accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,23 @@ func resourceCloudflareTeamsAccountSchema() map[string]*schema.Schema {
},
},
"custom_certificate": {
Type: schema.TypeList,
MaxItems: 1,
Optional: true,
Description: "Configuration for custom certificates / BYO-PKI.",
Deprecated: "Use `certificate` instead. Continuing to use custom_certificate may result in inconsistent configuration.",
ConflictsWith: []string{"certificate"},
Elem: &schema.Resource{
Schema: customCertificateSchema,
},
},
"certificate": {
Type: schema.TypeList,
MaxItems: 1,
Optional: true,
Description: "Configuration for custom certificates / BYO-PKI.",
Description: "Configuration for TLS interception certificate. This will be required starting Feb 2025.",
Elem: &schema.Resource{
Schema: customCertificateSchema,
Schema: certificateSettingSchema,
},
},
}
Expand Down Expand Up @@ -347,3 +358,11 @@ var customCertificateSchema = map[string]*schema.Schema{
Computed: true,
},
}

var certificateSettingSchema = map[string]*schema.Schema{
"id": {
Type: schema.TypeString,
Required: true,
Description: "ID of certificate for TLS interception.",
},
}
Loading

0 comments on commit d730849

Please sign in to comment.