From c2b2744f38da8cf690daa17dc650f68bbdd78832 Mon Sep 17 00:00:00 2001 From: Mateo Hernandez Date: Thu, 7 Aug 2025 09:48:50 -0300 Subject: [PATCH 1/2] Update of random password, documentation, and test for each database --- README.md | 22 +- docker-compose-mysql-test.yml | 30 ++ docker-compose-oracle-test.yml | 28 ++ docker-compose-sqlserver-test.yml | 45 +++ docker-compose-test.yml | 39 --- docker-compose-wordpress-test.yml | 44 +++ docs/docs-info.md | 157 ++++++++++ examples/example.yml | 113 ++++--- examples/mysql-local-test.yml | 143 --------- examples/mysql-test.yml | 84 ++++- examples/mysql.yml | 146 --------- examples/oracle-test.yml | 333 ++++++++++++++++++-- examples/oracle.yml | 290 ------------------ examples/postgres-test.yml | 81 ++++- examples/sqlserver-test.yml | 231 ++++++++++++++ examples/wordpress-test.yml | 146 +++++++++ examples/wordpress.yml | 160 ---------- go.mod | 2 +- pkg/bcel/functions/php_deserialize_test.go | 8 +- pkg/bsql/config_test.go | 22 +- pkg/bsql/helpers.go | 18 ++ pkg/bsql/helpers_test.go | 176 +++++++++++ pkg/bsql/provisioning.go | 44 --- pkg/bsql/query.go | 7 +- pkg/bsql/resource_types_test.go | 2 +- pkg/bsql/user_syncer.go | 180 +++++++++-- pkg/bsql/user_syncer_test.go | 168 ++++++++++ test/README.md | 338 ++++++++++++++++++--- test/create_baton_pdb.sql | 38 +++ test/mysql-init.sql | 3 +- test/mysql-schema.sql | 66 ---- test/oracle-init.sql | 117 +++++++ test/postgres-init.sql | 6 +- test/sqlserver-init.sql | 151 +++++++++ test/wordpress-init.sql | 56 ++++ 35 files changed, 2412 insertions(+), 1082 deletions(-) create mode 100644 docker-compose-mysql-test.yml create mode 100644 docker-compose-oracle-test.yml create mode 100644 docker-compose-sqlserver-test.yml delete mode 100644 docker-compose-test.yml create mode 100644 docker-compose-wordpress-test.yml create mode 100644 docs/docs-info.md delete mode 100644 examples/mysql-local-test.yml delete mode 100644 examples/mysql.yml delete mode 100644 examples/oracle.yml create mode 100644 examples/sqlserver-test.yml create mode 100644 examples/wordpress-test.yml delete mode 100644 examples/wordpress.yml create mode 100644 pkg/bsql/user_syncer_test.go create mode 100644 test/create_baton_pdb.sql delete mode 100644 test/mysql-schema.sql create mode 100644 test/oracle-init.sql create mode 100644 test/sqlserver-init.sql create mode 100644 test/wordpress-init.sql diff --git a/README.md b/README.md index d8b2f8a3..ba8e6e64 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,21 @@ +![Baton Logo](./baton-logo.png) + # `baton-sql` [![Go Reference](https://pkg.go.dev/badge/github.com/conductorone/baton-sql.svg)](https://pkg.go.dev/github.com/conductorone/baton-sql) ![main ci](https://github.com/conductorone/baton-sql/actions/workflows/main.yaml/badge.svg) `baton-sql` is a connector for built using the [Baton SDK](https://github.com/conductorone/baton-sdk). ## Overview -`baton-sql` is a connector that enables you to sync identities, resources, and permissions from SQL databases. It provides a flexible configuration system that allows you to map the results of database queries to resources and entitlements. +`baton-sql` is a flexible connector that enables you to sync identities, resources, and permissions from SQL databases. It provides a powerful configuration system that allows you to map database queries to resources and entitlements, with full support for account provisioning and automated password management. + +## Key Features + +- **Multi-Database Support**: Works with MySQL, PostgreSQL, Oracle, SQL Server, SQLite, and WordPress +- **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 +- **Role Management**: Sync and manage role assignments and permissions +- **Custom Schemas**: Support for any database schema through configurable SQL queries ## Supported Database Engines @@ -17,10 +28,11 @@ The connector is configured using a YAML file that defines: -- Database connection details via DSN or individual connection parameters -- Resource types (e.g. users, groups, roles) mapped to database tables/queries -- Entitlements that can be granted to resources -- Provisioning actions for granting/revoking entitlements +- **Database Connection**: Connection details via DSN (Data Source Name) +- **Resource Types**: Map database tables/queries to resources (users, roles, etc.) +- **Account Provisioning**: Define schemas and credential options for user creation +- **Entitlements**: Permissions and roles that can be granted to resources +- **Provisioning Actions**: SQL queries for granting/revoking entitlements See examples in the [examples](https://github.com/ConductorOne/baton-sql/tree/main/examples) directory. diff --git a/docker-compose-mysql-test.yml b/docker-compose-mysql-test.yml new file mode 100644 index 00000000..e76b3d20 --- /dev/null +++ b/docker-compose-mysql-test.yml @@ -0,0 +1,30 @@ +services: + # MySQL Database for testing employee_id and last_login features + mysql: + image: mysql:8.0 + container_name: baton-mysql-test + environment: + MYSQL_ROOT_PASSWORD: rootpassword + MYSQL_DATABASE: batondb + MYSQL_USER: baton + MYSQL_PASSWORD: password + ports: + - "3306:3306" + volumes: + - ./test/mysql-init.sql:/docker-entrypoint-initdb.d/init.sql + healthcheck: + test: + [ + "CMD", + "mysqladmin", + "ping", + "-h", + "localhost", + "-u", + "baton", + "-ppassword", + ] + interval: 5s + timeout: 5s + retries: 10 + command: --default-authentication-plugin=mysql_native_password diff --git a/docker-compose-oracle-test.yml b/docker-compose-oracle-test.yml new file mode 100644 index 00000000..dab83e46 --- /dev/null +++ b/docker-compose-oracle-test.yml @@ -0,0 +1,28 @@ +services: + # Oracle Database for testing employee_id and last_login features + oracle: + image: container-registry.oracle.com/database/express:21.3.0-xe + container_name: baton-oracle-test + environment: + ORACLE_PWD: OraclePassword123 + ORACLE_CHARACTERSET: AL32UTF8 + ports: + - "1521:1521" + - "5500:5500" + volumes: + - ./test/oracle-init.sql:/opt/oracle/scripts/setup/init.sql + healthcheck: + test: + [ + "CMD", + "sqlplus", + "-s", + "sys/OraclePassword123@//localhost:1521/XE", + "as", + "sysdba", + "@/opt/oracle/scripts/setup/healthcheck.sql", + ] + interval: 30s + timeout: 10s + retries: 10 + start_period: 5m diff --git a/docker-compose-sqlserver-test.yml b/docker-compose-sqlserver-test.yml new file mode 100644 index 00000000..128cdfaf --- /dev/null +++ b/docker-compose-sqlserver-test.yml @@ -0,0 +1,45 @@ +services: + # SQL Server Database for testing account provisioning and random password generation + sqlserver: + image: mcr.microsoft.com/mssql/server:2022-latest + container_name: baton-sqlserver-test + environment: + ACCEPT_EULA: Y + MSSQL_SA_PASSWORD: YourStrong@Passw0rd + MSSQL_PID: Developer + ports: + - "1433:1433" + volumes: + - ./test/sqlserver-init.sql:/tmp/init.sql + - sqlserver_data:/var/opt/mssql + healthcheck: + test: + [ + "CMD-SHELL", + "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P YourStrong@Passw0rd -C -Q 'SELECT 1' || exit 1", + ] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + + # Simple init container to set up the database after SQL Server is ready + sqlserver-init: + image: mcr.microsoft.com/mssql/server:2022-latest + container_name: baton-sqlserver-init + depends_on: + sqlserver: + condition: service_healthy + volumes: + - ./test/sqlserver-init.sql:/tmp/init.sql + command: > + /bin/bash -c " + echo 'Initializing SQL Server database...' + /opt/mssql-tools18/bin/sqlcmd -S sqlserver -U sa -P YourStrong@Passw0rd -C -Q 'CREATE DATABASE BatonTestDB' + echo 'Database created, running initialization script...' + /opt/mssql-tools18/bin/sqlcmd -S sqlserver -U sa -P YourStrong@Passw0rd -C -d BatonTestDB -i /tmp/init.sql + echo 'Database initialization complete!' + " + +volumes: + sqlserver_data: diff --git a/docker-compose-test.yml b/docker-compose-test.yml deleted file mode 100644 index beb5bc2a..00000000 --- a/docker-compose-test.yml +++ /dev/null @@ -1,39 +0,0 @@ -services: - # MySQL Database for testing employee_id and last_login features - mysql: - image: mysql:8.0 - container_name: baton-mysql-test - environment: - MYSQL_ROOT_PASSWORD: rootpassword - MYSQL_DATABASE: batondb - MYSQL_USER: baton - MYSQL_PASSWORD: password - ports: - - "3306:3306" - volumes: - - ./test/mysql-init.sql:/docker-entrypoint-initdb.d/init.sql - healthcheck: - test: ["CMD", "mysql", "-ubaton", "-ppassword", "-e", "SELECT 1"] - interval: 5s - timeout: 5s - retries: 5 - - # Connector test container - baton-sql-test: - build: - context: . - dockerfile: Dockerfile.test - container_name: baton-sql-test - depends_on: - mysql: - condition: service_healthy - volumes: - - ./examples:/examples - - ./test:/test - command: > - /bin/bash -c " - # Wait for MySQL to be fully ready - sleep 5 - # Run the baton-sql connector with test configuration - /app/baton-sql --config-path /examples/mysql-test.yml --log-level debug - " \ No newline at end of file diff --git a/docker-compose-wordpress-test.yml b/docker-compose-wordpress-test.yml new file mode 100644 index 00000000..c5d7570b --- /dev/null +++ b/docker-compose-wordpress-test.yml @@ -0,0 +1,44 @@ +services: + # MySQL Database for WordPress + wordpress-mysql: + image: mysql:8.0 + container_name: baton-wordpress-mysql-test + environment: + MYSQL_ROOT_PASSWORD: rootpassword + MYSQL_DATABASE: wordpress + MYSQL_USER: wp_user + MYSQL_PASSWORD: wp_password + ports: + - "3307:3306" # Using different port to avoid conflict with regular MySQL + volumes: + - ./test/wordpress-init.sql:/docker-entrypoint-initdb.d/init.sql + healthcheck: + test: + [ + "CMD", + "mysqladmin", + "ping", + "-h", + "localhost", + "-u", + "wp_user", + "-pwp_password", + ] + interval: 5s + timeout: 5s + retries: 10 + command: --default-authentication-plugin=mysql_native_password + + # WordPress Application (optional, for full testing) + wordpress: + image: wordpress:latest + container_name: baton-wordpress-test + depends_on: + - wordpress-mysql + ports: + - "8080:80" + environment: + WORDPRESS_DB_HOST: wordpress-mysql:3306 + WORDPRESS_DB_USER: wp_user + WORDPRESS_DB_PASSWORD: wp_password + WORDPRESS_DB_NAME: wordpress diff --git a/docs/docs-info.md b/docs/docs-info.md new file mode 100644 index 00000000..dac8f1af --- /dev/null +++ b/docs/docs-info.md @@ -0,0 +1,157 @@ +# 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. + +## Connector capabilities + +1. What resources does the connector sync? + + > The Baton SQL connector syncs users, roles, and other resource types from SQL databases. Supported database systems include: + > + > - MySQL + > - PostgreSQL + > - Oracle Database + > - SQL Server + > - WordPress (MySQL-based) + > + > The connector can sync custom user tables, role hierarchies, entitlements, and permissions based on configurable SQL queries. + +2. Can the connector provision any resources? If so, which ones? + + > Yes, the connector can provision user accounts with the following capabilities: + > + > - **User Account Creation**: Create new user accounts with configurable fields (username, email, employee_id, etc.) + > - **Random Password Generation**: Automatically generate secure random passwords (12-32 characters) with appropriate hashing for each database type: + > - MySQL: SHA2 hashing + > - PostgreSQL: bcrypt hashing + > - WordPress: MD5 hashing + > - Oracle: SHA2 hashing + > - SQL Server: SHA2 hashing + > - **Role Assignment Management**: Grant and revoke role memberships and entitlements for users + > - **Custom Provisioning Logic**: Execute custom SQL queries for complex provisioning workflows + +## Connector credentials + +1. What credentials or information are needed to set up the connector? + + > The connector requires database connection credentials and configuration: + > + > - **Database Connection String (DSN)**: A connection string containing host, port, database name, username, and password + > - **Database User Credentials**: Username and password for a database user with appropriate permissions + > - **Configuration File**: A YAML configuration file defining resource types, queries, and mappings + > + > **Example DSN formats:** + > + > - MySQL: `mysql://username:password@host:port/database` + > - PostgreSQL: `postgres://username:password@host:port/database` + > - Oracle: `oracle://username:password@host:port/service` + > - SQL Server: `sqlserver://username:password@host:port?database=dbname` + +2. For each item in the list above: + + - How does a user create or look up that credential or info? Please include links to (non-gated) documentation, screenshots (of the UI or of gated docs), or a video of the process. + + > **Database Connection String**: + > + > - Obtain from your database administrator or cloud provider console + > - For AWS RDS: Available in the RDS console under "Connectivity & security" + > - For Google Cloud SQL: Available in the Cloud SQL console under "Overview" + > - For Azure Database: Available in the Azure portal under "Connection strings" + > + > **Database User Credentials**: + > + > - Create a dedicated database user for the connector (recommended for security) + > - For MySQL: Use `CREATE USER` and `GRANT` statements + > - For PostgreSQL: Use `CREATE ROLE` and `GRANT` statements + > - For Oracle: Use `CREATE USER` and `GRANT` statements + > - Refer to your database documentation for user management procedures + > + > **Configuration File**: + > + > - Use the provided example configurations in the `examples/` directory + > - Customize SQL queries to match your database schema + > - Define resource mappings based on your table structure + + - Does the credential need any specific scopes or permissions? If so, list them here. + + > The database user account needs different permissions depending on the operations: + > + > **For Read-Only Sync Operations:** + > + > - `SELECT` permissions on user, role, and entitlement tables + > - Access to system tables for listing database users and roles (if applicable) + > + > **For Provisioning Operations (Read-Write):** + > + > - All read permissions listed above + > - `INSERT` permissions on user tables for account creation + > - `UPDATE` permissions for modifying user attributes + > - `DELETE` permissions for account deprovisioning (if implemented) + > - For Oracle: `CREATE USER`, `GRANT`, and `REVOKE` system privileges for database user management + > + > **Database-Specific Requirements:** + > + > - **MySQL**: `CREATE USER`, `GRANT OPTION` for user management + > - **PostgreSQL**: `CREATEROLE` privilege for user management, access to `pgcrypto` extension for password hashing + > - **Oracle**: `DBA` role or specific system privileges (`CREATE USER`, `ALTER USER`, `DROP USER`) + > - **WordPress**: Standard MySQL permissions on `wp_users` and `wp_usermeta` tables + + - If applicable: Is the list of scopes or permissions different to sync (read) versus provision (read-write)? If so, list the difference here. + + > Yes, the permissions differ significantly: + > + > **Sync (Read-only) Operations:** + > + > - `SELECT` on user and role tables + > - `SELECT` on system catalog tables (for database-native users/roles) + > - No modification permissions required + > + > **Provision (Read-Write) Operations:** + > + > - All read permissions from sync operations + > - `INSERT`, `UPDATE`, `DELETE` on user tables + > - System-level user management privileges (for database-native provisioning) + > - Permission to execute stored procedures or functions (if used for provisioning) + > - Access to password hashing functions (`SHA2`, `crypt`, `gen_salt`, etc.) + + - What level of access or permissions does the user need in order to create the credentials? + + > To create the necessary database credentials, you need: + > + > **Database Administrator Access:** + > + > - MySQL: `root` user or user with `GRANT OPTION` and `CREATE USER` privileges + > - PostgreSQL: Superuser or user with `CREATEROLE` and `GRANT` privileges + > - Oracle: `DBA` role or `SYSDBA` privileges + > - SQL Server: `sysadmin` server role or `securityadmin` + `dbowner` roles + > + > **Cloud Database Services:** + > + > - AWS RDS: IAM permissions to manage database users or use the master user + > - Google Cloud SQL: Cloud SQL Admin role or equivalent IAM permissions + > - Azure SQL Database: SQL authentication with admin credentials or Azure AD admin rights + > + > **Security Best Practices:** + > + > - Create a dedicated service account with minimal required permissions + > - Use connection pooling and SSL/TLS encryption + > - Rotate credentials regularly + > - Monitor database access logs for the connector account + +## Configuration Examples + +The connector includes example configurations for common scenarios: + +- `examples/mysql-test.yml` - MySQL with employee data and random password support +- `examples/postgres-test.yml` - PostgreSQL with bcrypt password hashing +- `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 + +Each example demonstrates: + +- Database connection configuration +- Resource type definitions (users, roles, etc.) +- Account provisioning with random password generation +- Entitlement management and role assignments +- Custom SQL queries for different use cases diff --git a/examples/example.yml b/examples/example.yml index 2847a3c5..d57ace48 100644 --- a/examples/example.yml +++ b/examples/example.yml @@ -27,7 +27,6 @@ connect: # Defines the resources that can be synchronized from the data source. # Each resource type represents a distinct entity type (e.g., users, groups, roles). resource_types: - # Example User Resource # ------------------- user: @@ -74,9 +73,9 @@ resource_types: user: # The trait name defines the schema emails: - # Array fields - - ".email" # Direct field mapping - - "lowercase(.email)" # CEL transformation + # Array fields + - ".email" # Direct field mapping + - "lowercase(.email)" # CEL transformation status: ".status" # Simple field mapping profile: department: ".department" @@ -95,65 +94,63 @@ resource_types: # ------------------ # Pre-defined permissions that can be granted static_entitlements: - - id: "access" # Unique identifier for this entitlement - display_name: "Basic Access" - description: "Provides basic access to the application" - purpose: "access" # Purpose: "access", "assignment", "permission" - grantable_to: - # Resource types that can receive this entitlement - - "user" - - "service_account" - # Provisioning Configuration - # ------------------------ - # Defines how to implement entitlement changes - provisioning: - vars: - # Variables available in provisioning queries - user_id: "principal.ID" - access_level: "'basic'" + - id: "access" # Unique identifier for this entitlement + display_name: "Basic Access" + description: "Provides basic access to the application" + purpose: "access" # Purpose: "access", "assignment", "permission" + grantable_to: + # Resource types that can receive this entitlement + - "user" + - "service_account" + # Provisioning Configuration + # ------------------------ + # Defines how to implement entitlement changes + provisioning: + vars: + # Variables available in provisioning queries + user_id: "principal.ID" + access_level: "'basic'" - # Grant Operations - # --------------- - grant: - # SQL statements to execute when granting - queries: - - | - INSERT INTO user_access (user_id, level) - VALUES (?, ?) + # Grant Operations + # --------------- + grant: + # SQL statements to execute when granting + queries: + - | + INSERT INTO user_access (user_id, level) + VALUES (?, ?) - # Revoke Operations - # ---------------- - revoke: - # SQL statements to execute when revoking - queries: - - | - DELETE FROM user_access - WHERE user_id = ? + # Revoke Operations + # ---------------- + revoke: + # SQL statements to execute when revoking + queries: + - | + DELETE FROM user_access + WHERE user_id = ? # Grants Query Configuration # ------------------------ # Defines how to discover existing entitlements grants: - - query: | - SELECT - user_id, - access_level, - granted_at - FROM user_access - LIMIT ? OFFSET ? + - query: | + SELECT + user_id, + access_level, + granted_at + FROM user_access + LIMIT ? OFFSET ? - # Grant Mapping - # ------------ - # Defines how to interpret grant query results - map: - - skip_if: ".access_level != 'basic'" # CEL condition to filter results - principal_id: ".user_id" - principal_type: "user" - entitlement_id: "access" - # Grants Pagination - # ---------------- - pagination: - strategy: "offset" - primary_key: "user_id" - -# Additional resource types would follow the same pattern + # Grant Mapping + # ------------ + # Defines how to interpret grant query results + map: + - skip_if: ".access_level != 'basic'" # CEL condition to filter results + principal_id: ".user_id" + principal_type: "user" + entitlement_id: "access" + # Grants Pagination + # ---------------- + pagination: + strategy: "offset" + primary_key: "user_id" # Example: groups, roles, applications, etc. diff --git a/examples/mysql-local-test.yml b/examples/mysql-local-test.yml deleted file mode 100644 index ca66bbc8..00000000 --- a/examples/mysql-local-test.yml +++ /dev/null @@ -1,143 +0,0 @@ ---- -# Application name for this connector configuration -app_name: MySQL Local Test -app_description: Test configuration for MySQL with employee_id and last_login support (for local testing) - -# Connection settings for the MySQL database -connect: - # Data Source Name (DSN) for our local MySQL instance running in Docker - dsn: "mysql://baton:password@localhost:3306/batondb" - -# Definition of different resource types managed by this connector -resource_types: - # Configuration for "user" resources in MySQL - user: - # Display name for this resource type - name: "User" - # Description providing context about what a user represents - description: "A user within the MySQL system" - # Settings for listing user records from the database - list: - # SQL query to fetch user-related data including employee_id, last_login, and manager information - query: | - SELECT - u.id, - u.username, - u.email, - u.employee_id, - u.status, - u.account_type, - u.created_at, - u.last_login, - u.manager_id, - m.username as manager_username, - m.email as manager_email - FROM - users u - LEFT JOIN - users m ON u.manager_id = m.id - # Pagination configuration (using offset for MySQL) - pagination: - strategy: "offset" - primary_key: "id" - # Mapping of query results to resource fields - map: - # Unique identifier for the user resource - id: ".username" - # Field to display as the resource's name - display_name: ".username" - # Additional description for the user resource - description: ".username" - # Extra attributes (traits) for the user resource - traits: - user: - # Map status from database (active, disabled) to proper resource status - status: ".status" - # Login identifier - username - login: ".username" - # Email addresses - emails: - - ".email" - # Account type mapping - account_type: ".account_type" - # Employee ID mapping - employee_ids: - - ".employee_id" - # Last login timestamp mapping - last_login: ".last_login" - # Manager information - manager_id: ".manager_id" - manager_email: ".manager_email" - - # Profile details for the user - profile: - user_id: ".id" - created_at: ".created_at" - last_login: ".last_login" - manager_id: ".manager_id" - manager_username: ".manager_username" - manager_email: ".manager_email" - - # Configuration for "role" resources - role: - # Display name for the role resource type - name: "Role" - # Brief description of what a role signifies - description: "A role within the MySQL system" - # Settings for listing roles from the database - list: - # SQL query to retrieve roles - query: | - SELECT - id, - role_name - FROM - roles - # Pagination configuration - pagination: - strategy: "offset" - primary_key: "id" - # Mapping of each query result to role resource fields - map: - # Unique role identifier - id: ".role_name" - # Display name for the role - display_name: ".role_name" - # Role description - description: ".role_name" - # Additional role-specific traits - traits: - role: - # Profile mapping for the role - profile: - role_id: ".id" - - # Define a static entitlement for role membership - static_entitlements: - - id: "member" - display_name: "'Member'" - description: "'Role member'" - purpose: "assignment" - grantable_to: - - "user" - - # Dynamic grants based on SQL queries to associate users with roles - grants: - - query: | - SELECT - u.username, - r.role_name - FROM - users u - JOIN - user_roles ur ON u.id = ur.user_id - JOIN - roles r ON r.id = ur.role_id - pagination: - strategy: "offset" - primary_key: "username" - map: - - skip_if: ".role_name != resource.ID" - principal_id: ".username" - principal_type: "user" - entitlement_id: "member" \ No newline at end of file diff --git a/examples/mysql-test.yml b/examples/mysql-test.yml index f80b586a..28ae7185 100644 --- a/examples/mysql-test.yml +++ b/examples/mysql-test.yml @@ -1,13 +1,12 @@ --- # Application name for this connector configuration app_name: MySQL Test -app_description: Test configuration for MySQL with employee_id and last_login support +app_description: Test configuration for MySQL with account provisioning and random password generation # Connection settings for the MySQL database connect: - # Data Source Name (DSN) for our test MySQL instance - # Use "localhost" when running locally and "mysql" when running in Docker Compose - dsn: "mysql://baton:password@mysql:3306/batondb" + # Data Source Name (DSN) for our local MySQL instance running in Docker + dsn: "mysql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_DATABASE}" # Definition of different resource types managed by this connector resource_types: @@ -29,7 +28,10 @@ resource_types: u.status, u.account_type, u.created_at, - u.last_login, + CASE + WHEN u.last_login IS NULL THEN '' + ELSE DATE_FORMAT(u.last_login, '%Y-%m-%dT%H:%i:%sZ') + END as last_login, u.manager_id, m.username as manager_username, m.email as manager_email @@ -43,7 +45,7 @@ resource_types: primary_key: "id" # Mapping of query results to resource fields map: - # Unique identifier for the user resource + # Unique identifier for the user resource id: ".username" # Field to display as the resource's name display_name: ".username" @@ -69,7 +71,7 @@ resource_types: # Manager information manager_id: ".manager_id" manager_email: ".manager_email" - + # Profile details for the user profile: user_id: ".id" @@ -78,8 +80,68 @@ resource_types: manager_id: ".manager_id" manager_username: ".manager_username" manager_email: ".manager_email" - - # Configuration for "role" resources + + # Account provisioning configuration with password support + account_provisioning: + schema: + - name: "username" + description: "The username for the new user" + type: "string" + placeholder: "new_user" + required: true + - name: "email" + description: "Email address for the new user" + type: "string" + placeholder: "user@example.com" + required: true + - name: "employee_id" + description: "Employee ID for the new user" + type: "string" + placeholder: "EMP001" + required: false + credentials: + random_password: + max_length: 32 + min_length: 16 + preferred: true + no_password: + preferred: false + validate: + vars: + username: "username" + email: "email" + query: | + SELECT + u.id, + u.username, + u.email, + u.employee_id, + u.status, + u.account_type, + u.created_at, + CASE + WHEN u.last_login IS NULL THEN '' + ELSE DATE_FORMAT(u.last_login, '%Y-%m-%dT%H:%i:%sZ') + END as last_login, + u.manager_id, + m.username as manager_username, + m.email as manager_email + FROM users u + LEFT JOIN users m ON u.manager_id = m.id + WHERE u.username = ? + create: + vars: + username: "input.username" + email: "input.email" + employee_id: "has(input.employee_id) ? input.employee_id : null" + password: "password" + queries: + # Insert the new user with generated password + - | + INSERT INTO users (username, email, employee_id, status, account_type, created_at, password_hash) + VALUES (?, ?, ?, 'active', 'human', NOW(), SHA2(?, 256)) + + # Configuration for "role" resources role: # Display name for the role resource type name: "Role" @@ -112,7 +174,7 @@ resource_types: # Profile mapping for the role profile: role_id: ".id" - + # Define a static entitlement for role membership static_entitlements: - id: "member" @@ -141,4 +203,4 @@ resource_types: - skip_if: ".role_name != resource.ID" principal_id: ".username" principal_type: "user" - entitlement_id: "member" \ No newline at end of file + entitlement_id: "member" diff --git a/examples/mysql.yml b/examples/mysql.yml deleted file mode 100644 index ac76518e..00000000 --- a/examples/mysql.yml +++ /dev/null @@ -1,146 +0,0 @@ ---- -# Application name for this connector configuration -app_name: MySQL Production -app_description: MySQL connector with employee_id and last_login support - -# Connection settings for the MySQL database -connect: - # Data Source Name (DSN) for our MySQL instance - replace with your actual database connection details - dsn: "mysql://baton:password@localhost:3306/batondb" - # If your database credentials contain special characters, you can specify them separately - # user: "username" - # password: "password" - -# Definition of different resource types managed by this connector -resource_types: - # Configuration for "user" resources - user: - # Display name for this resource type - name: "User" - # Description providing context about what a user represents - description: "A user within the MySQL system" - # Settings for listing user records from the database - list: - # SQL query to fetch user-related data including employee_id, last_login, and manager information - query: | - SELECT - u.id, - u.username, - u.email, - u.employee_id, - u.status, - u.account_type, - u.created_at, - u.last_login, - u.manager_id, - m.username as manager_username, - m.email as manager_email - FROM - users u - LEFT JOIN - users m ON u.manager_id = m.id - # Pagination configuration - pagination: - strategy: "offset" - primary_key: "id" - # Mapping of query results to resource fields - map: - # Unique identifier for the user resource - id: ".username" - # Field to display as the resource's name - display_name: ".username" - # Additional description for the user resource - description: ".username" - # Extra attributes (traits) for the user resource - traits: - user: - # Map status from database (active, disabled) to proper resource status - status: ".status" - # Login identifier - username - login: ".username" - # Email addresses - emails: - - ".email" - # Account type mapping - account_type: ".account_type" - # Employee ID mapping - newly added feature - employee_ids: - - ".employee_id" - # Last login timestamp mapping - newly added feature - last_login: ".last_login" - # Manager information - manager_id: ".manager_id" - manager_email: ".manager_email" - - # Profile details for the user - profile: - user_id: ".id" - created_at: ".created_at" - last_login: ".last_login" - manager_id: ".manager_id" - manager_username: ".manager_username" - manager_email: ".manager_email" - - # Configuration for "role" resources - role: - # Display name for the role resource type - name: "Role" - # Brief description of what a role signifies - description: "A role within the MySQL system" - # Settings for listing roles from the database - list: - # SQL query to retrieve roles - query: | - SELECT - id, - role_name - FROM - roles - # Pagination configuration - pagination: - strategy: "offset" - primary_key: "id" - # Mapping of each query result to role resource fields - map: - # Unique role identifier - id: ".role_name" - # Display name for the role - display_name: ".role_name" - # Role description - description: ".role_name" - # Additional role-specific traits - traits: - role: - # Profile mapping for the role - profile: - role_id: ".id" - - # Define a static entitlement for role membership - static_entitlements: - - id: "member" - display_name: "'Member'" - description: "'Role member'" - purpose: "assignment" - grantable_to: - - "user" - - # Dynamic grants based on SQL queries to associate users with roles - grants: - - query: | - SELECT - u.username, - r.role_name - FROM - users u - JOIN - user_roles ur ON u.id = ur.user_id - JOIN - roles r ON r.id = ur.role_id - pagination: - strategy: "offset" - primary_key: "username" - map: - - skip_if: ".role_name != resource.ID" - principal_id: ".username" - principal_type: "user" - entitlement_id: "member" \ No newline at end of file diff --git a/examples/oracle-test.yml b/examples/oracle-test.yml index 0d7ef1c1..79f11b3c 100644 --- a/examples/oracle-test.yml +++ b/examples/oracle-test.yml @@ -1,37 +1,324 @@ --- +# Application name for this connector configuration app_name: Oracle Test + +# Connection settings for the Oracle database connect: - dsn: "oracle://HR:password@localhost:1521/FREEPDB1" + # Data Source Name (DSN) including credentials, host, port, and service name + dsn: "oracle://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_SERVICE}" + +# Definition of different resource types managed by this connector resource_types: + # Configuration for "user" resources in Oracle user: + # Display name for this resource type name: "User" - description: "A user within the oracle test database" + # Description providing context about what a user represents in Oracle + description: "A user within the oracle system" + # Settings for listing user records from the database list: + # SQL query to fetch user-related data from custom users table query: | - select - EMPLOYEE_ID, - FIRST_NAME, - LAST_NAME, - EMAIL, - HIRE_DATE, - JOB_ID, - MANAGER_ID, - DEPARTMENT_ID - from EMPLOYEES + SELECT + id as "id", + username as "username", + email as "email", + employee_id as "employee_id", + status as "status", + account_type as "account_type", + created_at as "created_at", + last_login as "last_login", + manager_id as "manager_id" + FROM + users + ORDER BY id + OFFSET ? ROWS FETCH NEXT ? ROWS ONLY + # Pagination configuration + pagination: + strategy: "offset" + primary_key: "id" + # Mapping of query results to resource fields map: - id: ".EMPLOYEE_ID" - display_name: ".FIRST_NAME + ' ' + .LAST_NAME" - description: "" + # Unique identifier for the user resource + id: ".username" + # Field to display as the resource's name + display_name: ".username" + # Additional description for the user resource + description: ".username" + # Extra attributes (traits) for the user resource traits: user: + # Map status from database to proper resource status + status: ".status" + # Login identifier - username + login: ".username" + # Email addresses emails: - - ".EMAIL" - status: "active" - status_details: "'detailed status'" - login: ".EMAIL" + - ".email" + # Account type mapping + account_type: ".account_type" + # Employee ID mapping + employee_ids: + - ".employee_id" + # Last login timestamp mapping + last_login: ".last_login" + account_provisioning: + # Schema definition for account creation form + schema: + - name: "username" + description: "The username of the user" + type: "string" + placeholder: "user" + required: true + - name: "email" + description: "The email of the user" + type: "string" + placeholder: "user@example.com" + required: true + - name: "employee_id" + description: "The employee ID of the user" + type: "string" + placeholder: "EMP001" + required: false + credentials: + random_password: + max_length: 20 + min_length: 12 + # Oracle-safe character set (alphanumeric only to avoid SQL injection issues) + allowed_chars: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + preferred: true + no_password: + preferred: false + # Validation query to check if user already exists + validate: + vars: + username: "username" + email: "email" + query: | + SELECT + id as "id", + username as "username", + email as "email", + employee_id as "employee_id", + status as "status", + account_type as "account_type", + created_at as "created_at", + last_login as "last_login", + manager_id as "manager_id" + FROM users + WHERE username = ? + # Account creation configuration + create: + vars: + username: "input.username" + email: "input.email" + employee_id: "has(input.employee_id) ? input.employee_id : null" + password: "password" + queries: + # Insert the new user with generated password into our custom table (with SHA256 hashing) + - | + INSERT INTO users (username, email, employee_id, status, account_type, created_at, password_hash) + VALUES (?, ?, ?, 'active', 'human', CURRENT_TIMESTAMP, STANDARD_HASH(?, 'SHA256')) + + # Configuration for "role" resources in Oracle + role: + # Display name for the role resource type + name: "Role" + # Brief description of what a role signifies in Oracle + description: "A role within the Oracle system" + # Settings for listing roles from the database + list: + # SQL query to retrieve roles + query: | + SELECT + ROLE + FROM + DBA_ROLES + # Mapping of each query result to role resource fields + map: + # Unique role identifier (from the ROLE column) + id: ".ROLE" + # Display name for the role (also from the ROLE column) + display_name: ".ROLE" + # Role description; left empty for further customization + description: "" + # Additional role-specific traits + traits: + role: + # Profile mapping for the role; assigns the role name profile: - hire_date: ".HIRE_DATE" - manager_id: ".MANAGER_ID" - department_id: ".DEPARTMENT_ID" - job_id: ".JOB_ID" + role_name: ".ROLE" + # Static entitlements that can be assigned to a role resource + static_entitlements: + - id: "assigned" # Entitlement identifier for a standard role assignment + # Dynamic display name combining the resource display name with a suffix + display_name: "resource.DisplayName + ' Role Member'" + # Description explaining that the user is a member of the role + description: "'Member of the ' + resource.DisplayName + ' role'" + # Purpose indicating that this entitlement represents an assignment relationship + purpose: "assignment" + # Resource types that can be granted this entitlement + grantable_to: + - "user" + # Provisioning configuration for applying and revoking this entitlement + provisioning: + # Variables used within provisioning queries + vars: + principal_name: principal.ID # Identifier for the user (principal) receiving the role + role_name: resource.ID # Identifier for the role resource + grant: + # Indicates that no database transaction is needed for the grant operation + no_transaction: true + queries: + - | + GRANT ? TO ? + revoke: + # Indicates that the revoke operation runs without a transaction + no_transaction: true + queries: + - | + REVOKE ? FROM ? + - id: "admin" # Entitlement identifier for role administration privileges + # Dynamic display name for admin entitlement, appending ' Role Admin' + display_name: "resource.DisplayName + ' Role Admin'" + # Description indicating administrative rights over the role + description: "'Admin of the ' + resource.DisplayName + ' role'" + # Purpose showing that this is a permission granting administrative capabilities + purpose: "permission" + # Specifies that this entitlement can be granted to user resources + grantable_to: + - "user" + # Provisioning details for granting and revoking admin privileges + provisioning: + vars: + principal_name: principal.ID # Maps to the user receiving the admin rights + role_name: resource.ID # Maps to the role being administered + grant: + no_transaction: true + queries: + - | + GRANT ? TO ? WITH ADMIN OPTION + revoke: + no_transaction: true + queries: + - | + REVOKE ? FROM ? + - | + GRANT ? TO ? + # Dynamic grants based on SQL queries to associate users with roles + grants: + - query: | + SELECT + GRANTEE AS USERNAME, + GRANTED_ROLE AS ROLE, + ADMIN_OPTION + FROM + DBA_ROLE_PRIVS + WHERE + GRANTEE IN (SELECT USERNAME FROM DBA_USERS) + map: + - skip_if: ".ROLE != resource.ID" # Only map if the role from the query matches the current resource + principal_id: ".USERNAME" # Map the username as the principal identifier + principal_type: "user" # Define the principal type explicitly as "user" + entitlement_id: "assigned" # Use the 'assigned' entitlement for standard role membership + - skip_if: ".ROLE != resource.ID || .ADMIN_OPTION != 'YES'" # Only map if role matches and admin option is enabled + principal_id: ".USERNAME" # Map the user's identifier + principal_type: "user" # Set the principal type as user + entitlement_id: "admin" # Apply the 'admin' entitlement for administrative privileges + # Configuration for "privilege" resources in Oracle + privilege: + # Display name for this privilege resource type + name: "Privilege" + # Description providing context on what an Oracle privilege is + description: "A privilege within the Oracle system" + # Settings for listing privileges from the database + list: + query: | + SELECT NAME + FROM sys.system_privilege_map + ORDER BY NAME + map: + # Unique identifier for the privilege (from the NAME column) + id: ".NAME" + # Display name for the privilege (derived from NAME) + display_name: ".NAME" + # Additional description field for the privilege (left blank) + description: "" + # Static entitlements for privileges that can be granted to users + static_entitlements: + - id: "assigned" # Entitlement identifier for a standard privilege assignment + display_name: "resource.DisplayName + ' privileges'" # Dynamic display name for an assigned privilege + description: "'Is granted the ' + resource.DisplayName + ' privilege'" # Explanation of the assignment + purpose: "assignment" # Denotes that this is an assignment action + grantable_to: + - "user" # Specifies that this entitlement can be applied to user resources + provisioning: + vars: + principal_name: principal.ID # Maps the user identifier + privilege_name: resource.ID # Maps the privilege identifier + grant: + no_transaction: true + queries: + - | + GRANT ? TO ? + # Revoke section defines how to remove a privilege from a user + revoke: + # no_transaction indicates this should execute outside a transaction block + no_transaction: true + # SQL queries to execute when revoking the privilege + queries: + - | + REVOKE ? FROM ? + - id: "admin" # Entitlement identifier for administrative control over privileges + display_name: "resource.DisplayName + ' Privilege Admin'" # Dynamic display name for admin privileges on the resource + description: "'Can grant the ' + resource.DisplayName + ' privilege to other users'" # Describes the ability to manage privileges + purpose: "permission" # Indicates this entitlement is a permission setting + grantable_to: + - "user" # This permission can be granted to user resources + provisioning: + vars: + principal_name: principal.ID # User identifier for provisioning + privilege_name: resource.ID # Privilege identifier for provisioning + grant: + no_transaction: true + queries: + - | + # The ? placeholder will be replaced with the raw value of the privilege_name variable + # defined in the vars section above. The |unquoted flag means the value will be inserted directly into the SQL + # rather than using a prepared statement parameter. This is needed for DDL statements like GRANT that require + # the actual identifier names. + # + # Similarly, ? inserts the principal_name variable's value directly into the query. + # Without |unquoted, the values would be passed as bind parameters like: GRANT ? TO ? WITH ADMIN OPTION + GRANT ? TO ? WITH ADMIN OPTION + revoke: + no_transaction: true + queries: + - | + REVOKE ? FROM ? + - | + GRANT ? TO ? + # Dynamic grants to map privilege assignments based on database queries + grants: + - query: | + SELECT + GRANTEE AS USERNAME, + PRIVILEGE, + ADMIN_OPTION + FROM + DBA_SYS_PRIVS + WHERE + GRANTEE IN (SELECT USERNAME FROM DBA_USERS) + ORDER BY + USERNAME, + PRIVILEGE + map: + - skip_if: ".PRIVILEGE != resource.ID" # Skip mapping if the privilege does not match the current resource + principal_id: ".USERNAME" # Map the query's USERNAME field as the principal ID + principal_type: "user" # Explicitly set the principal type to "user" + entitlement_id: "assigned" # Use the 'assigned' entitlement for standard privilege assignments + - skip_if: ".PRIVILEGE != resource.ID || .ADMIN_OPTION != 'YES'" # Condition for admin-level privilege mapping + principal_id: ".USERNAME" # Map the USERNAME to the principal ID + principal_type: "user" # Define the principal type as user + entitlement_id: "admin" # Apply the 'admin' entitlement when administrative rights are present diff --git a/examples/oracle.yml b/examples/oracle.yml deleted file mode 100644 index 56269072..00000000 --- a/examples/oracle.yml +++ /dev/null @@ -1,290 +0,0 @@ ---- -# Application name for this connector configuration -app_name: Oracle - -# Connection settings for the Oracle database -connect: - # Data Source Name (DSN) including credentials, host, port, and service name - dsn: "oracle://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_SERVICE}" - -# Definition of different resource types managed by this connector -resource_types: - # Configuration for "user" resources in Oracle - user: - # Display name for this resource type - name: "User" - # Description providing context about what a user represents in Oracle - description: "A user within the oracle system" - # Settings for listing user records from the database - list: - # SQL query to fetch user-related data - query: | - SELECT - USERNAME, USER_ID, ACCOUNT_STATUS, CREATED, LAST_LOGIN - FROM - DBA_USERS - # Mapping of query results to resource fields - map: - # Unique identifier for the user resource (derived from the USERNAME column) - id: ".USERNAME" - # Field to display as the resource's name (from the USERNAME column) - display_name: ".USERNAME" - # Additional description for the user resource (empty by default) - description: "" - # Extra attributes (traits) for the user resource - traits: - user: - # Evaluate the account status; if 'OPEN' then 'enabled' otherwise 'disabled' - status: ".ACCOUNT_STATUS == 'OPEN' ? 'enabled' : 'disabled'" - # Detailed status information; shows the actual status when not 'OPEN' - status_details: ".ACCOUNT_STATUS != 'OPEN' ? .ACCOUNT_STATUS : ''" - # Login identifier for the user (using the USERNAME) - login: ".USERNAME" - # Last login timestamp - now properly set in user trait - last_login: ".LAST_LOGIN" - # Profile details for the user - profile: - # Mapped username from the USERNAME field - username: ".USERNAME" - # User's unique ID from the USER_ID column - user_id: ".USER_ID" - # Timestamp of when the user was created (from the CREATED column) - created_at: ".CREATED" - # Last login timestamp from the LAST_LOGIN column - last_login: ".LAST_LOGIN" - account_provisioning: - schema: - - name: "username" - description: "The username for the Oracle user" - type: "string" - placeholder: "NEW_USER" - required: true - credentials: - no_password: - preferred: true - validate: - vars: - username: "username" - query: | - SELECT - USERNAME, - USER_ID, - ACCOUNT_STATUS, - CREATED, - LAST_LOGIN - FROM - DBA_USERS - WHERE - USERNAME = UPPER(?) - create: - vars: - username: "username" - no_transaction: true - queries: - - | - CREATE USER ? DEFAULT TABLESPACE USERS TEMPORARY TABLESPACE TEMP ACCOUNT UNLOCK - - | - GRANT CREATE SESSION TO ? - - # Configuration for "role" resources in Oracle - role: - # Display name for the role resource type - name: "Role" - # Brief description of what a role signifies in Oracle - description: "A role within the Oracle system" - # Settings for listing roles from the database - list: - # SQL query to retrieve roles - query: | - SELECT - ROLE - FROM - DBA_ROLES - # Mapping of each query result to role resource fields - map: - # Unique role identifier (from the ROLE column) - id: ".ROLE" - # Display name for the role (also from the ROLE column) - display_name: ".ROLE" - # Role description; left empty for further customization - description: "" - # Additional role-specific traits - traits: - role: - # Profile mapping for the role; assigns the role name - profile: - role_name: ".ROLE" - # Static entitlements that can be assigned to a role resource - static_entitlements: - - id: "assigned" # Entitlement identifier for a standard role assignment - # Dynamic display name combining the resource display name with a suffix - display_name: "resource.DisplayName + ' Role Member'" - # Description explaining that the user is a member of the role - description: "'Member of the ' + resource.DisplayName + ' role'" - # Purpose indicating that this entitlement represents an assignment relationship - purpose: "assignment" - # Resource types that can be granted this entitlement - grantable_to: - - "user" - # Provisioning configuration for applying and revoking this entitlement - provisioning: - # Variables used within provisioning queries - vars: - principal_name: principal.ID # Identifier for the user (principal) receiving the role - role_name: resource.ID # Identifier for the role resource - grant: - # Indicates that no database transaction is needed for the grant operation - no_transaction: true - queries: - - | - GRANT ? TO ? - revoke: - # Indicates that the revoke operation runs without a transaction - no_transaction: true - queries: - - | - REVOKE ? FROM ? - - id: "admin" # Entitlement identifier for role administration privileges - # Dynamic display name for admin entitlement, appending ' Role Admin' - display_name: "resource.DisplayName + ' Role Admin'" - # Description indicating administrative rights over the role - description: "'Admin of the ' + resource.DisplayName + ' role'" - # Purpose showing that this is a permission granting administrative capabilities - purpose: "permission" - # Specifies that this entitlement can be granted to user resources - grantable_to: - - "user" - # Provisioning details for granting and revoking admin privileges - provisioning: - vars: - principal_name: principal.ID # Maps to the user receiving the admin rights - role_name: resource.ID # Maps to the role being administered - grant: - no_transaction: true - queries: - - | - GRANT ? TO ? WITH ADMIN OPTION - revoke: - no_transaction: true - queries: - - | - REVOKE ? FROM ? - - | - GRANT ? TO ? - # Dynamic grants based on SQL queries to associate users with roles - grants: - - query: | - SELECT - GRANTEE AS USERNAME, - GRANTED_ROLE AS ROLE, - ADMIN_OPTION - FROM - DBA_ROLE_PRIVS - WHERE - GRANTEE IN (SELECT USERNAME FROM DBA_USERS) - map: - - skip_if: ".ROLE != resource.ID" # Only map if the role from the query matches the current resource - principal_id: ".USERNAME" # Map the username as the principal identifier - principal_type: "user" # Define the principal type explicitly as "user" - entitlement_id: "assigned" # Use the 'assigned' entitlement for standard role membership - - skip_if: ".ROLE != resource.ID || .ADMIN_OPTION != 'YES'" # Only map if role matches and admin option is enabled - principal_id: ".USERNAME" # Map the user's identifier - principal_type: "user" # Set the principal type as user - entitlement_id: "admin" # Apply the 'admin' entitlement for administrative privileges - - # Configuration for "privilege" resources in Oracle - privilege: - # Display name for this privilege resource type - name: "Privilege" - # Description providing context on what an Oracle privilege is - description: "A privilege within the Oracle system" - # Settings for listing privileges from the database - list: - query: | - SELECT NAME - FROM sys.system_privilege_map - ORDER BY NAME - map: - # Unique identifier for the privilege (from the NAME column) - id: ".NAME" - # Display name for the privilege (derived from NAME) - display_name: ".NAME" - # Additional description field for the privilege (left blank) - description: "" - # Static entitlements for privileges that can be granted to users - static_entitlements: - - id: "assigned" # Entitlement identifier for a standard privilege assignment - display_name: "resource.DisplayName + ' privileges'" # Dynamic display name for an assigned privilege - description: "'Is granted the ' + resource.DisplayName + ' privilege'" # Explanation of the assignment - purpose: "assignment" # Denotes that this is an assignment action - grantable_to: - - "user" # Specifies that this entitlement can be applied to user resources - provisioning: - vars: - principal_name: principal.ID # Maps the user identifier - privilege_name: resource.ID # Maps the privilege identifier - grant: - no_transaction: true - queries: - - | - GRANT ? TO ? - # Revoke section defines how to remove a privilege from a user - revoke: - # no_transaction indicates this should execute outside a transaction block - no_transaction: true - # SQL queries to execute when revoking the privilege - queries: - - | - REVOKE ? FROM ? - - id: "admin" # Entitlement identifier for administrative control over privileges - display_name: "resource.DisplayName + ' Privilege Admin'" # Dynamic display name for admin privileges on the resource - description: "'Can grant the ' + resource.DisplayName + ' privilege to other users'" # Describes the ability to manage privileges - purpose: "permission" # Indicates this entitlement is a permission setting - grantable_to: - - "user" # This permission can be granted to user resources - provisioning: - vars: - principal_name: principal.ID # User identifier for provisioning - privilege_name: resource.ID # Privilege identifier for provisioning - grant: - no_transaction: true - queries: - - | - # The ? placeholder will be replaced with the raw value of the privilege_name variable - # defined in the vars section above. The |unquoted flag means the value will be inserted directly into the SQL - # rather than using a prepared statement parameter. This is needed for DDL statements like GRANT that require - # the actual identifier names. - # - # Similarly, ? inserts the principal_name variable's value directly into the query. - # Without |unquoted, the values would be passed as bind parameters like: GRANT ? TO ? WITH ADMIN OPTION - GRANT ? TO ? WITH ADMIN OPTION - revoke: - no_transaction: true - queries: - - | - REVOKE ? FROM ? - - | - GRANT ? TO ? - # Dynamic grants to map privilege assignments based on database queries - grants: - - query: | - SELECT - GRANTEE AS USERNAME, - PRIVILEGE, - ADMIN_OPTION - FROM - DBA_SYS_PRIVS - WHERE - GRANTEE IN (SELECT USERNAME FROM DBA_USERS) - ORDER BY - USERNAME, - PRIVILEGE - map: - - skip_if: ".PRIVILEGE != resource.ID" # Skip mapping if the privilege does not match the current resource - principal_id: ".USERNAME" # Map the query's USERNAME field as the principal ID - principal_type: "user" # Explicitly set the principal type to "user" - entitlement_id: "assigned" # Use the 'assigned' entitlement for standard privilege assignments - - skip_if: ".PRIVILEGE != resource.ID || .ADMIN_OPTION != 'YES'" # Condition for admin-level privilege mapping - principal_id: ".USERNAME" # Map the USERNAME to the principal ID - principal_type: "user" # Define the principal type as user - entitlement_id: "admin" # Apply the 'admin' entitlement when administrative rights are present diff --git a/examples/postgres-test.yml b/examples/postgres-test.yml index 2c3c8bc7..775fc2d3 100644 --- a/examples/postgres-test.yml +++ b/examples/postgres-test.yml @@ -6,7 +6,7 @@ app_description: Test configuration for PostgreSQL with employee_id and last_log # Connection settings for the PostgreSQL database connect: # Data Source Name (DSN) for our test PostgreSQL instance - dsn: "postgres://baton:password@localhost:5432/batondb?sslmode=disable" + dsn: "postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_DATABASE}?sslmode=disable" # Definition of different resource types managed by this connector resource_types: @@ -42,7 +42,7 @@ resource_types: primary_key: "id" # Mapping of query results to resource fields map: - # Unique identifier for the user resource + # Unique identifier for the user resource id: ".username" # Field to display as the resource's name display_name: ".username" @@ -58,27 +58,84 @@ resource_types: # Email addresses emails: - ".email" - # Account type mapping - account_type: ".account_type" + # Account type mapping (convert 'employee' to 'human') + account_type: ".account_type == 'employee' ? 'human' : .account_type" # Employee ID mapping employee_ids: - ".employee_id" - # Last login timestamp mapping - last_login: ".last_login" + # Last login timestamp mapping (only if not null) + last_login: ".last_login != null ? string(.last_login) : ''" # Manager information manager_id: ".manager_id" manager_email: ".manager_email" - + # Profile details for the user profile: user_id: ".id" created_at: ".created_at" - last_login: ".last_login" + last_login: ".last_login != null ? string(.last_login) : ''" manager_id: ".manager_id" manager_username: ".manager_username" manager_email: ".manager_email" - - # Configuration for "role" resources + + # Account provisioning configuration with password support + account_provisioning: + schema: + - name: "username" + description: "The username for the new PostgreSQL user" + type: "string" + placeholder: "new_user" + required: true + - name: "email" + description: "Email address for the new user" + type: "string" + placeholder: "user@example.com" + required: true + - name: "employee_id" + description: "Employee ID for the new user" + type: "string" + placeholder: "EMP001" + required: false + credentials: + random_password: + max_length: 32 + min_length: 16 + preferred: true + no_password: + preferred: false + validate: + vars: + username: "username" + email: "email" + query: | + SELECT + u.id, + u.username, + u.email, + u.employee_id, + u.status, + u.account_type, + u.created_at, + u.last_login, + u.manager_id, + m.username as manager_username, + m.email as manager_email + FROM users u + LEFT JOIN users m ON u.manager_id = m.id + WHERE u.username = ? + create: + vars: + username: "input.username" + email: "input.email" + employee_id: "has(input.employee_id) ? input.employee_id : null" + password: "password" + queries: + # Insert the new user with generated password (PostgreSQL style) + - | + INSERT INTO users (username, email, employee_id, status, account_type, created_at, password_hash) + VALUES (?, ?, ?, 'active', 'human', NOW(), crypt(?, gen_salt('bf'))) + + # Configuration for "role" resources role: # Display name for the role resource type name: "Role" @@ -111,7 +168,7 @@ resource_types: # Profile mapping for the role profile: role_id: ".id" - + # Define a static entitlement for role membership static_entitlements: - id: "member" @@ -156,7 +213,7 @@ resource_types: map: id: ".feat_id" display_name: ".feat_name" - description: ".feat_desc" + description: ".feat_desc" traits: feature: profile: diff --git a/examples/sqlserver-test.yml b/examples/sqlserver-test.yml new file mode 100644 index 00000000..a269293c --- /dev/null +++ b/examples/sqlserver-test.yml @@ -0,0 +1,231 @@ +--- +# Application name for this connector configuration +app_name: SQL Server Test +app_description: Test configuration for SQL Server with random password generation and account provisioning + +# Connection settings for the SQL Server database +connect: + # Data Source Name (DSN) for our test SQL Server instance + dsn: "sqlserver://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}?database=${DB_DATABASE}" + +# Definition of different resource types managed by this connector +resource_types: + # Configuration for "user" resources in SQL Server + user: + # Display name for this resource type + name: "User" + # Description providing context about what a user represents + description: "A user within the SQL Server system" + # Settings for listing user records from the database + list: + # SQL query to fetch user-related data including employee_id, last_login, and manager information + query: | + SELECT + u.UserID as id, + u.Username as username, + u.Email as email, + u.EmployeeID as employee_id, + CASE + WHEN u.IsActive = 1 THEN 'active' + ELSE 'inactive' + END as status, + u.AccountType as account_type, + u.CreatedAt as created_at, + CASE + WHEN u.LastLogin IS NULL THEN '' + ELSE FORMAT(u.LastLogin, 'yyyy-MM-ddTHH:mm:ssZ') + END as last_login, + u.ManagerID as manager_id, + m.Username as manager_username, + m.Email as manager_email + FROM + Users u + LEFT JOIN + Users m ON u.ManagerID = m.UserID + ORDER BY u.UserID + OFFSET ? ROWS FETCH NEXT ? ROWS ONLY + # Pagination configuration (using offset for SQL Server) + pagination: + strategy: "offset" + primary_key: "id" + # Mapping of query results to resource fields + map: + # Unique identifier for the user resource + id: ".username" + # Field to display as the resource's name + display_name: ".username" + # Additional description for the user resource + description: ".username" + # User-specific traits for detailed information + traits: + user: + # User's email addresses (array format) + emails: + - ".email" + # Account status (active, inactive, etc.) + status: ".status" + # Login identifier (usually username) + login: ".username" + # Last login timestamp + last_login: ".last_login" + # User profile information + profile: + employee_id: ".employee_id" + account_type: ".account_type" + created_at: ".created_at" + manager_id: ".manager_id" + manager_username: ".manager_username" + manager_email: ".manager_email" + + # Account provisioning configuration for creating new user accounts + account_provisioning: + # Schema definition for required and optional fields during user creation + schema: + - name: "username" + description: "The username for the new user" + type: "string" + placeholder: "new_user" + required: true + - name: "email" + description: "Email address for the new user" + type: "string" + placeholder: "user@example.com" + required: true + - name: "employee_id" + description: "Employee ID for the new user" + type: "string" + placeholder: "EMP001" + required: false + # Credential options for password management + credentials: + random_password: + max_length: 32 + min_length: 16 + preferred: true + no_password: + preferred: false + # Validation query to check if user already exists + validate: + vars: + username: "username" + email: "email" + query: | + SELECT + u.UserID as id, + u.Username as username, + u.Email as email, + u.EmployeeID as employee_id, + CASE + WHEN u.IsActive = 1 THEN 'active' + ELSE 'inactive' + END as status, + u.AccountType as account_type, + u.CreatedAt as created_at, + CASE + WHEN u.LastLogin IS NULL THEN '' + ELSE FORMAT(u.LastLogin, 'yyyy-MM-ddTHH:mm:ssZ') + END as last_login, + u.ManagerID as manager_id, + m.Username as manager_username, + m.Email as manager_email + FROM Users u + LEFT JOIN Users m ON u.ManagerID = m.UserID + WHERE u.Username = ? + # Account creation configuration + create: + vars: + username: "input.username" + email: "input.email" + employee_id: "has(input.employee_id) ? input.employee_id : null" + password: "password" + queries: + # Insert the new user with generated password (SQL Server style with HASHBYTES) + - | + INSERT INTO Users (Username, Email, EmployeeID, IsActive, AccountType, CreatedAt, PasswordHash) + VALUES (?, ?, ?, 1, 'human', GETDATE(), HASHBYTES('SHA2_256', ?)) + + # Configuration for "role" resources + role: + # Display name for the role resource type + name: "Role" + # Brief description of what a role signifies + description: "A role within the SQL Server system" + # Settings for listing roles from the database + list: + # SQL query to retrieve roles + query: | + SELECT + RoleID as id, + RoleName as role_name, + Description as description + FROM Roles + ORDER BY RoleID + OFFSET ? ROWS FETCH NEXT ? ROWS ONLY + # Pagination settings for role listing + pagination: + strategy: "offset" + primary_key: "id" + # Field mappings for role data + map: + id: ".role_name" + display_name: ".role_name" + description: ".description" + traits: + role: + profile: + role_name: ".role_name" + + # Static entitlements that can be granted for roles + static_entitlements: + - id: "member" + display_name: "resource.DisplayName + ' Membership'" + description: "'Member of the ' + resource.DisplayName + ' role'" + purpose: "assignment" + grantable_to: + - "user" + # Provisioning settings for granting/revoking role membership + provisioning: + vars: + username: "principal.ID" + role_name: "resource.ID" + grant: + queries: + - | + INSERT INTO UserRoles ( + UserID, + RoleID + ) + SELECT + u.UserID, + r.RoleID + FROM Users u, Roles r + WHERE u.Username = ? + AND r.RoleName = ? + revoke: + queries: + - | + DELETE ur FROM UserRoles ur + INNER JOIN Users u ON ur.UserID = u.UserID + INNER JOIN Roles r ON ur.RoleID = r.RoleID + WHERE u.Username = ? + AND r.RoleName = ? + + # Grant discovery for existing role assignments + grants: + - query: | + SELECT + u.Username as username, + r.RoleName as role_name + FROM UserRoles ur + INNER JOIN Users u ON ur.UserID = u.UserID + INNER JOIN Roles r ON ur.RoleID = r.RoleID + ORDER BY r.RoleID + OFFSET ? ROWS FETCH NEXT ? ROWS ONLY + map: + - skip_if: ".role_name != resource.ID" + principal_id: ".username" + principal_type: "user" + entitlement_id: "member" + pagination: + strategy: "offset" + primary_key: "role_name" diff --git a/examples/wordpress-test.yml b/examples/wordpress-test.yml new file mode 100644 index 00000000..f7037184 --- /dev/null +++ b/examples/wordpress-test.yml @@ -0,0 +1,146 @@ +--- +# Application name for the Wordpress connector. +app_name: Wordpress Test +app_description: Test configuration for WordPress with password support + +# Connection configuration using a MySQL DSN. +connect: + dsn: "mysql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_DATABASE}?charset=utf8mb4&parseTime=True&loc=Local" + +resource_types: + user: + name: "User" + description: "A user within the wordpress system" + list: + # SQL query to fetch user details from the WordPress database. + query: | + SELECT + u.ID AS user_id, + u.user_login AS username, + u.user_email AS email, + u.user_registered AS created_at + FROM wp_users u + ORDER BY user_id ASC + LIMIT ? OFFSET ? + map: + id: ".user_id" + display_name: ".username" + description: ".email" + traits: + user: + emails: + - ".email" + status: "active" + status_details: "'detailed status'" + login: ".username" + profile: + name: ".username" + created_at: ".created_at" + pagination: + strategy: "offset" + primary_key: "user_id" + account_provisioning: + schema: + - name: "username" + description: "The username of the user" + type: "string" + placeholder: "user" + required: true + - name: "email" + description: "The email of the user" + type: "string" + placeholder: "user@example.com" + required: true + credentials: + no_password: + preferred: false + random_password: + max_length: 128 + min_length: 12 + preferred: true + validate: + vars: + email: "email" + query: | + SELECT + u.ID AS user_id, + u.user_login AS username, + u.user_email AS email, + u.user_registered AS created_at + FROM wp_users u + WHERE u.user_email = ? + create: + vars: + username: "input.username" + email: "input.email" + password: "password" + queries: + - "INSERT INTO wp_users (user_login, user_email, user_pass) VALUES (?, ?, MD5(?))" + + role: + name: "Role" + description: "A role within the wordpress system that can be assigned to a user" + list: + query: | + SELECT DISTINCT + um.umeta_id AS row_id, + um.meta_value AS role_name + FROM wp_usermeta um + WHERE um.meta_key = 'wp_capabilities' + AND um.meta_value != 'a:0:{}' + AND um.umeta_id > ? + ORDER BY row_id ASC + LIMIT ? + map: + id: "phpDeserializeStringArray(string(.role_name))[0]" + display_name: "titleCase(phpDeserializeStringArray(string(.role_name))[0])" + description: "'Wordpress role for user'" + traits: + role: + profile: + pagination: + strategy: "cursor" + primary_key: "row_id" + + static_entitlements: + - id: "member" + display_name: "resource.DisplayName + ' Role Member'" + description: "'Member of the ' + resource.DisplayName + ' role'" + purpose: "assignment" + grantable_to: + - "user" + provisioning: + vars: + principal_id: principal.ID + role_string: "phpSerializeStringArray([resource.ID])" + grant: + queries: + - | + DELETE FROM wp_usermeta + WHERE user_id = ? AND meta_key = 'wp_capabilities' + - | + INSERT INTO wp_usermeta (user_id, meta_key, meta_value) + VALUES (?, 'wp_capabilities', ?) + revoke: + queries: + - | + DELETE FROM wp_usermeta + WHERE user_id = ? AND meta_key = 'wp_capabilities' + + grants: + - query: | + SELECT + u.ID AS user_id, + um.meta_value AS role_name + FROM wp_users u + JOIN wp_usermeta um ON u.ID = um.user_id + WHERE um.meta_key = 'wp_capabilities' + LIMIT ? OFFSET ? + map: + - skip_if: "(size(phpDeserializeStringArray(string(.role_name))) < 1 || phpDeserializeStringArray(string(.role_name))[0] != resource.ID)" + principal_id: ".user_id" + principal_type: "user" + entitlement_id: "member" + pagination: + strategy: "offset" + primary_key: "user_id" diff --git a/examples/wordpress.yml b/examples/wordpress.yml deleted file mode 100644 index 76350956..00000000 --- a/examples/wordpress.yml +++ /dev/null @@ -1,160 +0,0 @@ ---- -# Application name for the Wordpress connector. -app_name: Wordpress - -# Connection configuration using a MySQL DSN. -# This DSN string is constructed with environment variables to supply the necessary connection parameters. -connect: - dsn: "mysql://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?charset=utf8mb4&parseTime=True&loc=Local" - -resource_types: - user: - name: "User" - description: "A user within the wordpress system" - list: - # SQL query to fetch user details from the WordPress database. - # Tokens ? and ? are placeholders for pagination values. - query: | - SELECT - u.ID AS user_id, - u.user_login AS username, - u.user_email AS email, - u.user_registered AS created_at - FROM wp_users u - ORDER BY user_id ASC - LIMIT ? OFFSET ? - map: - # Mapping configuration using CEL (Common Expression Language). - # CEL is a lightweight, expressive language used here to transform and reference data. - # The dot syntax, such as ".user_id", is used to reference a specific column from the SQL query results. - id: ".user_id" # CEL: Retrieves the value from the "user_id" column of the query result. - display_name: ".username" # CEL: Retrieves the username from the "username" column. - description: ".email" # CEL: Uses the "email" column value for the description. - traits: - user: - emails: - - ".email" # CEL: References the "email" column; the dot indicates direct column access. - status: "active" # A static value indicating the user's status. - status_details: "'detailed status'" # Static string; FIXME: Confirm if this should dynamically adjust based on data. - login: ".username" # CEL: References the "username" column for the login field. - profile: - name: ".username" # CEL: Uses the "username" column value to assign a profile name. -# created_at: ".created_at" # CEL: Uses the "created_at" column value from the SQL result. - pagination: - strategy: "offset" # Offset-based pagination: retrieves records by skipping a specified number. - primary_key: "user_id" # The key column used to uniquely identify records for pagination. - account_provisioning: - schema: - - name: "username" - description: "The username of the user" - type: "string" - placeholder: "user" - required: true - - name: "email" - description: "The email of the user" - type: "string" - placeholder: "user@example.com" - required: true - credentials: - no_password: - preferred: true -# random_password: -# max_length: 128 -# min_length: 12 -# disallowed_characters: "!@#$%^&*()_+" -# preferred: true - # this option can be added after we add support for passwords - validate: - vars: - email: "email" - query: | - SELECT - u.ID AS user_id, - u.user_login AS username, - u.user_email AS email, - u.user_registered AS created_at - FROM wp_users u - WHERE u.user_email = ? - create: - vars: - username: "input.username" - email: "input.email" - queries: - - "INSERT INTO wp_users (user_login, user_email) VALUES (?, ?)" - - role: - name: "Role" - description: "A role within the wordpress system that can be assigned to a user" - list: - # SQL query to fetch distinct role records from the wp_usermeta table. - # Token ? is used for cursor-based pagination and will be replaced with the value of the last row_id from the previous page. - query: | - SELECT DISTINCT - um.umeta_id AS row_id, - um.meta_value AS role_name - FROM wp_usermeta um - WHERE um.meta_key = 'wp_capabilities' - AND um.meta_value != 'a:0:{}' - AND um.umeta_id > ? - ORDER BY row_id ASC - LIMIT ? - map: - # CEL expressions are used to process the query results. - # The dot syntax (e.g., ".role_name") references the corresponding column from the SQL row. - id: "phpDeserializeStringArray(string(.role_name))[0]" # CEL: Deserializes the 'role_name' column and selects the first element. - display_name: "titleCase(phpDeserializeStringArray(string(.role_name))[0])" # CEL: Formats the deserialized role name into title case. - description: "'Wordpress role for user'" # Static description for the role. - traits: - role: - profile: # Placeholder for further role profile details if needed. - pagination: - strategy: "cursor" # Cursor-based pagination: uses a token to mark the position for fetching subsequent records. - primary_key: "row_id" # Unique column used for maintaining pagination state. - - static_entitlements: - - id: "member" - # Display name and description are constructed via CEL expressions to incorporate resource details. - display_name: "resource.DisplayName + ' Role Member'" # CEL: Concatenates the resource's display name with ' Role Member'. - description: "'Member of the ' + resource.DisplayName + ' role'" # CEL: Dynamically builds a description using the resource's display name. - purpose: "assignment" - grantable_to: - - "user" # This entitlement is applicable to resources of type 'user'. - provisioning: - vars: - principal_id: principal.ID # CEL: Extracts the principal's ID for use in SQL queries. - role_string: "phpSerializeStringArray([resource.ID])" # CEL: Serializes the resource's ID into an array format suitable for SQL operations. - grant: - queries: - - | - DELETE FROM wp_usermeta - WHERE user_id = ? AND meta_key = 'wp_capabilities' - - | - INSERT INTO wp_usermeta (user_id, meta_key, meta_value) - VALUES (?, 'wp_capabilities', ?) - revoke: - queries: - - | - DELETE FROM wp_usermeta - WHERE user_id = ? AND meta_key = 'wp_capabilities' - - grants: - - query: | - SELECT - u.ID AS user_id, - um.meta_value AS role_name - FROM wp_users u - JOIN wp_usermeta um ON u.ID = um.user_id - WHERE um.meta_key = 'wp_capabilities' - LIMIT ? OFFSET ? - map: - - skip_if: "(size(phpDeserializeStringArray(string(.role_name))) < 1 || phpDeserializeStringArray(string(.role_name))[0] != resource.ID)" - # CEL Expression Explanation: - # This condition uses CEL to determine whether to skip processing the record. - # It checks if the deserialized array from the "role_name" column is empty or if its first element does not match the resource's ID. - # The dot syntax (e.g., ".role_name") is used to pull the value of the "role_name" column from the SQL result directly. - principal_id: ".user_id" # CEL: Retrieves the "user_id" value from the SQL result. - principal_type: "user" # Specifies that the principal type is 'user'. - entitlement_id: "member" # Associates this grant with the 'member' entitlement. - pagination: - strategy: "offset" # Uses offset-based pagination for processing grants. - primary_key: "user_id" # Pagination key referencing the unique user identifier. \ No newline at end of file diff --git a/go.mod b/go.mod index 1d8bf29c..f5ce060a 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/stretchr/testify v1.10.0 go.uber.org/zap v1.27.0 golang.org/x/text v0.24.0 + google.golang.org/protobuf v1.36.6 gopkg.in/yaml.v3 v3.0.1 ) @@ -128,7 +129,6 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e // indirect google.golang.org/grpc v1.71.1 // indirect - google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect modernc.org/libc v1.62.1 // indirect modernc.org/mathutil v1.7.1 // indirect diff --git a/pkg/bcel/functions/php_deserialize_test.go b/pkg/bcel/functions/php_deserialize_test.go index 4954e545..16180893 100644 --- a/pkg/bcel/functions/php_deserialize_test.go +++ b/pkg/bcel/functions/php_deserialize_test.go @@ -13,22 +13,22 @@ func TestPHPDeserializeArray(t *testing.T) { wantErr bool }{ { - name: "simple", + name: "administrator_role", expr: `a:1:{s:13:"administrator";b:1;}`, want: []string{"administrator"}, }, { - name: "simple", + name: "editor_role", expr: `a:1:{s:6:"editor";b:1;}`, want: []string{"editor"}, }, { - name: "simple", + name: "subscriber_role", expr: `a:1:{s:10:"subscriber";b:1;}`, want: []string{"subscriber"}, }, { - name: "simple", + name: "contributor_role", expr: `a:1:{s:11:"contributor";b:1;}`, want: []string{"contributor"}, }, diff --git a/pkg/bsql/config_test.go b/pkg/bsql/config_test.go index 7b0cb7e7..2b38103c 100644 --- a/pkg/bsql/config_test.go +++ b/pkg/bsql/config_test.go @@ -28,10 +28,10 @@ func TestParse(t *testing.T) { }{ { name: "wordpress-example", - input: loadExampleConfig(t, "wordpress"), + input: loadExampleConfig(t, "wordpress-test"), validate: func(t *testing.T, c *Config) { - require.Equal(t, "Wordpress", c.AppName) - require.Equal(t, "mysql://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?charset=utf8mb4&parseTime=True&loc=Local", c.Connect.DSN) + require.Equal(t, "Wordpress Test", c.AppName) + require.Equal(t, "mysql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_DATABASE}?charset=utf8mb4&parseTime=True&loc=Local", c.Connect.DSN) require.Len(t, c.ResourceTypes, 2) @@ -83,13 +83,13 @@ func TestParse(t *testing.T) { // Validate no_password config require.NotNil(t, userResourceType.AccountProvisioning.Credentials.NoPassword) - require.True(t, userResourceType.AccountProvisioning.Credentials.NoPassword.Preferred) + require.False(t, userResourceType.AccountProvisioning.Credentials.NoPassword.Preferred) // Validate random_password config - // require.NotNil(t, userResourceType.AccountProvisioning.Credentials.RandomPassword) - // require.Equal(t, 128, userResourceType.AccountProvisioning.Credentials.RandomPassword.MaxLength) - // require.Equal(t, 12, userResourceType.AccountProvisioning.Credentials.RandomPassword.MinLength) - // require.Equal(t, "!@#$%^&*()_+", userResourceType.AccountProvisioning.Credentials.RandomPassword.DisallowedCharacters) + require.NotNil(t, userResourceType.AccountProvisioning.Credentials.RandomPassword) + require.Equal(t, 128, userResourceType.AccountProvisioning.Credentials.RandomPassword.MaxLength) + require.Equal(t, 12, userResourceType.AccountProvisioning.Credentials.RandomPassword.MinLength) + require.True(t, userResourceType.AccountProvisioning.Credentials.RandomPassword.Preferred) // Validate account creation configuration require.NotNil(t, userResourceType.AccountProvisioning.Create) @@ -99,13 +99,13 @@ func TestParse(t *testing.T) { require.NotNil(t, userResourceType.AccountProvisioning.Create.Vars) require.Equal(t, "input.username", userResourceType.AccountProvisioning.Create.Vars["username"]) require.Equal(t, "input.email", userResourceType.AccountProvisioning.Create.Vars["email"]) - // require.Equal(t, "credentials.password", userResourceType.AccountProvisioning.Create.Vars["password"]) + require.Equal(t, "password", userResourceType.AccountProvisioning.Create.Vars["password"]) // Validate creation queries require.Len(t, userResourceType.AccountProvisioning.Create.Queries, 1) require.Equal(t, normalizeQueryString(` - INSERT INTO wp_users (user_login, user_email) - VALUES (?, ?) + INSERT INTO wp_users (user_login, user_email, user_pass) + VALUES (?, ?, MD5(?)) `), normalizeQueryString(userResourceType.AccountProvisioning.Create.Queries[0])) // Validate `role` resource type diff --git a/pkg/bsql/helpers.go b/pkg/bsql/helpers.go index 0b2490c6..6a4501a2 100644 --- a/pkg/bsql/helpers.go +++ b/pkg/bsql/helpers.go @@ -7,6 +7,9 @@ import ( "time" "github.com/conductorone/baton-sql/pkg/database" + + v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" + "github.com/conductorone/baton-sdk/pkg/crypto" ) // Common time formats that may be used in databases. @@ -134,3 +137,18 @@ func parseTimeWithEngine(value string, dbEngine database.DbEngine) (*time.Time, // Fall back to the generic parser if prioritized formats don't match return parseTime(value) } + +// generateCredentials generates a random password based on the credential options and configuration. +func generateCredentials(credentialOptions *v2.CredentialOptions) (string, error) { + if credentialOptions == nil || credentialOptions.GetRandomPassword() == nil { + return "", errors.New("unsupported credential option: only random password is supported") + } + + randomPasswordOpts := credentialOptions.GetRandomPassword() + + password, err := crypto.GenerateRandomPassword(randomPasswordOpts) + if err != nil { + return "", err + } + return password, nil +} diff --git a/pkg/bsql/helpers_test.go b/pkg/bsql/helpers_test.go index 86572e50..6fb00a6c 100644 --- a/pkg/bsql/helpers_test.go +++ b/pkg/bsql/helpers_test.go @@ -4,6 +4,7 @@ import ( "testing" "time" + v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" "github.com/conductorone/baton-sql/pkg/database" "github.com/stretchr/testify/require" ) @@ -162,3 +163,178 @@ func TestParseTimeWithEngine(t *testing.T) { }) } } + +func TestGenerateCredentials(t *testing.T) { + tests := []struct { + name string + credentialOptions *v2.CredentialOptions + expectError bool + expectNonEmpty bool + }{ + { + name: "nil credential options", + expectError: true, + }, + { + name: "no random password", + credentialOptions: &v2.CredentialOptions{ + Options: &v2.CredentialOptions_NoPassword_{ + NoPassword: &v2.CredentialOptions_NoPassword{}, + }, + }, + expectError: true, + }, + { + name: "valid random password with constraints", + credentialOptions: &v2.CredentialOptions{ + Options: &v2.CredentialOptions_RandomPassword_{ + RandomPassword: &v2.CredentialOptions_RandomPassword{ + Length: 16, + Constraints: []*v2.PasswordConstraint{ + { + CharSet: "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + MinCount: 2, + }, + { + CharSet: "abcdefghijklmnopqrstuvwxyz", + MinCount: 2, + }, + { + CharSet: "0123456789", + MinCount: 1, + }, + { + CharSet: "!@#$%^&*()_+-=[]{}|;:,.<>?", + MinCount: 1, + }, + }, + }, + }, + }, + expectError: false, + expectNonEmpty: true, + }, + { + name: "simple random password without constraints", + credentialOptions: &v2.CredentialOptions{ + Options: &v2.CredentialOptions_RandomPassword_{ + RandomPassword: &v2.CredentialOptions_RandomPassword{ + Length: 20, + }, + }, + }, + expectError: false, + expectNonEmpty: true, + }, + { + name: "very long password", + credentialOptions: &v2.CredentialOptions{ + Options: &v2.CredentialOptions_RandomPassword_{ + RandomPassword: &v2.CredentialOptions_RandomPassword{ + Length: 32, + Constraints: []*v2.PasswordConstraint{ + { + CharSet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", + MinCount: 5, + }, + }, + }, + }, + }, + expectError: false, + expectNonEmpty: true, + }, + { + name: "minimum practical length", + credentialOptions: &v2.CredentialOptions{ + Options: &v2.CredentialOptions_RandomPassword_{ + RandomPassword: &v2.CredentialOptions_RandomPassword{ + Length: 8, + }, + }, + }, + expectError: false, + expectNonEmpty: true, + }, + { + name: "database-style password (medium length)", + credentialOptions: &v2.CredentialOptions{ + Options: &v2.CredentialOptions_RandomPassword_{ + RandomPassword: &v2.CredentialOptions_RandomPassword{ + Length: 20, + Constraints: []*v2.PasswordConstraint{ + { + CharSet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", + MinCount: 8, + }, + { + CharSet: "0123456789", + MinCount: 2, + }, + { + CharSet: "!@#$%^&*()_+-=[]{}|;:,.<>?", + MinCount: 4, + }, + }, + }, + }, + }, + expectError: false, + expectNonEmpty: true, + }, + { + name: "enterprise-grade password", + credentialOptions: &v2.CredentialOptions{ + Options: &v2.CredentialOptions_RandomPassword_{ + RandomPassword: &v2.CredentialOptions_RandomPassword{ + Length: 24, + Constraints: []*v2.PasswordConstraint{ + { + CharSet: "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + MinCount: 3, + }, + { + CharSet: "abcdefghijklmnopqrstuvwxyz", + MinCount: 3, + }, + { + CharSet: "0123456789", + MinCount: 2, + }, + { + CharSet: "!@#$%^&*", + MinCount: 2, + }, + }, + }, + }, + }, + expectError: false, + expectNonEmpty: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + password, err := generateCredentials(tt.credentialOptions) + + if tt.expectError { + require.Error(t, err) + require.Empty(t, password) + return + } + + require.NoError(t, err) + if tt.expectNonEmpty { + require.NotEmpty(t, password) + // Verify the password length matches the requested length + if tt.credentialOptions.GetRandomPassword() != nil { + expectedLength := tt.credentialOptions.GetRandomPassword().GetLength() + if expectedLength > 0 { + require.Equal(t, int(expectedLength), len(password), "Password length should match requested length") + } + } + } + }) + } +} diff --git a/pkg/bsql/provisioning.go b/pkg/bsql/provisioning.go index c4317020..2f4b9258 100644 --- a/pkg/bsql/provisioning.go +++ b/pkg/bsql/provisioning.go @@ -150,50 +150,6 @@ func (s *SQLSyncer) prepareProvisioningVars(ctx context.Context, vars map[string return ret, nil } -func (s *SQLSyncer) prepareSchemaVars(accountProvisioning *AccountProvisioning, accountInfo *v2.AccountInfo) (map[string]any, error) { - inputs := make(map[string]any) - - for _, field := range accountProvisioning.Schema { - val, ok := accountInfo.Profile.Fields[field.Name] - if !ok { - continue - } - - switch field.Type { - case "string": - if strVal := val.GetStringValue(); strVal != "" { - inputs[field.Name] = strVal - } - - case "string_list": - if listVal := val.GetListValue(); listVal != nil { - var strList []string - for _, v := range listVal.Values { - if str := v.GetStringValue(); str != "" { - strList = append(strList, str) - } - } - inputs[field.Name] = strList - } - - case "boolean": - inputs[field.Name] = val.GetBoolValue() - - case "int": - if numVal := val.GetNumberValue(); numVal != 0 { - inputs[field.Name] = int(numVal) - } - - case "map": - if structVal := val.GetStructValue(); structVal != nil { - inputs[field.Name] = structVal.AsMap() - } - } - } - - return inputs, nil -} - func (s *SQLSyncer) validateAccount(ctx context.Context, accountProvisioning *AccountProvisioning, inputs map[string]any) (*v2.Resource, bool, error) { if accountProvisioning.Validate == nil { return nil, false, fmt.Errorf("validation configuration is not defined for account provisioning") diff --git a/pkg/bsql/query.go b/pkg/bsql/query.go index 2511b105..cacbc996 100644 --- a/pkg/bsql/query.go +++ b/pkg/bsql/query.go @@ -359,6 +359,11 @@ func (s *SQLSyncer) prepareQueryVars(ctx context.Context, inputs map[string]any, inputs = make(map[string]any) } + celInputs := make(map[string]any) + for k, v := range inputs { + celInputs[k] = v + } + for k, v := range vars { // Check if the value is a direct reference to an input field if inputVal, exists := inputs[v]; exists { @@ -367,7 +372,7 @@ func (s *SQLSyncer) prepareQueryVars(ctx context.Context, inputs map[string]any, } // Otherwise, evaluate it as a CEL expression - out, err := s.env.Evaluate(ctx, v, inputs) + out, err := s.env.Evaluate(ctx, v, celInputs) if err != nil { return nil, err } diff --git a/pkg/bsql/resource_types_test.go b/pkg/bsql/resource_types_test.go index 951f7e91..6de6ab3e 100644 --- a/pkg/bsql/resource_types_test.go +++ b/pkg/bsql/resource_types_test.go @@ -9,7 +9,7 @@ import ( func TestConfig_GetResourceTypes_wordpress(t *testing.T) { ctx := t.Context() - wordpressConfig := loadExampleConfig(t, "wordpress") + wordpressConfig := loadExampleConfig(t, "wordpress-test") c, err := Parse([]byte(wordpressConfig)) require.NoError(t, err) diff --git a/pkg/bsql/user_syncer.go b/pkg/bsql/user_syncer.go index 60bf077f..b395ebca 100644 --- a/pkg/bsql/user_syncer.go +++ b/pkg/bsql/user_syncer.go @@ -90,6 +90,9 @@ func (s *userSyncer) CreateAccountCapabilityDetails(ctx context.Context) (*v2.Cr }, nil, nil } +// CreateAccount creates a new user account in the database with optional credential generation. +// It validates inputs, generates credentials if required, executes provisioning queries, +// and validates the created account. func (s *userSyncer) CreateAccount( ctx context.Context, accountInfo *v2.AccountInfo, @@ -100,58 +103,177 @@ func (s *userSyncer) CreateAccount( annotations.Annotations, error, ) { - l := ctxzap.Extract(ctx) - resourceTypeID, accountProvisioning, err := s.fullConfig.ExtractAccountProvisioning() + logger := ctxzap.Extract(ctx) + + // Extract and validate account provisioning configuration + resourceTypeID, provisioningConfig, err := s.extractAndValidateProvisioning() if err != nil { - if errors.Is(err, ErrNoAccountProvisioningDefined) { - return nil, nil, nil, nil - } return nil, nil, nil, err } - if accountProvisioning == nil { - return nil, nil, nil, errors.New("no account provisioning defined") - } + logger.Debug("creating account", zap.String("resource_type_id", resourceTypeID)) - l.Debug("creating account", zap.String("resource_type_id", resourceTypeID)) + // Validate required input parameters + if err := s.validateAccountInfo(accountInfo); err != nil { + return nil, nil, nil, err + } - if accountInfo == nil || accountInfo.Profile == nil { - return nil, nil, nil, errors.New("account info and profile are required") + // Prepare all query inputs in one step + queryInputs, plaintextDataList, err := s.prepareAllQueryInputs(provisioningConfig, accountInfo, credentialOptions) + if err != nil { + return nil, nil, nil, err } - var ptds []*v2.PlaintextData + // Execute account creation queries + useTransaction := !provisioningConfig.Create.NoTransaction + if err := s.runProvisioningQueries(ctx, provisioningConfig.Create.Queries, queryInputs, useTransaction); err != nil { + return nil, nil, nil, err + } - inputs, err := s.prepareSchemaVars(accountProvisioning, accountInfo) + // Validate the created account + accountResource, err := s.validateCreatedAccount(ctx, provisioningConfig, queryInputs) if err != nil { return nil, nil, nil, err } - // only support no password for now - switch credentialOptions.Options.(type) { - case *v2.CredentialOptions_NoPassword_: - default: - return nil, nil, nil, fmt.Errorf("unsupported credential options %v", credentialOptions) + response := &v2.CreateAccountResponse_SuccessResult{ + Resource: accountResource, } - useTx := !accountProvisioning.Create.NoTransaction + return response, plaintextDataList, nil, nil +} - err = s.runProvisioningQueries(ctx, accountProvisioning.Create.Queries, inputs, useTx) +// extractAndValidateProvisioning extracts and validates the account provisioning configuration. +func (s *userSyncer) extractAndValidateProvisioning() (string, *AccountProvisioning, error) { + resourceTypeID, accountProvisioning, err := s.fullConfig.ExtractAccountProvisioning() if err != nil { - return nil, nil, nil, err + if errors.Is(err, ErrNoAccountProvisioningDefined) { + return "", nil, nil + } + return "", nil, err } - accountResource, ok, err := s.validateAccount(ctx, accountProvisioning, inputs) - if err != nil { - return nil, nil, nil, err + if accountProvisioning == nil { + return "", nil, errors.New("no account provisioning defined") } - if !ok { - return nil, nil, nil, fmt.Errorf("post account provisioning validation failed") + return resourceTypeID, accountProvisioning, nil +} + +// validateAccountInfo validates that the required account information is provided. +func (s *userSyncer) validateAccountInfo(accountInfo *v2.AccountInfo) error { + if accountInfo == nil { + return errors.New("account info is required") } - car := &v2.CreateAccountResponse_SuccessResult{ - Resource: accountResource, + if accountInfo.Profile == nil { + return errors.New("account profile is required") + } + + return nil +} + +// prepareAllQueryInputs prepares all query inputs including schema vars and credentials in one step. +// This eliminates the need for complex merging logic by doing everything together. +func (s *userSyncer) prepareAllQueryInputs( + provisioningConfig *AccountProvisioning, + accountInfo *v2.AccountInfo, + credentialOptions *v2.CredentialOptions, +) (map[string]any, []*v2.PlaintextData, error) { + queryInputs := make(map[string]any) + var plaintextDataList []*v2.PlaintextData + + // 1. Add schema variables (profile data) directly + schemaVars := make(map[string]any) + for _, field := range provisioningConfig.Schema { + if value, exists := accountInfo.Profile.Fields[field.Name]; exists { + var parsedValue any + switch field.Type { + case "string": + if strValue := value.GetStringValue(); strValue != "" { + parsedValue = strValue + } + case "string_list": + if listValue := value.GetListValue(); listValue != nil { + var strList []string + for _, v := range listValue.Values { + if strValue := v.GetStringValue(); strValue != "" { + strList = append(strList, strValue) + } + } + parsedValue = strList + } + case "boolean": + parsedValue = value.GetBoolValue() + case "int": + if numValue := value.GetNumberValue(); numValue != 0 { + parsedValue = int(numValue) + } + case "map": + if structValue := value.GetStructValue(); structValue != nil { + parsedValue = structValue.AsMap() + } + } + + if parsedValue != nil { + queryInputs[field.Name] = parsedValue + schemaVars[field.Name] = parsedValue + } + } + } + + // 2. Add credentials if required + credentials := make(map[string]any) + if credentialOptions != nil { + switch credentialOptions.Options.(type) { + case *v2.CredentialOptions_NoPassword_: + case *v2.CredentialOptions_RandomPassword_: + password, err := generateCredentials(credentialOptions) + if err != nil { + return nil, nil, fmt.Errorf("failed to generate password: %w", err) + } + + // Add password to queryInputs and credentials map + queryInputs["password"] = password + credentials["password"] = password + + // Create plaintext data for return + passwordData := &v2.PlaintextData{ + Name: "password", + Bytes: []byte(password), + } + plaintextDataList = append(plaintextDataList, passwordData) + default: + return nil, nil, fmt.Errorf("unsupported credential options: %v", credentialOptions) + } + } + + // 3. Add namespaced access for advanced CEL expressions + // Only add namespaces if they don't conflict with user-defined schema fields + if len(schemaVars) > 0 { + if _, exists := queryInputs["input"]; !exists { + queryInputs["input"] = schemaVars + } + } + if len(credentials) > 0 { + if _, exists := queryInputs["credentials"]; !exists { + queryInputs["credentials"] = credentials + } + } + + return queryInputs, plaintextDataList, nil +} + +// validateCreatedAccount validates that the account was created successfully. +func (s *userSyncer) validateCreatedAccount(ctx context.Context, provisioningConfig *AccountProvisioning, queryInputs map[string]any) (*v2.Resource, error) { + accountResource, isValid, err := s.validateAccount(ctx, provisioningConfig, queryInputs) + if err != nil { + return nil, fmt.Errorf("failed to validate created account: %w", err) + } + + if !isValid { + return nil, errors.New("account validation failed after creation") } - return car, ptds, nil, nil + return accountResource, nil } diff --git a/pkg/bsql/user_syncer_test.go b/pkg/bsql/user_syncer_test.go new file mode 100644 index 00000000..873e6a62 --- /dev/null +++ b/pkg/bsql/user_syncer_test.go @@ -0,0 +1,168 @@ +package bsql + +import ( + "testing" + + v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/structpb" +) + +func TestUserSyncer_prepareAllQueryInputs_KeyCollision(t *testing.T) { + tests := []struct { + name string + schema []*AccountProvisioningField + profileFields map[string]*structpb.Value + credentialOptions *v2.CredentialOptions + expectInputKey bool + expectCredKey bool + expectedInputVal any + expectedCredVal any + }{ + { + name: "No collision - normal schema fields", + schema: []*AccountProvisioningField{ + {Name: "username", Type: "string"}, + {Name: "email", Type: "string"}, + }, + profileFields: map[string]*structpb.Value{ + "username": structpb.NewStringValue("test_user"), + "email": structpb.NewStringValue("test@example.com"), + }, + credentialOptions: &v2.CredentialOptions{ + Options: &v2.CredentialOptions_RandomPassword_{ + RandomPassword: &v2.CredentialOptions_RandomPassword{ + Length: 12, + }, + }, + }, + expectInputKey: true, + expectCredKey: true, + }, + { + name: "Collision with 'input' field in schema", + schema: []*AccountProvisioningField{ + {Name: "input", Type: "string"}, + {Name: "username", Type: "string"}, + }, + profileFields: map[string]*structpb.Value{ + "input": structpb.NewStringValue("user_defined_input"), + "username": structpb.NewStringValue("test_user"), + }, + credentialOptions: &v2.CredentialOptions{ + Options: &v2.CredentialOptions_RandomPassword_{ + RandomPassword: &v2.CredentialOptions_RandomPassword{ + Length: 12, + }, + }, + }, + expectInputKey: true, + expectCredKey: true, + expectedInputVal: "user_defined_input", + }, + { + name: "Collision with 'credentials' field in schema", + schema: []*AccountProvisioningField{ + {Name: "credentials", Type: "string"}, + {Name: "username", Type: "string"}, + }, + profileFields: map[string]*structpb.Value{ + "credentials": structpb.NewStringValue("user_defined_creds"), + "username": structpb.NewStringValue("test_user"), + }, + credentialOptions: &v2.CredentialOptions{ + Options: &v2.CredentialOptions_RandomPassword_{ + RandomPassword: &v2.CredentialOptions_RandomPassword{ + Length: 12, + }, + }, + }, + expectInputKey: true, + expectCredKey: true, + expectedCredVal: "user_defined_creds", + }, + { + name: "Collision with both 'input' and 'credentials' fields", + schema: []*AccountProvisioningField{ + {Name: "input", Type: "string"}, + {Name: "credentials", Type: "string"}, + {Name: "username", Type: "string"}, + }, + profileFields: map[string]*structpb.Value{ + "input": structpb.NewStringValue("user_defined_input"), + "credentials": structpb.NewStringValue("user_defined_creds"), + "username": structpb.NewStringValue("test_user"), + }, + credentialOptions: &v2.CredentialOptions{ + Options: &v2.CredentialOptions_RandomPassword_{ + RandomPassword: &v2.CredentialOptions_RandomPassword{ + Length: 12, + }, + }, + }, + expectInputKey: true, + expectCredKey: true, + expectedInputVal: "user_defined_input", + expectedCredVal: "user_defined_creds", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create userSyncer instance + syncer := &userSyncer{} + + // Create provisioning config + provisioningConfig := &AccountProvisioning{ + Schema: tt.schema, + } + + // Create account info with profile fields + accountInfo := &v2.AccountInfo{ + Profile: &structpb.Struct{ + Fields: tt.profileFields, + }, + } + + // Call the method under test + queryInputs, _, err := syncer.prepareAllQueryInputs( + provisioningConfig, + accountInfo, + tt.credentialOptions, + ) + + require.NoError(t, err) + require.NotNil(t, queryInputs) + + require.Contains(t, queryInputs, "username") + require.Equal(t, "test_user", queryInputs["username"]) + + if tt.expectInputKey { + require.Contains(t, queryInputs, "input") + if tt.expectedInputVal != nil { + require.Equal(t, tt.expectedInputVal, queryInputs["input"]) + } else { + inputMap, ok := queryInputs["input"].(map[string]any) + require.True(t, ok, "input should be a map when not overridden") + require.Contains(t, inputMap, "username") + require.Equal(t, "test_user", inputMap["username"]) + } + } + + if tt.expectCredKey { + require.Contains(t, queryInputs, "credentials") + if tt.expectedCredVal != nil { + require.Equal(t, tt.expectedCredVal, queryInputs["credentials"]) + } else { + credMap, ok := queryInputs["credentials"].(map[string]any) + require.True(t, ok, "credentials should be a map when not overridden") + require.Contains(t, credMap, "password") + } + } + + if tt.credentialOptions != nil { + require.Contains(t, queryInputs, "password") + } + }) + } +} diff --git a/test/README.md b/test/README.md index 7ea60776..b79a191d 100644 --- a/test/README.md +++ b/test/README.md @@ -1,61 +1,327 @@ # Baton SQL Testing -This directory contains tests for the baton-sql connector, including tests for specific features like employee ID and last login. +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. -## Running the Tests +## Supported Databases -### Docker Compose Test Environment +The testing environment supports all major database engines: -ExampleDocker Compose setup to test the baton-sql connector with a MySQL database: +- **MySQL** - With SHA2-256 password hashing and account provisioning +- **PostgreSQL** - With bcrypt password hashing via pgcrypto extension +- **Oracle Database** - With DBA_USERS integration and custom table provisioning +- **SQL Server** - With SHA2-256 password hashing and HASHBYTES functions +- **WordPress** - MySQL-based WordPress user management with MD5 hashing + +## Test Files + +### Database Initialization Scripts + +- `mysql-init.sql` - Complete MySQL schema with password_hash column, roles, and test data +- `postgres-init.sql` - PostgreSQL schema with bcrypt support and pgcrypto extension +- `oracle-init.sql` - Complete Oracle setup with DBA user creation and custom tables +- `sqlserver-init.sql` - SQL Server schema with HASHBYTES password hashing +- `wordpress-init.sql` - WordPress wp_users table with MD5 password support +- `create_baton_pdb.sql` - Oracle Pluggable Database (PDB) specific setup + +### Docker Compose Configurations + +- `../docker-compose-mysql-test.yml` - MySQL test environment with account provisioning +- `../docker-compose-postgres-test.yml` - PostgreSQL test environment with pgcrypto +- `../docker-compose-oracle-test.yml` - Oracle XE test environment with DBA setup +- `../docker-compose-sqlserver-test.yml` - SQL Server test environment with HASHBYTES +- `../docker-compose-wordpress-test.yml` - WordPress + MySQL test environment + +## Quick Start Testing + +### Individual Database Testing + +#### MySQL Testing ```bash -# Start the test environment -docker-compose -f docker-compose-test.yml up +# Start MySQL test environment +docker-compose -f docker-compose-mysql-test.yml up -d + +# Wait for MySQL to be ready (5-10 seconds) +sleep 10 -# To run in the background -docker-compose -f docker-compose-test.yml up -d +# Test account creation with random password +./baton-sql --config-path examples/mysql-local-test.yml --provisioning \ + --create-account-login "test_mysql_user" \ + --create-account-profile '{"username": "test_mysql_user", "email": "test@mysql.com", "employee_id": "EMP001"}' \ + --log-level debug -# To stop the test environment -docker-compose -f docker-compose-test.yml down +# Verify user creation and password hash +docker exec -it baton-mysql-test mysql -u baton -ppassword -D batondb \ + -e "SELECT username, email, password_hash FROM users WHERE username = 'test_mysql_user';" ``` -### Validating Features +#### PostgreSQL Testing + +```bash +# Start PostgreSQL test environment +docker-compose -f docker-compose-postgres-test.yml up -d + +# Wait for PostgreSQL to be ready (10-15 seconds) +sleep 15 -To validate that the employee ID and last login features are working correctly: +# Test account creation with random password +./baton-sql --config-path examples/postgres-test.yml --provisioning \ + --create-account-login "test_postgres_user" \ + --create-account-profile '{"username": "test_postgres_user", "email": "test@postgres.com", "employee_id": "EMP002"}' \ + --log-level debug + +# Verify user creation and password hash +docker exec -it baton-postgres-test psql -U baton -d batondb \ + -c "SELECT username, email, password_hash FROM users WHERE username = 'test_postgres_user';" +``` -1. First, run the connector using the test configuration: - ```bash - # Bring up the test environment: - docker-compose -f docker-compose-test.yml up -d +#### Oracle Testing - # run a sync - ./dist/darwin_arm64/baton-sql \ - --config-path ./examples/mysql.yml \ +```bash +# Start Oracle test environment (takes 5-10 minutes to initialize) +docker-compose -f docker-compose-oracle-test.yml up -d + +# Wait for Oracle to be ready (check logs: docker logs baton-oracle-test) +# Once ready, test account creation +./baton-sql --config-path examples/oracle-test.yml --provisioning \ + --create-account-login "TEST_ORACLE_USER" \ + --create-account-profile '{"username": "TEST_ORACLE_USER"}' \ --log-level debug - ``` -2. Then run the validation script: - ```bash - ./test/validate_features.sh - ``` +# Verify user creation +echo "SELECT username, email, password_hash FROM users WHERE username = 'TEST_ORACLE_USER';" | \ + docker exec -i baton-oracle-test sqlplus -s baton/password@localhost:1521/XEPDB1 +``` -## Test Data +#### SQL Server Testing -The MySQL database is initialized with test data that includes: +```bash +# Start SQL Server test environment +docker-compose -f docker-compose-sqlserver-test.yml up -d -- Users with employee IDs and last login timestamps -- Different date formats for last login -- Roles and user-role relationships -- Additional tables for testing various aspects of the connector +# Wait for SQL Server to be ready (30-45 seconds) +sleep 45 -## Manual Testing +# Test account creation with random password +./baton-sql --config-path examples/sqlserver-test.yml --provisioning \ + --create-account-login "test_sqlserver_user" \ + --create-account-profile '{"username": "test_sqlserver_user", "email": "test@sqlserver.com", "employee_id": "EMP003"}' \ + --log-level debug + +# Verify user creation in SQL Server +docker exec -it baton-sqlserver-test /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P 'YourStrong@Passw0rd' -C \ + -Q "USE BatonTestDB; SELECT Username, Email, PasswordHash FROM Users WHERE Username = 'test_sqlserver_user';" +``` -You can also connect to the database manually to inspect or modify the test data: +#### WordPress Testing + +```bash +# Start WordPress test environment +docker-compose -f docker-compose-wordpress-test.yml up -d + +# Wait for WordPress to be ready (10-15 seconds) +sleep 15 + +# Test account creation with random password +./baton-sql --config-path examples/wordpress-test.yml --provisioning \ + --create-account-login "test_wp_user" \ + --create-account-profile '{"username": "test_wp_user", "email": "test@wordpress.com"}' \ + --log-level debug + +# Verify user creation in WordPress +docker exec -it baton-wordpress-mysql-test mysql -u wp_user -pwp_password -D wordpress \ + -e "SELECT user_login, user_email, user_pass FROM wp_users WHERE user_login = 'test_wp_user';" +``` + +## Database Connection Details + +### MySQL + +- **Host**: localhost:3306 +- **Database**: batondb +- **User**: baton +- **Password**: password +- **Container**: baton-mysql-test + +### PostgreSQL + +- **Host**: localhost:5432 +- **Database**: batondb +- **User**: baton +- **Password**: password +- **Container**: baton-postgres-test + +### Oracle + +- **Host**: localhost:1521 +- **Service**: XEPDB1 (Pluggable Database) +- **User**: baton +- **Password**: password +- **Container**: baton-oracle-test + +### SQL Server + +- **Host**: localhost:1433 +- **Database**: BatonTestDB +- **User**: sa +- **Password**: YourStrong@Passw0rd +- **Container**: baton-sqlserver-test + +### WordPress + +- **Host**: localhost:3307 (MySQL backend) +- **Database**: wordpress +- **User**: wp_user +- **Password**: wp_password +- **Container**: baton-wordpress-mysql-test + +## Testing Features + +### Random Password Generation + +Each database configuration tests: + +- **Secure password generation** (12-32 characters) +- **Database-specific hashing** (SHA2, bcrypt, MD5) +- **Account provisioning** with profile data +- **Password verification** in database + +### Account Provisioning Schema + +Test profiles include: + +- **username** (required) +- **email** (required for most DBs) +- **employee_id** (optional) +- **custom fields** per database + +### Manual Database Inspection + +#### Connect to MySQL ```bash -# Connect to the MySQL database docker exec -it baton-mysql-test mysql -u baton -ppassword batondb -# Example query to see users with their employee IDs and last login times -mysql> SELECT username, employee_id, last_login FROM users; -``` \ No newline at end of file +# Example queries +mysql> SELECT username, email, employee_id, password_hash FROM users; +mysql> SELECT COUNT(*) as total_users FROM users; +mysql> SHOW TABLES; +``` + +#### Connect to PostgreSQL + +```bash +docker exec -it baton-postgres-test psql -U baton -d batondb + +# Example queries +batondb=> SELECT username, email, employee_id, password_hash FROM users; +batondb=> SELECT COUNT(*) as total_users FROM users; +batondb=> \dt +``` + +#### Connect to Oracle + +```bash +# Connect via sqlplus +echo "SELECT username, email, password_hash FROM users;" | \ + docker exec -i baton-oracle-test sqlplus -s baton/password@localhost:1521/XEPDB1 + +# Interactive connection +docker exec -it baton-oracle-test sqlplus baton/password@localhost:1521/XEPDB1 +``` + +#### Connect to SQL Server + +```bash +docker exec -it baton-sqlserver-test /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P 'YourStrong@Passw0rd' -C + +# Example queries +1> USE BatonTestDB; +2> GO +1> SELECT Username, Email, PasswordHash FROM Users; +2> GO +1> SELECT COUNT(*) as total_users FROM Users; +2> GO +``` + +#### Connect to WordPress + +```bash +docker exec -it baton-wordpress-mysql-test mysql -u wp_user -pwp_password wordpress + +# Example queries +mysql> SELECT user_login, user_email, user_pass FROM wp_users; +mysql> SELECT COUNT(*) as total_users FROM wp_users; +mysql> SHOW TABLES LIKE 'wp_%'; +``` + +## Troubleshooting + +### Common Issues + +#### Database Connection Refused + +```bash +# Check if containers are running +docker ps | grep baton + +# Check container logs +docker logs baton-mysql-test +docker logs baton-postgres-test +docker logs baton-oracle-test +docker logs baton-sqlserver-test +docker logs baton-wordpress-mysql-test +``` + +#### Oracle Takes Too Long + +Oracle XE initialization can take 5-10 minutes: + +```bash +# Monitor Oracle startup +docker logs -f baton-oracle-test + +# Look for: "DATABASE IS READY TO USE!" +``` + +#### Permission Denied Errors + +```bash +# Ensure user has correct permissions +# For MySQL: +docker exec -it baton-mysql-test mysql -u root -ppassword -e "SHOW GRANTS FOR 'baton'@'%';" + +# For PostgreSQL: +docker exec -it baton-postgres-test psql -U postgres -c "\du baton" +``` + +### Debug Mode + +Enable debug logging to see generated passwords: + +```bash +./baton-sql --config-path examples/mysql-local-test.yml --provisioning \ + --create-account-login "debug_user" \ + --create-account-profile '{"username": "debug_user", "email": "debug@example.com"}' \ + --log-level debug +``` + +## Cleanup + +Stop and remove all test containers: + +```bash +# Stop individual environments +docker-compose -f docker-compose-mysql-test.yml down +docker-compose -f docker-compose-postgres-test.yml down +docker-compose -f docker-compose-oracle-test.yml down +docker-compose -f docker-compose-sqlserver-test.yml down +docker-compose -f docker-compose-wordpress-test.yml down + +# Remove all containers and volumes +docker-compose -f docker-compose-mysql-test.yml down -v +docker-compose -f docker-compose-postgres-test.yml down -v +docker-compose -f docker-compose-oracle-test.yml down -v +docker-compose -f docker-compose-sqlserver-test.yml down -v +docker-compose -f docker-compose-wordpress-test.yml down -v +``` diff --git a/test/create_baton_pdb.sql b/test/create_baton_pdb.sql new file mode 100644 index 00000000..3e0e03dd --- /dev/null +++ b/test/create_baton_pdb.sql @@ -0,0 +1,38 @@ +-- Connect to the Pluggable Database +ALTER SESSION SET CONTAINER = XEPDB1; + +-- Create baton user in PDB +CREATE USER baton IDENTIFIED BY password; +GRANT DBA TO baton; +GRANT CREATE SESSION TO baton; +ALTER USER baton ACCOUNT UNLOCK; + +-- Verify user creation +SELECT username, account_status FROM dba_users WHERE username = 'BATON'; + +-- Create test tables as baton +CONNECT baton/password@localhost:1521/XEPDB1; + +-- Create users table +CREATE TABLE users ( + id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + username VARCHAR2(100) NOT NULL, + email VARCHAR2(255), + employee_id VARCHAR2(50), + status VARCHAR2(20) DEFAULT 'active', + account_type VARCHAR2(20) DEFAULT 'human', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + last_login TIMESTAMP, + manager_id NUMBER, + password_hash VARCHAR2(255) +); + +-- Insert test user +INSERT INTO users (username, email, employee_id) +VALUES ('test_admin', 'admin@oracle.test', 'ORA001'); + +-- Show count +SELECT COUNT(*) as user_count FROM users; + +COMMIT; +EXIT; \ No newline at end of file diff --git a/test/mysql-init.sql b/test/mysql-init.sql index 1cfef39a..71b8fe75 100644 --- a/test/mysql-init.sql +++ b/test/mysql-init.sql @@ -15,7 +15,8 @@ CREATE TABLE users ( account_type VARCHAR(20) DEFAULT 'human', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_login TIMESTAMP NULL, - manager_id INT + manager_id INT, + password_hash VARCHAR(255) ); -- Insert sample users with different date formats for last_login (without manager relationships) diff --git a/test/mysql-schema.sql b/test/mysql-schema.sql deleted file mode 100644 index d2296023..00000000 --- a/test/mysql-schema.sql +++ /dev/null @@ -1,66 +0,0 @@ --- Create users table -CREATE TABLE users ( - id INT AUTO_INCREMENT PRIMARY KEY, - username VARCHAR(100) NOT NULL, - email VARCHAR(255) NOT NULL, - employee_id VARCHAR(50), - status VARCHAR(20) DEFAULT 'active', - account_type VARCHAR(20) DEFAULT 'human', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - last_login TIMESTAMP NULL, - manager_id INT -); - --- Insert sample users with different date formats for last_login -INSERT INTO users (username, email, employee_id, status, account_type, created_at, last_login) VALUES -('admin', 'admin@example.com', 'EMP001', 'active', 'human', '2025-01-01 12:00:00', '2025-04-15 09:30:00'), -('jane.doe', 'jane.doe@example.com', 'EMP002', 'active', 'human', '2025-01-05 14:30:00', '2025-04-17 08:45:00'), -('john.smith', 'john.smith@example.com', 'EMP003', 'active', 'human', '2025-01-10 09:45:00', '2025-04-16 16:20:00'), -('service.acct', 'service@example.com', 'SVC001', 'active', 'service', '2025-02-01 08:00:00', NULL), -('disabled.user', 'disabled@example.com', 'EMP004', 'disabled', 'human', '2025-02-15 10:15:00', '2025-03-01 11:10:00'); - --- Create roles table -CREATE TABLE roles ( - id INT AUTO_INCREMENT PRIMARY KEY, - role_name VARCHAR(100) NOT NULL -); - --- Insert sample roles -INSERT INTO roles (role_name) VALUES -('admin'), -('user'), -('reader'); - --- Create user_roles table for many-to-many relationship -CREATE TABLE user_roles ( - user_id INT, - role_id INT, - PRIMARY KEY (user_id, role_id), - FOREIGN KEY (user_id) REFERENCES users(id), - FOREIGN KEY (role_id) REFERENCES roles(id) -); - --- Assign roles to users -INSERT INTO user_roles (user_id, role_id) VALUES -(1, 1), -- admin has admin role -(2, 2), -- jane.doe has user role -(3, 2), -- john.smith has user role -(3, 3), -- john.smith also has reader role -(4, 2); -- service.acct has user role - --- Create table to track last login attempts (for testing date formats) -CREATE TABLE login_history ( - id INT AUTO_INCREMENT PRIMARY KEY, - user_id INT, - login_time TIMESTAMP, - login_time_text VARCHAR(50), - login_time_alt VARCHAR(50), - FOREIGN KEY (user_id) REFERENCES users(id) -); - --- Insert various date formats -INSERT INTO login_history (user_id, login_time, login_time_text, login_time_alt) VALUES -(1, '2025-04-15 09:30:00', '15-APR-2025 09:30:00', '15/04/2025 09:30:00'), -(2, '2025-04-17 08:45:00', '17-APR-2025 08:45:00', '17/04/2025 08:45:00'), -(3, '2025-04-16 16:20:00', '16-APR-2025 16:20:00', '16/04/2025 16:20:00'), -(5, '2025-03-01 11:10:00', '01-MAR-2025 11:10:00', '01/03/2025 11:10:00'); \ No newline at end of file diff --git a/test/oracle-init.sql b/test/oracle-init.sql new file mode 100644 index 00000000..75443910 --- /dev/null +++ b/test/oracle-init.sql @@ -0,0 +1,117 @@ +-- Connect as SYSDBA to create test user and schema +CONNECT sys/OraclePassword123@XE AS SYSDBA; + +-- Create a test user for baton +CREATE USER baton IDENTIFIED BY password +DEFAULT TABLESPACE users +TEMPORARY TABLESPACE temp +ACCOUNT UNLOCK; + +-- Grant necessary privileges +GRANT CREATE SESSION TO baton; +GRANT CREATE TABLE TO baton; +GRANT CREATE SEQUENCE TO baton; +GRANT CREATE VIEW TO baton; +GRANT UNLIMITED TABLESPACE TO baton; +GRANT DBA TO baton; -- For testing purposes, grant DBA access + +-- Connect as the test user +CONNECT baton/password@XE; + +-- Drop existing tables if they exist +BEGIN + EXECUTE IMMEDIATE 'DROP TABLE employee_data CASCADE CONSTRAINTS'; +EXCEPTION + WHEN OTHERS THEN NULL; +END; +/ + +BEGIN + EXECUTE IMMEDIATE 'DROP TABLE login_history CASCADE CONSTRAINTS'; +EXCEPTION + WHEN OTHERS THEN NULL; +END; +/ + +BEGIN + EXECUTE IMMEDIATE 'DROP TABLE user_roles CASCADE CONSTRAINTS'; +EXCEPTION + WHEN OTHERS THEN NULL; +END; +/ + +BEGIN + EXECUTE IMMEDIATE 'DROP TABLE roles CASCADE CONSTRAINTS'; +EXCEPTION + WHEN OTHERS THEN NULL; +END; +/ + +BEGIN + EXECUTE IMMEDIATE 'DROP TABLE users CASCADE CONSTRAINTS'; +EXCEPTION + WHEN OTHERS THEN NULL; +END; +/ + +-- Create users table +CREATE TABLE users ( + id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + username VARCHAR2(100) NOT NULL, + email VARCHAR2(255) NOT NULL, + employee_id VARCHAR2(50), + status VARCHAR2(20) DEFAULT 'active', + account_type VARCHAR2(20) DEFAULT 'human', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + last_login TIMESTAMP, + manager_id NUMBER, + password_hash VARCHAR2(255) +); + +-- Insert sample users +INSERT INTO users (username, email, employee_id, status, account_type, created_at, last_login) VALUES +('admin', 'admin@example.com', 'EMP001', 'active', 'human', TIMESTAMP '2025-01-01 12:00:00', TIMESTAMP '2025-04-15 09:30:00'); + +INSERT INTO users (username, email, employee_id, status, account_type, created_at, last_login) VALUES +('jane.doe', 'jane.doe@example.com', 'EMP002', 'active', 'human', TIMESTAMP '2025-01-05 14:30:00', TIMESTAMP '2025-04-17 08:45:00'); + +INSERT INTO users (username, email, employee_id, status, account_type, created_at, last_login) VALUES +('john.smith', 'john.smith@example.com', 'EMP003', 'active', 'human', TIMESTAMP '2025-01-10 09:45:00', TIMESTAMP '2025-04-16 16:20:00'); + +INSERT INTO users (username, email, employee_id, status, account_type, created_at, last_login) VALUES +('service.acct', 'service@example.com', 'SVC001', 'active', 'service', TIMESTAMP '2025-02-01 08:00:00', NULL); + +INSERT INTO users (username, email, employee_id, status, account_type, created_at, last_login) VALUES +('disabled.user', 'disabled@example.com', 'EMP004', 'disabled', 'human', TIMESTAMP '2025-02-15 10:15:00', TIMESTAMP '2025-03-01 11:10:00'); + +-- Create roles table +CREATE TABLE roles ( + id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + role_name VARCHAR2(100) NOT NULL +); + +-- Insert sample roles +INSERT INTO roles (role_name) VALUES ('admin'); +INSERT INTO roles (role_name) VALUES ('user'); +INSERT INTO roles (role_name) VALUES ('reader'); + +-- Create user_roles table for many-to-many relationship +CREATE TABLE user_roles ( + user_id NUMBER, + role_id NUMBER, + PRIMARY KEY (user_id, role_id), + FOREIGN KEY (user_id) REFERENCES users(id), + FOREIGN KEY (role_id) REFERENCES roles(id) +); + +-- Assign roles to users +INSERT INTO user_roles (user_id, role_id) VALUES (1, 1); -- admin has admin role +INSERT INTO user_roles (user_id, role_id) VALUES (2, 2); -- jane.doe has user role +INSERT INTO user_roles (user_id, role_id) VALUES (3, 2); -- john.smith has user role +INSERT INTO user_roles (user_id, role_id) VALUES (3, 3); -- john.smith also has reader role +INSERT INTO user_roles (user_id, role_id) VALUES (4, 2); -- service.acct has user role + +COMMIT; + +-- Print success message +SELECT 'Baton SQL Oracle test database initialized successfully' as message FROM dual; \ No newline at end of file diff --git a/test/postgres-init.sql b/test/postgres-init.sql index 042a13c2..d1cdca96 100644 --- a/test/postgres-init.sql +++ b/test/postgres-init.sql @@ -1,3 +1,6 @@ +-- Enable pgcrypto extension for password hashing +CREATE EXTENSION IF NOT EXISTS pgcrypto; + -- Drop existing tables if they exist DROP TABLE IF EXISTS employee_data; DROP TABLE IF EXISTS login_history; @@ -16,7 +19,8 @@ CREATE TABLE users ( account_type VARCHAR(20) DEFAULT 'human', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_login TIMESTAMP NULL, - manager_id INTEGER + manager_id INTEGER, + password_hash VARCHAR(255) ); -- Insert sample users with different date formats for last_login (without manager relationships) diff --git a/test/sqlserver-init.sql b/test/sqlserver-init.sql new file mode 100644 index 00000000..1f7784b3 --- /dev/null +++ b/test/sqlserver-init.sql @@ -0,0 +1,151 @@ +-- SQL Server initialization script for Baton SQL testing +-- This script creates the necessary tables and sample data for testing user provisioning +-- and random password generation functionality + +-- Create Users table +CREATE TABLE Users ( + UserID INT IDENTITY(1,1) PRIMARY KEY, + Username NVARCHAR(100) NOT NULL UNIQUE, + Email NVARCHAR(255) NOT NULL, + EmployeeID NVARCHAR(50) NULL, + IsActive BIT DEFAULT 1, + AccountType NVARCHAR(20) DEFAULT 'human', + CreatedAt DATETIME2 DEFAULT GETDATE(), + LastLogin DATETIME2 NULL, + ManagerID INT NULL, + PasswordHash VARBINARY(32) NULL, + FOREIGN KEY (ManagerID) REFERENCES Users(UserID) +); + +-- Create Roles table +CREATE TABLE Roles ( + RoleID INT IDENTITY(1,1) PRIMARY KEY, + RoleName NVARCHAR(100) NOT NULL UNIQUE, + Description NVARCHAR(255) NOT NULL +); + +-- Create UserRoles junction table for many-to-many relationship +CREATE TABLE UserRoles ( + UserID INT, + RoleID INT, + AssignedAt DATETIME2 DEFAULT GETDATE(), + PRIMARY KEY (UserID, RoleID), + FOREIGN KEY (UserID) REFERENCES Users(UserID) ON DELETE CASCADE, + FOREIGN KEY (RoleID) REFERENCES Roles(RoleID) ON DELETE CASCADE +); + +-- Create LoginHistory table for tracking login attempts +CREATE TABLE LoginHistory ( + LoginID INT IDENTITY(1,1) PRIMARY KEY, + UserID INT, + LoginTime DATETIME2 DEFAULT GETDATE(), + LoginTimeText NVARCHAR(50), + LoginTimeAlt NVARCHAR(50), + LoginSuccess BIT DEFAULT 1, + FOREIGN KEY (UserID) REFERENCES Users(UserID) ON DELETE CASCADE +); + +-- Create EmployeeData table for additional employee information +CREATE TABLE EmployeeData ( + EmployeeDataID INT IDENTITY(1,1) PRIMARY KEY, + UserID INT, + EmployeeID NVARCHAR(50), + EmployeeNumber INT, + EmployeeCode NVARCHAR(20), + Department NVARCHAR(100), + JobTitle NVARCHAR(100), + HireDate DATETIME2, + FOREIGN KEY (UserID) REFERENCES Users(UserID) ON DELETE CASCADE +); + +-- Insert sample roles +INSERT INTO Roles (RoleName, Description) VALUES +('admin', 'Administrator role with full access'), +('user', 'Regular user role with standard access'), +('reader', 'Read-only access role'), +('manager', 'Management role with team oversight'), +('developer', 'Developer role with development access'); + +-- Insert sample users (without manager relationships first) +INSERT INTO Users (Username, Email, EmployeeID, IsActive, AccountType, CreatedAt, LastLogin, PasswordHash) VALUES +('admin', 'admin@example.com', 'EMP001', 1, 'human', '2025-01-01 12:00:00', '2025-04-15 09:30:00', HASHBYTES('SHA2_256', 'password123')), +('jane.doe', 'jane.doe@example.com', 'EMP002', 1, 'human', '2025-01-05 14:30:00', '2025-04-17 08:45:00', HASHBYTES('SHA2_256', 'password123')), +('john.smith', 'john.smith@example.com', 'EMP003', 1, 'human', '2025-01-10 09:45:00', '2025-04-16 16:20:00', HASHBYTES('SHA2_256', 'password123')), +('service.acct', 'service@example.com', 'SVC001', 1, 'service', '2025-02-01 08:00:00', NULL, HASHBYTES('SHA2_256', 'password123')), +('disabled.user', 'disabled@example.com', 'EMP004', 0, 'human', '2025-02-15 10:15:00', '2025-03-01 11:10:00', HASHBYTES('SHA2_256', 'password123')), +('alice.manager', 'alice.manager@example.com', 'EMP005', 1, 'human', '2025-03-01 09:00:00', '2025-04-18 10:15:00', HASHBYTES('SHA2_256', 'password123')), +('bob.developer', 'bob.developer@example.com', 'EMP006', 1, 'human', '2025-03-05 11:30:00', '2025-04-18 14:30:00', HASHBYTES('SHA2_256', 'password123')); + +-- Update users to establish manager relationships +-- jane.doe and john.smith report to admin +UPDATE Users SET ManagerID = 1 WHERE Username IN ('jane.doe', 'john.smith'); + +-- service.acct reports to jane.doe +UPDATE Users SET ManagerID = (SELECT UserID FROM Users WHERE Username = 'jane.doe') WHERE Username = 'service.acct'; + +-- disabled.user reports to john.smith +UPDATE Users SET ManagerID = (SELECT UserID FROM Users WHERE Username = 'john.smith') WHERE Username = 'disabled.user'; + +-- alice.manager reports to admin +UPDATE Users SET ManagerID = 1 WHERE Username = 'alice.manager'; + +-- bob.developer reports to alice.manager +UPDATE Users SET ManagerID = (SELECT UserID FROM Users WHERE Username = 'alice.manager') WHERE Username = 'bob.developer'; + +-- Assign roles to users +INSERT INTO UserRoles (UserID, RoleID) VALUES +((SELECT UserID FROM Users WHERE Username = 'admin'), (SELECT RoleID FROM Roles WHERE RoleName = 'admin')), +((SELECT UserID FROM Users WHERE Username = 'jane.doe'), (SELECT RoleID FROM Roles WHERE RoleName = 'user')), +((SELECT UserID FROM Users WHERE Username = 'john.smith'), (SELECT RoleID FROM Roles WHERE RoleName = 'user')), +((SELECT UserID FROM Users WHERE Username = 'john.smith'), (SELECT RoleID FROM Roles WHERE RoleName = 'reader')), +((SELECT UserID FROM Users WHERE Username = 'service.acct'), (SELECT RoleID FROM Roles WHERE RoleName = 'user')), +((SELECT UserID FROM Users WHERE Username = 'alice.manager'), (SELECT RoleID FROM Roles WHERE RoleName = 'manager')), +((SELECT UserID FROM Users WHERE Username = 'alice.manager'), (SELECT RoleID FROM Roles WHERE RoleName = 'admin')), +((SELECT UserID FROM Users WHERE Username = 'bob.developer'), (SELECT RoleID FROM Roles WHERE RoleName = 'developer')), +((SELECT UserID FROM Users WHERE Username = 'bob.developer'), (SELECT RoleID FROM Roles WHERE RoleName = 'user')); + +-- Insert sample login history with various date formats +INSERT INTO LoginHistory (UserID, LoginTime, LoginTimeText, LoginTimeAlt, LoginSuccess) VALUES +((SELECT UserID FROM Users WHERE Username = 'admin'), '2025-04-15 09:30:00', '15-APR-2025 09:30:00', '15/04/2025 09:30:00', 1), +((SELECT UserID FROM Users WHERE Username = 'jane.doe'), '2025-04-17 08:45:00', '17-APR-2025 08:45:00', '17/04/2025 08:45:00', 1), +((SELECT UserID FROM Users WHERE Username = 'john.smith'), '2025-04-16 16:20:00', '16-APR-2025 16:20:00', '16/04/2025 16:20:00', 1), +((SELECT UserID FROM Users WHERE Username = 'disabled.user'), '2025-03-01 11:10:00', '01-MAR-2025 11:10:00', '01/03/2025 11:10:00', 1), +((SELECT UserID FROM Users WHERE Username = 'alice.manager'), '2025-04-18 10:15:00', '18-APR-2025 10:15:00', '18/04/2025 10:15:00', 1), +((SELECT UserID FROM Users WHERE Username = 'bob.developer'), '2025-04-18 14:30:00', '18-APR-2025 14:30:00', '18/04/2025 14:30:00', 1); + +-- Insert sample employee data +INSERT INTO EmployeeData (UserID, EmployeeID, EmployeeNumber, EmployeeCode, Department, JobTitle, HireDate) VALUES +((SELECT UserID FROM Users WHERE Username = 'admin'), 'EMP001', 10001, 'E-10001', 'IT', 'System Administrator', '2025-01-01'), +((SELECT UserID FROM Users WHERE Username = 'jane.doe'), 'EMP002', 10002, 'E-10002', 'HR', 'HR Specialist', '2025-01-05'), +((SELECT UserID FROM Users WHERE Username = 'john.smith'), 'EMP003', 10003, 'E-10003', 'Finance', 'Financial Analyst', '2025-01-10'), +((SELECT UserID FROM Users WHERE Username = 'service.acct'), 'SVC001', 20001, 'S-20001', 'IT', 'Service Account', '2025-02-01'), +((SELECT UserID FROM Users WHERE Username = 'disabled.user'), 'EMP004', 10004, 'E-10004', 'Marketing', 'Marketing Coordinator', '2025-02-15'), +((SELECT UserID FROM Users WHERE Username = 'alice.manager'), 'EMP005', 10005, 'E-10005', 'IT', 'IT Manager', '2025-03-01'), +((SELECT UserID FROM Users WHERE Username = 'bob.developer'), 'EMP006', 10006, 'E-10006', 'IT', 'Software Developer', '2025-03-05'); + +-- Create indexes for better performance +CREATE INDEX IX_Users_Username ON Users(Username); +CREATE INDEX IX_Users_Email ON Users(Email); +CREATE INDEX IX_Users_EmployeeID ON Users(EmployeeID); +CREATE INDEX IX_UserRoles_UserID ON UserRoles(UserID); +CREATE INDEX IX_UserRoles_RoleID ON UserRoles(RoleID); + +-- Print confirmation message +PRINT 'Baton SQL Server test database initialized successfully'; +PRINT 'Database: BatonTestDB'; +PRINT 'Tables created: Users, Roles, UserRoles, LoginHistory, EmployeeData'; +PRINT 'Sample data inserted for testing account provisioning and random password generation'; + +-- Display sample data +SELECT 'Sample Users:' AS Info; +SELECT Username, Email, EmployeeID, IsActive, AccountType FROM Users; + +SELECT 'Sample Roles:' AS Info; +SELECT RoleName, Description FROM Roles; + +SELECT 'Sample User-Role Assignments:' AS Info; +SELECT u.Username, r.RoleName +FROM UserRoles ur +JOIN Users u ON ur.UserID = u.UserID +JOIN Roles r ON ur.RoleID = r.RoleID +ORDER BY u.Username, r.RoleName; \ No newline at end of file diff --git a/test/wordpress-init.sql b/test/wordpress-init.sql new file mode 100644 index 00000000..be455cb1 --- /dev/null +++ b/test/wordpress-init.sql @@ -0,0 +1,56 @@ +-- Drop existing tables if they exist +DROP TABLE IF EXISTS wp_usermeta; +DROP TABLE IF EXISTS wp_users; + +-- Create wp_users table (WordPress standard table) +CREATE TABLE wp_users ( + ID bigint(20) unsigned NOT NULL AUTO_INCREMENT, + user_login varchar(60) NOT NULL DEFAULT '', + user_pass varchar(255) NOT NULL DEFAULT '', + user_nicename varchar(50) NOT NULL DEFAULT '', + user_email varchar(100) NOT NULL DEFAULT '', + user_url varchar(100) NOT NULL DEFAULT '', + user_registered datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + user_activation_key varchar(255) NOT NULL DEFAULT '', + user_status int(11) NOT NULL DEFAULT 0, + display_name varchar(250) NOT NULL DEFAULT '', + PRIMARY KEY (ID), + KEY user_login_key (user_login), + KEY user_nicename (user_nicename), + KEY user_email (user_email) +); + +-- Create wp_usermeta table (WordPress standard table for user metadata) +CREATE TABLE wp_usermeta ( + umeta_id bigint(20) unsigned NOT NULL AUTO_INCREMENT, + user_id bigint(20) unsigned NOT NULL DEFAULT 0, + meta_key varchar(255) DEFAULT NULL, + meta_value longtext DEFAULT NULL, + PRIMARY KEY (umeta_id), + KEY user_id (user_id), + KEY meta_key (meta_key(191)) +); + +-- Insert sample WordPress users +INSERT INTO wp_users (user_login, user_pass, user_nicename, user_email, user_registered, display_name) VALUES +('admin', MD5('admin123'), 'admin', 'admin@example.com', '2025-01-01 12:00:00', 'Administrator'), +('editor', MD5('editor123'), 'editor', 'editor@example.com', '2025-01-05 14:30:00', 'Editor User'), +('author', MD5('author123'), 'author', 'author@example.com', '2025-01-10 09:45:00', 'Author User'), +('subscriber', MD5('subscriber123'), 'subscriber', 'subscriber@example.com', '2025-02-01 08:00:00', 'Subscriber User'); + +-- Insert user capabilities (WordPress roles) +INSERT INTO wp_usermeta (user_id, meta_key, meta_value) VALUES +(1, 'wp_capabilities', 'a:1:{s:13:"administrator";b:1;}'), +(2, 'wp_capabilities', 'a:1:{s:6:"editor";b:1;}'), +(3, 'wp_capabilities', 'a:1:{s:6:"author";b:1;}'), +(4, 'wp_capabilities', 'a:1:{s:10:"subscriber";b:1;}'); + +-- Insert user levels (WordPress legacy) +INSERT INTO wp_usermeta (user_id, meta_key, meta_value) VALUES +(1, 'wp_user_level', '10'), +(2, 'wp_user_level', '7'), +(3, 'wp_user_level', '2'), +(4, 'wp_user_level', '0'); + +-- Print success message +SELECT 'WordPress test database initialized successfully' as message; \ No newline at end of file From 12f6284e5996a27079fd31fd17ed7f8c9d7b4a91 Mon Sep 17 00:00:00 2001 From: Mateo Hernandez Date: Fri, 8 Aug 2025 14:11:10 -0300 Subject: [PATCH 2/2] minor changes and improvements --- pkg/bsql/provisioning.go | 137 ++++++++++++++++++++++++++++++++-- pkg/bsql/query.go | 7 +- pkg/bsql/user_syncer.go | 141 +---------------------------------- pkg/bsql/user_syncer_test.go | 8 +- 4 files changed, 138 insertions(+), 155 deletions(-) diff --git a/pkg/bsql/provisioning.go b/pkg/bsql/provisioning.go index 2f4b9258..1c66b03b 100644 --- a/pkg/bsql/provisioning.go +++ b/pkg/bsql/provisioning.go @@ -150,18 +150,18 @@ func (s *SQLSyncer) prepareProvisioningVars(ctx context.Context, vars map[string return ret, nil } -func (s *SQLSyncer) validateAccount(ctx context.Context, accountProvisioning *AccountProvisioning, inputs map[string]any) (*v2.Resource, bool, error) { +func (s *SQLSyncer) validateAccount(ctx context.Context, accountProvisioning *AccountProvisioning, inputs map[string]any) (*v2.Resource, error) { if accountProvisioning.Validate == nil { - return nil, false, fmt.Errorf("validation configuration is not defined for account provisioning") + return nil, fmt.Errorf("validation configuration is not defined for account provisioning") } if accountProvisioning.Validate.Query == "" { - return nil, false, fmt.Errorf("validation query is not defined for account provisioning") + return nil, fmt.Errorf("validation query is not defined for account provisioning") } queryVars, err := s.prepareQueryVars(ctx, inputs, accountProvisioning.Validate.Vars) if err != nil { - return nil, false, err + return nil, err } var ret *v2.Resource @@ -175,12 +175,135 @@ func (s *SQLSyncer) validateAccount(ctx context.Context, accountProvisioning *Ac return false, nil }) if err != nil { - return nil, false, err + return nil, err } if ret == nil { - return nil, false, fmt.Errorf("unable to find resource for account provisioning") + return nil, fmt.Errorf("unable to find resource for account provisioning") + } + + return ret, nil +} + +// prepareQueryInputs prepares all query inputs including schema vars and credentials in one step. +// This eliminates the need for complex merging logic by doing everything together. +func (s *SQLSyncer) prepareQueryInputs( + provisioningConfig *AccountProvisioning, + accountInfo *v2.AccountInfo, + credentialOptions *v2.CredentialOptions, +) (map[string]any, []*v2.PlaintextData, error) { + queryInputs := make(map[string]any) + var plaintextDataList []*v2.PlaintextData + + // 1. Add schema variables (profile data) directly + schemaVars := make(map[string]any) + for _, field := range provisioningConfig.Schema { + if value, exists := accountInfo.Profile.Fields[field.Name]; exists { + var parsedValue any + switch field.Type { + case "string": + if strValue := value.GetStringValue(); strValue != "" { + parsedValue = strValue + } + case "string_list": + if listValue := value.GetListValue(); listValue != nil { + var strList []string + for _, v := range listValue.Values { + if strValue := v.GetStringValue(); strValue != "" { + strList = append(strList, strValue) + } + } + parsedValue = strList + } + case "boolean": + parsedValue = value.GetBoolValue() + case "int": + if numValue := value.GetNumberValue(); numValue != 0 { + parsedValue = int(numValue) + } + case "map": + if structValue := value.GetStructValue(); structValue != nil { + parsedValue = structValue.AsMap() + } + } + + if parsedValue != nil { + queryInputs[field.Name] = parsedValue + schemaVars[field.Name] = parsedValue + } + } + } + + // 2. Add credentials if required + credentials := make(map[string]any) + if credentialOptions != nil { + switch credentialOptions.Options.(type) { + case *v2.CredentialOptions_NoPassword_: + case *v2.CredentialOptions_RandomPassword_: + password, err := generateCredentials(credentialOptions) + if err != nil { + return nil, nil, fmt.Errorf("failed to generate password: %w", err) + } + + // Add password to queryInputs and credentials map + // NOTE: For future credential types (SSO, API keys), consider using only the + // 'credentials' namespace to avoid conflicts with user-defined schema fields + queryInputs["password"] = password + credentials["password"] = password + + // Create plaintext data for return + passwordData := &v2.PlaintextData{ + Name: "password", + Bytes: []byte(password), + } + plaintextDataList = append(plaintextDataList, passwordData) + default: + return nil, nil, fmt.Errorf("unsupported credential options: %v", credentialOptions) + } + } + + // 3. Add namespaced access for advanced CEL expressions + // Only add namespaces if they don't conflict with user-defined schema fields + if len(schemaVars) > 0 { + if _, exists := queryInputs["input"]; !exists { + queryInputs["input"] = schemaVars + } + } + if len(credentials) > 0 { + if _, exists := queryInputs["credentials"]; !exists { + queryInputs["credentials"] = credentials + } + } + + return queryInputs, plaintextDataList, nil +} + +// validateAccountInfo validates that the required account information is provided. +func (s *SQLSyncer) validateAccountInfo(accountInfo *v2.AccountInfo) error { + if accountInfo == nil { + return errors.New("account info is required") + } + + if accountInfo.Profile == nil { + return errors.New("account profile is required") + } + + return nil +} + +// extractAndValidateProvisioning extracts and validates the account provisioning configuration. +func (s *SQLSyncer) extractAndValidateProvisioning() (string, *AccountProvisioning, error) { + resourceTypeID, accountProvisioning, err := s.fullConfig.ExtractAccountProvisioning() + if err != nil { + if errors.Is(err, ErrNoAccountProvisioningDefined) { + return "", nil, nil + } + return "", nil, err + } + + if accountProvisioning == nil { + return "", nil, errors.New("no account provisioning defined") } - return ret, true, nil + return resourceTypeID, accountProvisioning, nil } diff --git a/pkg/bsql/query.go b/pkg/bsql/query.go index cacbc996..2511b105 100644 --- a/pkg/bsql/query.go +++ b/pkg/bsql/query.go @@ -359,11 +359,6 @@ func (s *SQLSyncer) prepareQueryVars(ctx context.Context, inputs map[string]any, inputs = make(map[string]any) } - celInputs := make(map[string]any) - for k, v := range inputs { - celInputs[k] = v - } - for k, v := range vars { // Check if the value is a direct reference to an input field if inputVal, exists := inputs[v]; exists { @@ -372,7 +367,7 @@ func (s *SQLSyncer) prepareQueryVars(ctx context.Context, inputs map[string]any, } // Otherwise, evaluate it as a CEL expression - out, err := s.env.Evaluate(ctx, v, celInputs) + out, err := s.env.Evaluate(ctx, v, inputs) if err != nil { return nil, err } diff --git a/pkg/bsql/user_syncer.go b/pkg/bsql/user_syncer.go index b395ebca..72f96297 100644 --- a/pkg/bsql/user_syncer.go +++ b/pkg/bsql/user_syncer.go @@ -119,7 +119,7 @@ func (s *userSyncer) CreateAccount( } // Prepare all query inputs in one step - queryInputs, plaintextDataList, err := s.prepareAllQueryInputs(provisioningConfig, accountInfo, credentialOptions) + queryInputs, plaintextDataList, err := s.prepareQueryInputs(provisioningConfig, accountInfo, credentialOptions) if err != nil { return nil, nil, nil, err } @@ -131,9 +131,9 @@ func (s *userSyncer) CreateAccount( } // Validate the created account - accountResource, err := s.validateCreatedAccount(ctx, provisioningConfig, queryInputs) + accountResource, err := s.validateAccount(ctx, provisioningConfig, queryInputs) if err != nil { - return nil, nil, nil, err + return nil, nil, nil, fmt.Errorf("failed to validate created account: %w", err) } response := &v2.CreateAccountResponse_SuccessResult{ @@ -142,138 +142,3 @@ func (s *userSyncer) CreateAccount( return response, plaintextDataList, nil, nil } - -// extractAndValidateProvisioning extracts and validates the account provisioning configuration. -func (s *userSyncer) extractAndValidateProvisioning() (string, *AccountProvisioning, error) { - resourceTypeID, accountProvisioning, err := s.fullConfig.ExtractAccountProvisioning() - if err != nil { - if errors.Is(err, ErrNoAccountProvisioningDefined) { - return "", nil, nil - } - return "", nil, err - } - - if accountProvisioning == nil { - return "", nil, errors.New("no account provisioning defined") - } - - return resourceTypeID, accountProvisioning, nil -} - -// validateAccountInfo validates that the required account information is provided. -func (s *userSyncer) validateAccountInfo(accountInfo *v2.AccountInfo) error { - if accountInfo == nil { - return errors.New("account info is required") - } - - if accountInfo.Profile == nil { - return errors.New("account profile is required") - } - - return nil -} - -// prepareAllQueryInputs prepares all query inputs including schema vars and credentials in one step. -// This eliminates the need for complex merging logic by doing everything together. -func (s *userSyncer) prepareAllQueryInputs( - provisioningConfig *AccountProvisioning, - accountInfo *v2.AccountInfo, - credentialOptions *v2.CredentialOptions, -) (map[string]any, []*v2.PlaintextData, error) { - queryInputs := make(map[string]any) - var plaintextDataList []*v2.PlaintextData - - // 1. Add schema variables (profile data) directly - schemaVars := make(map[string]any) - for _, field := range provisioningConfig.Schema { - if value, exists := accountInfo.Profile.Fields[field.Name]; exists { - var parsedValue any - switch field.Type { - case "string": - if strValue := value.GetStringValue(); strValue != "" { - parsedValue = strValue - } - case "string_list": - if listValue := value.GetListValue(); listValue != nil { - var strList []string - for _, v := range listValue.Values { - if strValue := v.GetStringValue(); strValue != "" { - strList = append(strList, strValue) - } - } - parsedValue = strList - } - case "boolean": - parsedValue = value.GetBoolValue() - case "int": - if numValue := value.GetNumberValue(); numValue != 0 { - parsedValue = int(numValue) - } - case "map": - if structValue := value.GetStructValue(); structValue != nil { - parsedValue = structValue.AsMap() - } - } - - if parsedValue != nil { - queryInputs[field.Name] = parsedValue - schemaVars[field.Name] = parsedValue - } - } - } - - // 2. Add credentials if required - credentials := make(map[string]any) - if credentialOptions != nil { - switch credentialOptions.Options.(type) { - case *v2.CredentialOptions_NoPassword_: - case *v2.CredentialOptions_RandomPassword_: - password, err := generateCredentials(credentialOptions) - if err != nil { - return nil, nil, fmt.Errorf("failed to generate password: %w", err) - } - - // Add password to queryInputs and credentials map - queryInputs["password"] = password - credentials["password"] = password - - // Create plaintext data for return - passwordData := &v2.PlaintextData{ - Name: "password", - Bytes: []byte(password), - } - plaintextDataList = append(plaintextDataList, passwordData) - default: - return nil, nil, fmt.Errorf("unsupported credential options: %v", credentialOptions) - } - } - - // 3. Add namespaced access for advanced CEL expressions - // Only add namespaces if they don't conflict with user-defined schema fields - if len(schemaVars) > 0 { - if _, exists := queryInputs["input"]; !exists { - queryInputs["input"] = schemaVars - } - } - if len(credentials) > 0 { - if _, exists := queryInputs["credentials"]; !exists { - queryInputs["credentials"] = credentials - } - } - - return queryInputs, plaintextDataList, nil -} - -// validateCreatedAccount validates that the account was created successfully. -func (s *userSyncer) validateCreatedAccount(ctx context.Context, provisioningConfig *AccountProvisioning, queryInputs map[string]any) (*v2.Resource, error) { - accountResource, isValid, err := s.validateAccount(ctx, provisioningConfig, queryInputs) - if err != nil { - return nil, fmt.Errorf("failed to validate created account: %w", err) - } - - if !isValid { - return nil, errors.New("account validation failed after creation") - } - - return accountResource, nil -} diff --git a/pkg/bsql/user_syncer_test.go b/pkg/bsql/user_syncer_test.go index 873e6a62..af58768b 100644 --- a/pkg/bsql/user_syncer_test.go +++ b/pkg/bsql/user_syncer_test.go @@ -8,7 +8,7 @@ import ( "google.golang.org/protobuf/types/known/structpb" ) -func TestUserSyncer_prepareAllQueryInputs_KeyCollision(t *testing.T) { +func TestUserSyncer_prepareQueryInputs_KeyCollision(t *testing.T) { tests := []struct { name string schema []*AccountProvisioningField @@ -109,8 +109,8 @@ func TestUserSyncer_prepareAllQueryInputs_KeyCollision(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Create userSyncer instance - syncer := &userSyncer{} + // Create SQLSyncer instance + syncer := &SQLSyncer{} // Create provisioning config provisioningConfig := &AccountProvisioning{ @@ -125,7 +125,7 @@ func TestUserSyncer_prepareAllQueryInputs_KeyCollision(t *testing.T) { } // Call the method under test - queryInputs, _, err := syncer.prepareAllQueryInputs( + queryInputs, _, err := syncer.prepareQueryInputs( provisioningConfig, accountInfo, tt.credentialOptions,