From 255b813b40574970f16c402ba4c0bf5d13ee7034 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Gniewek-W=C4=99grzyn?= Date: Wed, 17 Jul 2024 14:57:00 +0200 Subject: [PATCH] !feat: update module with latest snowflake provider changes - removals and deprecations --- .pre-commit-config.yaml | 10 +- .terraform-docs.yml | 4 + README.md | 101 ++++++++++----- examples/complete/README.md | 93 ++++++++++++++ examples/complete/fixtures.tfvars | 13 ++ examples/complete/main.tf | 187 ++++++++++++++++++++------- examples/complete/versions.tf | 7 ++ examples/simple/README.md | 85 +++++++++++++ examples/simple/main.tf | 19 +++ examples/simple/versions.tf | 7 ++ locals.tf | 152 ++++++++++++---------- main.tf | 161 +++++++++++------------- variables.tf | 202 +++++++++++++++++------------- versions.tf | 2 +- 14 files changed, 714 insertions(+), 329 deletions(-) create mode 100644 examples/complete/README.md create mode 100644 examples/complete/fixtures.tfvars create mode 100644 examples/simple/README.md diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2c59029..2631144 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,29 +2,29 @@ repos: - repo: https://github.com/gruntwork-io/pre-commit # Stick to v0.1.20 until this bug is fixed: https://github.com/gruntwork-io/pre-commit/issues/102 # When updating, also check if tflint version in pre-commit workflow can be updated. - rev: "v0.1.20" # Get the latest from: https://github.com/gruntwork-io/pre-commit/releases + rev: "v0.1.23" # Get the latest from: https://github.com/gruntwork-io/pre-commit/releases hooks: - id: terraform-validate # It should be the first step as it runs terraform init required by tflint - id: terraform-fmt - id: tflint args: - --module - - --config=.tflint.hcl + #- --config=.tflint.hcl - repo: https://github.com/terraform-docs/terraform-docs - rev: "v0.16.0" # Get the latest from: https://github.com/terraform-docs/terraform-docs/releases + rev: "v0.18.0" # Get the latest from: https://github.com/terraform-docs/terraform-docs/releases hooks: - id: terraform-docs-go args: ["."] - repo: https://github.com/bridgecrewio/checkov.git - rev: "2.5.13" # Get the latest from: https://github.com/bridgecrewio/checkov/releases + rev: "3.2.192" # Get the latest from: https://github.com/bridgecrewio/checkov/releases hooks: - id: checkov args: [--skip-check, "CKV_TF_1"] # Terraform module sources do not use a git url with a commit hash revision - repo: https://github.com/pre-commit/pre-commit-hooks - rev: "v4.5.0" # Get the latest from: https://github.com/pre-commit/pre-commit-hooks/releases + rev: "v4.6.0" # Get the latest from: https://github.com/pre-commit/pre-commit-hooks/releases hooks: - id: check-merge-conflict args: ["--assume-in-merge"] diff --git a/.terraform-docs.yml b/.terraform-docs.yml index 5d31cc9..f6ffcef 100644 --- a/.terraform-docs.yml +++ b/.terraform-docs.yml @@ -6,6 +6,10 @@ sections: hide: [] show: [all] +recursive: + enabled: true + path: examples + content: |- {{ .Header }} diff --git a/README.md b/README.md index 109025f..7e1cee9 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ --- Terraform module for managing Snowflake roles. -Additionally, this module allows creating multiple grants on different Snowflake resources. +Additionally, this module allows creating multiple grants on different Snowflake resources, specifying other roles to be granted and grantees (other roles and users). ## USAGE @@ -26,13 +26,21 @@ module "snowflake_role" { granted_to_users = ["JANE_SMITH", "JOHN_DOE"] - database_grants = [ + account_grants = [ { - database_name = "LOGS_DB" - privileges = ["USAGE"] + privileges = ["CREATE DATABASE"] } ] + account_objects_grants = { + "DATABASE" = [ + { + privileges = ["USAGE"] + object_name = "LOGS_DB" + } + ] + } + schema_grants = [ { database_name = "LOGS_DB" @@ -41,14 +49,24 @@ module "snowflake_role" { } ] - table_grants = [ - { - database_name = "LOGS_DB" - schema_name = "BRONZE" - on_future = true - privileges = ["SELECT"] - } - ] + schema_objects_grants = { + TABLE = [ + { + database_name = "LOGS_DB" + schema_name = "BRONZE" + on_future = true + privileges = ["SELECT"] + } + ] + + VIEW = [ + { + database_name = snowflake_database.this.name + on_future = true + all_privileges = true + } + ] + } } ``` @@ -57,6 +75,29 @@ module "snowflake_role" { - [Simple](examples/simple) - creates a role - [Complete](examples/complete) - creates a role with example grants +## Breaking changes in v2.x of the module + +Due to breaking changes in Snowflake provider and additional code optimizations, **breaking changes** were introduced in `v2.0.0` version of this module. + +List of code and variable (API) changes: + +- Switched to `snowflake_grant_privileges_to_account_role` resource instead of provider-removed `snowflake_*_grant` +- Switched to `snowflake_grant_account_role` resource instead of provider-removed `snowflake_role_grants` +- Switched to `snowflake_grant_ownership` resource instead of provider-removed `snowflake_role_ownership_grant` +- Variable `account_grants` type changed from `list(string)` to `list(object({..}))` +- Variable `schema_grants` type changed +- Below variables were removed and replaced with aggregated / complex `account_object_grants` and `schema_object_grants`: + - `database_grants` + - `table_grants` + - `external_table_grants` + - `view_grants` + - `dynamic_table_grants` + +When upgrading from `v1.x`, expect most of the resources to be recreated - if recreation is impossible, then it is possible to import some existing resources. + +For more information, refer to [variables.tf](variables.tf), list of inputs below and Snowflake provider documentation + + @@ -66,20 +107,18 @@ module "snowflake_role" { | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [account\_grants](#input\_account\_grants) | Grants on an account level | `list(string)` | `[]` | no | +| [account\_grants](#input\_account\_grants) | Grants on a account level |
list(object({
all_privileges = optional(bool)
with_grant_option = optional(bool, false)
privileges = optional(list(string), null)
}))
| `[]` | no | +| [account\_objects\_grants](#input\_account\_objects\_grants) | Grants on account object level.
Account objects list: USER \| RESOURCE MONITOR \| WAREHOUSE \| COMPUTE POOL \| DATABASE \| INTEGRATION \| FAILOVER GROUP \| REPLICATION GROUP \| EXTERNAL VOLUME
Object type is used as a key in the map.

Exmpale usage:
account_object_grants = {
"WAREHOUSE" = [
{
all_privileges = true
with_grant_option = true
object_name = "TEST_USER"
}
]
"DATABASE" = [
{
privileges = ["CREATE SCHEMA", "CREATE DATABASE ROLE"]
object_name = "TEST_DATABASE"
},
{
privileges = ["CREATE SCHEMA"]
object_name = "OTHER_DATABASE"
}
]
}
Note: You can find a list of all object types [here](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/grant_privileges_to_account_role#nested-schema-for-on_account_object) |
map(list(object({
all_privileges = optional(bool)
with_grant_option = optional(bool, false)
privileges = optional(list(string), null)
object_name = string
})))
| `{}` | no | | [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | | [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | | [comment](#input\_comment) | Role description | `string` | `null` | no | | [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | -| [database\_grants](#input\_database\_grants) | Grants on a database level |
list(object({
database_name = string
privileges = list(string)
enable_multiple_grants = optional(bool)
}))
| `[]` | no | | [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | | [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | | [descriptor\_name](#input\_descriptor\_name) | Name of the descriptor used to form a resource name | `string` | `"snowflake-role"` | no | -| [dynamic\_table\_grants](#input\_dynamic\_table\_grants) | Grants on a dynamic\_table level |
list(object({
database_name = string
schema_name = optional(string)
dynamic_table_name = optional(string)
on_future = optional(bool, false)
on_all = optional(bool, false)
all_privileges = optional(bool)
privileges = optional(list(string), null)
}))
| `[]` | no | -| [enable\_multiple\_grants](#input\_enable\_multiple\_grants) | When this is set to true, multiple grants of the same type can be created for all grants in the role. This will cause Terraform to not revoke grants applied to roles and objects outside Terraform | `bool` | `null` | no | | [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | | [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | -| [external\_table\_grants](#input\_external\_table\_grants) | Grants on a external table level |
list(object({
database_name = string
schema_name = string
external_table_name = optional(string)
on_future = optional(bool)
on_all = optional(bool)
privileges = list(string)
enable_multiple_grants = optional(bool)
}))
| `[]` | no | +| [granted\_database\_roles](#input\_granted\_database\_roles) | Database Roles granted to this role | `list(string)` | `[]` | no | | [granted\_roles](#input\_granted\_roles) | Roles granted to this role | `list(string)` | `[]` | no | | [granted\_to\_roles](#input\_granted\_to\_roles) | Roles which this role is granted to | `list(string)` | `[]` | no | | [granted\_to\_users](#input\_granted\_to\_users) | Users which this role is granted to | `list(string)` | `[]` | no | @@ -92,12 +131,11 @@ module "snowflake_role" { | [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | | [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | | [role\_ownership\_grant](#input\_role\_ownership\_grant) | The name of the role to grant ownership | `string` | `null` | no | -| [schema\_grants](#input\_schema\_grants) | Grants on a schema level |
list(object({
database_name = string
schema_name = optional(string)
privileges = list(string)
on_all = optional(bool)
on_future = optional(bool)
enable_multiple_grants = optional(bool)
}))
| `[]` | no | +| [schema\_grants](#input\_schema\_grants) | Grants on a schema level |
list(object({
all_privileges = optional(bool)
with_grant_option = optional(bool, false)
privileges = optional(list(string), null)
all_schemas_in_database = optional(bool, false)
future_schemas_in_database = optional(bool, false)
database_name = string
schema_name = optional(string, null)
}))
| `[]` | no | +| [schema\_objects\_grants](#input\_schema\_objects\_grants) | Grants on a schema object level

Example usage:
schema_objects_grants = {
"TABLE" = [
{
privileges = ["SELECT"]
object_name = snowflake_table.table_1.name
schema_name = snowflake_schema.this.name
},
{
all_privileges = true
object_name = snowflake_table.table_2.name
schema_name = snowflake_schema.this.name
}
]
"ALERT" = [
{
all_privileges = true
on_future = true
on_all = true
}
]
}
Note: If you don't provide a schema\_name, the grants will be created for all objects of that type in the database.
You can find a list of all object types [here](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/grant_privileges_to_database_role#object_type) |
map(list(object({
all_privileges = optional(bool)
with_grant_option = optional(bool)
privileges = optional(list(string))
object_name = optional(string)
on_all = optional(bool, false)
schema_name = optional(string)
database_name = string
on_future = optional(bool, false)
})))
| `{}` | no | | [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | -| [table\_grants](#input\_table\_grants) | Grants on a table level |
list(object({
database_name = string
schema_name = string
table_name = optional(string)
on_future = optional(bool)
on_all = optional(bool)
privileges = list(string)
enable_multiple_grants = optional(bool)
}))
| `[]` | no | | [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | | [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | -| [view\_grants](#input\_view\_grants) | Grants on a view level |
list(object({
database_name = string
schema_name = string
view_name = optional(string)
on_future = optional(bool)
on_all = optional(bool)
privileges = list(string)
enable_multiple_grants = optional(bool)
}))
| `[]` | no | ## Modules @@ -116,30 +154,29 @@ module "snowflake_role" { | Name | Version | |------|---------| -| [snowflake](#provider\_snowflake) | ~> 0.69 | +| [snowflake](#provider\_snowflake) | ~> 0.93 | ## Requirements | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.3 | -| [snowflake](#requirement\_snowflake) | ~> 0.69 | +| [snowflake](#requirement\_snowflake) | ~> 0.93 | ## Resources | Name | Type | |------|------| -| [snowflake_account_grant.this](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/account_grant) | resource | -| [snowflake_database_grant.this](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/database_grant) | resource | -| [snowflake_external_table_grant.this](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/external_table_grant) | resource | -| [snowflake_grant_privileges_to_role.dynamic_table](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/grant_privileges_to_role) | resource | +| [snowflake_grant_account_role.granted_roles](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/grant_account_role) | resource | +| [snowflake_grant_account_role.granted_to_roles](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/grant_account_role) | resource | +| [snowflake_grant_account_role.granted_to_users](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/grant_account_role) | resource | +| [snowflake_grant_database_role.granted_db_roles](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/grant_database_role) | resource | +| [snowflake_grant_ownership.this](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/grant_ownership) | resource | +| [snowflake_grant_privileges_to_account_role.account_grants](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/grant_privileges_to_account_role) | resource | +| [snowflake_grant_privileges_to_account_role.account_object_grants](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/grant_privileges_to_account_role) | resource | +| [snowflake_grant_privileges_to_account_role.schema_grants](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/grant_privileges_to_account_role) | resource | +| [snowflake_grant_privileges_to_account_role.schema_objects_grants](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/grant_privileges_to_account_role) | resource | | [snowflake_role.this](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/role) | resource | -| [snowflake_role_grants.granted_roles](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/role_grants) | resource | -| [snowflake_role_grants.granted_to](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/role_grants) | resource | -| [snowflake_role_ownership_grant.this](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/role_ownership_grant) | resource | -| [snowflake_schema_grant.this](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/schema_grant) | resource | -| [snowflake_table_grant.this](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/table_grant) | resource | -| [snowflake_view_grant.this](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/view_grant) | resource | ## CONTRIBUTING diff --git a/examples/complete/README.md b/examples/complete/README.md new file mode 100644 index 0000000..9bf26c5 --- /dev/null +++ b/examples/complete/README.md @@ -0,0 +1,93 @@ +# Complete example + +This is complete usage example of `snowflake-role` terraform module. + +## How to plan + +```shell +terraform init +terraform plan -var-file=fixtures.tfvars +``` + +## How to apply + +```shell +terraform init +terraform apply -var-file=fixtures.tfvars +``` + +## How to destroy + +```shell +terraform destroy -var-file=fixtures.tfvars +``` + + + + + + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [snowflake\_role](#module\_snowflake\_role) | ../../ | n/a | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Outputs + +| Name | Description | +|------|-------------| +| [snowflake\_role\_output](#output\_snowflake\_role\_output) | Snowflake role outputs | + +## Providers + +| Name | Version | +|------|---------| +| [snowflake](#provider\_snowflake) | ~> 0.93 | + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [snowflake](#requirement\_snowflake) | ~> 0.93 | + +## Resources + +| Name | Type | +|------|------| +| [snowflake_database.this](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/database) | resource | +| [snowflake_database_role.this](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/database_role) | resource | +| [snowflake_dynamic_table.this](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/dynamic_table) | resource | +| [snowflake_role.role_1](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/role) | resource | +| [snowflake_role.role_2](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/role) | resource | +| [snowflake_schema.schema_1](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/schema) | resource | +| [snowflake_schema.schema_2](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/schema) | resource | +| [snowflake_table.this](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/table) | resource | +| [snowflake_user.this](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/user) | resource | +| [snowflake_warehouse.this](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/warehouse) | resource | + diff --git a/examples/complete/fixtures.tfvars b/examples/complete/fixtures.tfvars new file mode 100644 index 0000000..9290e84 --- /dev/null +++ b/examples/complete/fixtures.tfvars @@ -0,0 +1,13 @@ +namespace = "gid" +stage = "example" + +descriptor_formats = { + snowflake-role = { + labels = ["attributes", "name"] + format = "%v_%v_ROLE" + } +} + +tags = { + Terraform = "True" +} diff --git a/examples/complete/main.tf b/examples/complete/main.tf index 2641985..727234e 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -1,63 +1,156 @@ +resource "snowflake_database" "this" { + name = "SAMPLE_DB" +} + +resource "snowflake_warehouse" "this" { + name = "SAMPLE_WAREHOUSE" +} + +resource "snowflake_schema" "schema_1" { + name = "BRONZE" + database = snowflake_database.this.name +} + +resource "snowflake_schema" "schema_2" { + name = "SILVER" + database = snowflake_database.this.name +} + +resource "snowflake_role" "role_1" { + name = "SAMPLE_ROLE_1" +} + +resource "snowflake_role" "role_2" { + name = "SAMPLE_ROLE_2" +} + +resource "snowflake_database_role" "this" { + name = "SAMPLE_DB_ROLE" + database = snowflake_database.this.name +} + +resource "snowflake_user" "this" { + name = "SAMPLE_USER" +} + +resource "snowflake_table" "this" { + name = "EXAMPLE" + schema = snowflake_schema.schema_1.name + database = snowflake_database.this.name + + column { + name = "ID" + type = "NUMBER" + } +} + +resource "snowflake_dynamic_table" "this" { + name = "EXAMPLE" + + database = snowflake_database.this.name + warehouse = snowflake_warehouse.this.name + schema = snowflake_schema.schema_2.name + query = "SELECT * from ${snowflake_table.this.database}.${snowflake_table.this.schema}.${snowflake_table.this.name}" + target_lag { + maximum_duration = 3600 + } +} + module "snowflake_role" { source = "../../" context = module.this.context - name = "LOGS_DATABASE_READER" + name = "SAMPLE_TEST" - granted_to_users = ["JANE_SMITH", "JOHN_DOE"] + role_ownership_grant = "SYSADMIN" - database_grants = [ - { - database_name = "LOGS_DB" - privileges = ["USAGE"] - enable_multiple_grants = true - } - ] + granted_to_users = ["SAMPLE_USER"] + granted_to_roles = [snowflake_role.role_1.name] - schema_grants = [ - { - database_name = "LOGS_DB" - schema_name = "BRONZE" - privileges = ["USAGE"] - } - ] + granted_roles = [snowflake_role.role_2.name] + granted_database_roles = ["${snowflake_database.this.name}.${snowflake_database_role.this.name}"] - table_grants = [ - { - database_name = "LOGS_DB" - schema_name = "BRONZE" - on_future = true - privileges = ["SELECT"] - } - ] + account_grants = [{ + privileges = ["CREATE DATABASE"] + }] - view_grants = [ - { - database_name = "LOGS_DB" - schema_name = "BRONZE" - on_all = true - privileges = ["SELECT"] - } - ] + account_objects_grants = { + DATABASE = [ + { + privileges = ["USAGE"] + object_name = snowflake_database.this.name + }, + ] + WAREHOUSE = [ + { + all_privileges = true + with_grant_option = true + object_name = snowflake_warehouse.this.name + } + ] + } - dynamic_table_grants = [ - { - database_name = "LOGS_DB" - on_all = true - on_future = true - all_privileges = true - }, + schema_grants = [ { - database_name = "TEST_DB" - schema_name = "BRONZE" - on_all = true - all_privileges = true + database_name = snowflake_database.this.name + schema_name = snowflake_schema.schema_1.name + privileges = ["USAGE"] }, { - database_name = "TEST_DB" - schema_name = "SILVER" - dynamic_table_name = "EXAMPLE" - privileges = ["SELECT"] + database_name = snowflake_database.this.name + schema_name = snowflake_schema.schema_2.name + all_privileges = true + future_schemas_in_database = true + with_grant_option = true }, ] + + schema_objects_grants = { + TABLE = [ + { + database_name = snowflake_database.this.name + schema_name = snowflake_schema.schema_1.name + on_all = true + on_future = true + all_privileges = true + with_grant_option = true + } + ] + + VIEW = [ + { + database_name = snowflake_database.this.name + on_future = true + privileges = ["SELECT"] + } + ] + + "DYNAMIC TABLE" = [ + { + database_name = snowflake_database.this.name + schema_name = snowflake_schema.schema_1.name + on_all = true + all_privileges = true + }, + { + database_name = snowflake_database.this.name + schema_name = snowflake_schema.schema_2.name + object_name = "EXAMPLE" + privileges = ["SELECT"] + }, + ] + } + + depends_on = [ + snowflake_database.this, + snowflake_warehouse.this, + snowflake_schema.schema_1, + snowflake_schema.schema_2, + snowflake_role.role_1, + snowflake_role.role_2, + snowflake_database_role.this, + snowflake_user.this, + snowflake_table.this, + snowflake_dynamic_table.this, + ] } diff --git a/examples/complete/versions.tf b/examples/complete/versions.tf index 12ad22a..24bf322 100644 --- a/examples/complete/versions.tf +++ b/examples/complete/versions.tf @@ -1,3 +1,10 @@ terraform { required_version = ">= 1.3.0" + + required_providers { + snowflake = { + source = "Snowflake-Labs/snowflake" + version = "~> 0.93" + } + } } diff --git a/examples/simple/README.md b/examples/simple/README.md new file mode 100644 index 0000000..51a0577 --- /dev/null +++ b/examples/simple/README.md @@ -0,0 +1,85 @@ +# Simple example + +This is simple usage example of `snowflake-role` terraform module. + +## How to plan + +```shell +terraform init +terraform plan +``` + +## How to apply + +```shell +terraform init +terraform apply +``` + +## How to destroy + +```shell +terraform destroy +``` + + + + + + + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [environment](#input\_environment) | ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT' | `string` | `null` | no | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [snowflake\_role](#module\_snowflake\_role) | ../../ | n/a | +| [this](#module\_this) | cloudposse/label/null | 0.25.0 | + +## Outputs + +| Name | Description | +|------|-------------| +| [snowflake\_role\_output](#output\_snowflake\_role\_output) | Snowflake role outputs | + +## Providers + +| Name | Version | +|------|---------| +| [snowflake](#provider\_snowflake) | ~> 0.93 | + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3 | +| [snowflake](#requirement\_snowflake) | ~> 0.93 | + +## Resources + +| Name | Type | +|------|------| +| [snowflake_database.this](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/database) | resource | + diff --git a/examples/simple/main.tf b/examples/simple/main.tf index ad2d315..431f8fe 100644 --- a/examples/simple/main.tf +++ b/examples/simple/main.tf @@ -1,6 +1,25 @@ +resource "snowflake_database" "this" { + name = "TEST_DB" +} + module "snowflake_role" { source = "../../" context = module.this.context name = "SIMPLE_ROLE" + + account_grants = [ + { + privileges = ["CREATE DATABASE"] + } + ] + + account_objects_grants = { + "DATABASE" = [ + { + all_privileges = true + object_name = snowflake_database.this.name + } + ] + } } diff --git a/examples/simple/versions.tf b/examples/simple/versions.tf index 6964268..ec9a7e5 100644 --- a/examples/simple/versions.tf +++ b/examples/simple/versions.tf @@ -1,3 +1,10 @@ terraform { required_version = ">= 1.3" + + required_providers { + snowflake = { + source = "Snowflake-Labs/snowflake" + version = "~> 0.93" + } + } } diff --git a/locals.tf b/locals.tf index a83d60c..510aa5c 100644 --- a/locals.tf +++ b/locals.tf @@ -5,75 +5,91 @@ locals { lookup(module.role_label.descriptors, var.descriptor_name, module.role_label.id), "/${module.role_label.delimiter}${module.role_label.delimiter}+/", module.role_label.delimiter ), module.role_label.delimiter) : null - granted_roles = var.granted_roles - granted_to_roles = var.granted_to_roles - granted_to_users = var.granted_to_users + account_grants = { + for index, grant in var.account_grants : grant.all_privileges == true ? "ALL" : "CUSTOM_${index}" => grant + } - database_grants = merge([for database_grant in var.database_grants : { - for privilege in database_grant.privileges : "${database_grant.database_name}/${privilege}" => { - database_name = database_grant.database_name - enable_multiple_grants = database_grant.enable_multiple_grants == null ? var.enable_multiple_grants : database_grant.enable_multiple_grants - privilege = privilege - } - }]...) + account_objects_grants = { + for index, grant in flatten([ + for object_type, grants in var.account_objects_grants : [ + for grant in grants : + merge( + grant, + { + object_type = object_type + } + ) + ] + ]) : grant.all_privileges == true ? "${grant.object_type}_${grant.object_name}_ALL" : "${grant.object_type}_${grant.object_name}_CUSTOM_${index}" => grant + } - schema_grants = merge([for schema_grant in var.schema_grants : { - for privilege in schema_grant.privileges : "${schema_grant.database_name}/${coalesce(schema_grant.schema_name, schema_grant.on_future != null ? "on_future" : "on_all")}/${privilege}" => { - database_name = schema_grant.database_name - schema_name = schema_grant.schema_name - on_future = schema_grant.on_future - on_all = schema_grant.on_all - enable_multiple_grants = schema_grant.enable_multiple_grants == null ? var.enable_multiple_grants : schema_grant.enable_multiple_grants - privilege = privilege - } - }]...) + schema_grants = { + for index, schema_grant in flatten([ + for grant in var.schema_grants : grant.future_schemas_in_database && grant.all_schemas_in_database ? [ + merge( + grant, + { + future_schemas_in_database = true, + all_schemas_in_database = false + } + ), + merge( + grant, + { + future_schemas_in_database = false, + all_schemas_in_database = true + } + ) + ] : [grant] + ]) : + "${schema_grant.schema_name != null ? "${schema_grant.database_name}_${schema_grant.schema_name}" : + schema_grant.all_schemas_in_database != false ? "${schema_grant.database_name}_ALL_SCHEMAS" : + schema_grant.future_schemas_in_database != false ? "${schema_grant.database_name}_FUTURE_SCHEMAS" : "" + }_${schema_grant.all_privileges == true ? "ALL" : "CUSTOM_${index}"}" => schema_grant + } - table_grants = merge([for table_grant in var.table_grants : { - for privilege in table_grant.privileges : "${table_grant.database_name}/${table_grant.schema_name}/${coalesce(table_grant.table_name, table_grant.on_future != null ? "on_future" : "on_all")}/${privilege}" => { - database_name = table_grant.database_name - schema_name = table_grant.schema_name - table_name = table_grant.table_name - on_future = table_grant.on_future - on_all = table_grant.on_all - enable_multiple_grants = table_grant.enable_multiple_grants == null ? var.enable_multiple_grants : table_grant.enable_multiple_grants - privilege = privilege - } - }]...) - - external_table_grants = merge([for table_grant in var.external_table_grants : { - for privilege in table_grant.privileges : "${table_grant.database_name}/${table_grant.schema_name}/${coalesce(table_grant.external_table_name, table_grant.on_future != null ? "on_future" : "on_all")}/${privilege}" => { - database_name = table_grant.database_name - schema_name = table_grant.schema_name - external_table_name = table_grant.external_table_name - on_future = table_grant.on_future - on_all = table_grant.on_all - enable_multiple_grants = table_grant.enable_multiple_grants == null ? var.enable_multiple_grants : table_grant.enable_multiple_grants - privilege = privilege - } - }]...) - - view_grants = merge([for view_grant in var.view_grants : { - for privilege in view_grant.privileges : "${view_grant.database_name}/${view_grant.schema_name}/${coalesce(view_grant.view_name, view_grant.on_future != null ? "on_future" : "on_all")}/${privilege}" => { - database_name = view_grant.database_name - schema_name = view_grant.schema_name - view_name = view_grant.view_name - on_future = view_grant.on_future - on_all = view_grant.on_all - enable_multiple_grants = view_grant.enable_multiple_grants == null ? var.enable_multiple_grants : view_grant.enable_multiple_grants - privilege = privilege - } - }]...) - - dynamic_table_grants = merge([for grant in var.dynamic_table_grants : { - for key, value in { "dynamic_table_name" = grant.dynamic_table_name, "on_all" = grant.on_all, "on_future" = grant.on_future } : - "${grant.database_name}/${coalesce(grant.schema_name, "all")}/${key == "dynamic_table_name" ? value : key}" => { - database_name = grant.database_name - schema_name = grant.schema_name - dynamic_table_name = key == "dynamic_table_name" ? value : null - on_future = key == "on_future" ? value : false - on_all = key == "on_all" ? value : false - privileges = grant.privileges - all_privileges = grant.all_privileges - } if(key == "dynamic_table_name" && value != null) || value == true - }]...) + schema_objects_grants = { + for index, grant in flatten([ + for object_type, grants in var.schema_objects_grants : [ + for grant in grants : + grant.on_all && grant.on_future ? [ + merge( + grant, + { + object_type = "${object_type}S", + on_future = true, + on_all = false + } + ), + merge( + grant, + { + object_type = "${object_type}S", + on_future = false, + on_all = true + } + ) + ] : [ + merge( + grant, + { + object_type = grant.on_all || grant.on_future ? "${object_type}S" : object_type + } + ) + ] + ] + ]) : "${ + grant.object_type != null && grant.object_name != null ? + "${grant.object_type}_${grant.database_name}_${grant.schema_name}_${grant.object_name}_${grant.all_privileges == true ? "ALL" : "CUSTOM_${index}"}" + : "" + }${ + grant.on_all != null && grant.on_all ? + "ALL_${grant.object_type}_${grant.database_name}${grant.schema_name != null ? "_${grant.schema_name}_${grant.all_privileges == true ? "ALL" : "CUSTOM_${index}"}" : ""}" + : "" + }${ + grant.on_future != null && grant.on_future ? + "FUTURE_${grant.object_type}_${grant.database_name}${grant.schema_name != null ? "_${grant.schema_name}_${grant.all_privileges == true ? "ALL" : "CUSTOM_${index}"}" : ""}" + : "" + }" => grant + } } diff --git a/main.tf b/main.tf index 4e7fb8f..df18bbc 100644 --- a/main.tf +++ b/main.tf @@ -15,127 +15,112 @@ resource "snowflake_role" "this" { comment = var.comment } -resource "snowflake_role_ownership_grant" "this" { +resource "snowflake_grant_ownership" "this" { count = module.this.enabled && var.role_ownership_grant != null ? 1 : 0 - on_role_name = one(snowflake_role.this[*].name) - to_role_name = var.role_ownership_grant + account_role_name = var.role_ownership_grant + outbound_privileges = "REVOKE" + on { + object_type = "ROLE" + object_name = one(snowflake_role.this[*].name) + } } -resource "snowflake_role_grants" "granted_roles" { - for_each = toset(module.this.enabled ? local.granted_roles : []) +resource "snowflake_grant_account_role" "granted_roles" { + for_each = toset(module.this.enabled ? var.granted_roles : []) - enable_multiple_grants = var.enable_multiple_grants - role_name = each.value - roles = [one(snowflake_role.this[*].name)] + parent_role_name = one(snowflake_role.this[*].name) + role_name = each.value } -resource "snowflake_role_grants" "granted_to" { - count = module.this.enabled && (length(local.granted_to_roles) > 0 || length(local.granted_to_users) > 0) ? 1 : 0 +resource "snowflake_grant_account_role" "granted_to_roles" { + for_each = toset(module.this.enabled ? var.granted_to_roles : []) - enable_multiple_grants = var.enable_multiple_grants - role_name = one(snowflake_role.this[*].name) - roles = local.granted_to_roles - users = local.granted_to_users + role_name = one(snowflake_role.this[*].name) + parent_role_name = each.value } -resource "snowflake_database_grant" "this" { - for_each = module.this.enabled ? local.database_grants : {} +resource "snowflake_grant_account_role" "granted_to_users" { + for_each = toset(module.this.enabled ? var.granted_to_users : []) - enable_multiple_grants = each.value.enable_multiple_grants - database_name = each.value.database_name - privilege = each.value.privilege - roles = [one(snowflake_role.this[*].name)] + role_name = one(snowflake_role.this[*].name) + user_name = each.value } -resource "snowflake_schema_grant" "this" { - for_each = module.this.enabled ? local.schema_grants : {} +resource "snowflake_grant_database_role" "granted_db_roles" { + for_each = toset(module.this.enabled ? var.granted_database_roles : []) - enable_multiple_grants = each.value.enable_multiple_grants - database_name = each.value.database_name - schema_name = each.value.schema_name - privilege = each.value.privilege - on_future = each.value.on_future - on_all = each.value.on_all - roles = [one(snowflake_role.this[*].name)] + database_role_name = each.value + parent_role_name = one(snowflake_role.this[*].name) } -resource "snowflake_table_grant" "this" { - for_each = module.this.enabled ? local.table_grants : {} - - enable_multiple_grants = each.value.enable_multiple_grants - database_name = each.value.database_name - schema_name = each.value.schema_name - table_name = each.value.table_name - privilege = each.value.privilege - on_future = each.value.on_future - on_all = each.value.on_all - roles = [one(snowflake_role.this[*].name)] -} -resource "snowflake_external_table_grant" "this" { - for_each = module.this.enabled ? local.external_table_grants : {} - - enable_multiple_grants = each.value.enable_multiple_grants - database_name = each.value.database_name - schema_name = each.value.schema_name - external_table_name = each.value.external_table_name - privilege = each.value.privilege - on_future = each.value.on_future - on_all = each.value.on_all - roles = [one(snowflake_role.this[*].name)] -} +resource "snowflake_grant_privileges_to_account_role" "account_grants" { + for_each = module.this.enabled ? local.account_grants : {} -resource "snowflake_view_grant" "this" { - for_each = module.this.enabled ? local.view_grants : {} - - enable_multiple_grants = each.value.enable_multiple_grants - database_name = each.value.database_name - schema_name = each.value.schema_name - view_name = each.value.view_name - privilege = each.value.privilege - on_future = each.value.on_future - on_all = each.value.on_all - roles = [one(snowflake_role.this[*].name)] + account_role_name = one(snowflake_role.this[*].name) + on_account = true + + all_privileges = each.value.all_privileges + privileges = each.value.privileges + with_grant_option = each.value.with_grant_option } -resource "snowflake_account_grant" "this" { - for_each = toset(module.this.enabled ? var.account_grants : []) +resource "snowflake_grant_privileges_to_account_role" "account_object_grants" { + for_each = module.this.enabled ? local.account_objects_grants : {} - enable_multiple_grants = var.enable_multiple_grants - privilege = each.value - roles = [one(snowflake_role.this[*].name)] + account_role_name = one(snowflake_role.this[*].name) + all_privileges = each.value.all_privileges + privileges = each.value.privileges + with_grant_option = each.value.with_grant_option - with_grant_option = false + on_account_object { + object_type = each.value.object_type + object_name = each.value.object_name + } } -resource "snowflake_grant_privileges_to_role" "dynamic_table" { - for_each = module.this.enabled ? local.dynamic_table_grants : {} +resource "snowflake_grant_privileges_to_account_role" "schema_grants" { + for_each = module.this.enabled ? local.schema_grants : {} - privileges = each.value.privileges - all_privileges = each.value.all_privileges - role_name = one(snowflake_role.this[*].name) + account_role_name = one(snowflake_role.this[*].name) + all_privileges = each.value.all_privileges + privileges = each.value.privileges + with_grant_option = each.value.with_grant_option - on_schema_object { + on_schema { + all_schemas_in_database = each.value.all_schemas_in_database == true ? each.value.database_name : null + schema_name = each.value.schema_name != null && !each.value.all_schemas_in_database && !each.value.future_schemas_in_database ? "\"${each.value.database_name}\".\"${each.value.schema_name}\"" : null + future_schemas_in_database = each.value.future_schemas_in_database == true ? each.value.database_name : null + } +} - object_type = each.value.dynamic_table_name != null ? "DYNAMIC TABLE" : null - object_name = each.value.dynamic_table_name != null ? join(".", [each.value.database_name, each.value.schema_name, each.value.dynamic_table_name]) : null +resource "snowflake_grant_privileges_to_account_role" "schema_objects_grants" { + for_each = module.this.enabled ? local.schema_objects_grants : {} - dynamic "future" { - for_each = each.value.on_future ? [1] : [] + account_role_name = one(snowflake_role.this[*].name) + all_privileges = each.value.all_privileges + privileges = each.value.privileges + with_grant_option = each.value.with_grant_option + + on_schema_object { + object_type = each.value.object_type != null && !try(each.value.on_all, false) && !try(each.value.on_future, false) ? each.value.object_type : null + object_name = each.value.object_name != null && !try(each.value.on_all, false) && !try(each.value.on_future, false) ? "\"${each.value.database_name}\".\"${each.value.schema_name}\".\"${each.value.object_name}\"" : null + dynamic "all" { + for_each = try(each.value.on_all, false) ? [1] : [] content { - object_type_plural = "DYNAMIC TABLES" - in_database = each.value.schema_name != null ? null : each.value.database_name - in_schema = each.value.schema_name != null ? join(".", [each.value.database_name, each.value.schema_name]) : null + object_type_plural = each.value.object_type + in_database = each.value.schema_name == null ? each.value.database_name : null + in_schema = each.value.schema_name != null ? "\"${each.value.database_name}\".\"${each.value.schema_name}\"" : null } } - dynamic "all" { - for_each = each.value.on_all ? [1] : [] + dynamic "future" { + for_each = try(each.value.on_future, false) ? [1] : [] content { - object_type_plural = "DYNAMIC TABLES" - in_database = each.value.schema_name != null ? null : each.value.database_name - in_schema = each.value.schema_name != null ? join(".", [each.value.database_name, each.value.schema_name]) : null + object_type_plural = each.value.object_type + in_database = each.value.schema_name == null ? each.value.database_name : null + in_schema = each.value.schema_name != null ? "\"${each.value.database_name}\".\"${each.value.schema_name}\"" : null } } } diff --git a/variables.tf b/variables.tf index f589d5c..6997c4d 100644 --- a/variables.tf +++ b/variables.tf @@ -4,12 +4,6 @@ variable "comment" { default = null } -variable "enable_multiple_grants" { - description = "When this is set to true, multiple grants of the same type can be created for all grants in the role. This will cause Terraform to not revoke grants applied to roles and objects outside Terraform" - type = bool - default = null -} - variable "role_ownership_grant" { description = "The name of the role to grant ownership" type = string @@ -22,6 +16,12 @@ variable "granted_roles" { default = [] } +variable "granted_database_roles" { + description = "Database Roles granted to this role" + type = list(string) + default = [] +} + variable "granted_to_roles" { description = "Roles which this role is granted to" type = list(string) @@ -35,111 +35,137 @@ variable "granted_to_users" { } variable "account_grants" { - description = "Grants on an account level" - type = list(string) - default = [] -} - -variable "database_grants" { - description = "Grants on a database level" - type = list(object({ - database_name = string - privileges = list(string) - enable_multiple_grants = optional(bool) - })) - default = [] -} - -variable "schema_grants" { - description = "Grants on a schema level" + description = "Grants on a account level" type = list(object({ - database_name = string - schema_name = optional(string) - privileges = list(string) - on_all = optional(bool) - on_future = optional(bool) - enable_multiple_grants = optional(bool) + all_privileges = optional(bool) + with_grant_option = optional(bool, false) + privileges = optional(list(string), null) })) default = [] validation { - condition = alltrue([for schema_grant in var.schema_grants : anytrue([schema_grant.schema_name != null, schema_grant.on_future, schema_grant.on_all])]) - error_message = "Variable `schema_grants` fails validation - one of `schema_name`, `on_future` or `on_all` has to be set (not null / true)." + condition = alltrue([for grant in var.account_grants : (grant.privileges != null) != (grant.all_privileges == true)]) + error_message = "Variable `account_grants` fails validation - only one of `privileges` or `all_privileges` can be set." } } -variable "table_grants" { - description = "Grants on a table level" - type = list(object({ - database_name = string - schema_name = string - table_name = optional(string) - on_future = optional(bool) - on_all = optional(bool) - privileges = list(string) - enable_multiple_grants = optional(bool) - })) - default = [] - validation { - condition = alltrue([for table_grant in var.table_grants : anytrue([table_grant.table_name != null, table_grant.on_future, table_grant.on_all])]) - error_message = "Variable `table_grants` fails validation - one of `table_name`, `on_future` or `on_all` has to be set (not null / true)." +variable "account_objects_grants" { + description = <