feat: add conditions and expiration support to IAM policies

Signed-off-by: Pascal T. <pascal@toepke.dev>
This commit is contained in:
Pascal T. 2025-11-27 23:45:18 +01:00
parent 78325622b7
commit 088c632a3c
13 changed files with 887 additions and 70 deletions

1
.gitignore vendored
View file

@ -24,6 +24,7 @@ website/node_modules
*.iml
*.test
*.iml
*.env
website/vendor

View file

@ -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.

View file

@ -31,6 +31,45 @@ resource "ovh_iam_policy" "manager" {
"account:apiovh:*",
]
}
resource "ovh_iam_policy" "ip_prod_access" {
name = "ip_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_expiring" {
name = "workdays_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 = "MATCH"
values = {
"date(Europe/Paris).WeekDay.In" = "monday,tuesday,wednesday,thursday,friday"
}
}
expired_at = "2026-12-31T23:59:59Z"
}
```
## Argument Reference
@ -43,6 +82,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`
## Attributes Reference

View file

@ -20,3 +20,42 @@ resource "ovh_iam_policy" "manager" {
"account:apiovh:*",
]
}
resource "ovh_iam_policy" "ip_prod_access" {
name = "ip_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_expiring" {
name = "workdays_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 = "MATCH"
values = {
"date(Europe/Paris).WeekDay.In" = "monday,tuesday,wednesday,thursday,friday"
}
}
expired_at = "2026-12-31T23:59:59Z"
}

View file

@ -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": {
@ -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,
}
@ -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
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
}

View file

