mirror of
https://github.com/ovh/terraform-provider-ovh.git
synced 2026-01-11 20:07:09 +00:00
feat: add conditions and expiration support to IAM policies
Signed-off-by: Pascal T. <pascal@toepke.dev>
This commit is contained in:
parent
78325622b7
commit
088c632a3c
13 changed files with 887 additions and 70 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -24,6 +24,7 @@ website/node_modules
|
|||
*.iml
|
||||
*.test
|
||||
*.iml
|
||||
*.env
|
||||
|
||||
website/vendor
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
`
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue