Skip to content

Commit

Permalink
WDAPI-645 add a device posture integration
Browse files Browse the repository at this point in the history
  • Loading branch information
andrew committed Dec 27, 2021
1 parent 4676e55 commit 5b71a30
Show file tree
Hide file tree
Showing 7 changed files with 429 additions and 12 deletions.
1 change: 1 addition & 0 deletions cloudflare/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ func Provider() *schema.Provider {
"cloudflare_custom_pages": resourceCloudflareCustomPages(),
"cloudflare_custom_ssl": resourceCloudflareCustomSsl(),
"cloudflare_device_posture_rule": resourceCloudflareDevicePostureRule(),
"cloudflare_device_posture_integration": resourceCloudflareDevicePostureIntegration(),
"cloudflare_filter": resourceCloudflareFilter(),
"cloudflare_firewall_rule": resourceCloudflareFirewallRule(),
"cloudflare_healthcheck": resourceCloudflareHealthcheck(),
Expand Down
188 changes: 188 additions & 0 deletions cloudflare/resource_cloudflare_device_posture_integration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package cloudflare

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

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

const ws1 = "workspace_one"

func resourceCloudflareDevicePostureIntegration() *schema.Resource {
return &schema.Resource{
Schema: resourceCloudflareDevicePostureIntegrationSchema(),
Create: resourceCloudflareDevicePostureIntegrationCreate,
Read: resourceCloudflareDevicePostureIntegrationRead,
Update: resourceCloudflareDevicePostureIntegrationUpdate,
Delete: resourceCloudflareDevicePostureIntegrationDelete,
Importer: &schema.ResourceImporter{
State: resourceCloudflareDevicePostureIntegrationImport,
},
}
}

func resourceCloudflareDevicePostureIntegrationCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudflare.API)
accountID := d.Get("account_id").(string)

newDevicePostureIntegration := cloudflare.DevicePostureIntegration{
Name: d.Get("name").(string),
Type: d.Get("type").(string),
Identifier: d.Get("identifier").(string),
Interval: d.Get("interval").(string),
}

err := setDevicePostureIntegrationConfig(&newDevicePostureIntegration, d)
if err != nil {
return fmt.Errorf("error creating Device Posture integration with provided config: %s", err)
}
fmt.Printf("[DEBUG] Creating Cloudflare Device Posture Integration from struct: %+v\n", newDevicePostureIntegration)

// The API does not return the client_secret so it must be stored in the state func on resource create.
savedSecret := newDevicePostureIntegration.Config.ClientSecret

newDevicePostureIntegration, err = client.CreateDevicePostureIntegration(context.Background(), accountID, newDevicePostureIntegration, false)
if err != nil {
return fmt.Errorf("error creating Device Posture Rule for account %q: %s %+v", accountID, err, newDevicePostureIntegration)
}

d.SetId(newDevicePostureIntegration.IntegrationID)

return devicePostureIntegrationReadHelper(d, meta, savedSecret)
}

func resourceCloudflareDevicePostureIntegrationRead(d *schema.ResourceData, meta interface{}) error {
// Client secret is always read from the local state.
secret, _ := d.Get("config.0.client_secret").(string)
return devicePostureIntegrationReadHelper(d, meta, secret)
}

func devicePostureIntegrationReadHelper(d *schema.ResourceData, meta interface{}, secret string) error {
client := meta.(*cloudflare.API)
accountID := d.Get("account_id").(string)

devicePostureIntegration, err := client.DevicePostureIntegration(context.Background(), accountID, d.Id())
if err != nil {
if strings.Contains(err.Error(), "HTTP status 404") {
log.Printf("[INFO] Device posture integration %s no longer exists", d.Id())
d.SetId("")
return nil
}
return fmt.Errorf("error finding device posture integration %q: %s", d.Id(), err)
}

devicePostureIntegration.Config.ClientSecret = secret
d.Set("name", devicePostureIntegration.Name)
d.Set("type", devicePostureIntegration.Type)
d.Set("identifier", devicePostureIntegration.Identifier)
d.Set("interval", devicePostureIntegration.Interval)
d.Set("config", convertIntegrationConfigToSchema(devicePostureIntegration.Config))

return nil
}

func resourceCloudflareDevicePostureIntegrationUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudflare.API)
accountID := d.Get("account_id").(string)

updatedDevicePostureIntegration := cloudflare.DevicePostureIntegration{
IntegrationID: d.Id(),
Name: d.Get("name").(string),
Type: d.Get("type").(string),
Interval: d.Get("interval").(string),
Identifier: d.Get("identifier").(string),
}

err := setDevicePostureIntegrationConfig(&updatedDevicePostureIntegration, d)
if err != nil {
return fmt.Errorf("error creating Device Posture Rule with provided match input: %s", err)
}

log.Printf("[DEBUG] Updating Cloudflare device posture integration from struct: %+v", updatedDevicePostureIntegration)

devicePostureIntegration, err := client.UpdateDevicePostureIntegration(context.Background(), accountID, updatedDevicePostureIntegration, false)
if err != nil {
return fmt.Errorf("error updating device posture integration for account %q: %s", accountID, err)
}

if devicePostureIntegration.IntegrationID == "" {
return fmt.Errorf("failed to find device posture integration_id in update response; resource was empty")
}

return resourceCloudflareDevicePostureIntegrationRead(d, meta)
}

func resourceCloudflareDevicePostureIntegrationDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cloudflare.API)
appID := d.Id()
accountID := d.Get("account_id").(string)

log.Printf("[DEBUG] Deleting Cloudflare device posture integration using ID: %s", appID)

err := client.DeleteDevicePostureIntegration(context.Background(), accountID, appID)
if err != nil {
return fmt.Errorf("error deleting Device Posture Rule for account %q: %s", accountID, err)
}

resourceCloudflareDevicePostureIntegrationRead(d, meta)

return nil
}

func resourceCloudflareDevicePostureIntegrationImport(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/devicePostureIntegrationID\"", d.Id())
}

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

log.Printf("[DEBUG] Importing Cloudflare device posture integration: id %s for account %s", devicePostureIntegrationID, accountID)

d.Set("account_id", accountID)
d.SetId(devicePostureIntegrationID)

resourceCloudflareDevicePostureIntegrationRead(d, meta)

return []*schema.ResourceData{d}, nil
}

func setDevicePostureIntegrationConfig(integration *cloudflare.DevicePostureIntegration, d *schema.ResourceData) error {
if _, ok := d.GetOk("config"); ok {
config := cloudflare.DevicePostureIntegrationConfig{}
switch integration.Type {
case ws1:
if config.ClientID, ok = d.Get("config.0.client_id").(string); !ok {
return fmt.Errorf("client_id is a string")
}
if config.ClientSecret, ok = d.Get("config.0.client_secret").(string); !ok {
return fmt.Errorf("client_secret is a string")
}
if config.AuthUrl, ok = d.Get("config.0.auth_url").(string); !ok {
return fmt.Errorf("auth_url is a string")
}
if config.ApiUrl, ok = d.Get("config.0.api_url").(string); !ok {
return fmt.Errorf("api_url is a string")
}
integration.Config = config
default:
return fmt.Errorf("unsupported integration type:%s", integration.Type)
}
}
return nil
}

func convertIntegrationConfigToSchema(input cloudflare.DevicePostureIntegrationConfig) []interface{} {
m := map[string]interface{}{
"client_id": input.ClientID,
"client_secret": input.ClientSecret,
"auth_url": input.AuthUrl,
"api_url": input.ApiUrl,
}
return []interface{}{m}
}
83 changes: 83 additions & 0 deletions cloudflare/resource_cloudflare_device_posture_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package cloudflare

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

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

func TestAccCloudflareDevicePostureIntegrationCreate(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") != "" {
defer func(apiToken string) {
os.Setenv("CLOUDFLARE_API_TOKEN", apiToken)
}(os.Getenv("CLOUDFLARE_API_TOKEN"))
os.Setenv("CLOUDFLARE_API_TOKEN", "")
}

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