@ -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{
@ -158,3 +192,43 @@ output "keys_present" {
)
}
`
const testAccIamPolicyDataSourceConfig = `
resource "ovh_me_identity_user" "test_user" {
login = "%s"
email = "%s@terraform.test"
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
}
`

View file

@ -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)

View file

@ -9,85 +9,278 @@ import (
)
func resourceIamPolicy() *schema.Resource {
return &schema.Resource{
Importer: &schema.ResourceImporter{
State: func(rd *schema.ResourceData, i interface{}) ([]*schema.ResourceData, error) {
return []*schema.ResourceData{rd}, nil
},
},
// Define the deepest level first (e.g., 3 levels deep)
conditionLevel3Schema := &schema.Resource{
Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
"operator": {
Type: schema.TypeString,
Required: true,
Description: "Operator for this condition (MATCH, AND, OR, NOT)",
},
"description": {
Type: schema.TypeString,
"values": {
Type: schema.TypeMap,
Optional: true,
Description: "Key-value pairs to match (e.g., resource.Tag(name), date(Europe/Paris).WeekDay, request.IP)",
Elem: &schema.Schema{Type: schema.TypeString},
},
"identities": {
Type: schema.TypeSet,
// 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,
Required: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
Description: "Operator for this condition (MATCH, AND, OR, NOT)",
},
"resources": {
Type: schema.TypeSet,
Required: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"allow": {
Type: schema.TypeSet,
"values": {
Type: schema.TypeMap,
Optional: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
Description: "Key-value pairs to match (e.g., resource.Tag(name), date(Europe/Paris).WeekDay, request.IP)",
Elem: &schema.Schema{Type: schema.TypeString},
},
"except": {
Type: schema.TypeSet,
"condition": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"deny": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"permissions_groups": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"owner": {
Type: schema.TypeString,
Computed: true,
},
"created_at": {
Type: schema.TypeString,
Computed: true,
},
"updated_at": {
Type: schema.TypeString,
Computed: true,
},
"read_only": {
Type: schema.TypeBool,
Computed: true,
Description: "A list of nested conditions. This is the recursive part.",
Elem: conditionLevel3Schema, // Points to the next level
},
},
ReadContext: resourceIamPolicyRead,
}
// Define the first level of conditions, pointing to the second level
conditionLevel1Schema := &schema.Resource{
Schema: map[string]*schema.Schema{
"operator": {
Type: schema.TypeString,
Required: true,
Description: "Operator for this condition (MATCH, AND, OR, NOT)",
},
"values": {
Type: schema.TypeMap,
Optional: 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,
Optional: true,
Description: "A list of nested conditions. This is the recursive part.",
Elem: conditionLevel2Schema, // Points to the next level
},
},
}
return &schema.Resource{
Importer: &schema.ResourceImporter{
State: func(rd *schema.ResourceData, i interface{}) ([]*schema.ResourceData, error) {
return []*schema.ResourceData{rd}, nil
},
},
Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
},
"description": {
Type: schema.TypeString,
Optional: true,
},
"identities": {
Type: schema.TypeSet,
Required: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"resources": {
Type: schema.TypeSet,
Required: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"allow": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"except": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"deny": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"permissions_groups": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
"owner": {
Type: schema.TypeString,
Computed: true,
},
"created_at": {
Type: schema.TypeString,
Computed: true,
},
"updated_at": {
Type: schema.TypeString,
Computed: true,
},
"read_only": {
Type: schema.TypeBool,
Computed: true,
},
"expired_at": {
Type: schema.TypeString,
Optional: true,
Description: "Expiration date of the policy, after this date it will no longer be applied",
},
"conditions": {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Description: "Conditions restrict permissions following resources, date or customer's information",
Elem: conditionLevel1Schema, // The top-level conditions use the first level schema
},
},
ReadContext: resourceIamPolicyRead,
CreateContext: resourceIamPolicyCreate,
UpdateContext: resourceIamPolicyUpdate,
DeleteContext: resourceIamPolicyDelete,
}
}
func resourceIamPolicyRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
@ -192,5 +385,67 @@ func prepareIamPolicyCall(d *schema.ResourceData) IamPolicy {
out.PermissionsGroups = append(out.PermissionsGroups, PermissionGroup{Urn: e.(string)})
}
}
if expiredAt, ok := d.GetOk("expired_at"); ok {
out.ExpiredAt = expiredAt.(string)
}
if conditions, ok := d.GetOk("conditions"); ok {
out.Conditions = expandConditions(conditions.([]interface{}))
}
return out
}
// expandConditions converts Terraform schema data to IamConditions
func expandConditions(tfList []interface{}) *IamConditions {
if len(tfList) == 0 {
return nil
}
tfMap := tfList[0].(map[string]interface{})
conditions := &IamConditions{
Operator: tfMap["operator"].(string),
}
if values, ok := tfMap["values"].(map[string]interface{}); ok {
conditions.Values = make(map[string]string)
for k, v := range values {
conditions.Values[k] = v.(string)
}
}
if condList, ok := tfMap["condition"].([]interface{}); ok {
for _, c := range condList {
condMap := c.(map[string]interface{})
condition := expandCondition(condMap)
conditions.Conditions = append(conditions.Conditions, condition)
}
}
return conditions
}
// expandCondition recursively expands a single condition
func expandCondition(tfMap map[string]interface{}) *IamCondition {
condition := &IamCondition{
Operator: tfMap["operator"].(string),
}
if values, ok := tfMap["values"].(map[string]interface{}); ok {
condition.Values = make(map[string]string)
for k, v := range values {
condition.Values[k] = v.(string)
}
}
// Handle nested conditions recursively
if nestedList, ok := tfMap["condition"].([]interface{}); ok {
for _, nested := range nestedList {
nestedMap := nested.(map[string]interface{})
condition.Conditions = append(condition.Conditions, expandCondition(nestedMap))
}
}
return condition
}

View file

@ -106,6 +106,108 @@ func TestAccIamPolicy_deny(t *testing.T) {
})
}
func TestAccIamPolicy_withConditions(t *testing.T) {
name := acctest.RandomWithPrefix(test_prefix)
desc := "IAM policy with conditions created by Terraform Acc"
userName := acctest.RandomWithPrefix(test_prefix)
res := "urn:v1:eu:resource:vps:*"
config := fmt.Sprintf(testAccIamPolicyConditionsConfig, userName, userName, name, desc, res)
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheckCredentials(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: config,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("ovh_iam_policy.policy1", "name", name),
resource.TestCheckResourceAttr("ovh_iam_policy.policy1", "description", desc),
resource.TestCheckResourceAttr("ovh_iam_policy.policy1", "conditions.0.operator", "OR"),
resource.TestCheckResourceAttr("ovh_iam_policy.policy1", "conditions.0.condition.0.operator", "MATCH"),
resource.TestCheckResourceAttr("ovh_iam_policy.policy1", "conditions.0.condition.0.values.resource.Tag(environment)", "production"),
),
},
},
})
}
func TestAccIamPolicy_withOrConditions(t *testing.T) {
name := acctest.RandomWithPrefix(test_prefix)
desc := "IAM policy with OR conditions created by Terraform Acc"
userName := acctest.RandomWithPrefix(test_prefix)
res := "urn:v1:eu:resource:vps:*"
config := fmt.Sprintf(testAccIamPolicyOrConditionsConfig, userName, userName, name, desc, res)
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheckCredentials(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: config,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("ovh_iam_policy.policy1", "name", name),
resource.TestCheckResourceAttr("ovh_iam_policy.policy1", "description", desc),
resource.TestCheckResourceAttr("ovh_iam_policy.policy1", "conditions.0.operator", "OR"),
resource.TestCheckResourceAttr("ovh_iam_policy.policy1", "conditions.0.condition.0.operator", "MATCH"),
resource.TestCheckResourceAttr("ovh_iam_policy.policy1", "conditions.0.condition.0.values.resource.Tag(environment)", "production"),
resource.TestCheckResourceAttr("ovh_iam_policy.policy1", "conditions.0.condition.1.operator", "MATCH"),
resource.TestCheckResourceAttr("ovh_iam_policy.policy1", "conditions.0.condition.1.values.resource.Tag(team)", "platform"),
),
},
},
})
}
func TestAccIamPolicy_withExpiration(t *testing.T) {
name := acctest.RandomWithPrefix(test_prefix)
desc := "IAM policy with expiration date created by Terraform Acc"
userName := acctest.RandomWithPrefix(test_prefix)
res := "urn:v1:eu:resource:vps:*"
expiration := "2025-12-31T23:59:59Z"
config := fmt.Sprintf(testAccIamPolicyWithExpirationConfig, 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("ovh_iam_policy.policy1", "name", name),
resource.TestCheckResourceAttr("ovh_iam_policy.policy1", "description", desc),
resource.TestCheckResourceAttr("ovh_iam_policy.policy1", "expired_at", expiration),
),
},
},
})
}
func TestAccIamPolicy_withExpirationAndConditions(t *testing.T) {
name := acctest.RandomWithPrefix(test_prefix)
desc := "IAM policy with expiration and conditions created by Terraform Acc"
userName := acctest.RandomWithPrefix(test_prefix)
res := "urn:v1:eu:resource:vps:*"
expiration := "2025-12-31T23:59:59Z"
config := fmt.Sprintf(testAccIamPolicyWithExpirationAndConditionsConfig, 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("ovh_iam_policy.policy1", "name", name),
resource.TestCheckResourceAttr("ovh_iam_policy.policy1", "description", desc),
resource.TestCheckResourceAttr("ovh_iam_policy.policy1", "expired_at", expiration),
resource.TestCheckResourceAttr("ovh_iam_policy.policy1", "conditions.0.operator", "MATCH"),
resource.TestCheckResourceAttr("ovh_iam_policy.policy1", "conditions.0.values.resource.Tag(Environment)", "development"),
),
},
},
})
}
const testAccIamPolicyConfig = `
resource "ovh_me_identity_user" "test_user" {
login = "%s"
@ -138,3 +240,116 @@ resource "ovh_iam_policy" "policy1" {
deny = ["%s"]
}
`
const testAccIamPolicyConditionsConfig = `
resource "ovh_me_identity_user" "test_user" {
login = "%s"
email = "%s@terraform.test"
password = "qwe123!@#"
}
resource "ovh_iam_policy" "policy1" {
name = "%s"
description = "%s"
identities = [ovh_me_identity_user.test_user.urn]
resources = ["%s"]
allow = ["vps:apiovh:*"]
conditions {
operator = "OR"
condition {
operator = "MATCH"
values = {
"resource.Tag(environment)" = "production"
"resource.Tag(team)" = "platform"
}
}
condition {
operator = "MATCH"
values = {
"date(Europe/Paris).WeekDay" = "monday"
}
}
}
}
`
const testAccIamPolicyOrConditionsConfig = `
resource "ovh_me_identity_user" "test_user" {
login = "%s"
email = "%s@terraform.test"
password = "qwe123!@#"
}
resource "ovh_iam_policy" "policy1" {
name = "%s"
description = "%s"
identities = [ovh_me_identity_user.test_user.urn]
resources = ["%s"]
allow = ["vps:apiovh:*"]
conditions {
operator = "OR"
condition {
operator = "MATCH"
values = {
"resource.Tag(environment)" = "production"
}
}
condition {
operator = "MATCH"
values = {
"resource.Tag(team)" = "platform"
}
}
}
}
`
const testAccIamPolicyWithExpirationConfig = `
resource "ovh_me_identity_user" "test_user" {
login = "%s"
email = "%s@terraform.test"
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"
}
`
const testAccIamPolicyWithExpirationAndConditionsConfig = `
resource "ovh_me_identity_user" "test_user" {
login = "%s"
email = "%s@terraform.test"
password = "qwe123!@#"
}
resource "ovh_iam_policy" "policy1" {
name = "%s"
description = "%s"
identities = [ovh_me_identity_user.test_user.urn]
resources = ["%s"]
allow = ["dnsZone:apiovh:get"]
expired_at = "%s"
conditions {
operator = "MATCH"
values = {
"resource.Tag(Environment)" = "development"
}
}
}
`

View file

@ -23,7 +23,7 @@ import (
// Helper
func diagnosticsToError(diags diag.Diagnostics) error {
if diags.HasError() {
return fmt.Errorf(diags[slices.IndexFunc(diags, func(d diag.Diagnostic) bool { return d.Severity == diag.Error })].Summary)
return fmt.Errorf("%s", diags[slices.IndexFunc(diags, func(d diag.Diagnostic) bool { return d.Severity == diag.Error })].Summary)
}
return nil
}

View file

@ -41,8 +41,10 @@ type IamPolicy struct {
Resources []IamResource `json:"resources"`
Permissions IamPermissions `json:"permissions"`
PermissionsGroups []PermissionGroup `json:"permissionsGroups"`
Conditions *IamConditions `json:"conditions,omitempty"`
CreatedAt string `json:"createdAt,omitempty"`
UpdatedAt string `json:"updatedAt,omitempty"`
ExpiredAt string `json:"expiredAt,omitempty"`
ReadOnly bool `json:"readOnly,omitempty"`
Owner string `json:"owner,omitempty"`
}
@ -51,6 +53,18 @@ type PermissionGroup struct {
Urn string `json:"urn"`
}
type IamConditions struct {
Operator string `json:"operator"`
Values map[string]string `json:"values,omitempty"`
Conditions []*IamCondition `json:"conditions,omitempty"`
}
type IamCondition struct {
Operator string `json:"operator"`
Values map[string]string `json:"values,omitempty"`
Conditions []*IamCondition `json:"conditions,omitempty"`
}
func (p IamPolicy) ToMap() map[string]any {
out := make(map[string]any, 0)
out["name"] = p.Name
@ -94,10 +108,57 @@ func (p IamPolicy) ToMap() map[string]any {
if p.UpdatedAt != "" {
out["updated_at"] = p.UpdatedAt
}
if p.ExpiredAt != "" {
out["expired_at"] = p.ExpiredAt
}
if p.Conditions != nil {
out["conditions"] = []interface{}{conditionsToMap(p.Conditions)}
}
return out
}
// conditionsToMap converts IamConditions to a map for Terraform state
func conditionsToMap(c *IamConditions) map[string]interface{} {
out := make(map[string]interface{})
out["operator"] = c.Operator
if len(c.Values) > 0 {
out["values"] = c.Values
}
if len(c.Conditions) > 0 {
conditions := make([]interface{}, 0, len(c.Conditions))
for _, cond := range c.Conditions {
conditions = append(conditions, conditionToMap(cond))
}
out["condition"] = conditions
}
return out
}
// conditionToMap converts a single IamCondition to a map, handling nested conditions recursively
func conditionToMap(cond *IamCondition) map[string]interface{} {
condMap := make(map[string]interface{})
condMap["operator"] = cond.Operator
if len(cond.Values) > 0 {
condMap["values"] = cond.Values
}
// Handle nested conditions recursively
if len(cond.Conditions) > 0 {
nestedConds := make([]interface{}, 0, len(cond.Conditions))
for _, nested := range cond.Conditions {
nestedConds = append(nestedConds, conditionToMap(nested))
}
condMap["condition"] = nestedConds
}
return condMap
}
// IamResource represent a possible information returned when viewing a policy
type IamResource struct {
// URN is always returned and is the urn of the resource or resource group

View file

@ -32,3 +32,14 @@ Use this data source to retrieve am IAM 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.

View file

@ -24,6 +24,20 @@ Creates an IAM policy.
* `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`
## Attributes Reference