crossplane-contrib / crossplane-contrib/provider-sql

Grant routines: search_path-dependent signature comparison, and remaining unexpressible arg types (review findings from #436)

Open
#439 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
Go
Stars
154
Forks
119
Avg merge
6d 17h
Merged PRs (30d)
8

Description

## What happened

While reviewing #436 (*Allow schema-qualified type names in Grant routine args*) I had Claude Code do an independent pass over the PR head. It surfaced three things that are worth tracking separately rather than blocking that PR. Filing them together here.

> **Note:** these findings were produced by Claude Code during review of #436, and verified by hand where noted below. Treat the reasoning as reviewed but the prioritisation as a suggestion.

Context: #436 widens the CRD pattern on `Grant.spec.forProvider.routines[].args` from `^[a-zA-Z_][a-zA-Z0-9_$]*$` to `^[a-zA-Z_][a-zA-Z0-9_$]*(\.[a-zA-Z_][a-zA-Z0-9_$]*)?$` so schema-qualified composite types (e.g. AWS RDS's `aws_commons._s3_uri_1`) can be expressed.

### Confirmed non-issue, for the record

The widened pattern still holds as the SQL-injection guard for this field, which matters because `quotedSignatures()` splices args in **unquoted**. Checked against Go's `regexp` — the engine CRD `pattern` validation actually uses, so `$` is end-of-text rather than end-of-line and there is no newline-smuggling bypass. Accepts `text`, `int4`, `aws_commons._s3_uri_1`; rejects `a.b.c`, `.text`, `text.`, `text[]`, `character varying`, `text; DROP TABLE x`, `text"`, `text)`, `text,text`, `$1`, `text\n; DROP TABLE x`, `a.b\n`.

---

## 1. Schema-qualified args can silently never converge, depending on `search_path`

`selectRoutineGrantQuery` builds the signature it compares against with `pg_catalog.format_type(a.t, NULL)`:

```go
"SELECT array_to_string(array_agg(pg_catalog.format_type(a.t, NULL) ORDER BY a.ord), ',') " +
"FROM unnest(p.proargtypes) WITH ORDINALITY AS a(t, ord)"
```

`format_type` schema-qualifies a type name **only when the type is not visible in the current `search_path`** — `format_type_extended()` gates qualification on `TypeIsVisible()` unless `FORMAT_TYPE_FORCE_QUALIFY` is passed, and `format_type/1,2` do not pass it.

The provider never sets `search_path` (no hits in `pkg/`), so it runs with the role/database default. That means the motivating case in #436 works end to end: `aws_commons` is not visible, `format_type` emits `aws_commons._s3_uri_1`, and the spec value matches.

But the widened pattern also admits qualifications that can never match. Granting on a type in a schema that *is* in the connection's `search_path` — `public.my_composite` being the obvious case — behaves like this:

1. admission passes (the pattern allows it),
2. the `GRANT` succeeds (PostgreSQL resolves the qualified name fine),
3. `format_type` returns bare `my_composite`, so `WHERE sub.signature = ANY($6)` never matches,
4. `ResourceUpToDate` is permanently `false`, and Update re-issues the `GRANT` on every reconcile.

The resource never reports synced and the provider hot-loops `GRANT` statements against the database. Before #436 that input was rejected at admission, so the loop wasn't reachable — the widening is what makes it expressible, though the underlying brittleness is in the string-signature comparison, not in the pattern.

Two options, smallest first:

- **Document it.** One sentence on the `Arguments` field: the schema qualifier is for types *outside* the provider connection's `search_path`; qualifying a visible type will not converge. Cheap, and honest about the sharp edge.
- **Compare resolved type OIDs instead of formatted strings.** Resolve each spec arg with `$n::regtype` and compare the resulting OID array against `p.proargtypes`, rather than string-matching `format_type` output. That makes the comparison spelling-independent: `integer`/`int4`, qualified/unqualified, and `character varying`/`varchar` all collapse to the same OID. It would also retire the `strings.ToLower()` + "must not quote type names" reasoning currently documented in `quotedSignatures()`, and it subsumes finding 3 below.

The second is the real fix but is a behaviour change to Observe, so it wants its own PR.

## 2. The pattern change has no test of its own

`apis/*/postgresql/v1alpha1/grant_types_test.go` was dropped from #436 (correctly — see #438 for the shared CRD-regex test helper). The coverage that remains is a `TestGrantSQL` case per API, which exercises `quotedSignatures()`. That function never applies the CRD pattern, so as merged the changed line has no regression protection.

Not a reason to hold #436, but two concrete gaps:

- #438's helper should cover `routines[].args` when it lands.
- `TestRoutineSignature` — the test that pins the format the Observe query compares against — was not extended with a schema-qualified case. That's the natural place to encode the round-trip invariant from finding 1.

## 3. Routines with array or multi-word arg types are still ungrantable

Because Observe compares against `format_type` output, the spec value has to be spelled the way `format_type` spells it. The pattern (before and after #436) rejects both:

- array types — `format_type` emits `text[]`, pattern rejects `[]`
- multi-word canonical names — `format_type` emits `character varying`, `timestamp with time zone`, `double precision`; pattern rejects the space

So `GRANT EXECUTE` on any routine taking `text[]` or `varchar` cannot be expressed today, which is the same "there's no way to express this grant" problem #436 fixes for composite types. Pre-existing rather than introduced.

Fixing this means widening the pattern again, and the `GRANT` text still needs the literal spelling either way — so the "identifier characters only" safety argument has to be re-made for whatever is admitted. A bounded shape (optional `[]` suffix, plus an allowlist of the multi-word built-in names) keeps that argument tractable. Finding 1's `::regtype` change is a prerequisite in practice: as long as Observe string-matches `format_type` output, every accepted spelling also has to be the canonical one, and `varchar` vs `character varying` would land straight in the non-convergence loop described above.

---

Happy to split these into separate issues if that's easier to track.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start with selectRoutineGrantQuery and quotedSignatures(), then read apis/*/postgresql/v1alpha1/grant_types_test.go and the shared CRD-regex helper from #438. Run TestRoutineSignature and the relevant TestGrantSQL cases; done means the chosen scope is split or defined, with regression coverage for the convergence and accepted-argument cases.

Written by the indexing model from the issue text.

Assessment

Tech stack
go, postgresql, sql
Domain
backend, databases
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
32/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.