{"id":50846038,"url":"https://github.com/sethbacon/terraform-suite-identity","last_synced_at":"2026-06-14T09:32:32.369Z","repository":{"id":362550671,"uuid":"1259498875","full_name":"sethbacon/terraform-suite-identity","owner":"sethbacon","description":"Shared identity component for the Terraform tooling suite (registry + state manager) — owned by neither app","archived":false,"fork":false,"pushed_at":"2026-06-04T19:15:32.000Z","size":24,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-04T21:27:50.594Z","etag":null,"topics":["authentication","go","identity","postgresql","terraform"],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sethbacon.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-04T15:10:30.000Z","updated_at":"2026-06-04T19:15:46.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/sethbacon/terraform-suite-identity","commit_stats":null,"previous_names":["sethbacon/terraform-suite-identity"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/sethbacon/terraform-suite-identity","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sethbacon%2Fterraform-suite-identity","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sethbacon%2Fterraform-suite-identity/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sethbacon%2Fterraform-suite-identity/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sethbacon%2Fterraform-suite-identity/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sethbacon","download_url":"https://codeload.github.com/sethbacon/terraform-suite-identity/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sethbacon%2Fterraform-suite-identity/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34316823,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-14T02:00:07.365Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["authentication","go","identity","postgresql","terraform"],"created_at":"2026-06-14T09:32:31.814Z","updated_at":"2026-06-14T09:32:32.362Z","avatar_url":"https://github.com/sethbacon.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# terraform-suite-identity\n\nShared identity \u0026 auth component for the Terraform tooling suite (the registry and\nthe state manager).\n\nIt is owned by **neither** consuming application: either app can stand the identity\nstore up at setup time, and whichever app is installed second detects that it already\nexists and attaches to it. See ADR 002 in the consuming repositories for the full\nrationale.\n\nThe module is a **Go library** — it is linked into each app's binary, not run as a\nseparate service. Consuming it has no runtime/operational footprint; an app can use the\nshared schema or keep identity in its own schema (see [Schema routing](#schema-routing)).\n\n## Packages\n\n| Package | Purpose |\n| ------- | ------- |\n| `identity` | Migration runner for the dedicated `identity` Postgres schema (isolated golang-migrate instance + `identity_schema_migrations` version table). |\n| `identity/models` | The canonical identity data types — `User`, `Organization`, `OrganizationMember` (+ membership views), `APIKey`, `RoleTemplate`, `OIDCConfig`, `AuditLog`. |\n| `identity/store` | The data-access layer (repository pattern) for those types, plus `TokenRepository` (JWT revocation). Repos use **unqualified** table names so the connection's `search_path` selects the schema. |\n| `identity/auth` | App-neutral auth primitives: scope checking (`HasScope`/`HasAnyScope`/`HasAllScopes` with wildcard `admin` + write-implies-read), the JWT `TokenManager` (HS256, JTI, secret rotation), and API-key generation/validation. |\n| `identity/auth/oidc` | A generic OpenID Connect provider (discovery, auth URL, code exchange, ID-token verification, group/user-info extraction). |\n\n## Canonical identity model\n\nThe data model is **canonical across the suite** — both apps use the same shapes. The\nonly per-app variance is the **role → scope mapping**: the module is app-agnostic about\nscope *contents*, and each app seeds its own scopes onto `role_templates` at setup (the\n\"identity-core + app-extended\" model).\n\nNotable modelling choices:\n\n- **No soft-active flag on users.** Access derives entirely from organization memberships\n  and the scopes their role templates grant; \"disabling\" a user means removing their\n  memberships (or deleting the user).\n- **API keys** are usable while they exist and have not passed `expires_at`; revocation is\n  a hard delete (no soft flag). JWT revocation is tracked separately in `revoked_tokens`.\n- **Multi-org by default** — `UserWithOrgRoles` aggregates scopes across all memberships.\n\n## Installation\n\n```bash\ngo get github.com/sethbacon/terraform-suite-identity@latest\n```\n\nPin a minimum version in `go.mod`. Schema migrations are additive within a major version.\n\n## Usage\n\n### Migrations\n\nApply the identity migrations before the application's own migrations:\n\n```go\nimport \"github.com/sethbacon/terraform-suite-identity/identity\"\n\nif err := identity.RunMigrations(db, \"up\"); err != nil {\n    return err\n}\nversion, dirty, err := identity.GetMigrationVersion(db)\n```\n\n`db` is a standard `*sql.DB` on the shared PostgreSQL database. The runner uses\n`CREATE … IF NOT EXISTS` / `ON CONFLICT DO NOTHING` with an advisory lock, so it is safe\nfor **detect-and-attach** when multiple apps run it against the same database.\n\n### Schema routing\n\nThe store repositories use unqualified table names, so *the connection decides the\nschema*. An app opts into the shared identity schema by giving the identity repositories a\nconnection whose `search_path` puts `identity` first, while its own feature tables fall\nback to `public`:\n\n```go\n// Identity connection → identity schema (feature tables still resolve at public).\ndsn := baseDSN + \" options='-c search_path=identity,public'\"\nidentityDB, _ := sql.Open(\"postgres\", dsn)\n\nuserRepo := store.NewUserRepository(identityDB) // reads/writes identity.users\n```\n\nWith a plain `public` connection the same repositories operate entirely in the app's own\nschema — so adopting the shared schema is **opt-in and reversible** behind a feature flag.\n\n### Data layer\n\n```go\nimport \"github.com/sethbacon/terraform-suite-identity/identity/store\"\n\nuserRepo := store.NewUserRepository(db)\nuser, err := userRepo.GetOrCreateUserFromOIDC(ctx, sub, email, name)\n\napiKeyRepo := store.NewAPIKeyRepository(db)\ntokenRepo  := store.NewTokenRepository(db) // revoked_tokens\n```\n\n### Auth\n\n```go\nimport \"github.com/sethbacon/terraform-suite-identity/identity/auth\"\n\n// Scope checks — the app injects its own write→read pairs and scope set.\nok := auth.HasScope(userScopes, \"modules:write\", auth.ReadWritePairs{...})\n\n// JWT — secret + issuer injected (never read from the environment by the module).\ntm := auth.NewTokenManager([]byte(secret), \"terraform-registry\")\ntoken, _ := tm.Generate(userID, email, scopes, 24*time.Hour)\nclaims, _ := tm.Validate(token) // tries current then previous secret (rotation)\n\n// API keys\nkey, hash, prefix, _ := auth.GenerateAPIKey(\"tfr\")\n```\n\nOIDC:\n\n```go\nimport identityoidc \"github.com/sethbacon/terraform-suite-identity/identity/auth/oidc\"\n\nprov, _ := identityoidc.NewProvider(identityoidc.Config{\n    IssuerURL: issuer, ClientID: id, ClientSecret: secret,\n    RedirectURL: cb, Scopes: []string{\"openid\", \"email\", \"profile\"},\n    RequireHTTPS: true,\n})\n```\n\n## Versioning\n\nReleased with release-please + goreleaser on Conventional Commits. The module is in the\n`0.x` series while the API stabilises — breaking changes bump the **minor** version, and\nconsumers pin and upgrade in lockstep. Schema migrations are additive.\n\n## Development\n\n```bash\ngo build ./...\ngo vet ./...\ngo test ./... -race -coverprofile=coverage.out -covermode=atomic   # sqlmock — no live DB\ngosec ./...\n```\n\nThe data layer is unit-tested with sqlmock (no live database). The migration runner is\nexercised against live PostgreSQL by the consuming apps' integration/UAT suites.\n\n## License\n\nApache-2.0.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsethbacon%2Fterraform-suite-identity","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsethbacon%2Fterraform-suite-identity","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsethbacon%2Fterraform-suite-identity/lists"}