Commit Graph
9 Commits
Author SHA1 Message Date
zeekay d97d3d6276 cloud sql answers where a deployment configured it
Every address the plugin publishes hands out, or acts on, the connection
string that opens a database, and that string carries the PostgreSQL
password the plugin holds. Register now returns on a Config that is not
Enabled, so /v1/cloud-sql and /v1/meta are addresses an unconfigured
process does not answer at rather than addresses it refuses at.

Opt in with CLOUD_SQL_ENABLED=true, the way bootnode and calendar opt in:
an Enabled field, a ConfigFromEnv that reads it, and a Register that
returns early without it.
2026-08-24 05:49:02 -07:00
zeekay 3d180758ed one place decides what an unstated sslmode means
ConnectionString supplies require for a database that states no mode. The
create path wrote a mode of its own beside it and wrote disable, so a
handed-out connection string turned TLS off. It writes none now, and the
default is the only answer.
2026-08-24 05:48:47 -07:00
zeekayandHanzo Dev 21af781d5a a database is reached by the org that owns it
Every address under /v1/cloud-sql and /v1/meta answers about one org's
database, and a database is reached with the password that opens it. So
the org is the one the door resolved from the verified credential —
published on the event by whichever door read it, an IAM token over JWKS
or an IAM key — and a request that carries none is refused before a
handler runs.

The rule is stated once per group, so a route added to either group
inherits it, and each handler asks the same question again for the value:
the list filters on that org, the routes taking {id} check the row belongs
to it, and create files the row under it rather than under whatever the
body said.

The meta proxy forwards on the connection that opens ONE database, so
every statement postgres-meta runs runs inside that database. An org with
no database of its own gets that answer; the admin connection opens every
database on the server and is lent to nobody.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-24 03:24:06 -07:00
zeekay 054d65663a they are Bases, one per org — the word tenant is gone
Base's own registry was already org-native in every method — OrgDB(orgID),
HasOrg(orgID), orgDir(orgID) — and only the type names said tenant. So this is
the vocabulary catching up with the code, not a change of behaviour.

	TenantRegistry -> Bases          app.Bases().OrgDB(org)
	TenantConfig   -> BasesConfig
	MultiTenant    -> PerOrg         it enables per-org databases; say that
	MULTI_TENANT   -> MULTI_BASE
	core/tenant.go -> core/bases.go

Tenant was the wrong noun twice over: it is the multi-tenancy word, and this
product's unit is the thing a customer creates per org — a Base. Identity here
is org, user, project, and nothing else.

The UI never said it; this was Go-side only. 64 packages green.