resource.Test(t, resource.TestCase{
PreCheck: func() {
testAccessAccPreCheck(t)
},
Providers: testAccProviders,
CheckDestroy: testAccCheckCloudflareDevicePostureIntegrationDestroy,
Steps: []resource.TestStep{
{
Config: testAccCloudflareDevicePostureIntegration(rnd, accountID),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(name, "account_id", accountID),
resource.TestCheckResourceAttr(name, "name", rnd),
resource.TestCheckResourceAttr(name, "type", "workspace_one"),
resource.TestCheckResourceAttr(name, "interval", "24h"),
resource.TestCheckResourceAttr(name, "config.0.auth_url", "https://test.uemauth.vmwservices.com/connect/token"),
resource.TestCheckResourceAttr(name, "config.0.api_url", "https://example.com/api-url"),
resource.TestCheckResourceAttr(name, "config.0.client_id", "client-id"),
),
},
},
})
}

func testAccCloudflareDevicePostureIntegration(rnd, accountID string) string {
return fmt.Sprintf(`
resource "cloudflare_device_posture_integration" "%[1]s" {
account_id = "%[2]s"
name = "%[1]s"
type = "workspace_one"
interval = "24h"
config {
api_url = "https://example.com/api-url"
auth_url = "https://test.uemauth.vmwservices.com/connect/token"
client_id = "client-id"
client_secret = "client-secret"
}
}
`, rnd, accountID)
}

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

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

_, err := client.DevicePostureIntegration(context.Background(), rs.Primary.Attributes["account_id"], rs.Primary.ID)
if err == nil {
return fmt.Errorf("Device Posture Integration still exists")
}
}

return nil
}
30 changes: 19 additions & 11 deletions cloudflare/resource_cloudflare_device_posture_rule.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,12 @@ func setDevicePostureRuleInput(rule *cloudflare.DevicePostureRule, d *schema.Res
if domain, ok := d.GetOk("input.0.domain"); ok {
input.Domain = domain.(string)
}
if complianceStatus, ok := d.GetOk("input.0.compliance_status"); ok {
input.ComplianceStatus = complianceStatus.(string)
}
if connectionID, ok := d.GetOk("input.0.connection_id"); ok {
input.ConnectionID = connectionID.(string)
}
rule.Input = input
}
}
Expand Down Expand Up @@ -218,17 +224,19 @@ func convertMatchToSchema(matches []cloudflare.DevicePostureRuleMatch) []map[str

func convertInputToSchema(input cloudflare.DevicePostureRuleInput) []map[string]interface{} {
m := map[string]interface{}{
"id": input.ID,
"path": input.Path,
"exists": input.Exists,
"thumbprint": input.Thumbprint,
"sha256": input.Sha256,
"running": input.Running,
"require_all": input.RequireAll,
"enabled": input.Enabled,
"version": input.Version,
"operator": input.Operator,
"domain": input.Domain,
"id": input.ID,
"path": input.Path,
"exists": input.Exists,
"thumbprint": input.Thumbprint,
"sha256": input.Sha256,
"running": input.Running,
"require_all": input.RequireAll,
"enabled": input.Enabled,
"version": input.Version,
"operator": input.Operator,
"domain": input.Domain,
"compliance_status": input.ComplianceStatus,
"connection_id": input.ConnectionID,
}

return []map[string]interface{}{m}
Expand Down
63 changes: 63 additions & 0 deletions cloudflare/schema_cloudflare_device_posture_integration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package cloudflare

import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
)

func resourceCloudflareDevicePostureIntegrationSchema() map[string]*schema.Schema {
return map[string]*schema.Schema{
"account_id": {
Type: schema.TypeString,
Required: true,
},
"id": {
Type: schema.TypeString,
Computed: true,
},
"name": {
Type: schema.TypeString,
Required: true,
},
"type": {
Type: schema.TypeString,
Required: true,
ValidateFunc: validation.StringInSlice([]string{ws1}, false),
},
"identifier": {
Type: schema.TypeString,
Optional: true,
},
"interval": {
Type: schema.TypeString,
Optional: true,
},
"config": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"auth_url": {
Type: schema.TypeString,
Optional: true,
ValidateFunc: validation.IsURLWithHTTPS,
},
"api_url": {
Type: schema.TypeString,
Optional: true,
ValidateFunc: validation.IsURLWithHTTPS,
},
"client_id": {
Type: schema.TypeString,
Optional: true,
},
"client_secret": {
Type: schema.TypeString,
Optional: true,
Sensitive: true,
},
},
},
},
}
}
Loading

0 comments on commit 5b71a30

Please sign in to comment.