From 777fe555a0d9b9217edc5f93bf477ff8b479941a Mon Sep 17 00:00:00 2001
From: Matthew Baggett <matthew@baggett.me>
Date: Sat, 21 Dec 2024 00:22:06 +0100
Subject: [PATCH] Giving up on IAM. 15 minute tokens aint worth it.

---
 cloud/aws/rds_serverless/admin_user.tf    |  10 ++
 cloud/aws/rds_serverless/debug.tf         |   4 +-
 cloud/aws/rds_serverless/iam.system.tf    |  61 ------------
 cloud/aws/rds_serverless/iam.tenants.tf   |  22 -----
 cloud/aws/rds_serverless/outputs.tf       |  10 +-
 cloud/aws/rds_serverless/rds.tf           |  23 +----
 cloud/aws/rds_serverless/tenant/db.tf     |  27 ++++--
 cloud/aws/rds_serverless/tenant/input.tf  |  11 +++
 cloud/aws/rds_serverless/tenant/output.tf |  15 +--
 cloud/aws/rds_serverless/tenant/tenant.tf | 107 ----------------------
 cloud/aws/rds_serverless/tenants.tf       |  21 +++++
 11 files changed, 74 insertions(+), 237 deletions(-)
 delete mode 100644 cloud/aws/rds_serverless/iam.system.tf
 delete mode 100644 cloud/aws/rds_serverless/iam.tenants.tf
 delete mode 100644 cloud/aws/rds_serverless/tenant/tenant.tf
 create mode 100644 cloud/aws/rds_serverless/tenants.tf

diff --git a/cloud/aws/rds_serverless/admin_user.tf b/cloud/aws/rds_serverless/admin_user.tf
index edba2f2..6cdabae 100644
--- a/cloud/aws/rds_serverless/admin_user.tf
+++ b/cloud/aws/rds_serverless/admin_user.tf
@@ -6,6 +6,16 @@ resource "random_pet" "admin_user" {
   count     = var.admin_username == null ? 1 : 0
   separator = "_"
 }
+variable "admin_password" {
+  type    = string
+  default = null
+}
+resource "random_password" "admin_pass" {
+  count   = var.admin_username == null ? 1 : 0
+  special = false
+  length  = 32
+}
 locals {
   admin_username = coalesce(var.admin_username, random_pet.admin_user[0].id)
+  admin_password = nonsensitive(coalesce(var.admin_password, random_password.admin_pass[0].result))
 }
