diff --git a/docs/db2.md b/docs/db2.md index 6922a829..363c5446 100644 --- a/docs/db2.md +++ b/docs/db2.md @@ -116,6 +116,43 @@ DB2's native form is also accepted as-is: HOSTNAME=localhost;PORT=50000;DATABASE=TESTDB;UID=db2inst1;PWD=pass123;PROTOCOL=TCPIP ``` +## DB2 for i (AS/400) + +Connecting to DB2 for i works through the same CLI driver but differs from LUW on every +DSN component, and connection details copied from an existing IBM i Access / JDBC (jt400) +setup will **not** work as-is: + +- **License (go/no-go):** the CLI driver requires a **Db2 Connect license** for IBM i + targets. The customer copies their `db2consv_*.lic` into `clidriver/license/`. Without it + every connection fails with `SQL1598N` — if the customer has no Db2 Connect entitlement, + this driver cannot connect at all. The `.lic` file comes from the customer's IBM Passport + Advantage downloads or from any Db2 Connect server they already run (`sqllib/license/`); + only the file is needed — no Db2 Connect installation on the connector host. + Alternatively, if they run a **Db2 Connect gateway server**, point the DSN at the gateway + instead of the IBM i system — licensing is then handled server-side and no local `.lic` + file is required. +- **Port is 446** (the DDM/DRDA service; confirm it's running with `STRTCPSVR SERVER(*DDM)` + on the i side). Port **8471** belongs to the IBM i Access database host server — a + different protocol our driver does not speak; using it fails with `SQL30081N`. +- **Database = the RDB name**, not an application library. Find it with `WRKRDBDIRE` + (the `*LOCAL` entry) on the system, or `SELECT CURRENT SERVER FROM SYSIBM.SYSDUMMY1` + from any working connection. It is usually the system name. +- **Libraries are schemas.** Set the default with `?CurrentSchema=MYLIB`, or + schema-qualify tables in the configured queries (`MYLIB.USERS`), which is more explicit + and recommended. The multi-library `DBQ` list is an IBM i Access ODBC feature and does + not exist here. + +```text +db2://USER:PASS@my-ibmi.example.com:446/RDBNAME?CurrentSchema=MYLIB +``` + +`USER`, `PASS`, `RDBNAME`, and `MYLIB` are placeholders — in particular `RDBNAME` is the +value `WRKRDBDIRE` reports, not the application library name. + +Columns tagged CCSID 65535 (binary) are common on IBM i application libraries and come +back untranslated; cast them in the configured query (`CAST(col AS CHAR(n) CCSID 37)`) if +values look like garbage. + ## Troubleshooting **`'sqlcli1.h' file not found`** — clidriver missing or `DB2HOME` wrong. Check that @@ -137,6 +174,18 @@ binaries — the baked paths are the reliable option. **`error while loading shared libraries: libxml2.so.2`** (Linux) — the clidriver depends on the OS libxml2 package: `apt-get install libxml2` / `yum install libxml2`. +**`SQL1598N` / SQLSTATE `42968` (licensing problem on connect)** — the target is z/OS or +IBM i and no valid Db2 Connect license is present. Diagnostically this means networking, +port, and RDB name are all correct — the DRDA handshake succeeded and only the entitlement +check failed. Fix per the license bullet in the DB2 for i section above: drop the +customer's `db2consv_*.lic` into `clidriver/license/` (no rebuild needed) or route through +their Db2 Connect gateway. License files are **Db2-version-specific**: a v11.5 license +will not activate a v12.x clidriver and produces this same error — check the driver level +with `clidriver/bin/db2level` and match the clidriver version to the customer's +entitlement (IBM's download site keeps per-version directories). `clidriver/bin/db2cli +validate` tests the DSN and license directly against the driver, independent of the +connector. + **`go vet` / `golangci-lint` with `-tags db2` fails** — type-checking the tagged path needs the clidriver headers too. Default-tag lint and vet need nothing. diff --git a/examples/db2-ibmi-test.yml b/examples/db2-ibmi-test.yml new file mode 100644 index 00000000..220c97c8 --- /dev/null +++ b/examples/db2-ibmi-test.yml @@ -0,0 +1,101 @@ +--- +# Application name for this connector configuration +app_name: DB2 for i Test + +# Connection settings for DB2 for i (AS/400 / IBM i). +# Requires a binary built with `make build-db2` AND a customer-supplied Db2 Connect +# license in clidriver/license/ — connections fail with SQL1598N without it. +# See the "DB2 for i (AS/400)" section in docs/db2.md. +connect: + # Port 446 is the DDM/DRDA service (NOT 8471, which is the IBM i Access host server). + # DB_RDB_NAME is the relational database name from WRKRDBDIRE (*LOCAL entry) — usually + # the system name, never an application library. CurrentSchema sets the default + # library for unqualified table names; queries below qualify QSYS2 explicitly, so it + # only matters for application-table queries you add. + dsn: "db2://${DB_HOST}:446/${DB_RDB_NAME}?CurrentSchema=${DB_LIBRARY}" + # Username and password are provided separately so they can be properly URL encoded. + user: "${DB_USER}" + password: "${DB_PASSWORD}" + +# This example is sync-only. Provisioning user profiles or group membership on IBM i +# is done with CL commands (CRTUSRPRF/CHGUSRPRF), not SQL — wire those through +# CALL QSYS2.QCMDEXC(...) only with care, since GRPPRF/SUPGRPPRF changes replace the +# current values rather than appending. +resource_types: + # IBM i user profiles + user: + name: "User" + description: "A user profile on the IBM i system" + list: + # QSYS2.USER_INFO is the SQL catalog view over user profiles. + # Group profiles also appear here; GROUP_ID_NUMBER > 0 filters them out of the + # user list (they are synced as groups below). OFFSET/FETCH needs IBM i 7.3+. + # Nullable columns are normalized in SQL (COALESCE) so CEL expressions only ever + # see strings — CEL operations on NULL or timestamp values fail with + # "no such overload". + query: | + SELECT + AUTHORIZATION_NAME AS "username", + STATUS AS "status", + COALESCE(TEXT_DESCRIPTION, '') AS "description", + USER_CLASS_NAME AS "user_class", + COALESCE(VARCHAR(PREVIOUS_SIGNON), '') AS "last_signon" + FROM QSYS2.USER_INFO + WHERE GROUP_ID_NUMBER = 0 + ORDER BY AUTHORIZATION_NAME + OFFSET ? ROWS FETCH NEXT ? ROWS ONLY + pagination: + strategy: "offset" + primary_key: "username" + map: + id: ".username" + display_name: ".username" + description: ".description" + traits: + user: + # *ENABLED / *DISABLED from the profile status + status: '.status == "*ENABLED" ? "enabled" : "disabled"' + login: ".username" + last_login: ".last_signon" + profile: + username: ".username" + user_class: ".user_class" + + # IBM i group profiles (a group is a user profile with a group ID) + group: + name: "Group" + description: "A group profile on the IBM i system" + list: + query: | + SELECT DISTINCT + GROUP_PROFILE_NAME AS "group_name" + FROM QSYS2.GROUP_PROFILE_ENTRIES + ORDER BY GROUP_PROFILE_NAME + map: + id: ".group_name" + display_name: ".group_name" + description: "" + traits: + group: + profile: + group_name: ".group_name" + static_entitlements: + - id: "member" + display_name: "resource.DisplayName + ' Group Member'" + description: "'Member of the ' + resource.DisplayName + ' group profile'" + purpose: "assignment" + grantable_to: + - "user" + grants: + # QSYS2.GROUP_PROFILE_ENTRIES lists every (group profile, member) pair, + # covering both the primary group (GRPPRF) and supplemental groups (SUPGRPPRF). + - query: | + SELECT + GROUP_PROFILE_NAME AS "group_name", + USER_PROFILE_NAME AS "username" + FROM QSYS2.GROUP_PROFILE_ENTRIES + map: + - skip_if: ".group_name != resource.ID" + principal_id: ".username" + principal_type: "user" + entitlement_id: "member" diff --git a/examples/db2-test.yml b/examples/db2-test.yml new file mode 100644 index 00000000..db9f5990 --- /dev/null +++ b/examples/db2-test.yml @@ -0,0 +1,122 @@ +--- +# Application name for this connector configuration +app_name: DB2 LUW Test + +# Connection settings for DB2 LUW (Linux/Unix/Windows). +# Requires a binary built with `make build-db2` — see docs/db2.md. +connect: + # Data Source Name (DSN) — default DB2 LUW port is 50000. + # Extra CLI keywords can be passed as query parameters, e.g. ?CurrentSchema=MYSCHEMA + dsn: "db2://${DB_HOST}:${DB_PORT}/${DB_NAME}" + # Username and password are provided separately so they can be properly URL encoded. + user: "${DB_USER}" + password: "${DB_PASSWORD}" + +# Definition of different resource types managed by this connector +resource_types: + # DB2 LUW does not store users itself (authentication is OS/LDAP); the closest + # identity inventory is the set of authorization IDs that hold database authorities. + user: + name: "User" + description: "An authorization ID with database authorities in DB2" + list: + # SYSCAT.DBAUTH lists database-level authorities; GRANTEETYPE 'U' = user IDs + query: | + SELECT DISTINCT + GRANTEE AS "username" + FROM SYSCAT.DBAUTH + WHERE GRANTEETYPE = 'U' + ORDER BY GRANTEE + OFFSET ? ROWS FETCH NEXT ? ROWS ONLY + pagination: + strategy: "offset" + primary_key: "username" + map: + id: ".username" + display_name: ".username" + description: "" + traits: + user: + login: ".username" + profile: + username: ".username" + + # Roles defined in the database (CREATE ROLE) + role: + name: "Role" + description: "A role within the DB2 database" + list: + query: | + SELECT + ROLENAME AS "role_name" + FROM SYSCAT.ROLES + ORDER BY ROLENAME + map: + id: ".role_name" + display_name: ".role_name" + description: "" + traits: + role: + profile: + role_name: ".role_name" + static_entitlements: + - id: "assigned" + display_name: "resource.DisplayName + ' Role Member'" + description: "'Member of the ' + resource.DisplayName + ' role'" + purpose: "assignment" + grantable_to: + - "user" + provisioning: + vars: + principal_name: principal.ID + role_name: resource.ID + grant: + no_transaction: true + queries: + # GRANT is DDL — parameter binding is not allowed, so identifiers are + # inlined with ?<...|unquoted> (values are sanitized, never escaped). + - | + GRANT ROLE ? TO USER ? + revoke: + no_transaction: true + queries: + - | + REVOKE ROLE ? FROM USER ? + - id: "admin" + display_name: "resource.DisplayName + ' Role Admin'" + description: "'Can administer the ' + resource.DisplayName + ' role'" + purpose: "permission" + grantable_to: + - "user" + provisioning: + vars: + principal_name: principal.ID + role_name: resource.ID + grant: + no_transaction: true + queries: + - | + GRANT ROLE ? TO USER ? WITH ADMIN OPTION + revoke: + no_transaction: true + queries: + - | + REVOKE ADMIN OPTION FOR ROLE ? FROM USER ? + grants: + # SYSCAT.ROLEAUTH holds role memberships; ADMIN = 'Y' marks WITH ADMIN OPTION + - query: | + SELECT + GRANTEE AS "username", + ROLENAME AS "role_name", + ADMIN AS "admin" + FROM SYSCAT.ROLEAUTH + WHERE GRANTEETYPE = 'U' + map: + - skip_if: ".role_name != resource.ID" + principal_id: ".username" + principal_type: "user" + entitlement_id: "assigned" + - skip_if: ".role_name != resource.ID || .admin != 'Y'" + principal_id: ".username" + principal_type: "user" + entitlement_id: "admin" diff --git a/pkg/bsql/config_test.go b/pkg/bsql/config_test.go index e966389b..47940d9d 100644 --- a/pkg/bsql/config_test.go +++ b/pkg/bsql/config_test.go @@ -248,6 +248,38 @@ resource_types: require.Equal(t, "'Grant cancelled by policy.'", grant.RejectIf.Reason) }, }, + { + name: "db2-luw-example", + input: loadExampleConfig(t, "db2-test"), + validate: func(t *testing.T, c *Config) { + require.Equal(t, "DB2 LUW Test", c.AppName) + require.Equal(t, "db2://${DB_HOST}:${DB_PORT}/${DB_NAME}", c.Connect.DSN) + require.Equal(t, "${DB_USER}", c.Connect.User) + require.Len(t, c.ResourceTypes, 2) + + roleResourceType := c.ResourceTypes["role"] + require.Len(t, roleResourceType.StaticEntitlements, 2) + require.Len(t, roleResourceType.Grants, 1) + require.NotNil(t, roleResourceType.StaticEntitlements[0].Provisioning) + }, + }, + { + name: "db2-ibmi-example", + input: loadExampleConfig(t, "db2-ibmi-test"), + validate: func(t *testing.T, c *Config) { + require.Equal(t, "DB2 for i Test", c.AppName) + require.Equal(t, "db2://${DB_HOST}:446/${DB_RDB_NAME}?CurrentSchema=${DB_LIBRARY}", c.Connect.DSN) + require.Len(t, c.ResourceTypes, 2) + + userResourceType := c.ResourceTypes["user"] + require.NotNil(t, userResourceType.List) + require.Equal(t, "offset", userResourceType.List.Pagination.Strategy) + + groupResourceType := c.ResourceTypes["group"] + require.Len(t, groupResourceType.StaticEntitlements, 1) + require.Len(t, groupResourceType.Grants, 1) + }, + }, } for _, tt := range tests {