One test was collateral and is fixed rather than renamed: TestToHTTPHeader
asserts a pure snake_case-to-Header-Case transform, so rewriting its EXPECTED
value while its input stayed "tenant_id" made it assert something false. The case
is dropped — org_id already covers that transform.
2026-08-10 18:23:40 -07:00
zeekay cc7a65b5cb base: route convention /api/* -> /v1/*
One path: /v1/*. We do not use /api/ — that's Casdoor's path. The
BASE_API_PREFIX env knob is gone; the prefix is hard-coded in
apis/base.go and apis/serve.go banner.

Server-side mounts:
- apis: bindHealthApi, bindCollectionApi, bindRecordCrudApi, etc. all
  attach under r.Group("/v1") (was r.Group(BASE_API_PREFIX, default /api))
- platform plugin: /api/platform/* -> /v1/platform/* (including auth and
  org sub-groups)
- kms plugin: /api/kms/* -> /v1/kms/*
- functions plugin: /api/functions/* -> /v1/functions/*
- cloudsql plugin: /api/cloud-sql/*, /api/meta/* -> /v1/*
- superuser OIDC: /_/api/oidc/config -> /_/v1/oidc/config
- core settings: default RateLimitRule labels /api/batch, /api/ ->
  /v1/batch, /v1/
- apis/batch.go: ValidBatchActions regexes match /v1/collections/*;
  internal rewrite targets /v1/collections/* (was /v1/base/collections/*,
  a stale artifact)

Clients:
- sdk/base-js (BaseClient.collections, files, realtime, CRDT WS,
  signIn/refresh/health) targets /v1/*
- sdk/base-react (use-crdt WS path) targets /v1/crdt
- ui-react admin (lib/api.ts, settings.application copy) targets /v1/*
- cmd/cli (NewClient.APIPrefix) defaults to /v1; client_test.go and
  cmd/cli_test.go stubServer use /v1/*
- cmd/typegen calls /v1/collections

Upstream paths untouched: Casdoor IAM (/api/login, /api/userinfo,
/api/signup, /api/send-verification-code, /api/get-user), KMS Infisical
(/api/v1/auth/universal-auth/login, /api/v2/workspace/environments),
Kubernetes tasks (/api/v1/namespaces/...), IPFS (/api/v0/...), Gitea/
GitLab/Discord/Strava/Patreon/Box/Wakatime upstream APIs. Those are
their contracts, not ours.

Tests: all 437 prior /api/ test URLs across apis/*_test.go and
plugins/platform/auth_proxy_test.go updated to /v1/. Build + go vet
clean. ./apis ./core ./plugins/platform ./plugins/jsvm ./forms all
green. Pre-existing red probes (network/TestRedProbe_*,
migratecmd/TestAutomigrateCollection*) unchanged.
2026-05-15 14:41:26 -07:00
zeekay 5939cbaa0c logger: switch attr-helpers to variadic positional pairs
Every non-handler call site that wrapped a value in
slog.String/Int/Bool/Any/Float64/Uint64 was passing an Attr through the
variadic any... slot of (*slog.Logger).Info/Warn/Error/Debug; the same
slot accepts the bare "key", value pair with identical semantics. Drop
the wrapper so the surface is one level less indirect and so the next
two commits can swap (*slog.Logger) for a small Logger interface
without rewriting every attr expression as well.

23 files, 156 call sites collapsed; no behavior change. Five files —
tools/logger/log.go, tools/logger/batch_handler.go +_test, and
core/log_printer.go +_test — are the handler-implementation layer and
legitimately reference slog.Attr/Level/Group; they were skipped.
2026-05-15 03:50:49 -07:00
zeekay 11e0d25235 feat(base): canonical 3-header claims helper, strip non-canonical
Introduces tools/claims as the single source of truth for reading the
authenticated caller's identity across every Base-derived service. One
way to do it, zero dispute.

The canonical 3 identity contract:

  X-User-Id <- JWT sub    (set by the Hanzo Gateway post-JWKS)
  X-Org-Id  <- JWT owner
  X-Roles   <- JWT roles (comma-joined if array)

Services must not read any other variant — no X-Hanzo-*, no X-IAM-*,
no X-User-Role (singular), no X-Tenant-Id alias. The Strip middleware
removes every historical variant on ingress so a spoofed header cannot
reach a handler.

Changes:

* tools/claims/claims.go (new):
  - Constants HeaderUserID/HeaderOrgID/HeaderRoles
  - Claims struct + FromHeaders(r) reader
  - Claims.HasRole(...)
  - StripIdentityHeaders(h) drops canonical + legacy + every
    X-IAM-*/X-HANZO-* prefix (case-insensitive)
  - Strip(next) net/http middleware

* tools/claims/claims_test.go (new): 7 tests covering canonical read,
  empty roles, legacy-variant rejection, HasRole semantics, strip
  coverage, strip preservation, and the Strip middleware.

* plugins/cloudsql/cloudsql.go: the postgres-meta proxy now keys
  tenant routing on X-Org-Id (the canonical JWT-owner header) instead
  of the legacy X-Tenant-ID.

* plugins/platform/auth_proxy_test.go: renamed the test marker
  X-IAM-Test -> X-Test-Endpoint so the canonical grep ("X-IAM-*")
  returns zero hits. Behavior unchanged.
2026-04-18 22:19:43 -07:00
zeekay 1e8435ade5 fix(cloudsql): avoid route conflict with static catch-all handler
Replace meta.Any("/{path...}") with explicit GET/POST/PUT/PATCH/DELETE
to prevent conflict with the static file handler's GET /{path...}.
2026-03-01 18:57:51 -08:00
zeekay 87701541f5 feat(base): add Cloud SQL plugin for serverless PostgreSQL
Adds plugins/cloudsql with per-tenant database provisioning via Neon,
postgres-meta reverse proxy for schema introspection, and REST API
at /api/cloud-sql/databases. Registers platform + cloudsql plugins
in examples/base/main.go.
2026-03-01 18:14:19 -08:00