From 9c1f05ed60387c586dea2bbbebdbdfac0f6274fe Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Fri, 16 Oct 2026 22:00:23 +1100 Subject: [PATCH] Add identity OIDC modules and YAML discovery (#166) Identity OIDC (token roles, provider clients, scopes, assignments) is not managed from YAML yet, and role/client names share one global credential path namespace that is easy to collide on. - add identity_oidc{,_key,_role,_client,_scope,_assignment,_provider} modules and config discovery; roles/clients live under config/// - allow each role/client client_id on its key via vault_identity_oidc_key_allowed_client_id - resolve provider allowed_clients from client names - add a test rejecting duplicate role/client names and references to missing keys, clients, scopes or assignments Reviewed-on: https://git.unkin.net/unkin/terraform-vault/pulls/166 Co-authored-by: unkin-agent Co-committed-by: unkin-agent --- config/config.hcl | 41 ++++++ environments/au/syd1/terragrunt.hcl | 8 ++ modules/vault_cluster/main.tf | 102 +++++++++++++++ .../modules/identity_oidc/main.tf | 3 + .../modules/identity_oidc/terraform.tf | 9 ++ .../modules/identity_oidc/variables.tf | 5 + .../modules/identity_oidc_assignment/main.tf | 5 + .../identity_oidc_assignment/terraform.tf | 9 ++ .../identity_oidc_assignment/variables.tf | 16 +++ .../modules/identity_oidc_client/main.tf | 9 ++ .../modules/identity_oidc_client/outputs.tf | 3 + .../modules/identity_oidc_client/terraform.tf | 9 ++ .../modules/identity_oidc_client/variables.tf | 39 ++++++ .../modules/identity_oidc_key/main.tf | 6 + .../modules/identity_oidc_key/terraform.tf | 9 ++ .../modules/identity_oidc_key/variables.tf | 22 ++++ .../main.tf | 4 + .../terraform.tf | 9 ++ .../variables.tf | 9 ++ .../modules/identity_oidc_provider/main.tf | 7 + .../identity_oidc_provider/terraform.tf | 9 ++ .../identity_oidc_provider/variables.tf | 28 ++++ .../modules/identity_oidc_role/main.tf | 7 + .../modules/identity_oidc_role/outputs.tf | 3 + .../modules/identity_oidc_role/terraform.tf | 9 ++ .../modules/identity_oidc_role/variables.tf | 27 ++++ .../modules/identity_oidc_scope/main.tf | 5 + .../modules/identity_oidc_scope/terraform.tf | 9 ++ .../modules/identity_oidc_scope/variables.tf | 16 +++ modules/vault_cluster/variables.tf | 73 +++++++++++ tests/test_identity_oidc.py | 120 ++++++++++++++++++ 31 files changed, 630 insertions(+) create mode 100644 modules/vault_cluster/modules/identity_oidc/main.tf create mode 100644 modules/vault_cluster/modules/identity_oidc/terraform.tf create mode 100644 modules/vault_cluster/modules/identity_oidc/variables.tf create mode 100644 modules/vault_cluster/modules/identity_oidc_assignment/main.tf create mode 100644 modules/vault_cluster/modules/identity_oidc_assignment/terraform.tf create mode 100644 modules/vault_cluster/modules/identity_oidc_assignment/variables.tf create mode 100644 modules/vault_cluster/modules/identity_oidc_client/main.tf create mode 100644 modules/vault_cluster/modules/identity_oidc_client/outputs.tf create mode 100644 modules/vault_cluster/modules/identity_oidc_client/terraform.tf create mode 100644 modules/vault_cluster/modules/identity_oidc_client/variables.tf create mode 100644 modules/vault_cluster/modules/identity_oidc_key/main.tf create mode 100644 modules/vault_cluster/modules/identity_oidc_key/terraform.tf create mode 100644 modules/vault_cluster/modules/identity_oidc_key/variables.tf create mode 100644 modules/vault_cluster/modules/identity_oidc_key_allowed_client_id/main.tf create mode 100644 modules/vault_cluster/modules/identity_oidc_key_allowed_client_id/terraform.tf create mode 100644 modules/vault_cluster/modules/identity_oidc_key_allowed_client_id/variables.tf create mode 100644 modules/vault_cluster/modules/identity_oidc_provider/main.tf create mode 100644 modules/vault_cluster/modules/identity_oidc_provider/terraform.tf create mode 100644 modules/vault_cluster/modules/identity_oidc_provider/variables.tf create mode 100644 modules/vault_cluster/modules/identity_oidc_role/main.tf create mode 100644 modules/vault_cluster/modules/identity_oidc_role/outputs.tf create mode 100644 modules/vault_cluster/modules/identity_oidc_role/terraform.tf create mode 100644 modules/vault_cluster/modules/identity_oidc_role/variables.tf create mode 100644 modules/vault_cluster/modules/identity_oidc_scope/main.tf create mode 100644 modules/vault_cluster/modules/identity_oidc_scope/terraform.tf create mode 100644 modules/vault_cluster/modules/identity_oidc_scope/variables.tf create mode 100644 tests/test_identity_oidc.py diff --git a/config/config.hcl b/config/config.hcl index 60dd823..ccfaadc 100644 --- a/config/config.hcl +++ b/config/config.hcl @@ -300,6 +300,47 @@ locals { }) if startswith(file_path, "netbox_secret_backend_role/") } + identity_oidc = { + for file_path, content in local.all_configs : + trimsuffix(basename(file_path), ".yaml") => content + if startswith(file_path, "identity_oidc/") + } + identity_oidc_key = { + for file_path, content in local.all_configs : + trimsuffix(basename(file_path), ".yaml") => content + if startswith(file_path, "identity_oidc_key/") + } + identity_oidc_role = { + for file_path, content in local.all_configs : + trimsuffix(replace(file_path, "identity_oidc_role/", ""), ".yaml") => merge(content, { + name = trimsuffix(basename(file_path), ".yaml") + key = dirname(replace(file_path, "identity_oidc_role/", "")) + }) + if startswith(file_path, "identity_oidc_role/") + } + identity_oidc_client = { + for file_path, content in local.all_configs : + trimsuffix(replace(file_path, "identity_oidc_client/", ""), ".yaml") => merge(content, { + name = trimsuffix(basename(file_path), ".yaml") + key = dirname(replace(file_path, "identity_oidc_client/", "")) + }) + if startswith(file_path, "identity_oidc_client/") + } + identity_oidc_scope = { + for file_path, content in local.all_configs : + trimsuffix(basename(file_path), ".yaml") => content + if startswith(file_path, "identity_oidc_scope/") + } + identity_oidc_assignment = { + for file_path, content in local.all_configs : + trimsuffix(basename(file_path), ".yaml") => content + if startswith(file_path, "identity_oidc_assignment/") + } + identity_oidc_provider = { + for file_path, content in local.all_configs : + trimsuffix(basename(file_path), ".yaml") => content + if startswith(file_path, "identity_oidc_provider/") + } ghp_secret_backend = { for file_path, content in local.all_configs : trimsuffix(basename(file_path), ".yaml") => content diff --git a/environments/au/syd1/terragrunt.hcl b/environments/au/syd1/terragrunt.hcl index 240ef68..326d6f5 100644 --- a/environments/au/syd1/terragrunt.hcl +++ b/environments/au/syd1/terragrunt.hcl @@ -98,6 +98,14 @@ inputs = { ghp_secret_backend = local.config.ghp_secret_backend ghp_secret_backend_role = local.config.ghp_secret_backend_role + identity_oidc = local.config.identity_oidc + identity_oidc_key = local.config.identity_oidc_key + identity_oidc_role = local.config.identity_oidc_role + identity_oidc_client = local.config.identity_oidc_client + identity_oidc_scope = local.config.identity_oidc_scope + identity_oidc_assignment = local.config.identity_oidc_assignment + identity_oidc_provider = local.config.identity_oidc_provider + # Pass policy maps to vault_cluster module policy_auth_map = local.policies.policy_auth_map policy_rules_map = local.policies.policy_rules_map diff --git a/modules/vault_cluster/main.tf b/modules/vault_cluster/main.tf index 52f045e..be0bfd0 100644 --- a/modules/vault_cluster/main.tf +++ b/modules/vault_cluster/main.tf @@ -683,3 +683,105 @@ module "pki_mount_only" { delta_rebuild_interval = each.value.delta_rebuild_interval } + +module "identity_oidc" { + source = "./modules/identity_oidc" + + for_each = var.identity_oidc + + issuer = each.value.issuer +} + +module "identity_oidc_key" { + source = "./modules/identity_oidc_key" + + for_each = var.identity_oidc_key + + name = each.key + algorithm = each.value.algorithm + rotation_period = each.value.rotation_period + verification_ttl = each.value.verification_ttl + + depends_on = [module.identity_oidc] +} + +module "identity_oidc_role" { + source = "./modules/identity_oidc_role" + + for_each = var.identity_oidc_role + + name = each.value.name + key = each.value.key + template = each.value.template + ttl = each.value.ttl + client_id = each.value.client_id + + depends_on = [module.identity_oidc, module.identity_oidc_key] +} + +module "identity_oidc_assignment" { + source = "./modules/identity_oidc_assignment" + + for_each = var.identity_oidc_assignment + + name = each.key + entity_ids = each.value.entity_ids + group_ids = each.value.group_ids +} + +module "identity_oidc_client" { + source = "./modules/identity_oidc_client" + + for_each = var.identity_oidc_client + + name = each.value.name + key = each.value.key + redirect_uris = each.value.redirect_uris + assignments = each.value.assignments + client_type = each.value.client_type + id_token_ttl = each.value.id_token_ttl + access_token_ttl = each.value.access_token_ttl + + depends_on = [module.identity_oidc, module.identity_oidc_key, module.identity_oidc_assignment] +} + +module "identity_oidc_key_allowed_client_id" { + source = "./modules/identity_oidc_key_allowed_client_id" + + for_each = merge( + { for k, v in var.identity_oidc_role : "role/${k}" => { key = v.key, client_id = module.identity_oidc_role[k].client_id } }, + { for k, v in var.identity_oidc_client : "client/${k}" => { key = v.key, client_id = module.identity_oidc_client[k].client_id } }, + ) + + key_name = each.value.key + allowed_client_id = each.value.client_id + + depends_on = [module.identity_oidc_key] +} + +module "identity_oidc_scope" { + source = "./modules/identity_oidc_scope" + + for_each = var.identity_oidc_scope + + name = each.key + description = each.value.description + template = each.value.template +} + +module "identity_oidc_provider" { + source = "./modules/identity_oidc_provider" + + for_each = var.identity_oidc_provider + + name = each.key + issuer_host = each.value.issuer_host + https_enabled = each.value.https_enabled + scopes_supported = each.value.scopes_supported + allowed_client_ids = [ + for c in each.value.allowed_clients : + c == "*" ? c : one([for k, v in var.identity_oidc_client : module.identity_oidc_client[k].client_id if v.name == c]) + ] + + depends_on = [module.identity_oidc, module.identity_oidc_scope, module.identity_oidc_client] +} diff --git a/modules/vault_cluster/modules/identity_oidc/main.tf b/modules/vault_cluster/modules/identity_oidc/main.tf new file mode 100644 index 0000000..2e1091b --- /dev/null +++ b/modules/vault_cluster/modules/identity_oidc/main.tf @@ -0,0 +1,3 @@ +resource "vault_identity_oidc" "this" { + issuer = var.issuer +} diff --git a/modules/vault_cluster/modules/identity_oidc/terraform.tf b/modules/vault_cluster/modules/identity_oidc/terraform.tf new file mode 100644 index 0000000..ce22437 --- /dev/null +++ b/modules/vault_cluster/modules/identity_oidc/terraform.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.10" + required_providers { + vault = { + source = "hashicorp/vault" + version = "5.6.0" + } + } +} diff --git a/modules/vault_cluster/modules/identity_oidc/variables.tf b/modules/vault_cluster/modules/identity_oidc/variables.tf new file mode 100644 index 0000000..dba367f --- /dev/null +++ b/modules/vault_cluster/modules/identity_oidc/variables.tf @@ -0,0 +1,5 @@ +variable "issuer" { + description = "Issuer URL for identity tokens (scheme://host:port, no path); null uses Vault's api_addr" + type = string + default = null +} diff --git a/modules/vault_cluster/modules/identity_oidc_assignment/main.tf b/modules/vault_cluster/modules/identity_oidc_assignment/main.tf new file mode 100644 index 0000000..10abe59 --- /dev/null +++ b/modules/vault_cluster/modules/identity_oidc_assignment/main.tf @@ -0,0 +1,5 @@ +resource "vault_identity_oidc_assignment" "this" { + name = var.name + entity_ids = var.entity_ids + group_ids = var.group_ids +} diff --git a/modules/vault_cluster/modules/identity_oidc_assignment/terraform.tf b/modules/vault_cluster/modules/identity_oidc_assignment/terraform.tf new file mode 100644 index 0000000..ce22437 --- /dev/null +++ b/modules/vault_cluster/modules/identity_oidc_assignment/terraform.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.10" + required_providers { + vault = { + source = "hashicorp/vault" + version = "5.6.0" + } + } +} diff --git a/modules/vault_cluster/modules/identity_oidc_assignment/variables.tf b/modules/vault_cluster/modules/identity_oidc_assignment/variables.tf new file mode 100644 index 0000000..7efb302 --- /dev/null +++ b/modules/vault_cluster/modules/identity_oidc_assignment/variables.tf @@ -0,0 +1,16 @@ +variable "name" { + description = "Assignment name" + type = string +} + +variable "entity_ids" { + description = "Identity entity ids in this assignment" + type = list(string) + default = [] +} + +variable "group_ids" { + description = "Identity group ids in this assignment" + type = list(string) + default = [] +} diff --git a/modules/vault_cluster/modules/identity_oidc_client/main.tf b/modules/vault_cluster/modules/identity_oidc_client/main.tf new file mode 100644 index 0000000..63ed035 --- /dev/null +++ b/modules/vault_cluster/modules/identity_oidc_client/main.tf @@ -0,0 +1,9 @@ +resource "vault_identity_oidc_client" "this" { + name = var.name + key = var.key + redirect_uris = var.redirect_uris + assignments = var.assignments + client_type = var.client_type + id_token_ttl = var.id_token_ttl + access_token_ttl = var.access_token_ttl +} diff --git a/modules/vault_cluster/modules/identity_oidc_client/outputs.tf b/modules/vault_cluster/modules/identity_oidc_client/outputs.tf new file mode 100644 index 0000000..0bfe981 --- /dev/null +++ b/modules/vault_cluster/modules/identity_oidc_client/outputs.tf @@ -0,0 +1,3 @@ +output "client_id" { + value = vault_identity_oidc_client.this.client_id +} diff --git a/modules/vault_cluster/modules/identity_oidc_client/terraform.tf b/modules/vault_cluster/modules/identity_oidc_client/terraform.tf new file mode 100644 index 0000000..ce22437 --- /dev/null +++ b/modules/vault_cluster/modules/identity_oidc_client/terraform.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.10" + required_providers { + vault = { + source = "hashicorp/vault" + version = "5.6.0" + } + } +} diff --git a/modules/vault_cluster/modules/identity_oidc_client/variables.tf b/modules/vault_cluster/modules/identity_oidc_client/variables.tf new file mode 100644 index 0000000..34783cd --- /dev/null +++ b/modules/vault_cluster/modules/identity_oidc_client/variables.tf @@ -0,0 +1,39 @@ +variable "name" { + description = "OIDC client name" + type = string +} + +variable "key" { + description = "Named key that signs this client's ID tokens" + type = string +} + +variable "redirect_uris" { + description = "Allowed redirect URIs" + type = list(string) + default = [] +} + +variable "assignments" { + description = "Assignment names allowed to authenticate with this client" + type = list(string) + default = [] +} + +variable "client_type" { + description = "confidential or public" + type = string + default = null +} + +variable "id_token_ttl" { + description = "ID token TTL in seconds" + type = number + default = null +} + +variable "access_token_ttl" { + description = "Access token TTL in seconds" + type = number + default = null +} diff --git a/modules/vault_cluster/modules/identity_oidc_key/main.tf b/modules/vault_cluster/modules/identity_oidc_key/main.tf new file mode 100644 index 0000000..2d56330 --- /dev/null +++ b/modules/vault_cluster/modules/identity_oidc_key/main.tf @@ -0,0 +1,6 @@ +resource "vault_identity_oidc_key" "this" { + name = var.name + algorithm = var.algorithm + rotation_period = var.rotation_period + verification_ttl = var.verification_ttl +} diff --git a/modules/vault_cluster/modules/identity_oidc_key/terraform.tf b/modules/vault_cluster/modules/identity_oidc_key/terraform.tf new file mode 100644 index 0000000..ce22437 --- /dev/null +++ b/modules/vault_cluster/modules/identity_oidc_key/terraform.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.10" + required_providers { + vault = { + source = "hashicorp/vault" + version = "5.6.0" + } + } +} diff --git a/modules/vault_cluster/modules/identity_oidc_key/variables.tf b/modules/vault_cluster/modules/identity_oidc_key/variables.tf new file mode 100644 index 0000000..3b38a2d --- /dev/null +++ b/modules/vault_cluster/modules/identity_oidc_key/variables.tf @@ -0,0 +1,22 @@ +variable "name" { + description = "Name of the named key" + type = string +} + +variable "algorithm" { + description = "Signing algorithm (RS256, RS384, RS512, ES256, ES384, ES512, EdDSA)" + type = string + default = null +} + +variable "rotation_period" { + description = "Seconds between key rotations" + type = number + default = null +} + +variable "verification_ttl" { + description = "Seconds a rotated public key stays published for verification" + type = number + default = null +} diff --git a/modules/vault_cluster/modules/identity_oidc_key_allowed_client_id/main.tf b/modules/vault_cluster/modules/identity_oidc_key_allowed_client_id/main.tf new file mode 100644 index 0000000..56af4c9 --- /dev/null +++ b/modules/vault_cluster/modules/identity_oidc_key_allowed_client_id/main.tf @@ -0,0 +1,4 @@ +resource "vault_identity_oidc_key_allowed_client_id" "this" { + key_name = var.key_name + allowed_client_id = var.allowed_client_id +} diff --git a/modules/vault_cluster/modules/identity_oidc_key_allowed_client_id/terraform.tf b/modules/vault_cluster/modules/identity_oidc_key_allowed_client_id/terraform.tf new file mode 100644 index 0000000..ce22437 --- /dev/null +++ b/modules/vault_cluster/modules/identity_oidc_key_allowed_client_id/terraform.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.10" + required_providers { + vault = { + source = "hashicorp/vault" + version = "5.6.0" + } + } +} diff --git a/modules/vault_cluster/modules/identity_oidc_key_allowed_client_id/variables.tf b/modules/vault_cluster/modules/identity_oidc_key_allowed_client_id/variables.tf new file mode 100644 index 0000000..0e189c6 --- /dev/null +++ b/modules/vault_cluster/modules/identity_oidc_key_allowed_client_id/variables.tf @@ -0,0 +1,9 @@ +variable "key_name" { + description = "Named key the client id may sign with" + type = string +} + +variable "allowed_client_id" { + description = "Role or client id allowed to use the key" + type = string +} diff --git a/modules/vault_cluster/modules/identity_oidc_provider/main.tf b/modules/vault_cluster/modules/identity_oidc_provider/main.tf new file mode 100644 index 0000000..f442b55 --- /dev/null +++ b/modules/vault_cluster/modules/identity_oidc_provider/main.tf @@ -0,0 +1,7 @@ +resource "vault_identity_oidc_provider" "this" { + name = var.name + issuer_host = var.issuer_host + https_enabled = var.https_enabled + scopes_supported = var.scopes_supported + allowed_client_ids = var.allowed_client_ids +} diff --git a/modules/vault_cluster/modules/identity_oidc_provider/terraform.tf b/modules/vault_cluster/modules/identity_oidc_provider/terraform.tf new file mode 100644 index 0000000..ce22437 --- /dev/null +++ b/modules/vault_cluster/modules/identity_oidc_provider/terraform.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.10" + required_providers { + vault = { + source = "hashicorp/vault" + version = "5.6.0" + } + } +} diff --git a/modules/vault_cluster/modules/identity_oidc_provider/variables.tf b/modules/vault_cluster/modules/identity_oidc_provider/variables.tf new file mode 100644 index 0000000..1303048 --- /dev/null +++ b/modules/vault_cluster/modules/identity_oidc_provider/variables.tf @@ -0,0 +1,28 @@ +variable "name" { + description = "OIDC provider name" + type = string +} + +variable "issuer_host" { + description = "Issuer host[:port]; null uses Vault's api_addr" + type = string + default = null +} + +variable "https_enabled" { + description = "Use https in the issuer URL" + type = bool + default = true +} + +variable "scopes_supported" { + description = "Scope names the provider serves" + type = list(string) + default = [] +} + +variable "allowed_client_ids" { + description = "Client ids allowed to use the provider (\"*\" allows all)" + type = list(string) + default = [] +} diff --git a/modules/vault_cluster/modules/identity_oidc_role/main.tf b/modules/vault_cluster/modules/identity_oidc_role/main.tf new file mode 100644 index 0000000..7e0c502 --- /dev/null +++ b/modules/vault_cluster/modules/identity_oidc_role/main.tf @@ -0,0 +1,7 @@ +resource "vault_identity_oidc_role" "this" { + name = var.name + key = var.key + template = var.template + ttl = var.ttl + client_id = var.client_id +} diff --git a/modules/vault_cluster/modules/identity_oidc_role/outputs.tf b/modules/vault_cluster/modules/identity_oidc_role/outputs.tf new file mode 100644 index 0000000..280378a --- /dev/null +++ b/modules/vault_cluster/modules/identity_oidc_role/outputs.tf @@ -0,0 +1,3 @@ +output "client_id" { + value = vault_identity_oidc_role.this.client_id +} diff --git a/modules/vault_cluster/modules/identity_oidc_role/terraform.tf b/modules/vault_cluster/modules/identity_oidc_role/terraform.tf new file mode 100644 index 0000000..ce22437 --- /dev/null +++ b/modules/vault_cluster/modules/identity_oidc_role/terraform.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.10" + required_providers { + vault = { + source = "hashicorp/vault" + version = "5.6.0" + } + } +} diff --git a/modules/vault_cluster/modules/identity_oidc_role/variables.tf b/modules/vault_cluster/modules/identity_oidc_role/variables.tf new file mode 100644 index 0000000..4ad38ec --- /dev/null +++ b/modules/vault_cluster/modules/identity_oidc_role/variables.tf @@ -0,0 +1,27 @@ +variable "name" { + description = "Role name (read identity/oidc/token/ to mint a token)" + type = string +} + +variable "key" { + description = "Named key that signs this role's tokens" + type = string +} + +variable "template" { + description = "JSON claims template for issued tokens" + type = string + default = null +} + +variable "ttl" { + description = "Token TTL in seconds" + type = number + default = null +} + +variable "client_id" { + description = "Token audience (aud); null lets Vault generate one" + type = string + default = null +} diff --git a/modules/vault_cluster/modules/identity_oidc_scope/main.tf b/modules/vault_cluster/modules/identity_oidc_scope/main.tf new file mode 100644 index 0000000..ee8e255 --- /dev/null +++ b/modules/vault_cluster/modules/identity_oidc_scope/main.tf @@ -0,0 +1,5 @@ +resource "vault_identity_oidc_scope" "this" { + name = var.name + description = var.description + template = var.template +} diff --git a/modules/vault_cluster/modules/identity_oidc_scope/terraform.tf b/modules/vault_cluster/modules/identity_oidc_scope/terraform.tf new file mode 100644 index 0000000..ce22437 --- /dev/null +++ b/modules/vault_cluster/modules/identity_oidc_scope/terraform.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.10" + required_providers { + vault = { + source = "hashicorp/vault" + version = "5.6.0" + } + } +} diff --git a/modules/vault_cluster/modules/identity_oidc_scope/variables.tf b/modules/vault_cluster/modules/identity_oidc_scope/variables.tf new file mode 100644 index 0000000..11ddf06 --- /dev/null +++ b/modules/vault_cluster/modules/identity_oidc_scope/variables.tf @@ -0,0 +1,16 @@ +variable "name" { + description = "Scope name" + type = string +} + +variable "description" { + description = "Scope description" + type = string + default = null +} + +variable "template" { + description = "JSON claims template added when this scope is requested" + type = string + default = null +} diff --git a/modules/vault_cluster/variables.tf b/modules/vault_cluster/variables.tf index 9d08db6..cdcee55 100644 --- a/modules/vault_cluster/variables.tf +++ b/modules/vault_cluster/variables.tf @@ -578,3 +578,76 @@ variable "policy_rules_map" { default = {} } + +variable "identity_oidc" { + description = "Identity token issuer config (singleton in practice)" + type = map(object({ + issuer = optional(string) + })) + default = {} +} + +variable "identity_oidc_key" { + description = "Map of identity OIDC named keys to create" + type = map(object({ + algorithm = optional(string, "RS256") + rotation_period = optional(number, 86400) + verification_ttl = optional(number, 86400) + })) + default = {} +} + +variable "identity_oidc_role" { + description = "Map of identity token roles to create, keyed /" + type = map(object({ + name = string + key = string + template = optional(string) + ttl = optional(number, 86400) + client_id = optional(string) + })) + default = {} +} + +variable "identity_oidc_client" { + description = "Map of identity OIDC provider clients to create, keyed /" + type = map(object({ + name = string + key = string + redirect_uris = optional(list(string), []) + assignments = optional(list(string), []) + client_type = optional(string, "confidential") + id_token_ttl = optional(number, 86400) + access_token_ttl = optional(number, 86400) + })) + default = {} +} + +variable "identity_oidc_scope" { + description = "Map of identity OIDC provider scopes to create" + type = map(object({ + description = optional(string) + template = optional(string) + })) + default = {} +} + +variable "identity_oidc_assignment" { + description = "Map of identity OIDC client assignments to create" + type = map(object({ + entity_ids = optional(list(string), []) + group_ids = optional(list(string), []) + })) + default = {} +} + +variable "identity_oidc_provider" { + description = "Map of identity OIDC providers to create; allowed_clients are client names or \"*\"" + type = map(object({ + issuer_host = optional(string) + https_enabled = optional(bool, true) + scopes_supported = optional(list(string), []) + allowed_clients = optional(list(string), []) + })) + default = {} +} diff --git a/tests/test_identity_oidc.py b/tests/test_identity_oidc.py new file mode 100644 index 0000000..7a92289 --- /dev/null +++ b/tests/test_identity_oidc.py @@ -0,0 +1,120 @@ +"""Validate identity OIDC role/client definitions under config/.""" +import tempfile +import unittest +from collections import defaultdict +from pathlib import Path + +import yaml + +REPO_ROOT = Path(__file__).resolve().parent.parent + + +def problems(config_dir): + """Duplicate role/client names and references to missing keys, clients, scopes or assignments.""" + config_dir = Path(config_dir) + keys = {f.stem for f in config_dir.glob("identity_oidc_key/*.yaml")} + names = defaultdict(list) + found = [] + for kind in ("identity_oidc_role", "identity_oidc_client"): + for f in sorted(config_dir.glob(f"{kind}/**/*.yaml")): + rel = f.relative_to(config_dir).as_posix() + names[f.stem].append(rel) + key = f.parent.relative_to(config_dir / kind).as_posix() + if key not in keys: + found.append(f'{rel}: key "{key}" has no config/identity_oidc_key/{key}.yaml') + for name, files in sorted(names.items()): + if len(files) > 1: + found.append(f'role/client name "{name}" is used by {", ".join(files)}') + + def stems(kind): + return {f.stem for f in config_dir.glob(f"{kind}/*.yaml")} + + clients = {f.stem for f in config_dir.glob("identity_oidc_client/**/*.yaml")} + refs = [ + ("identity_oidc_provider", "allowed_clients", clients | {"*"}), + ("identity_oidc_provider", "scopes_supported", stems("identity_oidc_scope")), + # allow_all is Vault's built-in assignment. + ("identity_oidc_client", "assignments", stems("identity_oidc_assignment") | {"allow_all"}), + ] + for kind, field, known in refs: + for f in sorted(config_dir.glob(f"{kind}/**/*.yaml")): + for ref in (yaml.safe_load(f.read_text()) or {}).get(field, []): + if ref not in known: + found.append(f'{f.relative_to(config_dir).as_posix()}: {field} "{ref}" does not exist') + return found + + +class RepoTests(unittest.TestCase): + def test_repo_config(self): + self.assertEqual(problems(REPO_ROOT / "config"), []) + + +class ProblemsTests(unittest.TestCase): + def tree(self, *paths, **content): + tmp = tempfile.TemporaryDirectory() + self.addCleanup(tmp.cleanup) + for p in (*paths, *content): + f = Path(tmp.name, p) + f.parent.mkdir(parents=True, exist_ok=True) + f.write_text(yaml.safe_dump(content.get(p, {}))) + return tmp.name + + def test_clean(self): + d = self.tree( + "identity_oidc_key/a.yaml", + "identity_oidc_key/b.yaml", + "identity_oidc_role/a/r1.yaml", + "identity_oidc_role/b/r2.yaml", + "identity_oidc_client/a/c1.yaml", + ) + self.assertEqual(problems(d), []) + + def test_role_client_collision(self): + d = self.tree( + "identity_oidc_key/a.yaml", + "identity_oidc_role/a/x.yaml", + "identity_oidc_client/a/x.yaml", + ) + self.assertEqual(problems(d), [ + 'role/client name "x" is used by identity_oidc_role/a/x.yaml, identity_oidc_client/a/x.yaml' + ]) + + def test_collision_across_keys(self): + d = self.tree( + "identity_oidc_key/a.yaml", + "identity_oidc_key/b.yaml", + "identity_oidc_role/a/x.yaml", + "identity_oidc_role/b/x.yaml", + ) + self.assertEqual(problems(d), [ + 'role/client name "x" is used by identity_oidc_role/a/x.yaml, identity_oidc_role/b/x.yaml' + ]) + + def test_missing_key(self): + d = self.tree("identity_oidc_client/nope/c.yaml") + self.assertEqual(problems(d), [ + 'identity_oidc_client/nope/c.yaml: key "nope" has no config/identity_oidc_key/nope.yaml' + ]) + + def test_references(self): + d = self.tree( + "identity_oidc_key/a.yaml", + "identity_oidc_scope/groups.yaml", + "identity_oidc_assignment/admins.yaml", + **{ + "identity_oidc_client/a/c.yaml": {"assignments": ["admins", "allow_all", "ghost"]}, + "identity_oidc_provider/p.yaml": { + "allowed_clients": ["c", "*", "nope"], + "scopes_supported": ["groups", "email"], + }, + }, + ) + self.assertEqual(problems(d), [ + 'identity_oidc_provider/p.yaml: allowed_clients "nope" does not exist', + 'identity_oidc_provider/p.yaml: scopes_supported "email" does not exist', + 'identity_oidc_client/a/c.yaml: assignments "ghost" does not exist', + ]) + + +if __name__ == "__main__": + unittest.main()