\ No newline at end of file
diff --git a/cloud/aws/rds_serverless/debug.tf b/cloud/aws/rds_serverless/debug.tf
index 62c3852..94a3c52 100644
--- a/cloud/aws/rds_serverless/debug.tf
+++ b/cloud/aws/rds_serverless/debug.tf
@@ -13,8 +13,8 @@ resource "local_file" "debug" {
         read  = aws_rds_cluster_endpoint.endpoint["read"].endpoint
       }
       admin = {
-        username = local.admin.username
-        password = local.admin.password
+        username = local.admin_username
+        password = local.admin_password
       }
     }
     tenants = {
diff --git a/cloud/aws/rds_serverless/iam.system.tf b/cloud/aws/rds_serverless/iam.system.tf
deleted file mode 100644
index 53132b5..0000000
--- a/cloud/aws/rds_serverless/iam.system.tf
+++ /dev/null
@@ -1,61 +0,0 @@
-data "aws_iam_policy_document" "assume_role_policy" {
-  statement {
-    actions = ["sts:AssumeRole"]
-    principals {
-      type        = "Service"
-      identifiers = ["ec2.amazonaws.com"]
-    }
-  }
-}
-data "aws_iam_policy_document" "rds_instance_policy" {
-  statement {
-    actions   = ["iam:GetGroup"]
-    effect    = "Allow"
-    resources = ["*"]
-  }
-  statement {
-    actions   = ["iam:GetSSHPublicKey", "iam:ListSSHPublicKeys"]
-    effect    = "Allow"
-    resources = ["arn:aws:iam::*:user/*"]
-  }
-  statement {
-    actions   = ["ec2:*"]
-    effect    = "Allow"
-    resources = ["*"]
-  }
-  statement {
-    actions   = ["rds:*"]
-    effect    = "Allow"
-    resources = ["*"]
-  }
-  statement {
-    actions   = ["secretsmanager:*"]
-    effect    = "Allow"
-    resources = ["*"]
-  }
-}
-resource "aws_iam_policy" "rds_instance_policy" {
-  policy = data.aws_iam_policy_document.rds_instance_policy.json
-  name   = join("", [local.app_name, "RDS", "InstancePolicy"])
-  path   = "/${join("/", [local.app_name, "RDS", "InstancePolicy"])}/"
-  tags = merge(
-    try(var.application.application_tag, {}),
-    {}
-  )
-}
-resource "aws_iam_role" "rds_instance_policy" {
-  assume_role_policy    = data.aws_iam_policy_document.assume_role_policy.json
-  name                  = join("", [local.app_name, "RDS", ])
-  path                  = "/${join("/", [local.app_name, "RDS", ])}/"
-  force_detach_policies = true
-  tags = merge(
-    try(var.application.application_tag, {}),
-    {}
-  )
-}
-
-resource "aws_iam_policy_attachment" "rds_instance_policy" {
-  name       = aws_iam_policy.rds_instance_policy.name
-  policy_arn = aws_iam_policy.rds_instance_policy.arn
-  roles      = [aws_iam_role.rds_instance_policy.name]
-}
\ No newline at end of file
diff --git a/cloud/aws/rds_serverless/iam.tenants.tf b/cloud/aws/rds_serverless/iam.tenants.tf
deleted file mode 100644
index 1be88f9..0000000
--- a/cloud/aws/rds_serverless/iam.tenants.tf
+++ /dev/null
@@ -1,22 +0,0 @@
-module "tenants" {
-  depends_on               = [aws_rds_cluster.cluster, aws_rds_cluster_instance.instance]
-  for_each                 = var.tenants
-  source                   = "./tenant"
-  username                 = each.value.username
-  database                 = each.value.database
-  app_name                 = local.app_name
-  vpc_id                   = data.aws_vpc.current.id
-  aws_profile              = var.aws_profile
-  cluster_id               = aws_rds_cluster.cluster.id
-  super_user_iam_role_name = aws_iam_role.rds_instance_policy.name
-  engine                   = aws_rds_cluster.cluster.engine
-  admin_username           = local.admin.username
-  admin_password           = local.admin.password
-  tags = merge(
-    try(var.application.application_tag, {}),
-    {
-      "TerraformRDSClusterName" = var.instance_name
-      "TerraformRDSTenantName"  = each.value.username
-    }
-  )
-}
\ No newline at end of file
diff --git a/cloud/aws/rds_serverless/outputs.tf b/cloud/aws/rds_serverless/outputs.tf
index 25c3c16..76c012e 100644
--- a/cloud/aws/rds_serverless/outputs.tf
+++ b/cloud/aws/rds_serverless/outputs.tf
@@ -3,13 +3,17 @@ locals {
     for key, tenant in module.tenants : key => {
       username          = tenant.username
       database          = tenant.database
-      access_key        = tenant.access_key
-      secret_key        = tenant.secret_key
-      auth_token        = tenant.auth_token
+      password          = tenant.password
       connection_string = tenant.connection_string
     }
   }
 }
 output "tenants" {
   value = local.output_tenants
+}
+output "admin" {
+  value = {
+    username = local.admin_username
+    password = local.admin_password
+  }
 }
\ No newline at end of file
diff --git a/cloud/aws/rds_serverless/rds.tf b/cloud/aws/rds_serverless/rds.tf
index 25a3e85..81613f9 100644
--- a/cloud/aws/rds_serverless/rds.tf
+++ b/cloud/aws/rds_serverless/rds.tf
@@ -15,15 +15,6 @@ resource "aws_kms_key" "db_key" {
     }
   )
 }
