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

feat(resource_access_policy): Add support for allowed_subnets #2007

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/resources/cloud_access_policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ resource "grafana_cloud_access_policy_token" "test" {

### Optional

- `conditions` (Block Set) Conditions for the access policy. (see [below for nested schema](#nestedblock--conditions))
- `display_name` (String) Display name of the access policy. Defaults to the name.

### Read-Only
Expand Down Expand Up @@ -92,6 +93,15 @@ Required:

- `selector` (String) The label selector to match in metrics or logs query. Should be in PromQL or LogQL format.



<a id="nestedblock--conditions"></a>
### 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. Ensure you review the [Caveats of IP range based access control](https://grafana.com/docs/grafana-cloud/security-and-account-management/authentication-and-permissions/access-policies/ip-ranges-access-policies/#caveats) before using this parameter.

## Import

Import is supported using the following syntax:
Expand Down
71 changes: 66 additions & 5 deletions internal/resources/cloud/resource_cloud_access_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cloud
import (
"context"
"fmt"
"net"
"strings"
"time"

Expand All @@ -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: "IP range based access control for the access policy. Connections initiated from IP addresses outside of the specified ranges will be denied. Ensure you review the [Caveats of IP range based access control](https://grafana.com/docs/grafana-cloud/security-and-account-management/authentication-and-permissions/access-policies/ip-ranges-access-policies/#caveats) before using this parameter.",
Elem: &schema.Schema{
Type: schema.TypeString,
ValidateDiagFunc: validateCloudAccessPolicyAllowedSubnets,
},
},
},
}
cloudAccessPolicyRealmSchema := &schema.Resource{
Schema: map[string]*schema.Schema{
"type": {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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": {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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))
Expand Down Expand Up @@ -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{}

Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -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, `","`))
}
Loading