Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,13 @@ 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
Comment thread
al-conductorone marked this conversation as resolved.
with:
connector: ./baton-sql
baton-entitlement: 'role:admin:member'
baton-principal: john.smith
baton-principal-type: user
bad-credentials: DB_PASSWORD=invalid
Comment thread
al-conductorone marked this conversation as resolved.
- name: Run account provisioning tests
uses: ConductorOne/github-workflows/actions/account-provisioning@v3
with:
Expand Down
18 changes: 18 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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
Expand All @@ -23,8 +23,10 @@
- Microsoft SQL Server
- Oracle
- PostgreSQL
- 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
Expand Down
9 changes: 3 additions & 6 deletions cmd/baton-sql/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
Comment thread
al-conductorone marked this conversation as resolved.
}
}

Expand Down
81 changes: 81 additions & 0 deletions docs/db2.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,87 @@ 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. 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()`

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 <authid>` 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, 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

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
Expand Down
13 changes: 12 additions & 1 deletion docs/docs-info.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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))
Comment thread
al-conductorone marked this conversation as resolved.
> - WordPress (MySQL-based)
>
> The connector can sync custom user tables, role hierarchies, entitlements, and permissions based on configurable SQL queries.
Expand Down Expand Up @@ -46,6 +50,10 @@ While developing the connector, please fill out this form. This information is n
> - 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:

Expand Down Expand Up @@ -147,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:

Expand Down
2 changes: 1 addition & 1 deletion pkg/config/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ var (
Config = field.NewConfiguration(
ConfigurationFields,
field.WithConnectorDisplayName("SQL"),
field.WithHelpUrl("/docs/baton/sql"),
field.WithHelpUrl("/docs/baton/baton-sql"),
Comment thread
al-conductorone marked this conversation as resolved.
field.WithIconUrl("/static/app-icons/sql.svg"),
)
)
3 changes: 3 additions & 0 deletions pkg/connector/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Comment on lines +100 to +102

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: this early return discards both the database name and the driver's message, so a multi-database config that fails auth on one connection logs only database authentication failed with no way to tell which DSN or why. Consider carrying the context through, e.g. return nil, status.Errorf(codes.Unauthenticated, "database %q authentication failed: %v", name, err) — the exit code stays 16 while the operator keeps the diagnostic detail.

return nil, fmt.Errorf("database %q ping failed: %w", name, err)
}
}
Expand Down
35 changes: 35 additions & 0 deletions pkg/database/autherror.go
Original file line number Diff line number Diff line change
@@ -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")
}
Comment on lines +14 to +27

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: the SQLState() interface probe only matches pgx — Vertica is named in the comment but doesn't qualify. vertica-sql-go@v1.3.6 declares SQLState as a struct field on VError (its only method is Error()), so errors.As against interface{ SQLState() string } never matches it; same for SQL Server (mssql.Error exposes SQLErrorState() uint8, not SQLState()), Oracle and HANA. Net effect is that only Postgres/Redshift and MySQL map to Unauthenticated — worth correcting the comment to say so, and optionally adding *vertigo.VError (SQLState 28000) / mssql.Error (18456) / go-ora ORA-01017 cases so the other engines return 16 instead of 2.


var myErr *mysql.MySQLError
if errors.As(err, &myErr) && myErr.Number == mysqlAccessDenied {
return status.Error(codes.Unauthenticated, "database authentication failed")
}

return nil
}
44 changes: 44 additions & 0 deletions pkg/database/autherror_test.go
Original file line number Diff line number Diff line change
@@ -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))
}
})
}
}
6 changes: 4 additions & 2 deletions test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading