From 185feaeef7ecd18fe238569e453ab4c12f122d07 Mon Sep 17 00:00:00 2001 From: Alejandro Bernal Date: Wed, 2 Sep 2026 14:39:06 -0500 Subject: [PATCH 1/6] CXH-2381: document Db2 config requirements and correct the engine lists Add a "Writing a Db2 spec" section to docs/db2.md (wrap every column reference in string(), alias columns to double-quoted lowercase), notes that account provisioning is unavailable and group principals can't be represented, and a "Running the Db2 tests" section with make test-db2 / make vet-db2 and the raw CGO_CFLAGS/CGO_LDFLAGS + library-path invocation. Add test-db2 and vet-db2 Makefile targets scoping the CGO flags to the recipe. Reconcile the engine lists (README, docs/docs-info.md, test/README.md) so they agree with the dispatch switch in pkg/database/database.go, and drop the unfilled template sentence from docs-info.md. Point the connector help URL at the live docs page (/docs/baton/baton-sql); the old /docs/baton/sql returns 404. Bump the ci.yaml sync-test action to @v4 and gitignore the root baton-sql build output. --- .github/workflows/ci.yaml | 2 +- .gitignore | 4 +++ Makefile | 18 ++++++++++ README.md | 3 +- docs/db2.md | 76 +++++++++++++++++++++++++++++++++++++++ docs/docs-info.md | 6 +++- pkg/config/schema.go | 2 +- test/README.md | 6 ++-- 8 files changed, 111 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1df22297..0437c6af 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -41,7 +41,7 @@ jobs: - name: Build baton-sql run: go build ./cmd/baton-sql - name: Run sync tests - uses: ConductorOne/github-workflows/actions/sync-test@v2 + uses: ConductorOne/github-workflows/actions/sync-test@v4 with: connector: ./baton-sql baton-entitlement: 'role:admin:member' diff --git a/.gitignore b/.gitignore index d4f6c13d..38c124d8 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,10 @@ *.dylib *.c1z +# Root build output (go build ./cmd/baton-sql without -o) +/baton-sql +/baton-sql.exe + # Test binary, built with `go test -c` *.test diff --git a/Makefile b/Makefile index 69ac8e04..47f82b5b 100644 --- a/Makefile +++ b/Makefile @@ -39,6 +39,24 @@ ifeq ($(GOOS),darwin) codesign -f -s - ${OUTPUT_PATH} endif +# go test builds and runs its own binary, which the build-db2 install_name/rpath rewrite never +# touches, so it needs the clidriver on the library path at run time (DYLD_ macOS, LD_ elsewhere). +ifeq ($(GOOS),darwin) +DB2_LIB_ENV = DYLD_LIBRARY_PATH=$(DB2HOME)/lib +else +DB2_LIB_ENV = LD_LIBRARY_PATH=$(DB2HOME)/lib +endif + +.PHONY: test-db2 +test-db2: + CGO_CFLAGS='-I$(DB2HOME)/include' CGO_LDFLAGS='-L$(DB2HOME)/lib' $(DB2_LIB_ENV) \ + go test -tags db2 ./... + +.PHONY: vet-db2 +vet-db2: + CGO_CFLAGS='-I$(DB2HOME)/include' CGO_LDFLAGS='-L$(DB2HOME)/lib' \ + go vet -tags db2 ./... + # Self-contained DB2 distribution: binary + clidriver (including its license/ directory, # which the IBM redistribution terms require shipping) in one archive. Untar and run — # no installation, no environment variables. diff --git a/README.md b/README.md index 92a32f40..c1b87ec5 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ ## Key Features -- **Multi-Database Support**: Works with MySQL, PostgreSQL, Oracle, SQL Server, Vertica, SQLite, and WordPress +- **Multi-Database Support**: Works with MySQL, PostgreSQL, Oracle, SQL Server, SAP HANA, Vertica, and Amazon Redshift, plus opt-in IBM DB2 (see [Supported Database Engines](#supported-database-engines)) - **Account Provisioning**: Create user accounts with automatic random password generation - **Secure Password Management**: Database-appropriate password hashing (SHA2, bcrypt, MD5) - **Flexible Configuration**: Map any SQL query results to resources and entitlements @@ -23,6 +23,7 @@ - Microsoft SQL Server - Oracle - PostgreSQL +- SAP HANA - Vertica - Amazon Redshift - IBM DB2 — opt-in: requires a binary built with the `db2` tag and IBM's native CLI driver; see [docs/db2.md](docs/db2.md) diff --git a/docs/db2.md b/docs/db2.md index 6922a829..95fa64bd 100644 --- a/docs/db2.md +++ b/docs/db2.md @@ -116,6 +116,82 @@ DB2's native form is also accepted as-is: HOSTNAME=localhost;PORT=50000;DATABASE=TESTDB;UID=db2inst1;PWD=pass123;PROTOCOL=TCPIP ``` +## Writing a Db2 spec + +Db2 needs two things in every spec that the other engines don't. Both fail loudly, but the +error names neither the column nor the query, so they're easy to miss when adapting another +engine's spec. + +### Wrap every column reference in `string()` + +A bare column reference inside a CEL concatenation or comparison aborts the whole sync at +`list-resources`, before any resource is emitted: + +``` +error: listing resources failed: no such overload +``` + +The error names neither the column nor the expression. Wrap each reference in `string()`: + +```yaml +id: "string(.database_name) + '.' + string(.schema_name)" +``` + +`examples/redshift-test.yml` uses this form throughout — copy it when adapting a spec to Db2. + +### Alias columns to double-quoted lowercase names + +Db2 returns column names uppercase, and the engine keys each row on the driver's names, so +`SELECT GRANTEE` yields `.GRANTEE`, not `.grantee`. Alias every selected column to the +lowercase name the CEL expressions reference: + +```sql +SELECT RTRIM(GRANTEE) AS "grantee" FROM SYSCAT.DBAUTH +``` + +The double quotes are required; without them Db2 folds the alias back to uppercase. + +## Provisioning support + +Entitlement provisioning works normally: `GRANT`/`REVOKE` of roles, authorities and object +privileges behave the same as on any other engine. + +**Account creation and deletion are not available for Db2.** Db2 LUW has no `CREATE USER` +statement — authorization IDs are operating-system, LDAP or Kerberos identities managed +outside the database, and `GRANT ... TO USER ` succeeds even for names Db2 has never +seen. A create-account request against a `db2://` DSN has nothing to call. + +**Group principals can't be synced or provisioned.** Db2 has no in-database group table; +group membership is reachable only through the per-authorization-ID function +`SYSPROC.AUTH_LIST_GROUPS_FOR_AUTHID`, which a YAML resource list can't express. A grant +emitted with `principal_type: group` is dropped at ingest (visible as `grants_dropped` and +`ingest_quality.reason_flags` in the sync token), and `group` is omitted from `grantableTo` +even if a spec declares it. This is a property of Db2, not a limitation of the connector. + +## Running the Db2 tests + +Like the build, the test and vet targets carry the CGO flags inline: + +```bash +make test-db2 # go test -tags db2 ./... +make vet-db2 # go vet -tags db2 ./... +DB2HOME=/opt/clidriver make test-db2 # clidriver elsewhere +``` + +Running the raw commands instead of the make targets needs the same two CGO variables the +`build-db2` target sets, plus a library path — `go test` builds and runs its own binary, +which the build-time install-name/rpath rewrite never touches: + +```bash +CGO_CFLAGS="-I$DB2HOME/include" CGO_LDFLAGS="-L$DB2HOME/lib" \ + DYLD_LIBRARY_PATH="$DB2HOME/lib" \ + go test -tags db2 ./... +``` + +Use `LD_LIBRARY_PATH` instead of `DYLD_LIBRARY_PATH` on Linux. Without the CGO flags you get +`fatal error: 'sqlcli1.h' file not found`; without the library path on macOS, +`Library not loaded: libdb2.dylib` (both covered under Troubleshooting). + ## Troubleshooting **`'sqlcli1.h' file not found`** — clidriver missing or `DB2HOME` wrong. Check that diff --git a/docs/docs-info.md b/docs/docs-info.md index dac8f1af..25af0f2b 100644 --- a/docs/docs-info.md +++ b/docs/docs-info.md @@ -1,6 +1,6 @@ # Baton SQL Connector Documentation -While developing the connector, please fill out this form. This information is needed to write docs and to help other users set up the connector. +This document describes the Baton SQL connector's capabilities and the credentials needed to set it up. ## Connector capabilities @@ -12,6 +12,10 @@ While developing the connector, please fill out this form. This information is n > - PostgreSQL > - Oracle Database > - SQL Server + > - SAP HANA + > - Vertica + > - Amazon Redshift + > - IBM DB2 (opt-in; requires a binary built with the `db2` tag — see [docs/db2.md](db2.md)) > - WordPress (MySQL-based) > > The connector can sync custom user tables, role hierarchies, entitlements, and permissions based on configurable SQL queries. diff --git a/pkg/config/schema.go b/pkg/config/schema.go index 514bb709..43654aa3 100644 --- a/pkg/config/schema.go +++ b/pkg/config/schema.go @@ -20,7 +20,7 @@ var ( Config = field.NewConfiguration( ConfigurationFields, field.WithConnectorDisplayName("SQL"), - field.WithHelpUrl("/docs/baton/sql"), + field.WithHelpUrl("/docs/baton/baton-sql"), field.WithIconUrl("/static/app-icons/sql.svg"), ) ) diff --git a/test/README.md b/test/README.md index b79a191d..15911340 100644 --- a/test/README.md +++ b/test/README.md @@ -2,9 +2,11 @@ This directory contains comprehensive testing infrastructure for the baton-sql connector, including database initialization scripts and Docker configurations for random password generation across multiple database engines. -## Supported Databases +## Databases with a local test harness -The testing environment supports all major database engines: +This directory provides Docker-based test harnesses for the engines below. For the full set +of database engines the connector supports (including SAP HANA, Vertica, Amazon Redshift, and +opt-in IBM DB2), see [Supported Database Engines](../README.md#supported-database-engines). - **MySQL** - With SHA2-256 password hashing and account provisioning - **PostgreSQL** - With bcrypt password hashing via pgcrypto extension From 37b4e0c2a2cd75f13367066b716e02b0e21e3ff4 Mon Sep 17 00:00:00 2001 From: Alejandro Bernal Date: Wed, 2 Sep 2026 15:13:15 -0500 Subject: [PATCH 2/6] CXH-2381: restore WordPress mention in README, exercise sync-test auth check Add WordPress (MySQL-based) back to the README engine list so it agrees with docs/docs-info.md, which still ships it. Pass bad-credentials: DB_PASSWORD=invalid to sync-test@v4 so the new auth-error check actually runs; without it the step skips (DB_PASSWORD is not a BATON_* var, so nothing gets invalidated). --- .github/workflows/ci.yaml | 1 + README.md | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0437c6af..618c4264 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -47,6 +47,7 @@ jobs: baton-entitlement: 'role:admin:member' baton-principal: john.smith baton-principal-type: user + bad-credentials: DB_PASSWORD=invalid - name: Run account provisioning tests uses: ConductorOne/github-workflows/actions/account-provisioning@v3 with: diff --git a/README.md b/README.md index c1b87ec5..e9cbae44 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ - SAP HANA - Vertica - Amazon Redshift +- WordPress (MySQL-based) - IBM DB2 — opt-in: requires a binary built with the `db2` tag and IBM's native CLI driver; see [docs/db2.md](docs/db2.md) ## Configuration From 3b9c320de8573bd483b3f36b52fd4afcd44d9bb5 Mon Sep 17 00:00:00 2001 From: Alejandro Bernal Date: Thu, 3 Sep 2026 06:18:39 -0500 Subject: [PATCH 3/6] CXH-2381: address doc review feedback on Db2 and engine lists - db2.md: reframe the "Writing a Db2 spec" intro. The string()-wrapping and lowercase-alias patterns are not Db2-only; Oracle folds unquoted identifiers and Redshift needs string() in CEL concatenations. Db2 just needs both everywhere. - db2.md: correct the group-provisioning note. grantableTo is spec-driven and is not filtered by engine, so a spec that declares group still advertises it as grantable; the group grant is dropped at ingest, not at grantableTo. Split the two into separate statements. - docs-info.md: bring the "Example DSN formats" and "Configuration Examples" lists in line with the supported-engines list (SAP HANA, Vertica, Amazon Redshift, IBM DB2). - README.md: drop the hand-maintained engine enumeration in Key Features that had drifted from the Supported Database Engines list on WordPress; link to that list instead. --- README.md | 2 +- docs/db2.md | 19 ++++++++++++------- docs/docs-info.md | 7 +++++++ 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index e9cbae44..a3e00fd7 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ ## Key Features -- **Multi-Database Support**: Works with MySQL, PostgreSQL, Oracle, SQL Server, SAP HANA, Vertica, and Amazon Redshift, plus opt-in IBM DB2 (see [Supported Database Engines](#supported-database-engines)) +- **Multi-Database Support**: Works with a range of SQL engines (see [Supported Database Engines](#supported-database-engines) below) - **Account Provisioning**: Create user accounts with automatic random password generation - **Secure Password Management**: Database-appropriate password hashing (SHA2, bcrypt, MD5) - **Flexible Configuration**: Map any SQL query results to resources and entitlements diff --git a/docs/db2.md b/docs/db2.md index 95fa64bd..751c3709 100644 --- a/docs/db2.md +++ b/docs/db2.md @@ -118,9 +118,10 @@ HOSTNAME=localhost;PORT=50000;DATABASE=TESTDB;UID=db2inst1;PWD=pass123;PROTOCOL= ## Writing a Db2 spec -Db2 needs two things in every spec that the other engines don't. Both fail loudly, but the -error names neither the column nor the query, so they're easy to miss when adapting another -engine's spec. +Db2 needs two things in every spec. Other engines need them only in spots (Oracle folds +unquoted identifiers to uppercase; Redshift needs `string()` around columns in CEL +concatenations), but Db2 needs both everywhere. Both fail loudly, but the error names neither +the column nor the query, so they're easy to miss when adapting another engine's spec. ### Wrap every column reference in `string()` @@ -163,10 +164,14 @@ seen. A create-account request against a `db2://` DSN has nothing to call. **Group principals can't be synced or provisioned.** Db2 has no in-database group table; group membership is reachable only through the per-authorization-ID function -`SYSPROC.AUTH_LIST_GROUPS_FOR_AUTHID`, which a YAML resource list can't express. A grant -emitted with `principal_type: group` is dropped at ingest (visible as `grants_dropped` and -`ingest_quality.reason_flags` in the sync token), and `group` is omitted from `grantableTo` -even if a spec declares it. This is a property of Db2, not a limitation of the connector. +`SYSPROC.AUTH_LIST_GROUPS_FOR_AUTHID`, which a YAML resource list can't express, so a Db2 +spec should not declare `group` in an entitlement's `grantable_to`. + +Two caveats on how this surfaces. `grantableTo` is spec-driven: the connector copies whatever +the spec declares and does not filter by engine, so a spec that still lists `group` will +advertise it as grantable even on Db2. Enforcement happens at ingest instead. A grant emitted +with `principal_type: group` is dropped (visible as `grants_dropped` and +`ingest_quality.reason_flags` in the sync token). ## Running the Db2 tests diff --git a/docs/docs-info.md b/docs/docs-info.md index 25af0f2b..5f5f7cc5 100644 --- a/docs/docs-info.md +++ b/docs/docs-info.md @@ -50,6 +50,10 @@ This document describes the Baton SQL connector's capabilities and the credentia > - PostgreSQL: `postgres://username:password@host:port/database` > - Oracle: `oracle://username:password@host:port/service` > - SQL Server: `sqlserver://username:password@host:port?database=dbname` + > - SAP HANA: `hdb://username:password@host:port/database` + > - Vertica: `vertica://username:password@host:port/database` + > - Amazon Redshift: `postgres://username:password@host:port/database` (uses the PostgreSQL scheme) + > - IBM DB2: `db2://username:password@host:port/database` (opt-in; see [docs/db2.md](db2.md)) 2. For each item in the list above: @@ -151,6 +155,9 @@ The connector includes example configurations for common scenarios: - `examples/oracle-test.yml` - Oracle with SHA2-256 - `examples/wordpress-test.yml` - WordPress user and role management - `examples/sqlserver-test.yml` - SQL Server with SHA2-256 password hashing +- `examples/sap-hana-test.yml` - SAP HANA user and role management +- `examples/vertica-test.yml` - Vertica user and role management +- `examples/redshift-test.yml` - Amazon Redshift schema and role management Each example demonstrates: From fde489fbb493e6bd4e3f985c66247f3c4d795cb4 Mon Sep 17 00:00:00 2001 From: Alejandro Bernal Date: Thu, 3 Sep 2026 14:39:47 -0500 Subject: [PATCH 4/6] CXH-2381: drop root binary from tracked gitignore (use .git/info/exclude) --- .gitignore | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.gitignore b/.gitignore index 38c124d8..d4f6c13d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,10 +6,6 @@ *.dylib *.c1z -# Root build output (go build ./cmd/baton-sql without -o) -/baton-sql -/baton-sql.exe - # Test binary, built with `go test -c` *.test From 2f228219969911918b94e758b9ca864a4d1abc01 Mon Sep 17 00:00:00 2001 From: Alejandro Bernal Date: Thu, 3 Sep 2026 16:10:47 -0500 Subject: [PATCH 5/6] CXH-2381: exit with gRPC status code on error via exit.LogExit Replace os.Exit(1) in main.go with exit.LogExit(err) so an auth failure exits with the mapped gRPC status code (Unauthenticated/PermissionDenied) instead of a bare 1, which lets the CI sync-test auth-error check actually assert. --- cmd/baton-sql/main.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/cmd/baton-sql/main.go b/cmd/baton-sql/main.go index 5fa963d5..90245d3d 100644 --- a/cmd/baton-sql/main.go +++ b/cmd/baton-sql/main.go @@ -2,11 +2,10 @@ package main import ( "context" - "fmt" - "os" configSdk "github.com/conductorone/baton-sdk/pkg/config" "github.com/conductorone/baton-sdk/pkg/connectorbuilder" + "github.com/conductorone/baton-sdk/pkg/exit" "github.com/conductorone/baton-sdk/pkg/field" "github.com/conductorone/baton-sdk/pkg/types" "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" @@ -31,16 +30,14 @@ func main() { }, ) if err != nil { - fmt.Fprintln(os.Stderr, err.Error()) - os.Exit(1) + exit.LogExit(err) } cmd.Version = version err = cmd.Execute() if err != nil { - fmt.Fprintln(os.Stderr, err.Error()) - os.Exit(1) + exit.LogExit(err) } } From 8de4cbd01f18614da78039349abb59765cc8ec48 Mon Sep 17 00:00:00 2001 From: Alejandro Bernal Date: Thu, 3 Sep 2026 16:39:28 -0500 Subject: [PATCH 6/6] CXH-2381: map DB auth failures to Unauthenticated in Validate exit.LogExit alone made the CI bad-credentials check hard-fail: Validate wrapped the ping error with plain fmt.Errorf, so exit mapped it to Unknown(2) while auth-error.sh expects Unauthenticated(16)/PermissionDenied(7). Add database.AuthError: SQLSTATE class 28 (Postgres/Redshift/Vertica/etc.) and MySQL 1045 map to codes.Unauthenticated. Verified live: bad postgres creds now exit 16, good creds pass Validate. --- pkg/connector/connector.go | 3 +++ pkg/database/autherror.go | 35 +++++++++++++++++++++++++++ pkg/database/autherror_test.go | 44 ++++++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+) create mode 100644 pkg/database/autherror.go create mode 100644 pkg/database/autherror_test.go diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index cb69ba02..eabbf346 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -97,6 +97,9 @@ func (c *Connector) Validate(ctx context.Context) (annotations.Annotations, erro for name, db := range c.dbs { if err := db.PingContext(ctx); err != nil { + if authErr := database.AuthError(err); authErr != nil { + return nil, authErr + } return nil, fmt.Errorf("database %q ping failed: %w", name, err) } } diff --git a/pkg/database/autherror.go b/pkg/database/autherror.go new file mode 100644 index 00000000..96593707 --- /dev/null +++ b/pkg/database/autherror.go @@ -0,0 +1,35 @@ +package database + +import ( + "errors" + "strings" + + "github.com/go-sql-driver/mysql" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +const mysqlAccessDenied = 1045 + +// AuthError returns an Unauthenticated gRPC status when err is a database +// authentication/authorization failure, or nil otherwise. SQLSTATE class 28 +// ("invalid authorization") is the ANSI code drivers report on bad credentials +// (Postgres/Redshift/Vertica/etc. surface it via SQLState()); MySQL is the +// exception, reporting error 1045 with no SQLSTATE. +func AuthError(err error) error { + if err == nil { + return nil + } + + var sqlState interface{ SQLState() string } + if errors.As(err, &sqlState) && strings.HasPrefix(sqlState.SQLState(), "28") { + return status.Error(codes.Unauthenticated, "database authentication failed") + } + + var myErr *mysql.MySQLError + if errors.As(err, &myErr) && myErr.Number == mysqlAccessDenied { + return status.Error(codes.Unauthenticated, "database authentication failed") + } + + return nil +} diff --git a/pkg/database/autherror_test.go b/pkg/database/autherror_test.go new file mode 100644 index 00000000..3e1153cc --- /dev/null +++ b/pkg/database/autherror_test.go @@ -0,0 +1,44 @@ +package database + +import ( + "errors" + "fmt" + "testing" + + "github.com/go-sql-driver/mysql" + "github.com/jackc/pgx/v5/pgconn" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func TestAuthError(t *testing.T) { + tests := []struct { + name string + err error + want codes.Code // codes.OK means expect nil + }{ + {"nil", nil, codes.OK}, + {"postgres invalid_password 28P01", &pgconn.PgError{Code: "28P01"}, codes.Unauthenticated}, + {"postgres invalid_authorization 28000", &pgconn.PgError{Code: "28000"}, codes.Unauthenticated}, + {"postgres non-auth relation missing 42P01", &pgconn.PgError{Code: "42P01"}, codes.OK}, + {"postgres auth error wrapped", fmt.Errorf("ping: %w", &pgconn.PgError{Code: "28P01"}), codes.Unauthenticated}, + {"mysql access denied 1045", &mysql.MySQLError{Number: 1045}, codes.Unauthenticated}, + {"mysql other 1146", &mysql.MySQLError{Number: 1146}, codes.OK}, + {"plain error", errors.New("boom"), codes.OK}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := AuthError(tt.err) + if tt.want == codes.OK { + if got != nil { + t.Fatalf("want nil, got %v", got) + } + return + } + if status.Code(got) != tt.want { + t.Fatalf("want %v, got %v", tt.want, status.Code(got)) + } + }) + } +}