diff --git a/terraform/README.md b/terraform/README.md index 0d9a02f..e72c7ce 100644 --- a/terraform/README.md +++ b/terraform/README.md @@ -32,6 +32,8 @@ | Name | Type | |------|------| | [aws_cloudwatch_log_group.database](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | +| [aws_cognito_user_pool.shared](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cognito_user_pool) | resource | +| [aws_cognito_user_pool_domain.shared](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cognito_user_pool_domain) | resource | | [aws_db_instance.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/db_instance) | resource | | [aws_db_parameter_group.postgres15](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/db_parameter_group) | resource | | [aws_db_subnet_group.incubator_prod](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/db_subnet_group) | resource | diff --git a/terraform/cognito.tf b/terraform/cognito.tf new file mode 100644 index 0000000..e9774d4 --- /dev/null +++ b/terraform/cognito.tf @@ -0,0 +1,83 @@ +# The shared Cognito user pool, for Hack for LA apps whose users are HfLA volunteers +# rather than each app's own external users. Projects get access to it through +# modules/shared-user-pool-access, which gives each one its own app clients and, +# optionally, admin rights for its task role. See hackforla/incubator#17. +# +# Adopted in place from the pool that was created by hand in 2022 as `vrms-dev`. Despite +# the name it is not specific to VRMS: three of its four app clients belong to +# people-depot, which is also the only project whose running container points at it. +# The name stays because renaming a Cognito user pool replaces it, which would destroy +# its user accounts. +# +# Deliberately carries no `project` tag. It belongs to no single project, and the +# container module grants Cognito admin rights on pools tagged with a project's name, so +# a tag here would hand that project admin rights over every other project's users. +# Access is granted explicitly by modules/shared-user-pool-access instead. +# +# Every value below is written to match live AWS, so that the plan after the imports in +# import.tf reports no changes apart from the provider's default tags. +resource "aws_cognito_user_pool" "shared" { + name = "vrms-dev" + + // The pool predates Cognito's tier feature and is on LITE. The provider defaults this + // attribute to ESSENTIALS, so omitting it would plan a billing upgrade. + user_pool_tier = "LITE" + + mfa_configuration = "OFF" + username_attributes = ["email"] + auto_verified_attributes = ["email"] + deletion_protection = "INACTIVE" + + account_recovery_setting { + recovery_mechanism { + name = "verified_email" + priority = 1 + } + } + + // Cognito's default wording, but it is stored on the pool and the provider does not + // fill it in when the attribute is omitted, so leaving these out plans to clear them. + sms_authentication_message = "Your authentication code is {####}. " + + admin_create_user_config { + allow_admin_create_user_only = false + + invite_message_template { + email_message = "Your username is {username} and temporary password is {####}. " + email_subject = "Your temporary password" + sms_message = "Your username is {username} and temporary password is {####}. " + } + } + + email_configuration { + email_sending_account = "COGNITO_DEFAULT" + } + + password_policy { + minimum_length = 8 + require_lowercase = true + require_numbers = true + require_symbols = true + require_uppercase = true + temporary_password_validity_days = 7 + } + + username_configuration { + case_sensitive = false + } + + verification_message_template { + default_email_option = "CONFIRM_WITH_CODE" + } + + // Destroying this pool destroys every user account in it, which recreating it cannot + // bring back. + lifecycle { + prevent_destroy = true + } +} + +resource "aws_cognito_user_pool_domain" "shared" { + domain = "hackforla-vrms-dev" + user_pool_id = aws_cognito_user_pool.shared.id +} diff --git a/terraform/import.tf b/terraform/import.tf index 216cae1..e4558ec 100644 --- a/terraform/import.tf +++ b/terraform/import.tf @@ -582,3 +582,31 @@ import { to = module.home-unite-us.aws_cloudwatch_log_group.lambda[each.key] id = "/aws/lambda/home-unite-us-${each.key}" } + +# Adopts the shared Cognito user pool -- created by hand in 2022 as `vrms-dev` -- its +# domain, and its four app clients: three people-depot's, one VRMS's. See +# hackforla/incubator#17. +import { + to = aws_cognito_user_pool.shared + id = "us-west-2_Fn4rkZpuB" +} + +import { + to = aws_cognito_user_pool_domain.shared + id = "hackforla-vrms-dev" +} + +import { + for_each = { + peopledepot = "52n88hbq9kn00utcjk2hg0e8nl" + pd-2 = "2pn3qa717ae8lq8u801v8t9hps" + backend = "3e3bi1ct2ks9rcktrde8v60v3u" + } + to = module.people-depot.module.shared_user_pool_access.aws_cognito_user_pool_client.this[each.key] + id = "us-west-2_Fn4rkZpuB/${each.value}" +} + +import { + to = module.vrms.module.shared_user_pool_access.aws_cognito_user_pool_client.this["vrms"] + id = "us-west-2_Fn4rkZpuB/5u7s2nj55mp9v5qmt9scja4hnr" +} diff --git a/terraform/main.tf b/terraform/main.tf index cdd25e0..53693f1 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -9,6 +9,9 @@ module "people-depot" { // peopledepot-dev.vrms.io sits in a zone the vrms project owns. vrms_zone_id = module.vrms.zone_id + + shared_user_pool_id = aws_cognito_user_pool.shared.id + shared_user_pool_arn = aws_cognito_user_pool.shared.arn } module "civic-tech-jobs" { @@ -21,6 +24,9 @@ module "home-unite-us" { module "vrms" { source = "./projects/vrms" + + shared_user_pool_id = aws_cognito_user_pool.shared.id + shared_user_pool_arn = aws_cognito_user_pool.shared.arn } module "civic-tech-index" { diff --git a/terraform/modules/shared-user-pool-access/README.md b/terraform/modules/shared-user-pool-access/README.md new file mode 100644 index 0000000..78943a7 --- /dev/null +++ b/terraform/modules/shared-user-pool-access/README.md @@ -0,0 +1,65 @@ + +# shared-user-pool-access + +Gives one project access to the shared Cognito user pool declared in +`terraform/cognito.tf`: the project's own app clients in that pool and, optionally, +admin rights on the pool for the project's task role. See hackforla/incubator#17. + +Every client in the pool shares the pool's users, so a project that wants users of its +own should declare its own pool instead, as home-unite-us does. + +## Admin rights + +Set `task_role_name` to grant the role the four Cognito operations that need IAM -- +`AdminGetUser`, `AdminCreateUser`, `AdminAddUserToGroup` and `AdminDeleteUser` -- on the +shared pool only. These are the same four the container module already grants on pools +tagged with the project's name; the shared pool carries no `project` tag, so this module +is the only way a project gets them there. Leave it unset for a project that only signs +users in: those APIs authorize against the end user's own credentials and need no IAM. + +Note that the pool is shared, so these rights cover every project's users in it, not +just this project's. + +## Client secrets + +`generate_secret` cannot be read back from the API, so an imported client plans a +replacement unless it is ignored -- and replacing a client mints a new client id, which +breaks any application configured with the old one. The module therefore ignores it, +and prevents destroy so any future replacement fails loudly instead. + +## Requirements + +No requirements. + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | n/a | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [aws_cognito_user_pool_client.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cognito_user_pool_client) | resource | +| [aws_iam_role_policy.admin](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [clients](#input\_clients) | this project's app clients in the shared pool, keyed by a short stable name. `name` is the client name Cognito shows, which is also what an application sees. |
map(object({
name = string
generate_secret = optional(bool, false)
explicit_auth_flows = optional(list(string), ["ALLOW_REFRESH_TOKEN_AUTH", "ALLOW_USER_SRP_AUTH"])
supported_identity_providers = optional(list(string))
callback_urls = optional(list(string))
allowed_oauth_flows = optional(list(string))
allowed_oauth_scopes = optional(list(string))
allowed_oauth_flows_user_pool_client = optional(bool, false)
})) | n/a | yes |
+| [task\_role\_name](#input\_task\_role\_name) | name of the project's task role, to grant it Cognito admin operations on the shared pool. Leave unset if the project only signs users in. | `string` | `null` | no |
+| [user\_pool\_arn](#input\_user\_pool\_arn) | ARN of the shared user pool, the resource the admin policy is scoped to | `string` | n/a | yes |
+| [user\_pool\_id](#input\_user\_pool\_id) | id of the shared user pool, from aws\_cognito\_user\_pool.shared in terraform/cognito.tf | `string` | n/a | yes |
+
+## Outputs
+
+| Name | Description |
+|------|-------------|
+| [client\_ids](#output\_client\_ids) | app client id for each entry in `clients`, keyed the same way |
+
diff --git a/terraform/modules/shared-user-pool-access/main.tf b/terraform/modules/shared-user-pool-access/main.tf
new file mode 100644
index 0000000..df2e2cf
--- /dev/null
+++ b/terraform/modules/shared-user-pool-access/main.tf
@@ -0,0 +1,116 @@
+/**
+ * # shared-user-pool-access
+ *
+ * Gives one project access to the shared Cognito user pool declared in
+ * `terraform/cognito.tf`: the project's own app clients in that pool and, optionally,
+ * admin rights on the pool for the project's task role. See hackforla/incubator#17.
+ *
+ * Every client in the pool shares the pool's users, so a project that wants users of its
+ * own should declare its own pool instead, as home-unite-us does.
+ *
+ * ## Admin rights
+ *
+ * Set `task_role_name` to grant the role the four Cognito operations that need IAM --
+ * `AdminGetUser`, `AdminCreateUser`, `AdminAddUserToGroup` and `AdminDeleteUser` -- on the
+ * shared pool only. These are the same four the container module already grants on pools
+ * tagged with the project's name; the shared pool carries no `project` tag, so this module
+ * is the only way a project gets them there. Leave it unset for a project that only signs
+ * users in: those APIs authorize against the end user's own credentials and need no IAM.
+ *
+ * Note that the pool is shared, so these rights cover every project's users in it, not
+ * just this project's.
+ *
+ * ## Client secrets
+ *
+ * `generate_secret` cannot be read back from the API, so an imported client plans a
+ * replacement unless it is ignored -- and replacing a client mints a new client id, which
+ * breaks any application configured with the old one. The module therefore ignores it,
+ * and prevents destroy so any future replacement fails loudly instead.
+ */
+
+locals {
+ // Every client in the pool reads and writes the full set of standard attributes, bar
+ // the two verification flags, which are read-only.
+ write_attributes = [
+ "address",
+ "birthdate",
+ "email",
+ "family_name",
+ "gender",
+ "given_name",
+ "locale",
+ "middle_name",
+ "name",
+ "nickname",
+ "phone_number",
+ "picture",
+ "preferred_username",
+ "profile",
+ "updated_at",
+ "website",
+ "zoneinfo",
+ ]
+ read_attributes = concat(local.write_attributes, ["email_verified", "phone_number_verified"])
+}
+
+resource "aws_cognito_user_pool_client" "this" {
+ for_each = var.clients
+
+ name = each.value.name
+ user_pool_id = var.user_pool_id
+
+ generate_secret = each.value.generate_secret
+ explicit_auth_flows = each.value.explicit_auth_flows
+ supported_identity_providers = each.value.supported_identity_providers
+ callback_urls = each.value.callback_urls
+ allowed_oauth_flows = each.value.allowed_oauth_flows
+ allowed_oauth_scopes = each.value.allowed_oauth_scopes
+ allowed_oauth_flows_user_pool_client = each.value.allowed_oauth_flows_user_pool_client
+
+ read_attributes = local.read_attributes
+ write_attributes = local.write_attributes
+
+ access_token_validity = 60
+ id_token_validity = 60
+ refresh_token_validity = 30
+ auth_session_validity = 3
+
+ token_validity_units {
+ access_token = "minutes"
+ id_token = "minutes"
+ refresh_token = "days"
+ }
+
+ prevent_user_existence_errors = "ENABLED"
+ enable_token_revocation = true
+ enable_propagate_additional_user_context_data = false
+
+ lifecycle {
+ ignore_changes = [generate_secret]
+ prevent_destroy = true
+ }
+}
+
+resource "aws_iam_role_policy" "admin" {
+ count = var.task_role_name == null ? 0 : 1
+
+ name = "shared-user-pool-admin"
+ role = var.task_role_name
+
+ policy = jsonencode({
+ Version = "2012-10-17"
+ Statement = [
+ {
+ Sid = "SharedUserPoolAdmin"
+ Effect = "Allow"
+ Action = [
+ "cognito-idp:AdminGetUser",
+ "cognito-idp:AdminCreateUser",
+ "cognito-idp:AdminAddUserToGroup",
+ "cognito-idp:AdminDeleteUser",
+ ]
+ Resource = var.user_pool_arn
+ },
+ ]
+ })
+}
diff --git a/terraform/modules/shared-user-pool-access/outputs.tf b/terraform/modules/shared-user-pool-access/outputs.tf
new file mode 100644
index 0000000..a7efd05
--- /dev/null
+++ b/terraform/modules/shared-user-pool-access/outputs.tf
@@ -0,0 +1,4 @@
+output "client_ids" {
+ description = "app client id for each entry in `clients`, keyed the same way"
+ value = { for k, c in aws_cognito_user_pool_client.this : k => c.id }
+}
diff --git a/terraform/modules/shared-user-pool-access/variables.tf b/terraform/modules/shared-user-pool-access/variables.tf
new file mode 100644
index 0000000..04ea0be
--- /dev/null
+++ b/terraform/modules/shared-user-pool-access/variables.tf
@@ -0,0 +1,29 @@
+variable "user_pool_id" {
+ type = string
+ description = "id of the shared user pool, from aws_cognito_user_pool.shared in terraform/cognito.tf"
+}
+
+variable "user_pool_arn" {
+ type = string
+ description = "ARN of the shared user pool, the resource the admin policy is scoped to"
+}
+
+variable "clients" {
+ type = map(object({
+ name = string
+ generate_secret = optional(bool, false)
+ explicit_auth_flows = optional(list(string), ["ALLOW_REFRESH_TOKEN_AUTH", "ALLOW_USER_SRP_AUTH"])
+ supported_identity_providers = optional(list(string))
+ callback_urls = optional(list(string))
+ allowed_oauth_flows = optional(list(string))
+ allowed_oauth_scopes = optional(list(string))
+ allowed_oauth_flows_user_pool_client = optional(bool, false)
+ }))
+ description = "this project's app clients in the shared pool, keyed by a short stable name. `name` is the client name Cognito shows, which is also what an application sees."
+}
+
+variable "task_role_name" {
+ type = string
+ default = null
+ description = "name of the project's task role, to grant it Cognito admin operations on the shared pool. Leave unset if the project only signs users in."
+}
diff --git a/terraform/projects/people-depot/README.md b/terraform/projects/people-depot/README.md
index 0c4bbe9..a4d3777 100644
--- a/terraform/projects/people-depot/README.md
+++ b/terraform/projects/people-depot/README.md
@@ -19,6 +19,7 @@ No requirements.
| [dev\_dns\_entry](#module\_dev\_dns\_entry) | ../../modules/dns-entry | n/a |
| [people\_depot\_cicd](#module\_people\_depot\_cicd) | ../../modules/cicd_integration | n/a |
| [people\_depot\_ecr\_backend](#module\_people\_depot\_ecr\_backend) | ../../modules/ecr | n/a |
+| [shared\_user\_pool\_access](#module\_shared\_user\_pool\_access) | ../../modules/shared-user-pool-access | n/a |
## Resources
@@ -30,6 +31,8 @@ No requirements.
| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
+| [shared\_user\_pool\_arn](#input\_shared\_user\_pool\_arn) | ARN of the shared Cognito user pool | `string` | n/a | yes |
+| [shared\_user\_pool\_id](#input\_shared\_user\_pool\_id) | id of the shared Cognito user pool | `string` | n/a | yes |
| [vrms\_zone\_id](#input\_vrms\_zone\_id) | the vrms.io hosted zone id, owned by the vrms project | `string` | n/a | yes |
## Outputs
diff --git a/terraform/projects/people-depot/cognito.tf b/terraform/projects/people-depot/cognito.tf
new file mode 100644
index 0000000..b90b19a
--- /dev/null
+++ b/terraform/projects/people-depot/cognito.tf
@@ -0,0 +1,45 @@
+// people-depot's app clients in the shared Cognito user pool, adopted in place. See
+// hackforla/incubator#17.
+//
+// The client names are long descriptions rather than names, and they are kept as-is:
+// they are what Cognito shows, and changing them is a live change for no benefit.
+module "shared_user_pool_access" {
+ source = "../../modules/shared-user-pool-access"
+
+ user_pool_id = var.shared_user_pool_id
+ user_pool_arn = var.shared_user_pool_arn
+
+ // Grants the dev backend's task role admin operations on the shared pool. Note this
+ // covers every user in the pool, not only people-depot's.
+ task_role_name = module.backend_dev_service.task_role_name
+
+ clients = {
+ peopledepot = {
+ name = "PEOPLEDEPOT is a backend app client that contains a secret"
+ generate_secret = true
+ supported_identity_providers = ["COGNITO"]
+ callback_urls = ["http://localhost:8000/accounts/amazon-cognito/login/callback/", "http://localhost:8000/admin/"]
+ allowed_oauth_flows = ["code"]
+ allowed_oauth_scopes = ["email", "openid", "profile"]
+ allowed_oauth_flows_user_pool_client = true
+ }
+ pd-2 = {
+ name = "pd-2 is a secret-less app client which should be used with a frontend and NOT with a backend"
+ supported_identity_providers = ["COGNITO"]
+ callback_urls = ["http://localhost:8000/accounts/amazon-cognito/login/callback/", "http://localhost:8000/admin/"]
+ allowed_oauth_flows = ["code"]
+ allowed_oauth_scopes = ["email", "openid", "profile"]
+ allowed_oauth_flows_user_pool_client = true
+ }
+ backend = {
+ name = "backend is the old app client used by PD, which returns the auth token in the url"
+ generate_secret = true
+ explicit_auth_flows = ["ALLOW_ADMIN_USER_PASSWORD_AUTH", "ALLOW_REFRESH_TOKEN_AUTH"]
+ supported_identity_providers = ["COGNITO"]
+ callback_urls = ["http://localhost:8000/admin"]
+ allowed_oauth_flows = ["implicit"]
+ allowed_oauth_scopes = ["openid"]
+ allowed_oauth_flows_user_pool_client = true
+ }
+ }
+}
diff --git a/terraform/projects/people-depot/environment-dev.tf b/terraform/projects/people-depot/environment-dev.tf
index eadee58..1fe53be 100644
--- a/terraform/projects/people-depot/environment-dev.tf
+++ b/terraform/projects/people-depot/environment-dev.tf
@@ -53,7 +53,7 @@ module "backend_dev_service" {
{ "name": "SQL_PORT", "value": module.dev_database.port},
{ "name": "COGNITO_DOMAIN", "value": "peopledepot"},
{ "name": "COGNITO_AWS_REGION", "value": "us-west-2"},
- { "name": "COGNITO_USER_POOL", "value": "us-west-2_Fn4rkZpuB"},
+ { "name": "COGNITO_USER_POOL", "value": var.shared_user_pool_id},
]
container_environment_secrets = [
{ "name": "SQL_PASSWORD", "valueFrom": module.dev_database.owner_password_arn},
diff --git a/terraform/projects/people-depot/variables.tf b/terraform/projects/people-depot/variables.tf
index a65b2d8..667f7f7 100644
--- a/terraform/projects/people-depot/variables.tf
+++ b/terraform/projects/people-depot/variables.tf
@@ -4,3 +4,14 @@ variable "vrms_zone_id" {
type = string
description = "the vrms.io hosted zone id, owned by the vrms project"
}
+
+// The shared Cognito user pool in ../../cognito.tf. See hackforla/incubator#17.
+variable "shared_user_pool_id" {
+ type = string
+ description = "id of the shared Cognito user pool"
+}
+
+variable "shared_user_pool_arn" {
+ type = string
+ description = "ARN of the shared Cognito user pool"
+}
diff --git a/terraform/projects/vrms/README.md b/terraform/projects/vrms/README.md
index cf26708..051272d 100644
--- a/terraform/projects/vrms/README.md
+++ b/terraform/projects/vrms/README.md
@@ -30,6 +30,7 @@ No requirements.
| [mailhog\_password\_secret](#module\_mailhog\_password\_secret) | ../../modules/secret | n/a |
| [mailhog\_user\_secret](#module\_mailhog\_user\_secret) | ../../modules/secret | n/a |
| [prod\_database\_url\_secret](#module\_prod\_database\_url\_secret) | ../../modules/secret | n/a |
+| [shared\_user\_pool\_access](#module\_shared\_user\_pool\_access) | ../../modules/shared-user-pool-access | n/a |
| [slack\_bot\_token\_secret](#module\_slack\_bot\_token\_secret) | ../../modules/secret | n/a |
| [slack\_client\_secret\_secret](#module\_slack\_client\_secret\_secret) | ../../modules/secret | n/a |
| [slack\_oauth\_token\_secret](#module\_slack\_oauth\_token\_secret) | ../../modules/secret | n/a |
@@ -45,7 +46,10 @@ No requirements.
## Inputs
-No inputs.
+| Name | Description | Type | Default | Required |
+|------|-------------|------|---------|:--------:|
+| [shared\_user\_pool\_arn](#input\_shared\_user\_pool\_arn) | ARN of the shared Cognito user pool | `string` | n/a | yes |
+| [shared\_user\_pool\_id](#input\_shared\_user\_pool\_id) | id of the shared Cognito user pool | `string` | n/a | yes |
## Outputs
diff --git a/terraform/projects/vrms/cognito.tf b/terraform/projects/vrms/cognito.tf
new file mode 100644
index 0000000..7820063
--- /dev/null
+++ b/terraform/projects/vrms/cognito.tf
@@ -0,0 +1,17 @@
+// VRMS's app client in the shared Cognito user pool, adopted in place. See
+// hackforla/incubator#17.
+//
+// No task_role_name: nothing in VRMS's container configuration references Cognito, so
+// it is not granted admin operations on the pool.
+module "shared_user_pool_access" {
+ source = "../../modules/shared-user-pool-access"
+
+ user_pool_id = var.shared_user_pool_id
+ user_pool_arn = var.shared_user_pool_arn
+
+ clients = {
+ vrms = {
+ name = "VRMS"
+ }
+ }
+}
diff --git a/terraform/projects/vrms/variables.tf b/terraform/projects/vrms/variables.tf
new file mode 100644
index 0000000..32d54b1
--- /dev/null
+++ b/terraform/projects/vrms/variables.tf
@@ -0,0 +1,10 @@
+// The shared Cognito user pool in ../../cognito.tf. See hackforla/incubator#17.
+variable "shared_user_pool_id" {
+ type = string
+ description = "id of the shared Cognito user pool"
+}
+
+variable "shared_user_pool_arn" {
+ type = string
+ description = "ARN of the shared Cognito user pool"
+}