Skip to content
Open
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
49 changes: 49 additions & 0 deletions docs/db2.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.

Expand Down
101 changes: 101 additions & 0 deletions examples/db2-ibmi-test.yml
Original file line number Diff line number Diff line change
@@ -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 ?<Offset> ROWS FETCH NEXT ?<Limit> 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"
122 changes: 122 additions & 0 deletions examples/db2-test.yml
Original file line number Diff line number Diff line change
@@ -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 ?<Offset> ROWS FETCH NEXT ?<Limit> 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 ?<role_name|unquoted> TO USER ?<principal_name|unquoted>

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: Consider ?<role_name|identifier> / ?<principal_name|identifier> here (and in the revoke/admin queries) instead of unquoted. This connector's own token docs recommend identifier for GRANT/DDL, and the reference redshift-test.yml uses it. unquoted strips every char outside [a-zA-Z0-9_], so a DB2 authorization ID containing a legal special char (e.g. #, $, @) would be silently rewritten and the grant applied to the wrong identifier. Both options are injection-safe; identifier just avoids the silent corruption. (Confidence: medium)

revoke:
no_transaction: true
queries:
- |
REVOKE ROLE ?<role_name|unquoted> FROM USER ?<principal_name|unquoted>
- 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 ?<role_name|unquoted> TO USER ?<principal_name|unquoted> WITH ADMIN OPTION
revoke:
no_transaction: true
queries:
- |
REVOKE ADMIN OPTION FOR ROLE ?<role_name|unquoted> FROM USER ?<principal_name|unquoted>
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"
32 changes: 32 additions & 0 deletions pkg/bsql/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading