Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions terraform/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
83 changes: 83 additions & 0 deletions terraform/cognito.tf
Original file line number Diff line number Diff line change
@@ -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
}
28 changes: 28 additions & 0 deletions terraform/import.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
6 changes: 6 additions & 0 deletions terraform/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand All @@ -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" {
Expand Down
65 changes: 65 additions & 0 deletions terraform/modules/shared-user-pool-access/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<!-- BEGIN_TF_DOCS -->
# 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 |
|------|---------|
| <a name="provider_aws"></a> [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 |
|------|-------------|------|---------|:--------:|
| <a name="input_clients"></a> [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. | <pre>map(object({<br/> name = string<br/> generate_secret = optional(bool, false)<br/> explicit_auth_flows = optional(list(string), ["ALLOW_REFRESH_TOKEN_AUTH", "ALLOW_USER_SRP_AUTH"])<br/> supported_identity_providers = optional(list(string))<br/> callback_urls = optional(list(string))<br/> allowed_oauth_flows = optional(list(string))<br/> allowed_oauth_scopes = optional(list(string))<br/> allowed_oauth_flows_user_pool_client = optional(bool, false)<br/> }))</pre> | n/a | yes |
| <a name="input_task_role_name"></a> [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 |
| <a name="input_user_pool_arn"></a> [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 |
| <a name="input_user_pool_id"></a> [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 |
|------|-------------|
| <a name="output_client_ids"></a> [client\_ids](#output\_client\_ids) | app client id for each entry in `clients`, keyed the same way |
<!-- END_TF_DOCS -->
116 changes: 116 additions & 0 deletions terraform/modules/shared-user-pool-access/main.tf
Original file line number Diff line number Diff line change
@@ -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
},
]
})
}
4 changes: 4 additions & 0 deletions terraform/modules/shared-user-pool-access/outputs.tf
Original file line number Diff line number Diff line change
@@ -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 }
}
29 changes: 29 additions & 0 deletions terraform/modules/shared-user-pool-access/variables.tf
Original file line number Diff line number Diff line change
@@ -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."
}
3 changes: 3 additions & 0 deletions terraform/projects/people-depot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ No requirements.
| <a name="module_dev_dns_entry"></a> [dev\_dns\_entry](#module\_dev\_dns\_entry) | ../../modules/dns-entry | n/a |
| <a name="module_people_depot_cicd"></a> [people\_depot\_cicd](#module\_people\_depot\_cicd) | ../../modules/cicd_integration | n/a |
| <a name="module_people_depot_ecr_backend"></a> [people\_depot\_ecr\_backend](#module\_people\_depot\_ecr\_backend) | ../../modules/ecr | n/a |
| <a name="module_shared_user_pool_access"></a> [shared\_user\_pool\_access](#module\_shared\_user\_pool\_access) | ../../modules/shared-user-pool-access | n/a |

## Resources

Expand All @@ -30,6 +31,8 @@ No requirements.

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_shared_user_pool_arn"></a> [shared\_user\_pool\_arn](#input\_shared\_user\_pool\_arn) | ARN of the shared Cognito user pool | `string` | n/a | yes |
| <a name="input_shared_user_pool_id"></a> [shared\_user\_pool\_id](#input\_shared\_user\_pool\_id) | id of the shared Cognito user pool | `string` | n/a | yes |
| <a name="input_vrms_zone_id"></a> [vrms\_zone\_id](#input\_vrms\_zone\_id) | the vrms.io hosted zone id, owned by the vrms project | `string` | n/a | yes |

## Outputs
Expand Down
Loading
Loading