Skip to content
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ website/node_modules
*.iml
*.test
*.iml
*.env

website/vendor

Expand Down
11 changes: 11 additions & 0 deletions docs/data-sources/iam_policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,14 @@ data "ovh_iam_policy" "my_policy" {
* `created_at` - Creation date of this group.
* `updated_at` - Date of the last update of this group.
* `read_only` - Indicates that the policy is a default one.
* `expired_at` - Expiration date of the policy.
* `conditions` - Conditions restricting the policy.

### Conditions

The `conditions` block returns:

* `operator` - Operator to combine conditions.
* `condition` - List of condition blocks. Each condition supports:
* `operator` - Operator for this condition.
* `values` - Map of key-value pairs to match.
62 changes: 62 additions & 0 deletions docs/resources/iam_policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,54 @@ resource "ovh_iam_policy" "manager" {
"account:apiovh:*",
]
}

resource "ovh_iam_policy" "ip_restricted_prod_access" {
name = "ip_restricted_prod_access"
description = "Allow access only from a specific IP to resources tagged prod"
identities = [ovh_me_identity_group.my_group.urn]
resources = ["urn:v1:eu:resource:vps:*"]

allow = [
"vps:apiovh:*",
]

conditions {
operator = "MATCH"
values = {
"resource.Tag(environment)" = "prod"
"request.IP" = "192.72.0.1"
}
}
}

resource "ovh_iam_policy" "workdays_and_ip_restricted_and_expiring" {
name = "workdays_and_ip_restricted_and_expiring"
description = "Allow access only on workdays, expires end of 2026"
identities = [ovh_me_identity_group.my_group.urn]
resources = ["urn:v1:eu:resource:vps:*"]

allow = [
"vps:apiovh:*",
]

conditions {
operator = "AND"
condition {
operator = "MATCH"
values = {
"date(Europe/Paris).WeekDay.In" = "monday,tuesday,wednesday,thursday,friday"
}
}
condition {
operator = "MATCH"
values = {
"request.IP" = "192.72.0.1"
}
}
}

expired_at = "2026-12-31T23:59:59Z"
}
```

## Argument Reference
Expand All @@ -43,6 +91,20 @@ resource "ovh_iam_policy" "manager" {
* `except` - List of overrides of action that must not be allowed even if they are caught by allow. Only makes sens if allow contains wildcards.
* `deny` - List of actions that will always be denied even if also allowed by this policy or another one.
* `permissions_groups` - Set of permissions groups included in the policy. At evaluation, these permissions groups are each evaluated independently (notably, excepts actions only affect actions in the same permission group).
* `expired_at` - (Optional) Expiration date of the policy in RFC3339 format (e.g., `2025-12-31T23:59:59Z`). After this date, the policy will no longer be applied.
* `conditions` - (Optional) Conditions restrict permissions based on resource tags, date/time, or request attributes. See Conditions below.

### Conditions

The `conditions` block supports:

* `operator` - (Required) Operator to combine conditions. Valid values are `AND`, `OR`, `NOT`, or `MATCH`.
* `condition` - (Optional) List of condition blocks. Each condition supports:
* `operator` - (Required) Operator for this condition (typically `MATCH`).
* `values` - (Optional) Map of key-value pairs to match. Keys can reference:
* Resource tags: `resource.Tag(tag_name)` (e.g., `resource.Tag(environment)`)
* Date/time: `date(timezone).WeekDay`, `date(timezone).WeekDay.In` (e.g., `date(Europe/Paris).WeekDay`)
* Request attributes: `request.IP`
Comment on lines +94 to +107
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it could be useful to emphasize the 3-level depth limitation of conditions here


## Attributes Reference

Expand Down
48 changes: 48 additions & 0 deletions examples/resources/iam_policy/example_1.tf
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,51 @@ resource "ovh_iam_policy" "manager" {
"account:apiovh:*",
]
}

resource "ovh_iam_policy" "ip_restricted_prod_access" {
name = "ip_restricted_prod_access"
description = "Allow access only from a specific IP to resources tagged prod"
identities = [ovh_me_identity_group.my_group.urn]
resources = ["urn:v1:eu:resource:vps:*"]

allow = [
"vps:apiovh:*",
]

conditions {
operator = "MATCH"
values = {
"resource.Tag(environment)" = "prod"
"request.IP" = "192.72.0.1"
}
}
}

resource "ovh_iam_policy" "workdays_and_ip_restricted_and_expiring" {
name = "workdays_and_ip_restricted_and_expiring"
description = "Allow access only on workdays, expires end of 2026"
identities = [ovh_me_identity_group.my_group.urn]
resources = ["urn:v1:eu:resource:vps:*"]

allow = [
"vps:apiovh:*",
]

conditions {
operator = "AND"
condition {
operator = "MATCH"
values = {
"date(Europe/Paris).WeekDay.In" = "monday,tuesday,wednesday,thursday,friday"
}
}
condition {
operator = "MATCH"
values = {
"request.IP" = "192.72.0.1"
}
}
}

expired_at = "2026-12-31T23:59:59Z"
}
93 changes: 88 additions & 5 deletions ovh/data_iam_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,70 @@ import (
)

func dataSourceIamPolicy() *schema.Resource {
// Define the deepest level first (e.g., 3 levels deep)
conditionLevel3Schema := &schema.Resource{
Schema: map[string]*schema.Schema{
"operator": {
Type: schema.TypeString,
Computed: true,
Description: "Operator for this condition (MATCH, AND, OR, NOT)",
},
"values": {
Type: schema.TypeMap,
Computed: true,
Description: "Key-value pairs to match (e.g., resource.Tag(name), date(Europe/Paris).WeekDay, request.IP)",
Elem: &schema.Schema{Type: schema.TypeString},
},
// No further "condition" Elem here to limit depth
},
}

// Define the second level of conditions, pointing to the third level
conditionLevel2Schema := &schema.Resource{
Schema: map[string]*schema.Schema{
"operator": {
Type: schema.TypeString,
Computed: true,
Description: "Operator for this condition (MATCH, AND, OR, NOT)",
},
"values": {
Type: schema.TypeMap,
Computed: true,
Description: "Key-value pairs to match (e.g., resource.Tag(name), date(Europe/Paris).WeekDay, request.IP)",
Elem: &schema.Schema{Type: schema.TypeString},
},
"condition": {
Type: schema.TypeList,
Computed: true,
Description: "A list of nested conditions. This is the recursive part.",
Elem: conditionLevel3Schema, // Points to the next level
},
},
}

// Define the first level of conditions, pointing to the second level
conditionLevel1Schema := &schema.Resource{
Schema: map[string]*schema.Schema{
"operator": {
Type: schema.TypeString,
Computed: true,
Description: "Operator for this condition (MATCH, AND, OR, NOT)",
},
"values": {
Type: schema.TypeMap,
Computed: true,
Description: "Key-value pairs to match (e.g., resource.Tag(name), date(Europe/Paris).WeekDay, request.IP)",
Elem: &schema.Schema{Type: schema.TypeString},
},
"condition": {
Type: schema.TypeList,
Computed: true,
Description: "A list of nested conditions. This is the recursive part.",
Elem: conditionLevel2Schema, // Points to the next level
},
},
}

return &schema.Resource{
Schema: map[string]*schema.Schema{
"id": {
Expand Down Expand Up @@ -81,6 +145,17 @@ func dataSourceIamPolicy() *schema.Resource {
Type: schema.TypeBool,
Computed: true,
},
"expired_at": {
Type: schema.TypeString,
Computed: true,
Description: "Expiration date of the policy, after this date it will no longer be applied",
},
"conditions": {
Type: schema.TypeList,
Computed: true,
Description: "Conditions restrict permissions following resources, date or customer's information",
Elem: conditionLevel1Schema, // The top-level conditions use the first level schema
},
},
ReadContext: datasourceIamPolicyRead,
}
Expand All @@ -96,12 +171,20 @@ func datasourceIamPolicyRead(ctx context.Context, d *schema.ResourceData, meta a
return diag.FromErr(err)
}

for k, v := range pol.ToMap() {
err := d.Set(k, v)
if err != nil {
return diag.Errorf("key: %s; value: %v; err: %v", k, v, err)
}
// Debug: Log what we got from the API
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment to remove ?

polMap := pol.ToMap()
for k, v := range polMap {
d.Set(k, v)
}

// Explicitly set the new attributes to ensure they're available
if pol.ExpiredAt != "" {
d.Set("expired_at", pol.ExpiredAt)
}
if pol.Conditions != nil {
d.Set("conditions", []interface{}{conditionsToMap(pol.Conditions)})
}

d.SetId(id)
return nil
}
74 changes: 74 additions & 0 deletions ovh/data_iam_policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,40 @@ func TestAccIamPolicyDataSource_basic(t *testing.T) {
})
}

func TestAccIamPolicyDataSource_withConditionsAndExpiration(t *testing.T) {
name := acctest.RandomWithPrefix(test_prefix)
desc := "IAM policy with conditions and expiration created by Terraform Acc"
userName := acctest.RandomWithPrefix(test_prefix)
res := "urn:v1:eu:resource:vps:*"
expiration := "2025-12-31T23:59:59Z"
config := fmt.Sprintf(testAccIamPolicyDataSourceConfig, userName, userName, name, desc, res, expiration)

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheckCredentials(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: config,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("data.ovh_iam_policy.policy", "name", name),
resource.TestCheckResourceAttr("data.ovh_iam_policy.policy", "description", desc),
resource.TestCheckResourceAttr("data.ovh_iam_policy.policy", "expired_at", expiration),
resource.TestCheckResourceAttr("data.ovh_iam_policy.policy", "conditions.#", "1"),
resource.TestCheckResourceAttr("data.ovh_iam_policy.policy", "conditions.0.operator", "OR"),
resource.TestCheckResourceAttr("data.ovh_iam_policy.policy", "conditions.0.condition.#", "2"),
resource.TestCheckResourceAttr("data.ovh_iam_policy.policy", "conditions.0.condition.0.operator", "MATCH"),
resource.TestCheckResourceAttr("data.ovh_iam_policy.policy", "conditions.0.condition.0.values.%", "2"),
resource.TestCheckResourceAttr("data.ovh_iam_policy.policy", "conditions.0.condition.0.values.resource.Tag(environment)", "production"),
resource.TestCheckResourceAttr("data.ovh_iam_policy.policy", "conditions.0.condition.0.values.resource.Tag(team)", "platform"),
resource.TestCheckResourceAttr("data.ovh_iam_policy.policy", "conditions.0.condition.1.operator", "MATCH"),
resource.TestCheckResourceAttr("data.ovh_iam_policy.policy", "conditions.0.condition.1.values.%", "1"),
resource.TestCheckResourceAttr("data.ovh_iam_policy.policy", "conditions.0.condition.1.values.date(Europe/Paris).WeekDay", "monday"),
),
},
},
})
}

func checkIamPolicyResourceAttr(name, polName, desc, resourceURN, allowAction, exceptAction, denyAction string) []resource.TestCheckFunc {
// we are not checking identity urn because they are dynamic and depend on the test account NIC
checks := []resource.TestCheckFunc{
Expand Down Expand Up @@ -158,3 +192,43 @@ output "keys_present" {
)
}
`

