From fd82876f5f230e446d284e45dbf35e5fc9f88dc7 Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Mon, 27 Jul 2026 17:22:19 +1000 Subject: [PATCH] Manage Gitea users; add teabot personality bot accounts Add a data-driven 'user' config kind so Gitea accounts are declared as config/git.unkin.net/user/.yaml, mirroring the existing repository and team kinds. Wire the yaml into a new modules/user submodule that creates a gitea_user (provider go-gitea/gitea 0.7.0, already pinned). The provider's user resource requires a password; generate a per-user random_password so nothing sensitive is hardcoded and only a placeholder lives in state (tokens come later from vault-plugin-secrets-gitea). Provision teabot's implementer and reviewer personality accounts with a conservative posture: not site admins, no org creation, no repo creation, limited profile visibility. Claude-Session: https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv --- config/config.hcl | 8 +++ .../user/teabot-implementer.yaml | 14 ++++ .../git.unkin.net/user/teabot-reviewer.yaml | 14 ++++ environments/au/syd1/terragrunt.hcl | 1 + modules/gitea_instance/main.tf | 19 +++++ modules/gitea_instance/modules/user/main.tf | 37 ++++++++++ .../gitea_instance/modules/user/terraform.tf | 13 ++++ .../gitea_instance/modules/user/variables.tf | 69 +++++++++++++++++++ modules/gitea_instance/terraform.tf | 4 ++ modules/gitea_instance/variables.tf | 19 +++++ 10 files changed, 198 insertions(+) create mode 100644 config/git.unkin.net/user/teabot-implementer.yaml create mode 100644 config/git.unkin.net/user/teabot-reviewer.yaml create mode 100644 modules/gitea_instance/modules/user/main.tf create mode 100644 modules/gitea_instance/modules/user/terraform.tf create mode 100644 modules/gitea_instance/modules/user/variables.tf diff --git a/config/config.hcl b/config/config.hcl index b86e152..ad99dbc 100644 --- a/config/config.hcl +++ b/config/config.hcl @@ -33,6 +33,14 @@ locals { }) if length(regexall("/team/", file_path)) > 0 } + user = { + for file_path, content in local.all_configs : + "${split("/", file_path)[0]}/${trimsuffix(basename(file_path), ".yaml")}" => merge(content, { + username = trimsuffix(basename(file_path), ".yaml") + gitea_url = split("/", file_path)[0] + }) + if length(regexall("/user/", file_path)) > 0 + } branch_protection = merge([ for file_path, content in local.all_configs : { for idx, rule in try(content.branch_protection, []) : diff --git a/config/git.unkin.net/user/teabot-implementer.yaml b/config/git.unkin.net/user/teabot-implementer.yaml new file mode 100644 index 0000000..50d9cb5 --- /dev/null +++ b/config/git.unkin.net/user/teabot-implementer.yaml @@ -0,0 +1,14 @@ +# teabot "implementer" personality account. +# Used by the teabot daemon (unkin/teabot) to open issues/PRs as a distinct +# identity so its work is attributable. Auth is via an API token issued +# out-of-band (vault-plugin-secrets-gitea / static KV) -- no token or usable +# password lives in this repo's state beyond the generated placeholder. +email: teabot-implementer@unkin.net +full_name: "Teabot Implementer" +description: "teabot implementer bot -- automated agent (unkin/teabot)" +# Conservative bot posture: not a site admin, cannot create orgs or repos, +# profile visible only to signed-in users. +visibility: limited +admin: false +allow_create_organization: false +max_repo_creation: 0 diff --git a/config/git.unkin.net/user/teabot-reviewer.yaml b/config/git.unkin.net/user/teabot-reviewer.yaml new file mode 100644 index 0000000..3b579be --- /dev/null +++ b/config/git.unkin.net/user/teabot-reviewer.yaml @@ -0,0 +1,14 @@ +# teabot "reviewer" personality account. +# Used by the teabot daemon (unkin/teabot) to review pull requests as a distinct +# identity so its reviews are attributable. Auth is via an API token issued +# out-of-band (vault-plugin-secrets-gitea / static KV) -- no token or usable +# password lives in this repo's state beyond the generated placeholder. +email: teabot-reviewer@unkin.net +full_name: "Teabot Reviewer" +description: "teabot reviewer bot -- automated agent (unkin/teabot)" +# Conservative bot posture: not a site admin, cannot create orgs or repos, +# profile visible only to signed-in users. +visibility: limited +admin: false +allow_create_organization: false +max_repo_creation: 0 diff --git a/environments/au/syd1/terragrunt.hcl b/environments/au/syd1/terragrunt.hcl index 73a4bdf..ab4e5da 100644 --- a/environments/au/syd1/terragrunt.hcl +++ b/environments/au/syd1/terragrunt.hcl @@ -21,4 +21,5 @@ inputs = { repository = local.config.repository branch_protection = local.config.branch_protection team = local.config.team + user = local.config.user } diff --git a/modules/gitea_instance/main.tf b/modules/gitea_instance/main.tf index c25e538..8a93840 100644 --- a/modules/gitea_instance/main.tf +++ b/modules/gitea_instance/main.tf @@ -38,6 +38,25 @@ module "repository" { depends_on = [module.organisation] } +module "user" { + source = "./modules/user" + + for_each = var.user + + username = each.value.username + email = each.value.email + login_name = each.value.login_name + full_name = each.value.full_name + description = each.value.description + visibility = each.value.visibility + admin = each.value.admin + restricted = each.value.restricted + active = each.value.active + allow_create_organization = each.value.allow_create_organization + max_repo_creation = each.value.max_repo_creation + must_change_password = each.value.must_change_password +} + module "team" { source = "./modules/team" diff --git a/modules/gitea_instance/modules/user/main.tf b/modules/gitea_instance/modules/user/main.tf new file mode 100644 index 0000000..c51e41e --- /dev/null +++ b/modules/gitea_instance/modules/user/main.tf @@ -0,0 +1,37 @@ +# The gitea_user resource requires a password argument. These accounts +# authenticate via API tokens (issued out-of-band by the +# vault-plugin-secrets-gitea engine, or a static KV entry initially), so the +# password is never used interactively. It is generated here so nothing +# sensitive is hardcoded, and kept only in state. +resource "random_password" "this" { + length = 32 + special = true + override_special = "!@#%^&*()-_=+" +} + +resource "gitea_user" "this" { + username = var.username + login_name = coalesce(var.login_name, var.username) + email = var.email + password = random_password.this.result + + full_name = var.full_name + description = var.description + visibility = var.visibility + + admin = var.admin + restricted = var.restricted + active = var.active + allow_create_organization = var.allow_create_organization + max_repo_creation = var.max_repo_creation + must_change_password = var.must_change_password + + # Do not e-mail the (possibly unmonitored) bot address on creation. + send_notification = false + + lifecycle { + # The provider cannot read the password back from the API; ignore it so the + # account never shows perpetual drift once created. + ignore_changes = [password] + } +} diff --git a/modules/gitea_instance/modules/user/terraform.tf b/modules/gitea_instance/modules/user/terraform.tf new file mode 100644 index 0000000..0befbc9 --- /dev/null +++ b/modules/gitea_instance/modules/user/terraform.tf @@ -0,0 +1,13 @@ +terraform { + required_version = ">= 1.10" + required_providers { + gitea = { + source = "go-gitea/gitea" + version = "0.7.0" + } + random = { + source = "hashicorp/random" + version = ">= 3.5" + } + } +} diff --git a/modules/gitea_instance/modules/user/variables.tf b/modules/gitea_instance/modules/user/variables.tf new file mode 100644 index 0000000..4ccea98 --- /dev/null +++ b/modules/gitea_instance/modules/user/variables.tf @@ -0,0 +1,69 @@ +variable "username" { + description = "Username of the Gitea account (immutable; changing it recreates the user)" + type = string +} + +variable "login_name" { + description = "Login name, may differ from username. Defaults to the username." + type = string + default = null +} + +variable "email" { + description = "E-Mail address of the user" + type = string +} + +variable "full_name" { + description = "Display / full name of the user" + type = string + default = "" +} + +variable "description" { + description = "Profile description of the user" + type = string + default = "" +} + +variable "visibility" { + description = "Profile visibility: public, limited or private" + type = string + default = "limited" +} + +variable "admin" { + description = "Whether the user is a site administrator" + type = bool + default = false +} + +variable "restricted" { + description = "Restricted users can only access repositories/orgs they are explicitly added to" + type = bool + default = false +} + +variable "active" { + description = "Whether the account is active" + type = bool + default = true +} + +variable "allow_create_organization" { + description = "Whether the user may create organisations" + type = bool + default = false +} + +variable "max_repo_creation" { + description = "Maximum number of repositories the user may create (0 disallows creation)" + type = number + default = 0 +} + +variable "must_change_password" { + description = "Force a password change on first login" + type = bool + default = false +} diff --git a/modules/gitea_instance/terraform.tf b/modules/gitea_instance/terraform.tf index 4a11ea7..98258fa 100644 --- a/modules/gitea_instance/terraform.tf +++ b/modules/gitea_instance/terraform.tf @@ -9,5 +9,9 @@ terraform { source = "Kichiyaki/woodpecker" version = "0.5.0" } + random = { + source = "hashicorp/random" + version = ">= 3.5" + } } } diff --git a/modules/gitea_instance/variables.tf b/modules/gitea_instance/variables.tf index 6ce9653..5b70da7 100644 --- a/modules/gitea_instance/variables.tf +++ b/modules/gitea_instance/variables.tf @@ -64,6 +64,25 @@ variable "branch_protection" { } +variable "user" { + description = "Map of Gitea user accounts to create" + type = map(object({ + username = string + email = string + login_name = optional(string) + full_name = optional(string, "") + description = optional(string, "") + visibility = optional(string, "limited") + admin = optional(bool, false) + restricted = optional(bool, false) + active = optional(bool, true) + allow_create_organization = optional(bool, false) + max_repo_creation = optional(number, 0) + must_change_password = optional(bool, false) + })) + default = {} +} + variable "team" { description = "Map of teams to create" type = map(object({