From ac0f03c5a9e1b2a4d41e695d1014d0d4f217830d Mon Sep 17 00:00:00 2001 From: Len Smith Date: Tue, 28 Jan 2025 18:52:09 -0800 Subject: [PATCH] feat(resource_access_policy): Add support for allowed_subnets --- docs/resources/cloud_access_policy.md | 8 ++ .../cloud/resource_cloud_access_policy.go | 71 +++++++++++++- ...cloud_access_policy_allowed_subnet_test.go | 96 +++++++++++++++++++ 3 files changed, 170 insertions(+), 5 deletions(-) create mode 100644 internal/resources/cloud/resource_cloud_access_policy_allowed_subnet_test.go diff --git a/docs/resources/cloud_access_policy.md b/docs/resources/cloud_access_policy.md index 5692d6edf..79510d62d 100644 --- a/docs/resources/cloud_access_policy.md +++ b/docs/resources/cloud_access_policy.md @@ -64,6 +64,7 @@ resource "grafana_cloud_access_policy_token" "test" { ### Optional +- `conditions` (Block Set ) (see [below for nested schema](#nestedblock--conditions)) - `display_name` (String) Display name of the access policy. Defaults to the name. ### Read-Only @@ -73,6 +74,13 @@ resource "grafana_cloud_access_policy_token" "test" { - `policy_id` (String) ID of the access policy. - `updated_at` (String) Last update date of the access policy. + +### Nested Schema for `conditions` + +Required: + +- `allowed_subnets` (Set of String) IP range based access control for the access policy. Connections initiated from IP addresses outside of the specified ranges will be denied. + ### Nested Schema for `realm` diff --git a/internal/resources/cloud/resource_cloud_access_policy.go b/internal/resources/cloud/resource_cloud_access_policy.go index a50c548c6..c0dcac361 100644 --- a/internal/resources/cloud/resource_cloud_access_policy.go +++ b/internal/resources/cloud/resource_cloud_access_policy.go @@ -3,6 +3,7 @@ package cloud import ( "context" "fmt" + "net" "strings" "time" @@ -22,6 +23,19 @@ var ( ) func resourceAccessPolicy() *common.Resource { + cloudAccessPolicyConditionSchema := &schema.Resource{ + Schema: map[string]*schema.Schema{ + "allowed_subnets": { + Type: schema.TypeSet, + Required: true, + Description: "Conditions that apply to the access policy,such as IP Allow lists.", + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: validateCloudAccessPolicyAllowedSubnets, + }, + }, + }, + } cloudAccessPolicyRealmSchema := &schema.Resource{ Schema: map[string]*schema.Schema{ "type": { @@ -74,11 +88,10 @@ Required access policy scopes: Schema: map[string]*schema.Schema{ "region": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - Description: "Region where the API is deployed. Generally where the stack is deployed. Use the region list API to get the list of available regions: https://grafana.com/docs/grafana-cloud/developer-resources/api-reference/cloud-api/#list-regions.", - ValidateFunc: validation.StringIsNotEmpty, + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Region where the API is deployed. Generally where the stack is deployed. Use the region list API to get the list of available regions: https://grafana.com/docs/grafana-cloud/developer-resources/api-reference/cloud-api/#list-regions.", }, "name": { Type: schema.TypeString, @@ -111,6 +124,12 @@ Required access policy scopes: Required: true, Elem: cloudAccessPolicyRealmSchema, }, + "conditions": { + Type: schema.TypeSet, + Optional: true, + Description: "Conditions for the access policy.", + Elem: cloudAccessPolicyConditionSchema, + }, // Computed "policy_id": { @@ -184,7 +203,9 @@ func createCloudAccessPolicy(ctx context.Context, d *schema.ResourceData, client DisplayName: &displayName, Scopes: common.ListToStringSlice(d.Get("scopes").(*schema.Set).List()), Realms: expandCloudAccessPolicyRealm(d.Get("realm").(*schema.Set).List()), + Conditions: expandCloudAccessPolicyConditions(d.Get("conditions").(*schema.Set).List()), }) + result, _, err := req.Execute() if err != nil { return apiError(err) @@ -212,6 +233,7 @@ func updateCloudAccessPolicy(ctx context.Context, d *schema.ResourceData, client DisplayName: &displayName, Scopes: common.ListToStringSlice(d.Get("scopes").(*schema.Set).List()), Realms: expandCloudAccessPolicyRealm(d.Get("realm").(*schema.Set).List()), + Conditions: expandCloudAccessPolicyConditions(d.Get("conditions").(*schema.Set).List()), }) if _, _, err = req.Execute(); err != nil { return apiError(err) @@ -238,6 +260,7 @@ func readCloudAccessPolicy(ctx context.Context, d *schema.ResourceData, client * d.Set("display_name", result.DisplayName) d.Set("scopes", result.Scopes) d.Set("realm", flattenCloudAccessPolicyRealm(result.Realms)) + d.Set("conditions", flattenCloudAccessPolicyConditions(result.Conditions)) d.Set("created_at", result.CreatedAt.Format(time.RFC3339)) if updated := result.UpdatedAt; updated != nil { d.Set("updated_at", updated.Format(time.RFC3339)) @@ -266,6 +289,14 @@ func validateCloudAccessPolicyScope(v interface{}, path cty.Path) diag.Diagnosti return nil } +func validateCloudAccessPolicyAllowedSubnets(v interface{}, path cty.Path) diag.Diagnostics { + _, _, err := net.ParseCIDR(v.(string)) + if err == nil { + return nil + } + return diag.Errorf("Invalid IP CIDR : %s.", v.(string)) +} + func flattenCloudAccessPolicyRealm(realm []gcom.AuthAccessPolicyRealmsInner) []interface{} { var result []interface{} @@ -283,9 +314,39 @@ func flattenCloudAccessPolicyRealm(realm []gcom.AuthAccessPolicyRealmsInner) []i "label_policy": labelPolicy, }) } + + return result +} + +func flattenCloudAccessPolicyConditions(condition *gcom.AuthAccessPolicyConditions) []interface{} { + var result []interface{} + var allowedSubnets []string + + for _, sn := range condition.GetAllowedSubnets() { + allowedSubnets = append(allowedSubnets, *sn.String) + } + + result = append(result, map[string]interface{}{ + "allowed_subnets": allowedSubnets, + }) + return result } +func expandCloudAccessPolicyConditions(condition []interface{}) *gcom.PostAccessPoliciesRequestConditions { + + var result gcom.PostAccessPoliciesRequestConditions + + for _, c := range condition { + c := c.(map[string]interface{}) + for _, as := range c["allowed_subnets"].(*schema.Set).List() { + result.AllowedSubnets = append(result.AllowedSubnets, as.(string)) + } + } + + return &result +} + func expandCloudAccessPolicyRealm(realm []interface{}) []gcom.PostAccessPoliciesRequestRealmsInner { var result []gcom.PostAccessPoliciesRequestRealmsInner diff --git a/internal/resources/cloud/resource_cloud_access_policy_allowed_subnet_test.go b/internal/resources/cloud/resource_cloud_access_policy_allowed_subnet_test.go new file mode 100644 index 000000000..7b65efe1c --- /dev/null +++ b/internal/resources/cloud/resource_cloud_access_policy_allowed_subnet_test.go @@ -0,0 +1,96 @@ +package cloud_test + +import ( + "fmt" + "os" + "strings" + + "testing" + + "github.com/grafana/grafana-com-public-clients/go/gcom" + "github.com/grafana/terraform-provider-grafana/v3/internal/testutils" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestResourceAccessPolicy_AllowedSubnets(t *testing.T) { + + t.Parallel() + testutils.CheckCloudAPITestsEnabled(t) + + var policy gcom.AuthAccessPolicy + + scopes := []string{ + "accesspolicies:read", + } + + initialAllowedSubnets := []string{ + "10.0.0.29/32", + } + updatedAllowedSubnets := []string{ + "10.0.0.29/32", + "10.0.0.20/32", + } + + randomName := acctest.RandStringFromCharSet(6, acctest.CharSetAlpha) + + resource.Test(t, resource.TestCase{ + ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories, + CheckDestroy: testAccCloudAccessPolicyCheckDestroy("us", &policy), + Steps: []resource.TestStep{ + { + Config: testAccCloudAccessPolicyConfigAllowedSubnets(randomName, "display name", "us", scopes, initialAllowedSubnets), + Check: resource.ComposeTestCheckFunc( + testAccCloudAccessPolicyCheckExists("grafana_cloud_access_policy.test", &policy), + + resource.TestCheckResourceAttr("grafana_cloud_access_policy.test", "conditions.#", "1"), + resource.TestCheckResourceAttr("grafana_cloud_access_policy.test", "conditions.0.allowed_subnets.#", "1"), + resource.TestCheckResourceAttr("grafana_cloud_access_policy.test", "conditions.0.allowed_subnets.0", "10.0.0.29/32"), + ), + }, + { + Config: testAccCloudAccessPolicyConfigAllowedSubnets(randomName, "display name", "us", scopes, updatedAllowedSubnets), + Check: resource.ComposeTestCheckFunc( + testAccCloudAccessPolicyCheckExists("grafana_cloud_access_policy.test", &policy), + + resource.TestCheckResourceAttr("grafana_cloud_access_policy.test", "conditions.#", "1"), + resource.TestCheckResourceAttr("grafana_cloud_access_policy.test", "conditions.0.allowed_subnets.#", "2"), + resource.TestCheckResourceAttr("grafana_cloud_access_policy.test", "conditions.0.allowed_subnets.0", "10.0.0.20/32"), + resource.TestCheckResourceAttr("grafana_cloud_access_policy.test", "conditions.0.allowed_subnets.1", "10.0.0.29/32"), + ), + }, + }, + }) +} + +// Returns terraform manifests for an Cloud Access Policy with Allowed Subnets defined. +func testAccCloudAccessPolicyConfigAllowedSubnets(name, displayName, region string, scopes []string, allowedSubnets []string) string { + if displayName != "" { + displayName = fmt.Sprintf("display_name = \"%s\"", displayName) + } + + return fmt.Sprintf(` + data "grafana_cloud_organization" "current" { + slug = "%[4]s" + } + + resource "grafana_cloud_access_policy" "test" { + region = "%[5]s" + name = "%[1]s" + %[2]s + + scopes = ["%[3]s"] + + realm { + type = "org" + identifier = data.grafana_cloud_organization.current.id + } + + conditions { + allowed_subnets = ["%[6]s"] + } + + } + + `, name, displayName, strings.Join(scopes, `","`), os.Getenv("GRAFANA_CLOUD_ORG"), region, strings.Join(allowedSubnets, `","`)) +}