const testAccIamPolicyDataSourceConfig = `
resource "ovh_me_identity_user" "test_user" {
login = "%s"
email = "%[email protected]"
password = "qwe123!@#"
}

resource "ovh_iam_policy" "policy1" {
name = "%s"
description = "%s"
identities = [ovh_me_identity_user.test_user.urn]
resources = ["%s"]
allow = ["vps:apiovh:*"]
expired_at = "%s"

conditions {
operator = "OR"

condition {
operator = "MATCH"
values = {
"resource.Tag(environment)" = "production"
"resource.Tag(team)" = "platform"
}
}

condition {
operator = "MATCH"
values = {
"date(Europe/Paris).WeekDay" = "monday"
}
}
}
}

data "ovh_iam_policy" "policy" {
id = ovh_iam_policy.policy1.id
}
`
2 changes: 1 addition & 1 deletion ovh/resource_cloud_project_network_private.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ func resourceCloudProjectNetworkPrivateRead(d *schema.ResourceData, meta interfa
region_status["status"] = r.Regions[i].Status
regions_status = append(regions_status, region_status)

regions = append(regions, fmt.Sprintf(r.Regions[i].Region))
regions = append(regions, r.Regions[i].Region)
}
d.Set("regions_attributes", regions_attributes)
d.Set("regions_openstack_ids", regions_openstack_ids)
Expand Down
Loading