-resource "aws_kms_key" "master_key" {
-  description = "RDS ${var.instance_name} Master Account Key"
-  tags = merge(
-    try(var.application.application_tag, {}),
-    {
-      TerraformSecretType = "RDSMasterAccountKey"
-    }
-  )
-}
 resource "aws_rds_cluster" "cluster" {
   cluster_identifier                  = local.sanitised_name
   engine_mode                         = "provisioned"
@@ -31,8 +22,7 @@ resource "aws_rds_cluster" "cluster" {
   engine_version                      = data.aws_rds_engine_version.latest.version
   database_name                       = local.admin_username
   master_username                     = local.admin_username
-  manage_master_user_password         = true
-  master_user_secret_kms_key_id       = aws_kms_key.master_key.arn
+  master_password                     = local.admin_password
   storage_encrypted                   = true
   enable_local_write_forwarding       = true
   backup_retention_period             = var.backup_retention_period_days
@@ -61,17 +51,6 @@ resource "aws_rds_cluster" "cluster" {
   )
 }
 
-data "aws_secretsmanager_secret" "admin" {
-  arn = join("", aws_rds_cluster.cluster.master_user_secret.*.secret_arn)
-}
-data "aws_secretsmanager_secret_version" "admin" {
-  secret_id     = data.aws_secretsmanager_secret.admin.id
-  version_stage = "AWSCURRENT"
-}
-locals {
-  admin = nonsensitive(jsondecode(data.aws_secretsmanager_secret_version.admin.secret_string))
-}
-
 resource "aws_rds_cluster_instance" "instance" {
   cluster_identifier   = aws_rds_cluster.cluster.id
   identifier_prefix    = "${local.sanitised_name}-"
diff --git a/cloud/aws/rds_serverless/tenant/db.tf b/cloud/aws/rds_serverless/tenant/db.tf
index 3068e46..edf7de9 100644
--- a/cloud/aws/rds_serverless/tenant/db.tf
+++ b/cloud/aws/rds_serverless/tenant/db.tf
@@ -4,8 +4,8 @@ locals {
     host = data.aws_rds_cluster.cluster.endpoint
     port = local.is_mysql ? 3306 : 5432
   }
-  mysql_command    = "${var.mysql_binary} -h ${data.ssh_tunnel.db.local.host} -P ${data.ssh_tunnel.db.local.port} -u ${var.admin_username}"
-  postgres_command = "${var.postgres_binary} -h ${data.ssh_tunnel.db.local.host} -p ${data.ssh_tunnel.db.local.port} -U ${var.admin_username} -d ${var.admin_username}"
+  mysql_command    = try("${var.mysql_binary} -h ${data.ssh_tunnel.db.local.host} -P ${data.ssh_tunnel.db.local.port} -u ${var.admin_username}", "")
+  postgres_command = try("${var.postgres_binary} -h ${data.ssh_tunnel.db.local.host} -p ${data.ssh_tunnel.db.local.port} -U ${var.admin_username} -d ${var.admin_username}", "")
   database_environment_variables = {
     PGPASSWORD = !local.is_mysql ? var.admin_password : null,
     MYSQL_PWD  = local.is_mysql ? var.admin_password : null,
@@ -40,32 +40,39 @@ resource "terraform_data" "db" {
     )
     environment = local.database_environment_variables
   }
+  #provisioner "local-exec" {
+  #  command = (local.is_mysql
+  #    ? "echo \"CREATE USER IF NOT EXISTS '${var.username}' IDENTIFIED WITH AWSAuthenticationPlugin AS 'RDS' | ${local.mysql_command}"
+  #    : "echo \"CREATE USER ${var.username}; GRANT rds_iam TO ${var.username}\" | ${local.postgres_command}"
+  #  )
+  #  environment = local.database_environment_variables
+  #}
   provisioner "local-exec" {
     command = (local.is_mysql
-      ? "echo \"CREATE USER IF NOT EXISTS '${var.username}' IDENTIFIED WITH AWSAuthenticationPlugin AS 'RDS' | ${local.mysql_command}"
-      : "echo \"CREATE USER ${var.username}; GRANT rds_iam TO ${var.username}\" | ${local.postgres_command}"
+      ? "echo \"CREATE USER IF NOT EXISTS '${local.username}' IDENTIFIED BY '${local.password}'\" | ${local.mysql_command}"
+      : "echo \"CREATE USER ${local.username} WITH PASSWORD '${local.password}; \" | ${local.postgres_command}"
     )
     environment = local.database_environment_variables
   }
   provisioner "local-exec" {
     command = (local.is_mysql
-      ? "GRANT ALL PRIVILEGES ON ${var.database}.* TO '${var.username}'@'%'\""
-      : ""
+      ? "echo \"GRANT ALL PRIVILEGES ON ${local.database}.* TO '${local.username}'@'%'\" | ${local.mysql_command}"
+      : "echo \"ALTER DATABASE ${local.database} OWNER TO ${local.username}\" | ${local.postgres_command}"
     )
     environment = local.database_environment_variables
   }
   #provisioner "local-exec" {
   #  when = destroy
   #  command = (local.is_mysql
-  #    ? "DROP USER '${var.username}'@'%';"
-  #    : "DROP USER ${var.username};"
+  #    ? "DROP USER '${local.username}'@'%';"
+  #    : "DROP USER ${local.username};"
   #  )
   #}
   #provisioner "local-exec" {
   #  when = destroy
   #  command = (local.is_mysql
-  #    ? "echo 'DROP DATABASE ${var.database}' | ${local.mysql_command}"
-  #    : "echo 'DROP DATABASE ${var.database}' | ${local.postgres_command}"
+  #    ? "echo 'DROP DATABASE ${local.database}' | ${local.mysql_command}"
+  #    : "echo 'DROP DATABASE ${local.database}' | ${local.postgres_command}"
   #  )
   #}
 }
\ No newline at end of file
diff --git a/cloud/aws/rds_serverless/tenant/input.tf b/cloud/aws/rds_serverless/tenant/input.tf
index 48064c4..21dd935 100644
--- a/cloud/aws/rds_serverless/tenant/input.tf
+++ b/cloud/aws/rds_serverless/tenant/input.tf
@@ -13,6 +13,16 @@ variable "username" {
   type        = string
   description = "The username for the tenant"
 }
+variable "password" {
+  type        = string
+  description = "The password for the tenant"
+  default     = null
+}
+resource "random_password" "password" {
+  count   = var.password == null ? 1 : 0
+  special = false
+  length  = 32
+}
 variable "database" {
   type        = string
   description = "The database for the tenant"
@@ -20,6 +30,7 @@ variable "database" {
 locals {
   username = lower(var.username)
   database = lower(var.database)
+  password = try(random_password.password[0].result, var.password)
 }
 variable "app_name" {
   type        = string
diff --git a/cloud/aws/rds_serverless/tenant/output.tf b/cloud/aws/rds_serverless/tenant/output.tf
index 57f8fe1..9df173e 100644
--- a/cloud/aws/rds_serverless/tenant/output.tf
+++ b/cloud/aws/rds_serverless/tenant/output.tf
@@ -1,18 +1,13 @@
 output "username" {
   value = local.username
 }
+output "password" {
+  value = local.password
+}
 output "database" {
   value = local.database
 }
-output "access_key" {
-  value = aws_iam_access_key.tenants.id
-}
-output "secret_key" {
-  value = aws_iam_access_key.tenants.secret
-}
-output "auth_token" {
-  value = data.external.rds_auth_token.result.password
-}
+
 output "connection_string" {
   value = join(" ", [
     "mysql",
@@ -20,6 +15,6 @@ output "connection_string" {
     "-P", data.aws_rds_cluster.cluster.port,
     "-D", local.database,
     "-u", local.username,
-    "-p'${data.external.rds_auth_token.result.password}'",
+    "-p'${local.password}'",
   ])
 }
\ No newline at end of file
diff --git a/cloud/aws/rds_serverless/tenant/tenant.tf b/cloud/aws/rds_serverless/tenant/tenant.tf
deleted file mode 100644
index 2ced46e..0000000
--- a/cloud/aws/rds_serverless/tenant/tenant.tf
+++ /dev/null
@@ -1,107 +0,0 @@
-data "aws_arn" "tenant" {
-  arn = join(":", [
-    "arn", "aws", "rds-db",
-    data.aws_region.current.name,
-    data.aws_caller_identity.current.account_id,
-    "dbuser",
-    "${lower(data.aws_rds_cluster.cluster.cluster_resource_id)}/${local.username}"
-  ])
-}
-data "aws_iam_policy_document" "assume_role_policy" {
-  statement {
-    actions = ["sts:AssumeRole"]
-    principals {
-      type        = "Service"
-      identifiers = ["ec2.amazonaws.com"]
-    }
-  }
-}
-data "aws_iam_policy_document" "tenant_user_policy_document" {
-  statement {
-    actions   = ["rds-db:*"]
-    effect    = "Allow"
-    resources = [data.aws_arn.tenant.arn]
-  }
-}
-
-# Create IAM and key for each tenant
-resource "aws_iam_user" "tenant" {
-  name = join("", ["RDS", var.app_name, title(local.username)])
-  tags = var.tags
-  path = "/${join("/", [var.app_name, "RDS", title(local.username), ])}/"
-}
-
-data "external" "rds_auth_token" {
-  program = [
-    "bash", "-c", replace(
-      <<-EOF
-        aws
-          --profile ${var.aws_profile}
-          rds
-            generate-db-auth-token
-              --hostname  ${data.aws_rds_cluster.cluster.endpoint}
-              --port      ${data.aws_rds_cluster.cluster.port}
-              --region    ${replace(data.aws_region.current.name, "/[[:lower:]]$/", "")}
-              --username  ${local.username}
-        | jq --raw-input '{ password: . }'
-      EOF
-    , "\n", " ")
-  ]
-}
-
-# Here's that key we were just talking about
-resource "aws_iam_access_key" "tenants" {
-  user   = aws_iam_user.tenant.name
-  status = var.is_active ? "Active" : "Inactive"
-}
-
-# Make a role for the IAM user to assume
-resource "aws_iam_role" "rds_instance_policy_per_user" {
-  assume_role_policy = data.aws_iam_policy_document.assume_role_policy.json
-  name               = join("", [var.app_name, "RDS", "Tenant", title(local.username), ])
-  path               = "/${join("/", [var.app_name, "RDS", "Tenant", title(local.username), ])}/"
-  tags               = var.tags
-}
-
-# Create a policy per-tenant
-resource "aws_iam_policy" "tenant_policy" {
-  name        = join("", [var.app_name, "RDS", title(local.username), "TenantUserPolicy"])
-  path        = "/RDS/${var.app_name}/${local.username}/"
-  description = "Allow user ${local.username} Data Access to RDS database ${local.database}"
-  policy      = data.aws_iam_policy_document.tenant_user_policy_document.json
-  tags        = var.tags
-}
-
-# find the superuser role if supplied
-data "aws_iam_role" "superuser" {
-  count = var.super_user_iam_role_name != null ? 1 : 0
-  name  = var.super_user_iam_role_name
-}
-# Attach the tenant policy role to the tenants role
-resource "aws_iam_policy_attachment" "tenant_policy_attachment" {
-  name       = join("", [var.app_name, "RDS", title(local.username), "TenantUserPolicyAttachment"])
-  policy_arn = aws_iam_policy.tenant_policy.arn
-  roles = compact([
-    aws_iam_role.rds_instance_policy_per_user.name,
-    try(data.aws_iam_role.superuser[0].name, null)
-  ])
-}
-
-# Create the tenants own group
-resource "aws_iam_group" "tenant" {
-  name = join("", [var.app_name, "RDS", title(local.username), "TenantGroup"])
-  path = "/${join("/", [var.app_name, "RDS", title(local.username), ])}/"
-}
-
-# Attach the tenant role to the tenant group
-resource "aws_iam_group_policy_attachment" "tenant_policy_attachment" {
-  group      = aws_iam_group.tenant.name
-  policy_arn = aws_iam_policy.tenant_policy.arn
-}
-
-# Attach the tenant user to the tenant group
-resource "aws_iam_group_membership" "tenant" {
-  name  = join("", [var.app_name, "RDS", title(local.username), "TenantGroupMembership"])
-  group = aws_iam_group.tenant.name
-  users = [aws_iam_user.tenant.name]
-}
\ No newline at end of file
diff --git a/cloud/aws/rds_serverless/tenants.tf b/cloud/aws/rds_serverless/tenants.tf
new file mode 100644
index 0000000..b2a632b
--- /dev/null
+++ b/cloud/aws/rds_serverless/tenants.tf
@@ -0,0 +1,21 @@
+module "tenants" {
+  depends_on     = [aws_rds_cluster.cluster, aws_rds_cluster_instance.instance]
+  for_each       = var.tenants
+  source         = "./tenant"
+  username       = each.value.username
+  database       = each.value.database
+  app_name       = local.app_name
+  vpc_id         = data.aws_vpc.current.id
+  aws_profile    = var.aws_profile
+  cluster_id     = aws_rds_cluster.cluster.id
+  engine         = aws_rds_cluster.cluster.engine
+  admin_username = local.admin_username
+  admin_password = local.admin_password
+  tags = merge(
+    try(var.application.application_tag, {}),
+    {
+      "TerraformRDSClusterName" = var.instance_name
+      "TerraformRDSTenantName"  = each.value.username
+    }
+  )
+}
\ No newline at end of file