diff --git a/README.md b/README.md index 89c65cd..8c8c741 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,10 @@ -# Terraform Module Template +# Snowflake Role Terraform Module - -> **Warning**: -> This is a template document. Remember to **remove** all text in _italics_ and **update** Module name, Repo name and links/badges to the acual name of your GitHub repository/module!!! - - - - -![AWS](https://img.shields.io/badge/AWS-%23FF9900.svg?style=for-the-badge&logo=amazon-aws&logoColor=white) - +![Snowflake](https://img.shields.io/badge/-SNOWFLAKE-249edc?style=for-the-badge&logo=snowflake&logoColor=white) ![Terraform](https://img.shields.io/badge/terraform-%235835CC.svg?style=for-the-badge&logo=terraform&logoColor=white) - -![License](https://badgen.net/github/license/getindata/terraform-module-template/) -![Release](https://badgen.net/github/release/getindata/terraform-module-template/) +![License](https://badgen.net/github/license/getindata/terraform-snowflake-role/) +![Release](https://badgen.net/github/release/getindata/terraform-snowflake-role/)

@@ -22,36 +13,60 @@ --- -_Brief Description of MODULE:_ +Terraform module for managing Snowflake Database roles. -* _What it does_ -* _What technologies it uses_ - -> **Warning**: -> _When using "Invicton-Labs/deepmerge/null" module - pin `tflint` version to `v0.41.0` in [pre-commit.yaml](.github/workflows/pre-commit.yml) to avoid failing `tflint` checks_ +- Creates Snowflake database role with specific privileges on database and schemas. +- Allows granting of privileges on future schemas in a database. +- Allows granting of privileges on all existing schemas in a database. +- Allows granting of privileges on specific schema objects like tables. +- Supports granting of all privileges or specific ones based on the configuration. +- Can be used to create a hierarchy of roles by assigning parent roles. +- Can be used to grant roles to other roles. ## USAGE -_Example usage of the module - terraform code snippet_ - ```terraform -module "template" { - source = "getindata/template/null" - # version = "x.x.x" - - example_var = "foo" +resource "snowflake_database" "this" { + name = "TEST_DB" } -``` -## NOTES +resource "snowflake_schema" "this" { + database = snowflake_database.this.name + name = "BRONZE" +} -_Additional information that should be made public, for ex. how to solve known issues, additional descriptions/suggestions_ +module "snowflake_database_role" { + source = "../../" + + database_name = snowflake_database.this.name + name = "TEST_DB_ROLE" + + + schema_grants = [ + { + future_schemas_in_database = true + all_schemas_in_database = true + all_privileges = true + }, + ] + + schema_objects_grants = { + "TABLE" = [ + { + all_privileges = true + on_future = true + on_all = true + schema_name = snowflake_schema.this.name + } + ] + } +} +``` ## EXAMPLES - [Simple](examples/simple) - Basic usage of the module - [Complete](examples/complete) - Advanced usage of the module - @@ -63,13 +78,16 @@ _Additional information that should be made public, for ex. how to solve known i |------|-------------|------|---------|:--------:| | [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) | Database 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 |
object({
all_privileges = optional(bool)
with_grant_option = optional(bool, false)
privileges = optional(list(string), null)
})
| `{}` | no | +| [database\_name](#input\_database\_name) | The name of the database to create the role in | `string` | n/a | yes | | [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` | `"resource-type"` | no | +| [descriptor\_name](#input\_descriptor\_name) | Name of the descriptor used to form a resource name | `string` | `"snowflake-database-role"` | 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 | -| [example\_var](#input\_example\_var) | Example variable passed into the module | `string` | n/a | yes | +| [granted\_database\_roles](#input\_granted\_database\_roles) | Database Roles granted to this role | `list(string)` | `[]` | 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 | @@ -77,9 +95,11 @@ _Additional information that should be made public, for ex. how to solve known i | [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 | +| [parent\_database\_role](#input\_parent\_database\_role) | Fully qualified Parent Database Role name (`DB_NAME.ROLE_NAME`), to create parent-child relationship | `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 | +| [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)
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)
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 | -| [sub\_resource](#input\_sub\_resource) | Some other resource that is part of stack/module |
object({
descriptor_name = optional(string, "sub-resource-type")
example_var = string
})
| n/a | yes | | [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 | @@ -87,34 +107,39 @@ _Additional information that should be made public, for ex. how to solve known i | Name | Source | Version | |------|--------|---------| -| [subresource\_label](#module\_subresource\_label) | cloudposse/label/null | 0.25.0 | +| [role\_label](#module\_role\_label) | cloudposse/label/null | 0.25.0 | | [this](#module\_this) | cloudposse/label/null | 0.25.0 | ## Outputs | Name | Description | |------|-------------| -| [example\_output](#output\_example\_output) | Example output of the module | +| [fully\_qualified\_name](#output\_fully\_qualified\_name) | Name of the database role in fully qualified format ("DB\_NAME"."ROLE\_NAME") | +| [name](#output\_name) | Name of the database role | ## Providers | Name | Version | |------|---------| -| [null](#provider\_null) | 3.1.1 | +| [snowflake](#provider\_snowflake) | >= 0.87 | ## Requirements | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 1.3.0 | -| [null](#requirement\_null) | 3.1.1 | +| [terraform](#requirement\_terraform) | >= 1.3 | +| [snowflake](#requirement\_snowflake) | >= 0.87 | ## Resources | Name | Type | |------|------| -| [null_resource.output_input](https://registry.terraform.io/providers/hashicorp/null/3.1.1/docs/resources/resource) | resource | -| [null_resource.subresource](https://registry.terraform.io/providers/hashicorp/null/3.1.1/docs/resources/resource) | resource | +| [snowflake_database_role.this](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/database_role) | resource | +| [snowflake_grant_database_role.granted_database_roles](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/grant_database_role) | resource | +| [snowflake_grant_database_role.parent_database_role](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/grant_database_role) | resource | +| [snowflake_grant_privileges_to_database_role.database_grants](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/grant_privileges_to_database_role) | resource | +| [snowflake_grant_privileges_to_database_role.schema_grants](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/grant_privileges_to_database_role) | resource | +| [snowflake_grant_privileges_to_database_role.schema_objects_grants](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/grant_privileges_to_database_role) | resource | ## CONTRIBUTING diff --git a/examples/complete/.env.dist b/examples/complete/.env.dist index e69de29..275e5eb 100644 --- a/examples/complete/.env.dist +++ b/examples/complete/.env.dist @@ -0,0 +1,6 @@ +export SNOWFLAKE_PRIVATE_KEY_PATH="" +export SNOWFLAKE_USER="" +export SNOWFLAKE_ROLE="" +export SNOWFLAKE_ACCOUNT="" +export SNOWFLAKE_AUTHENTICATOR="" +export SNOWFLAKE_REGION="" diff --git a/examples/complete/README.md b/examples/complete/README.md index 1c0282f..be3a884 100644 --- a/examples/complete/README.md +++ b/examples/complete/README.md @@ -1,13 +1,115 @@ # Complete Example ```terraform -module "terraform_module_template" { +resource "snowflake_database" "this" { + name = "TEST_DB" +} + +resource "snowflake_schema" "this" { + database = snowflake_database.this.name + name = "BRONZE" +} + +resource "snowflake_table" "table_1" { + database = snowflake_schema.this.database + schema = snowflake_schema.this.name + name = "TEST_TABLE_1" + + column { + name = "identity" + type = "NUMBER(38,0)" + nullable = true + + identity { + start_num = 1 + step_num = 3 + } + } +} + +resource "snowflake_table" "table_2" { + database = snowflake_schema.this.database + schema = snowflake_schema.this.name + name = "TEST_TABLE_2" + + column { + name = "identity" + type = "NUMBER(38,0)" + nullable = true + + identity { + start_num = 1 + step_num = 3 + } + } +} + +resource "snowflake_database_role" "db_role_1" { + database = snowflake_database.this.name + name = "DB_ROLE_1" +} + +resource "snowflake_database_role" "db_role_2" { + database = snowflake_database.this.name + name = "DB_ROLE_2" +} + +resource "snowflake_database_role" "db_role_3" { + database = snowflake_database.this.name + name = "DB_ROLE_3" +} + +module "snowflake_database_role" { source = "../../" context = module.this.context - example_var = "This is a example value." - sub_resource = { - example_var = "This is a example value of sub resource." + database_name = snowflake_database.this.name + name = "TEST_DB_ROLE" + + + parent_database_role = snowflake_database_role.db_role_1.name + granted_database_roles = [ + snowflake_database_role.db_role_2.name, + snowflake_database_role.db_role_3.name + ] + database_grants = [ + { + privileges = ["USAGE", "CREATE SCHEMA"] + }, + ] + + schema_grants = [ + { + schema_name = snowflake_schema.this.name + privileges = ["USAGE"] + }, + { + future_schemas_in_database = true + all_schemas_in_database = true + privileges = ["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 + } + ] } } ``` @@ -15,6 +117,6 @@ module "terraform_module_template" { ## Usage ``` terraform init -terraform plan -var-file fixtures.tfvars -out tfplan +terraform plan -out tfplan terraform apply tfplan ``` diff --git a/examples/complete/fixtures.tfvars b/examples/complete/fixtures.tfvars deleted file mode 100644 index 11358b0..0000000 --- a/examples/complete/fixtures.tfvars +++ /dev/null @@ -1,7 +0,0 @@ -descriptor_formats = { - -} - -tags = { - Terraform = "True" -} diff --git a/examples/complete/main.tf b/examples/complete/main.tf index bd89c41..06f5e97 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -1,9 +1,111 @@ -module "terraform_module_template" { +resource "snowflake_database" "this" { + name = "TEST_DB" +} + +resource "snowflake_schema" "this" { + database = snowflake_database.this.name + name = "BRONZE" +} + +resource "snowflake_table" "table_1" { + database = snowflake_schema.this.database + schema = snowflake_schema.this.name + name = "TEST_TABLE_1" + + column { + name = "identity" + type = "NUMBER(38,0)" + nullable = true + + identity { + start_num = 1 + step_num = 3 + } + } +} + +resource "snowflake_table" "table_2" { + database = snowflake_schema.this.database + schema = snowflake_schema.this.name + name = "TEST_TABLE_2" + + column { + name = "identity" + type = "NUMBER(38,0)" + nullable = true + + identity { + start_num = 1 + step_num = 3 + } + } +} + +resource "snowflake_database_role" "db_role_1" { + database = snowflake_database.this.name + name = "DB_ROLE_1" +} + +resource "snowflake_database_role" "db_role_2" { + database = snowflake_database.this.name + name = "DB_ROLE_2" +} + +resource "snowflake_database_role" "db_role_3" { + database = snowflake_database.this.name + name = "DB_ROLE_3" +} + +module "snowflake_database_role" { source = "../../" context = module.this.context - example_var = "This is a example value." - sub_resource = { - example_var = "This is a example value of sub resource." + database_name = snowflake_database.this.name + name = "TEST_DB_ROLE" + + + parent_database_role = snowflake_database_role.db_role_1.name + granted_database_roles = [ + snowflake_database_role.db_role_2.name, + snowflake_database_role.db_role_3.name + ] + + database_grants = { + privileges = ["USAGE", "CREATE SCHEMA"] + } + + + schema_grants = [ + { + schema_name = snowflake_schema.this.name + privileges = ["USAGE"] + }, + { + future_schemas_in_database = true + all_schemas_in_database = true + privileges = ["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 + } + ] } } diff --git a/examples/complete/outputs.tf b/examples/complete/outputs.tf index c2007a2..1b42ee5 100644 --- a/examples/complete/outputs.tf +++ b/examples/complete/outputs.tf @@ -1,4 +1,4 @@ -output "example_output" { - description = "Example output of the module" - value = module.terraform_module_template +output "snowflake_database_role" { + description = "Snowflake database role outputs" + value = module.snowflake_database_role } diff --git a/examples/complete/providers.tf b/examples/complete/providers.tf index c793099..d343f0d 100644 --- a/examples/complete/providers.tf +++ b/examples/complete/providers.tf @@ -1,3 +1 @@ -provider "null" { - # Configuration options -} +provider "snowflake" {} diff --git a/examples/complete/versions.tf b/examples/complete/versions.tf index daa4ce8..d0efb7a 100644 --- a/examples/complete/versions.tf +++ b/examples/complete/versions.tf @@ -1,10 +1,11 @@ terraform { - required_version = ">= 1.3.0" + required_version = ">= 1.3" required_providers { - null = { - source = "hashicorp/null" - version = "3.1.1" + snowflake = { + source = "Snowflake-Labs/snowflake" + version = "0.87.2" } } + } diff --git a/examples/simple/.env.dist b/examples/simple/.env.dist index e69de29..275e5eb 100644 --- a/examples/simple/.env.dist +++ b/examples/simple/.env.dist @@ -0,0 +1,6 @@ +export SNOWFLAKE_PRIVATE_KEY_PATH="" +export SNOWFLAKE_USER="" +export SNOWFLAKE_ROLE="" +export SNOWFLAKE_ACCOUNT="" +export SNOWFLAKE_AUTHENTICATOR="" +export SNOWFLAKE_REGION="" diff --git a/examples/simple/README.md b/examples/simple/README.md index bb30bd9..275f420 100644 --- a/examples/simple/README.md +++ b/examples/simple/README.md @@ -1,12 +1,39 @@ # Simple Example ```terraform -module "terraform_module_template" { +resource "snowflake_database" "this" { + name = "TEST_DB" +} + +resource "snowflake_schema" "this" { + database = snowflake_database.this.name + name = "BRONZE" +} + +module "snowflake_database_role" { source = "../../" - example_var = "This is a example value." - sub_resource = { - example_var = "This is a example value of sub resource." + database_name = snowflake_database.this.name + name = "TEST_DB_ROLE" + + + schema_grants = [ + { + future_schemas_in_database = true + all_schemas_in_database = true + all_privileges = true + }, + ] + + schema_objects_grants = { + "TABLE" = [ + { + all_privileges = true + on_future = true + on_all = true + schema_name = snowflake_schema.this.name + } + ] } } ``` diff --git a/examples/simple/main.tf b/examples/simple/main.tf index c729d72..813f843 100644 --- a/examples/simple/main.tf +++ b/examples/simple/main.tf @@ -1,8 +1,34 @@ -module "terraform_module_template" { +resource "snowflake_database" "this" { + name = "TEST_DB" +} + +resource "snowflake_schema" "this" { + database = snowflake_database.this.name + name = "BRONZE" +} + +module "snowflake_database_role" { source = "../../" - example_var = "This is a example value." - sub_resource = { - example_var = "This is a example value of sub resource." + database_name = snowflake_database.this.name + name = "TEST_DB_ROLE" + + schema_grants = [ + { + future_schemas_in_database = true + all_schemas_in_database = true + all_privileges = true + }, + ] + + schema_objects_grants = { + "TABLE" = [ + { + all_privileges = true + on_future = true + on_all = true + schema_name = snowflake_schema.this.name + } + ] } } diff --git a/examples/simple/outputs.tf b/examples/simple/outputs.tf index c2007a2..1b42ee5 100644 --- a/examples/simple/outputs.tf +++ b/examples/simple/outputs.tf @@ -1,4 +1,4 @@ -output "example_output" { - description = "Example output of the module" - value = module.terraform_module_template +output "snowflake_database_role" { + description = "Snowflake database role outputs" + value = module.snowflake_database_role } diff --git a/examples/simple/providers.tf b/examples/simple/providers.tf index c793099..d343f0d 100644 --- a/examples/simple/providers.tf +++ b/examples/simple/providers.tf @@ -1,3 +1 @@ -provider "null" { - # Configuration options -} +provider "snowflake" {} diff --git a/examples/simple/versions.tf b/examples/simple/versions.tf index daa4ce8..d0efb7a 100644 --- a/examples/simple/versions.tf +++ b/examples/simple/versions.tf @@ -1,10 +1,11 @@ terraform { - required_version = ">= 1.3.0" + required_version = ">= 1.3" required_providers { - null = { - source = "hashicorp/null" - version = "3.1.1" + snowflake = { + source = "Snowflake-Labs/snowflake" + version = "0.87.2" } } + } diff --git a/locals.tf b/locals.tf index 5778a0d..48bca52 100644 --- a/locals.tf +++ b/locals.tf @@ -1,15 +1,82 @@ locals { # Get a name from the descriptor. If not available, use default naming convention. # Trim and replace function are used to avoid bare delimiters on both ends of the name and situation of adjacent delimiters. - # - # todo: Build a wrapper module around context module with name from descriptor feature - name_from_descriptor = module.this.enabled ? trim(replace( - lookup(module.this.descriptors, var.descriptor_name, module.this.id), "/${module.this.delimiter}${module.this.delimiter}+/", module.this.delimiter - ), module.this.delimiter) : null + name_from_descriptor = module.role_label.enabled ? trim(replace( + 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 - subresource_name_from_descriptor = module.subresource_label.enabled ? trim(replace( - lookup(module.subresource_label.descriptors, var.sub_resource.descriptor_name, module.subresource_label.id), "/${module.subresource_label.delimiter}${module.subresource_label.delimiter}+/", module.subresource_label.delimiter - ), module.subresource_label.delimiter) : null + database_role_name = "\"${one(snowflake_database_role.this[*].database)}\".\"${one(snowflake_database_role.this[*].name)}\"" - enabled = module.this.enabled + database_grants = var.database_grants.all_privileges == null && var.database_grants.privileges == null ? {} : { + var.database_grants.all_privileges == true ? "ALL" : "CUSTOM" = var.database_grants + } + + 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.schema_name : + schema_grant.all_schemas_in_database != false ? "ALL_SCHEMAS" : + schema_grant.future_schemas_in_database != false ? "FUTURE_SCHEMAS" : "" + }_${schema_grant.all_privileges == true ? "ALL" : "CUSTOM"}_${index}" => schema_grant + } + 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.object_name}_${grant.all_privileges == true ? "ALL" : "CUSTOM"}" + : "" + }${ + grant.on_all != null && grant.on_all ? + "_ALL_${grant.object_type}${grant.schema_name != null ? "_${grant.schema_name}_${grant.all_privileges == true ? "ALL" : "CUSTOM"}" : ""}" + : "" + }${ + grant.on_future != null && grant.on_future ? + "_FUTURE_${grant.object_type}${grant.schema_name != null ? "_${grant.schema_name}_${grant.all_privileges == true ? "ALL" : "CUSTOM"}" : ""}" + : "" + }" => grant + } } diff --git a/main.tf b/main.tf index 740356a..78a9ea2 100644 --- a/main.tf +++ b/main.tf @@ -1,36 +1,88 @@ -# Example resource that outputs the input value and -# echoes it's base64 encoded version locally +module "role_label" { + source = "cloudposse/label/null" + version = "0.25.0" + context = module.this.context -resource "null_resource" "output_input" { - count = local.enabled ? 1 : 0 + delimiter = coalesce(module.this.context.delimiter, "_") + regex_replace_chars = coalesce(module.this.context.regex_replace_chars, "/[^_a-zA-Z0-9]/") + label_value_case = coalesce(module.this.context.label_value_case, "upper") +} - triggers = { - name = local.name_from_descriptor - input = var.example_var - } +resource "snowflake_database_role" "this" { + count = module.this.enabled ? 1 : 0 - provisioner "local-exec" { - command = "echo ${var.example_var} | base64" - } + database = var.database_name + name = local.name_from_descriptor + comment = var.comment } -module "subresource_label" { - source = "cloudposse/label/null" - version = "0.25.0" - context = module.this.context +resource "snowflake_grant_database_role" "parent_database_role" { + count = module.this.enabled && var.parent_database_role != null ? 1 : 0 - attributes = ["sub"] + database_role_name = local.database_role_name + parent_database_role_name = "${one(snowflake_database_role.this[*].database)}.${var.parent_database_role}" } -resource "null_resource" "subresource" { - count = local.enabled ? 1 : 0 +resource "snowflake_grant_database_role" "granted_database_roles" { + for_each = toset(module.this.enabled ? var.granted_database_roles : []) + + database_role_name = each.value != null ? "${one(snowflake_database_role.this[*].database)}.${each.value}" : null + parent_database_role_name = local.database_role_name +} - triggers = { - name = local.subresource_name_from_descriptor - input = var.sub_resource.example_var +resource "snowflake_grant_privileges_to_database_role" "database_grants" { + for_each = module.this.enabled ? local.database_grants : {} + + all_privileges = each.value.all_privileges + privileges = each.value.privileges + with_grant_option = each.value.with_grant_option + database_role_name = local.database_role_name + + on_database = one(snowflake_database_role.this[*].database) +} + +resource "snowflake_grant_privileges_to_database_role" "schema_grants" { + for_each = module.this.enabled ? local.schema_grants : {} + + all_privileges = each.value.all_privileges + privileges = each.value.privileges + with_grant_option = each.value.with_grant_option + database_role_name = local.database_role_name + + on_schema { + all_schemas_in_database = each.value.all_schemas_in_database == true ? one(snowflake_database_role.this[*].database) : null + schema_name = each.value.schema_name != null ? "\"${one(snowflake_database_role.this[*].database)}\".\"${each.value.schema_name}\"" : null + future_schemas_in_database = each.value.future_schemas_in_database == true ? one(snowflake_database_role.this[*].database) : null } +} + +resource "snowflake_grant_privileges_to_database_role" "schema_objects_grants" { + for_each = module.this.enabled ? local.schema_objects_grants : {} + + all_privileges = each.value.all_privileges + privileges = each.value.privileges + with_grant_option = each.value.with_grant_option + database_role_name = local.database_role_name + + 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) ? "\"${one(snowflake_database_role.this[*].database)}\".\"${each.value.schema_name}\".\"${each.value.object_name}\"" : null + dynamic "all" { + for_each = try(each.value.on_all, false) ? [1] : [] + content { + object_type_plural = each.value.object_type + in_database = each.value.schema_name == null ? one(snowflake_database_role.this[*].database) : null + in_schema = each.value.schema_name != null ? "\"${one(snowflake_database_role.this[*].database)}\".\"${each.value.schema_name}\"" : null + } + } - provisioner "local-exec" { - command = "echo ${var.sub_resource.example_var} | base64" + dynamic "future" { + for_each = try(each.value.on_future, false) ? [1] : [] + content { + object_type_plural = each.value.object_type + in_database = each.value.schema_name == null ? one(snowflake_database_role.this[*].database) : null + in_schema = each.value.schema_name != null ? "\"${one(snowflake_database_role.this[*].database)}\".\"${each.value.schema_name}\"" : null + } + } } } diff --git a/outputs.tf b/outputs.tf index b1bbafe..bb4da77 100644 --- a/outputs.tf +++ b/outputs.tf @@ -1,6 +1,9 @@ -# Example output from the module +output "name" { + description = "Name of the database role" + value = one(snowflake_database_role.this[*].name) +} -output "example_output" { - description = "Example output of the module" - value = one(null_resource.output_input[*].id) +output "fully_qualified_name" { + description = "Name of the database role in fully qualified format (\"DB_NAME\".\"ROLE_NAME\")" + value = local.database_role_name } diff --git a/variables.tf b/variables.tf index 291a160..4b283ed 100644 --- a/variables.tf +++ b/variables.tf @@ -1,18 +1,114 @@ -variable "example_var" { - description = "Example variable passed into the module" +variable "database_name" { + description = "The name of the database to create the role in" type = string } +variable "comment" { + description = "Database Role description" + type = string + default = null +} + variable "descriptor_name" { description = "Name of the descriptor used to form a resource name" type = string - default = "resource-type" + default = "snowflake-database-role" } -variable "sub_resource" { - description = "Some other resource that is part of stack/module" +variable "parent_database_role" { + description = "Fully qualified Parent Database Role name (`DB_NAME.ROLE_NAME`), to create parent-child relationship" + type = string + default = null +} + +variable "granted_database_roles" { + description = "Database Roles granted to this role" + type = list(string) + default = [] +} +variable "database_grants" { + description = "Grants on a database level" type = object({ - descriptor_name = optional(string, "sub-resource-type") - example_var = string + all_privileges = optional(bool) + with_grant_option = optional(bool, false) + privileges = optional(list(string), null) }) + default = {} + + validation { + condition = var.database_grants == {} ? (var.database_grants.privileges != null) != (var.database_grants.all_privileges == true) : true + error_message = "Variable `database_grants` fails validation - only one of `privileges` or `all_privileges` can be set." + } +} + +variable "schema_grants" { + description = "Grants on a schema level" + type = 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) + schema_name = optional(string, null) + })) + default = [] + validation { + condition = alltrue([for grant in var.schema_grants : (grant.privileges != null) != (grant.all_privileges == true)]) + error_message = "Variable `schema_grants` fails validation - only one of `privileges` or `all_privileges` can be set." + } +} + +variable "schema_objects_grants" { + description = <