From b8ec5f8fa9f6d24fb5aeef745000658ed663b074 Mon Sep 17 00:00:00 2001 From: agustin-conductor Date: Mon, 17 Nov 2025 15:31:21 -0300 Subject: [PATCH 1/6] add push attribute action support and example --- examples/mysql-test.yml | 138 +++++++++++++++++- examples/oracle-test.yml | 290 +++++++++++++++++++++---------------- examples/postgres-test.yml | 137 +++++++++++++++++- pkg/bcel/bcel.go | 1 + pkg/bsql/entitlements.go | 2 +- pkg/bsql/grants.go | 2 +- pkg/bsql/provisioning.go | 2 +- pkg/bsql/query.go | 54 ++++++- pkg/bsql/resources.go | 2 +- pkg/connector/action.go | 16 ++ test/mysql-init.sql | 29 ++-- test/oracle-init.sql | 43 ++++-- test/postgres-init.sql | 21 ++- 13 files changed, 573 insertions(+), 164 deletions(-) diff --git a/examples/mysql-test.yml b/examples/mysql-test.yml index b4ddd515..47f98707 100644 --- a/examples/mysql-test.yml +++ b/examples/mysql-test.yml @@ -11,6 +11,110 @@ connect: user: "${DB_USER}" password: "${DB_PASSWORD}" +actions: + update_user_attributes: + name: Update User Attributes + description: Update the attributes of a user. Only provide the attributes you want to update. + action_type: + - account + - account_update_profile + arguments: + user_id: + name: User ID + type: string + required: true + description: The ID of the user to update + attrs: + name: Attributes + type: string_map + required: true + description: The updated attribute data (map of attribute names to values) + attrs_update_mask: + name: Attributes Update Mask + type: string_list + required: true + description: The attributes to update (list of attribute names from attrs to actually update) + vars: + # Map each attribute in the attrs argument to a variable and a flag to indicate if the attribute should be updated + manager_id: "'manager_id' in input.attrs ? input.attrs['manager_id'] : null" + update_manager_id: "'manager_id' in input.attrs_update_mask" + first_name: "'first_name' in input.attrs ? input.attrs['first_name'] : null" + update_first_name: "'first_name' in input.attrs_update_mask" + middle_name: "'middle_name' in input.attrs ? input.attrs['middle_name'] : null" + update_middle_name: "'middle_name' in input.attrs_update_mask" + last_name: "'last_name' in input.attrs ? input.attrs['last_name'] : null" + update_last_name: "'last_name' in input.attrs_update_mask" + display_name: "'display_name' in input.attrs ? input.attrs['display_name'] : null" + update_display_name: "'display_name' in input.attrs_update_mask" + job_title: "'job_title' in input.attrs ? input.attrs['job_title'] : null" + update_job_title: "'job_title' in input.attrs_update_mask" + department: "'department' in input.attrs ? input.attrs['department'] : null" + update_department: "'department' in input.attrs_update_mask" + division: "'division' in input.attrs ? input.attrs['division'] : null" + update_division: "'division' in input.attrs_update_mask" + company: "'company' in input.attrs ? input.attrs['company'] : null" + update_company: "'company' in input.attrs_update_mask" + employee_id: "'employee_id' in input.attrs ? input.attrs['employee_id'] : null" + update_employee_id: "'employee_id' in input.attrs_update_mask" + employee_number: "'employee_number' in input.attrs ? input.attrs['employee_number'] : null" + update_employee_number: "'employee_number' in input.attrs_update_mask" + employment_type: "'employment_type' in input.attrs ? input.attrs['employment_type'] : null" + update_employment_type: "'employment_type' in input.attrs_update_mask" + # We define the whole update logic with conditional attributes using CASE statements + query: | + UPDATE users + SET + manager_id = CASE + WHEN ? THEN ? + ELSE manager_id + END, + attr_first_name = CASE + WHEN ? THEN ? + ELSE attr_first_name + END, + attr_middle_name = CASE + WHEN ? THEN ? + ELSE attr_middle_name + END, + attr_last_name = CASE + WHEN ? THEN ? + ELSE attr_last_name + END, + attr_display_name = CASE + WHEN ? THEN ? + ELSE attr_display_name + END, + attr_job_title = CASE + WHEN ? THEN ? + ELSE attr_job_title + END, + attr_department = CASE + WHEN ? THEN ? + ELSE attr_department + END, + attr_division = CASE + WHEN ? THEN ? + ELSE attr_division + END, + attr_company = CASE + WHEN ? THEN ? + ELSE attr_company + END, + employee_id = CASE + WHEN ? THEN ? + ELSE employee_id + END, + attr_employee_number = CASE + WHEN ? THEN ? + ELSE attr_employee_number + END, + attr_employment_type = CASE + WHEN ? THEN ? + ELSE attr_employment_type + END + WHERE + username = ? + # Definition of different resource types managed by this connector resource_types: # Configuration for "user" resources in MySQL @@ -37,7 +141,17 @@ resource_types: END as last_login, u.manager_id, m.username as manager_username, - m.email as manager_email + m.email as manager_email, + u.attr_first_name, + u.attr_middle_name, + u.attr_last_name, + u.attr_display_name, + u.attr_job_title, + u.attr_department, + u.attr_division, + u.attr_company, + u.attr_employee_number, + u.attr_employment_type FROM users u LEFT JOIN @@ -83,6 +197,16 @@ resource_types: manager_id: ".manager_id" manager_username: ".manager_username" manager_email: ".manager_email" + attr_first_name: ".attr_first_name" + attr_middle_name: ".attr_middle_name" + attr_last_name: ".attr_last_name" + attr_display_name: ".attr_display_name" + attr_job_title: ".attr_job_title" + attr_department: ".attr_department" + attr_division: ".attr_division" + attr_company: ".attr_company" + attr_employee_number: ".attr_employee_number" + attr_employment_type: ".attr_employment_type" # Account provisioning configuration with password support account_provisioning: @@ -128,7 +252,17 @@ resource_types: END as last_login, u.manager_id, m.username as manager_username, - m.email as manager_email + m.email as manager_email, + u.attr_first_name, + u.attr_middle_name, + u.attr_last_name, + u.attr_display_name, + u.attr_job_title, + u.attr_department, + u.attr_division, + u.attr_company, + u.attr_employee_number, + u.attr_employment_type FROM users u LEFT JOIN users m ON u.manager_id = m.id WHERE u.username = ? diff --git a/examples/oracle-test.yml b/examples/oracle-test.yml index 467d66d0..0a9860d9 100644 --- a/examples/oracle-test.yml +++ b/examples/oracle-test.yml @@ -10,6 +10,100 @@ connect: user: "${DB_USER}" password: "${DB_PASSWORD}" +actions: + enable_user: + name: Enable User + description: Enable a disabled user account + action_type: + - account_enable + arguments: + user_id: + name: User ID + type: string + required: true + description: The ID of the user to enable + query: | + UPDATE users SET status = 'active' WHERE username = ? + disable_user: + name: Disable User + description: Disable a user account + action_type: + - account_disable + arguments: + user_id: + name: User ID + type: string + required: true + description: The ID of the user to disable + query: | + UPDATE users SET status = 'disabled' WHERE username = ? + update_user_attributes: + name: Update User Attributes + description: Update the attributes of a user. Only provide the attributes you want to update. + action_type: + - account + - account_update_profile + arguments: + user_id: + name: User ID + type: string + required: true + description: The ID of the user to update + attrs: + name: Attributes + type: string_map + required: true + description: The updated attribute data (map of attribute names to values) + attrs_update_mask: + name: Attributes Update Mask + type: string_list + required: true + description: The attributes to update (list of attribute names from attrs to actually update) + vars: + # Map each attribute in the attrs argument to a variable and a flag to indicate if the attribute should be updated + manager_id: "'manager_id' in input.attrs ? input.attrs['manager_id'] : null" + update_manager_id: "'manager_id' in input.attrs_update_mask" + first_name: "'first_name' in input.attrs ? input.attrs['first_name'] : null" + update_first_name: "'first_name' in input.attrs_update_mask" + middle_name: "'middle_name' in input.attrs ? input.attrs['middle_name'] : null" + update_middle_name: "'middle_name' in input.attrs_update_mask" + last_name: "'last_name' in input.attrs ? input.attrs['last_name'] : null" + update_last_name: "'last_name' in input.attrs_update_mask" + display_name: "'display_name' in input.attrs ? input.attrs['display_name'] : null" + update_display_name: "'display_name' in input.attrs_update_mask" + job_title: "'job_title' in input.attrs ? input.attrs['job_title'] : null" + update_job_title: "'job_title' in input.attrs_update_mask" + department: "'department' in input.attrs ? input.attrs['department'] : null" + update_department: "'department' in input.attrs_update_mask" + division: "'division' in input.attrs ? input.attrs['division'] : null" + update_division: "'division' in input.attrs_update_mask" + company: "'company' in input.attrs ? input.attrs['company'] : null" + update_company: "'company' in input.attrs_update_mask" + employee_id: "'employee_id' in input.attrs ? input.attrs['employee_id'] : null" + update_employee_id: "'employee_id' in input.attrs_update_mask" + employee_number: "'employee_number' in input.attrs ? input.attrs['employee_number'] : null" + update_employee_number: "'employee_number' in input.attrs_update_mask" + employment_type: "'employment_type' in input.attrs ? input.attrs['employment_type'] : null" + update_employment_type: "'employment_type' in input.attrs_update_mask" + # We define the whole update logic with conditional attributes + query: | + UPDATE users + SET + manager_id = DECODE(?, '1', ?, manager_id), + attr_first_name = DECODE(?, '1', ?, attr_first_name), + attr_middle_name = DECODE(?, '1', ?, attr_middle_name), + attr_last_name = DECODE(?, '1', ?, attr_last_name), + attr_display_name = DECODE(?, '1', ?, attr_display_name), + attr_job_title = DECODE(?, '1', ?, attr_job_title), + attr_department = DECODE(?, '1', ?, attr_department), + attr_division = DECODE(?, '1', ?, attr_division), + attr_company = DECODE(?, '1', ?, attr_company), + employee_id = DECODE(?, '1', ?, employee_id), + attr_employee_number = DECODE(?, '1', ?, attr_employee_number), + attr_employment_type = DECODE(?, '1', ?, attr_employment_type) + WHERE + username = ? + # Definition of different resource types managed by this connector resource_types: # Configuration for "user" resources in Oracle @@ -31,7 +125,17 @@ resource_types: account_type as "account_type", created_at as "created_at", last_login as "last_login", - manager_id as "manager_id" + manager_id as "manager_id", + attr_first_name as "attr_first_name", + attr_middle_name as "attr_middle_name", + attr_last_name as "attr_last_name", + attr_display_name as "attr_display_name", + attr_job_title as "attr_job_title", + attr_department as "attr_department", + attr_division as "attr_division", + attr_company as "attr_company", + attr_employee_number as "attr_employee_number", + attr_employment_type as "attr_employment_type" FROM users ORDER BY id @@ -65,6 +169,25 @@ resource_types: - ".employee_id" # Last login timestamp mapping last_login: ".last_login" + # Manager information + manager_id: ".manager_id" + + # Profile details for the user + profile: + user_id: ".id" + created_at: ".created_at" + last_login: ".last_login" + manager_id: ".manager_id" + attr_first_name: ".attr_first_name" + attr_middle_name: ".attr_middle_name" + attr_last_name: ".attr_last_name" + attr_display_name: ".attr_display_name" + attr_job_title: ".attr_job_title" + attr_department: ".attr_department" + attr_division: ".attr_division" + attr_company: ".attr_company" + attr_employee_number: ".attr_employee_number" + attr_employment_type: ".attr_employment_type" account_provisioning: # Schema definition for account creation form schema: @@ -131,18 +254,19 @@ resource_types: description: "A role within the Oracle system" # Settings for listing roles from the database list: - # SQL query to retrieve roles + # SQL query to retrieve roles from custom roles table query: | SELECT - ROLE + id as "roleId", + role_name as "roleName" FROM - DBA_ROLES + ROLES # Mapping of each query result to role resource fields map: - # Unique role identifier (from the ROLE column) - id: ".ROLE" + # Unique role identifier (using role name to match provisioning queries) + id: ".roleName" # Display name for the role (also from the ROLE column) - display_name: ".ROLE" + display_name: ".roleName" # Role description; left empty for further customization description: "" # Additional role-specific traits @@ -150,7 +274,7 @@ resource_types: role: # Profile mapping for the role; assigns the role name profile: - role_name: ".ROLE" + role_name: ".roleName" # Static entitlements that can be assigned to a role resource static_entitlements: - id: "assigned" # Entitlement identifier for a standard role assignment @@ -170,17 +294,23 @@ resource_types: 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 ? + INSERT INTO user_roles (user_id, role_id) + SELECT u.id, r.id + FROM users u, roles r + WHERE u.username = ? + AND r.role_name = ? + AND NOT EXISTS ( + SELECT 1 FROM user_roles ur + WHERE ur.user_id = u.id AND ur.role_id = r.id + ) revoke: - # Indicates that the revoke operation runs without a transaction - no_transaction: true queries: - | - REVOKE ? FROM ? + DELETE FROM user_roles + WHERE user_id = (SELECT id FROM users WHERE username = ?) + AND role_id = (SELECT id FROM roles WHERE role_name = ?) - id: "admin" # Entitlement identifier for role administration privileges # Dynamic display name for admin entitlement, appending ' Role Admin' display_name: "resource.DisplayName + ' Role Admin'" @@ -197,131 +327,35 @@ resource_types: 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 + INSERT INTO user_roles (user_id, role_id) + SELECT u.id, r.id + FROM users u, roles r + WHERE u.username = ? + AND r.role_name = ? + AND NOT EXISTS ( + SELECT 1 FROM user_roles ur + WHERE ur.user_id = u.id AND ur.role_id = r.id + ) revoke: - no_transaction: true queries: - | - REVOKE ? FROM ? - - | - GRANT ? TO ? + DELETE FROM user_roles + WHERE user_id = (SELECT id FROM users WHERE username = ?) + AND role_id = (SELECT id FROM roles WHERE role_name = ?) # Dynamic grants based on SQL queries to associate users with roles grants: - query: | SELECT - GRANTEE AS USERNAME, - GRANTED_ROLE AS ROLE, - ADMIN_OPTION + u.username AS USERNAME, + r.role_name AS ROLE FROM - DBA_ROLE_PRIVS - WHERE - GRANTEE IN (SELECT USERNAME FROM DBA_USERS) + user_roles ur + INNER JOIN users u ON ur.user_id = u.id + INNER JOIN roles r ON ur.role_id = r.id 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 + entitlement_id: "assigned" # Use the 'assigned' entitlement for standard role membership \ No newline at end of file diff --git a/examples/postgres-test.yml b/examples/postgres-test.yml index d9cd236f..68c44182 100644 --- a/examples/postgres-test.yml +++ b/examples/postgres-test.yml @@ -2,7 +2,6 @@ # Application name for this connector configuration app_name: PostgreSQL Test app_description: Test configuration for PostgreSQL with employee_id and last_login support - # Connection settings for the PostgreSQL database connect: # Data Source Name (DSN) for our test PostgreSQL instance @@ -40,6 +39,108 @@ actions: # username: "input.user_id" query: | UPDATE users SET status = 'disabled' WHERE username = ? + update_user_attributes: + name: Update User Attributes + description: Update the attributes of a user. Only provide the attributes you want to update. + action_type: + - account + - account_update_profile + arguments: + user_id: + name: User ID + type: string + required: true + description: The ID of the user to update + attrs: + name: Attributes + type: string_map + required: true + description: The updated attribute data (map of attribute names to values) + attrs_update_mask: + name: Attributes Update Mask + type: string_list + required: true + description: The attributes to update (list of attribute names from attrs to actually update) + vars: + # Map each attribute in the attrs argument to a variable and a flag to indicate if the attribute should be updated + manager_id: "'manager_id' in input.attrs ? input.attrs['manager_id'] : null" + update_manager_id: "'manager_id' in input.attrs_update_mask" + first_name: "'first_name' in input.attrs ? input.attrs['first_name'] : null" + update_first_name: "'first_name' in input.attrs_update_mask" + middle_name: "'middle_name' in input.attrs ? input.attrs['middle_name'] : null" + update_middle_name: "'middle_name' in input.attrs_update_mask" + last_name: "'last_name' in input.attrs ? input.attrs['last_name'] : null" + update_last_name: "'last_name' in input.attrs_update_mask" + display_name: "'display_name' in input.attrs ? input.attrs['display_name'] : null" + update_display_name: "'display_name' in input.attrs_update_mask" + job_title: "'job_title' in input.attrs ? input.attrs['job_title'] : null" + update_job_title: "'job_title' in input.attrs_update_mask" + department: "'department' in input.attrs ? input.attrs['department'] : null" + update_department: "'department' in input.attrs_update_mask" + division: "'division' in input.attrs ? input.attrs['division'] : null" + update_division: "'division' in input.attrs_update_mask" + company: "'company' in input.attrs ? input.attrs['company'] : null" + update_company: "'company' in input.attrs_update_mask" + employee_id: "'employee_id' in input.attrs ? input.attrs['employee_id'] : null" + update_employee_id: "'employee_id' in input.attrs_update_mask" + employee_number: "'employee_number' in input.attrs ? input.attrs['employee_number'] : null" + update_employee_number: "'employee_number' in input.attrs_update_mask" + employment_type: "'employment_type' in input.attrs ? input.attrs['employment_type'] : null" + update_employment_type: "'employment_type' in input.attrs_update_mask" + # We define the whole update logic with conditional attributes using CASE statements + query: | + UPDATE users + SET + manager_id = CASE + WHEN ? THEN ? + ELSE manager_id + END, + attr_first_name = CASE + WHEN ? THEN ? + ELSE attr_first_name + END, + attr_middle_name = CASE + WHEN ? THEN ? + ELSE attr_middle_name + END, + attr_last_name = CASE + WHEN ? THEN ? + ELSE attr_last_name + END, + attr_display_name = CASE + WHEN ? THEN ? + ELSE attr_display_name + END, + attr_job_title = CASE + WHEN ? THEN ? + ELSE attr_job_title + END, + attr_department = CASE + WHEN ? THEN ? + ELSE attr_department + END, + attr_division = CASE + WHEN ? THEN ? + ELSE attr_division + END, + attr_company = CASE + WHEN ? THEN ? + ELSE attr_company + END, + employee_id = CASE + WHEN ? THEN ? + ELSE employee_id + END, + attr_employee_number = CASE + WHEN ? THEN ? + ELSE attr_employee_number + END, + attr_employment_type = CASE + WHEN ? THEN ? + ELSE attr_employment_type + END + WHERE + username = ? # Definition of different resource types managed by this connector resource_types: @@ -64,7 +165,17 @@ resource_types: u.last_login, u.manager_id, m.username as manager_username, - m.email as manager_email + m.email as manager_email, + u.attr_first_name, + u.attr_middle_name, + u.attr_last_name, + u.attr_display_name, + u.attr_job_title, + u.attr_department, + u.attr_division, + u.attr_company, + u.attr_employee_number, + u.attr_employment_type FROM users u LEFT JOIN @@ -110,6 +221,16 @@ resource_types: manager_id: ".manager_id" manager_username: ".manager_username" manager_email: ".manager_email" + attr_first_name: ".attr_first_name" + attr_middle_name: ".attr_middle_name" + attr_last_name: ".attr_last_name" + attr_display_name: ".attr_display_name" + attr_job_title: ".attr_job_title" + attr_department: ".attr_department" + attr_division: ".attr_division" + attr_company: ".attr_company" + attr_employee_number: ".attr_employee_number" + attr_employment_type: ".attr_employment_type" # Account provisioning configuration with password support account_provisioning: @@ -154,7 +275,17 @@ resource_types: u.last_login, u.manager_id, m.username as manager_username, - m.email as manager_email + m.email as manager_email, + u.attr_first_name, + u.attr_middle_name, + u.attr_last_name, + u.attr_display_name, + u.attr_job_title, + u.attr_department, + u.attr_division, + u.attr_company, + u.attr_employee_number, + u.attr_employment_type FROM users u LEFT JOIN users m ON u.manager_id = m.id WHERE u.username = ? diff --git a/pkg/bcel/bcel.go b/pkg/bcel/bcel.go index a940e4e5..794d3090 100644 --- a/pkg/bcel/bcel.go +++ b/pkg/bcel/bcel.go @@ -27,6 +27,7 @@ func NewEnv(ctx context.Context) (*Env, error) { cel.Variable("resource", cel.MapType(types.StringType, types.StringType)), cel.Variable("principal", cel.MapType(types.StringType, types.StringType)), cel.Variable("entitlement", cel.MapType(types.StringType, types.StringType)), + cel.Variable("input", cel.MapType(cel.StringType, cel.AnyType)), // For action vars and account provisioning ) // CEL functions diff --git a/pkg/bsql/entitlements.go b/pkg/bsql/entitlements.go index 6c1b7e89..e366a2c1 100644 --- a/pkg/bsql/entitlements.go +++ b/pkg/bsql/entitlements.go @@ -81,7 +81,7 @@ func (s *SQLSyncer) dynamicEntitlements(ctx context.Context, resource *v2.Resour inputs := s.env.SyncInputsWithResource(nil, resource) - queryVars, err := s.prepareQueryVars(ctx, inputs, s.config.Entitlements.Vars) + queryVars, err := s.PrepareQueryVars(ctx, inputs, s.config.Entitlements.Vars) if err != nil { return nil, "", nil, err } diff --git a/pkg/bsql/grants.go b/pkg/bsql/grants.go index 51fa2a12..36fe9b38 100644 --- a/pkg/bsql/grants.go +++ b/pkg/bsql/grants.go @@ -77,7 +77,7 @@ func (s *SQLSyncer) listGrants(ctx context.Context, resource *v2.Resource, pToke inputs := s.env.SyncInputsWithResource(nil, resource) - queryVars, err := s.prepareQueryVars(ctx, inputs, grantConfig.Vars) + queryVars, err := s.PrepareQueryVars(ctx, inputs, grantConfig.Vars) if err != nil { return nil, "", err } diff --git a/pkg/bsql/provisioning.go b/pkg/bsql/provisioning.go index ccab9ed8..c9ff40f3 100644 --- a/pkg/bsql/provisioning.go +++ b/pkg/bsql/provisioning.go @@ -160,7 +160,7 @@ func (s *SQLSyncer) validateAccount(ctx context.Context, accountProvisioning *Ac return nil, fmt.Errorf("validation query is not defined for account provisioning") } - queryVars, err := s.prepareQueryVars(ctx, inputs, accountProvisioning.Validate.Vars) + queryVars, err := s.PrepareQueryVars(ctx, inputs, accountProvisioning.Validate.Vars) if err != nil { return nil, err } diff --git a/pkg/bsql/query.go b/pkg/bsql/query.go index 4a6358b4..a668e3e9 100644 --- a/pkg/bsql/query.go +++ b/pkg/bsql/query.go @@ -5,10 +5,12 @@ import ( "database/sql" "errors" "fmt" + "reflect" "regexp" "strconv" "strings" + "github.com/google/cel-go/common/types" "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" "go.uber.org/zap" @@ -352,7 +354,7 @@ func (s *SQLSyncer) RunProvisioningQueries(ctx context.Context, queries []string return nil } -func (s *SQLSyncer) prepareQueryVars(ctx context.Context, inputs map[string]any, vars map[string]string) (map[string]any, error) { +func (s *SQLSyncer) PrepareQueryVars(ctx context.Context, inputs map[string]any, vars map[string]string) (map[string]any, error) { ret := make(map[string]any) if inputs == nil { @@ -362,7 +364,8 @@ func (s *SQLSyncer) prepareQueryVars(ctx context.Context, inputs map[string]any, for k, v := range vars { // Check if the value is a direct reference to an input field if inputVal, exists := inputs[v]; exists { - ret[k] = inputVal + normalizedValue := s.normalizeValue(inputVal) + ret[k] = normalizedValue continue } @@ -371,12 +374,57 @@ func (s *SQLSyncer) prepareQueryVars(ctx context.Context, inputs map[string]any, if err != nil { return nil, err } - ret[k] = out + normalizedValue := s.normalizeValue(out) + ret[k] = normalizedValue } return ret, nil } +// normalizeValue converts CEL null types and other special values to Go nil for SQL compatibility +// Also converts booleans to strings ('1'/'0') for Oracle compatibility when used in DECODE statements. +func (s *SQLSyncer) normalizeValue(val interface{}) interface{} { + if val == nil { + return nil + } + + // Check for CEL null types + switch v := val.(type) { + case string: + // CEL might return "NULL_VALUE" as a string representation + if v == "NULL_VALUE" { + return nil + } + return v + case types.Null: + // CEL Null type + return nil + case bool: + // Convert boolean to string for Oracle compatibility (Oracle DECODE expects CHAR) + // Only convert for Oracle to avoid breaking other databases + if s.dbEngine == database.Oracle { + if v { + return "1" + } + return "0" + } + // For other databases, return as-is (let the driver handle it) + return v + } + + // Use reflection to check for CEL null value types + valType := reflect.TypeOf(val) + if valType != nil { + typeName := valType.String() + // Check for CEL null value types + if strings.Contains(typeName, "NullValue") || strings.Contains(typeName, "types.Null") { + return nil + } + } + + return val +} + func (s *SQLSyncer) runQuery( ctx context.Context, pToken *pagination.Token, diff --git a/pkg/bsql/resources.go b/pkg/bsql/resources.go index 33e81bd8..4713c10f 100644 --- a/pkg/bsql/resources.go +++ b/pkg/bsql/resources.go @@ -21,7 +21,7 @@ func (s *SQLSyncer) List(ctx context.Context, parentResourceID *v2.ResourceId, p return nil, "", nil, errors.New("no resource list configuration provided") } - queryVars, err := s.prepareQueryVars(ctx, nil, s.config.List.Vars) + queryVars, err := s.PrepareQueryVars(ctx, nil, s.config.List.Vars) if err != nil { return nil, "", nil, err } diff --git a/pkg/connector/action.go b/pkg/connector/action.go index 4b37da24..b309b6dd 100644 --- a/pkg/connector/action.go +++ b/pkg/connector/action.go @@ -188,6 +188,22 @@ func (c *Connector) handleQueryAction(ctx context.Context, actionKey string, act } } + // Wrap argMap in "input" container for CEL expressions + celInputs := map[string]any{ + "input": argMap, + } + + // Evaluate CEL expressions in vars to prepare query variables + queryVars, err := sqlSyncer.PrepareQueryVars(ctx, celInputs, actionCfg.Vars) + if err != nil { + return nil, nil, fmt.Errorf("failed to prepare query vars: %w", err) + } + + // Merge evaluated vars into argMap (queryVars take precedence) + for k, v := range queryVars { + argMap[k] = v + } + queries := []string{actionCfg.Query} err = sqlSyncer.RunProvisioningQueries(ctx, queries, argMap, !actionCfg.NoTransaction) if err != nil { diff --git a/test/mysql-init.sql b/test/mysql-init.sql index 71b8fe75..8fcd4e53 100644 --- a/test/mysql-init.sql +++ b/test/mysql-init.sql @@ -16,18 +16,29 @@ CREATE TABLE users ( created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_login TIMESTAMP NULL, manager_id INT, - password_hash VARCHAR(255) + password_hash VARCHAR(255), + -- Additional attributes with attr_ prefix for testing attribute mapping + attr_first_name VARCHAR(100), + attr_middle_name VARCHAR(100), + attr_last_name VARCHAR(100), + attr_display_name VARCHAR(255), + attr_job_title VARCHAR(100), + attr_department VARCHAR(100), + attr_division VARCHAR(100), + attr_company VARCHAR(100), + attr_employee_number VARCHAR(50), + attr_employment_type VARCHAR(50) ); -- Insert sample users with different date formats for last_login (without manager relationships) -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'), -('bjorn.tipling.c1', 'bjorn.tipling@conductorone.com', 'EMP005', 'active', 'human', '2025-03-01 09:00:00', '2025-04-18 10:15:00'), -('bjorn.tipling.ins', 'bjorn.tipling@insulator.one', 'EMP006', 'active', 'human', '2025-03-05 11:30:00', '2025-04-18 14:30:00'); +INSERT INTO users (username, email, employee_id, status, account_type, created_at, last_login, attr_first_name, attr_middle_name, attr_last_name, attr_display_name, attr_job_title, attr_department, attr_division, attr_company, attr_employee_number, attr_employment_type) VALUES +('admin', 'admin@example.com', 'EMP001', 'active', 'human', '2025-01-01 12:00:00', '2025-04-15 09:30:00', 'Admin', NULL, 'User', 'Admin User', 'System Administrator', 'IT', 'Technology', 'Example Corp', '10001', 'full_time'), +('jane.doe', 'jane.doe@example.com', 'EMP002', 'active', 'human', '2025-01-05 14:30:00', '2025-04-17 08:45:00', 'Jane', 'Marie', 'Doe', 'Jane Doe', 'Software Engineer', 'Engineering', 'Technology', 'Example Corp', '10002', 'full_time'), +('john.smith', 'john.smith@example.com', 'EMP003', 'active', 'human', '2025-01-10 09:45:00', '2025-04-16 16:20:00', 'John', 'Michael', 'Smith', 'John Smith', 'Product Manager', 'Product', 'Business', 'Example Corp', '10003', 'full_time'), +('service.acct', 'service@example.com', 'SVC001', 'active', 'service', '2025-02-01 08:00:00', NULL, NULL, NULL, NULL, 'Service Account', NULL, 'IT', 'Technology', 'Example Corp', '20001', 'service'), +('disabled.user', 'disabled@example.com', 'EMP004', 'disabled', 'human', '2025-02-15 10:15:00', '2025-03-01 11:10:00', 'Disabled', NULL, 'User', 'Disabled User', 'Former Employee', 'HR', 'Operations', 'Example Corp', '10004', 'contractor'), +('bjorn.tipling.c1', 'bjorn.tipling@conductorone.com', 'EMP005', 'active', 'human', '2025-03-01 09:00:00', '2025-04-18 10:15:00', 'Bjorn', NULL, 'Tipling', 'Bjorn Tipling', 'CTO', 'Executive', 'Leadership', 'ConductorOne', '10005', 'full_time'), +('bjorn.tipling.ins', 'bjorn.tipling@insulator.one', 'EMP006', 'active', 'human', '2025-03-05 11:30:00', '2025-04-18 14:30:00', 'Bjorn', NULL, 'Tipling', 'Bjorn Tipling', 'Founder', 'Executive', 'Leadership', 'Insulator', '10006', 'full_time'); -- Update users to establish manager relationships -- jane.doe and john.smith report to admin diff --git a/test/oracle-init.sql b/test/oracle-init.sql index 75443910..440243bf 100644 --- a/test/oracle-init.sql +++ b/test/oracle-init.sql @@ -1,5 +1,6 @@ -- Connect as SYSDBA to create test user and schema -CONNECT sys/OraclePassword123@XE AS SYSDBA; +-- Note: Oracle Express uses XEPDB1 as the pluggable database +CONNECT sys/OraclePassword123@XEPDB1 AS SYSDBA; -- Create a test user for baton CREATE USER baton IDENTIFIED BY password @@ -14,9 +15,12 @@ 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 +-- Grant access to DBA dictionary views (required for DBA_ROLES, DBA_USERS, etc.) +GRANT SELECT ANY DICTIONARY TO baton; +GRANT SELECT_CATALOG_ROLE TO baton; -- Connect as the test user -CONNECT baton/password@XE; +CONNECT baton/password@XEPDB1; -- Drop existing tables if they exist BEGIN @@ -65,24 +69,35 @@ CREATE TABLE users ( created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_login TIMESTAMP, manager_id NUMBER, - password_hash VARCHAR2(255) + password_hash VARCHAR2(255), + -- Additional attributes with attr_ prefix for testing attribute mapping + attr_first_name VARCHAR2(100), + attr_middle_name VARCHAR2(100), + attr_last_name VARCHAR2(100), + attr_display_name VARCHAR2(255), + attr_job_title VARCHAR2(100), + attr_department VARCHAR2(100), + attr_division VARCHAR2(100), + attr_company VARCHAR2(100), + attr_employee_number VARCHAR2(50), + attr_employment_type VARCHAR2(50) ); --- 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 sample users with attr_* data +INSERT INTO users (username, email, employee_id, status, account_type, created_at, last_login, attr_first_name, attr_middle_name, attr_last_name, attr_display_name, attr_job_title, attr_department, attr_division, attr_company, attr_employee_number, attr_employment_type) VALUES +('admin', 'admin@example.com', 'EMP001', 'active', 'human', TIMESTAMP '2025-01-01 12:00:00', TIMESTAMP '2025-04-15 09:30:00', 'Admin', NULL, 'User', 'Admin User', 'System Administrator', 'IT', 'Technology', 'Example Corp', '10001', 'full_time'); -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, attr_first_name, attr_middle_name, attr_last_name, attr_display_name, attr_job_title, attr_department, attr_division, attr_company, attr_employee_number, attr_employment_type) VALUES +('jane.doe', 'jane.doe@example.com', 'EMP002', 'active', 'human', TIMESTAMP '2025-01-05 14:30:00', TIMESTAMP '2025-04-17 08:45:00', 'Jane', 'Marie', 'Doe', 'Jane Doe', 'Software Engineer', 'Engineering', 'Technology', 'Example Corp', '10002', 'full_time'); -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, attr_first_name, attr_middle_name, attr_last_name, attr_display_name, attr_job_title, attr_department, attr_division, attr_company, attr_employee_number, attr_employment_type) VALUES +('john.smith', 'john.smith@example.com', 'EMP003', 'active', 'human', TIMESTAMP '2025-01-10 09:45:00', TIMESTAMP '2025-04-16 16:20:00', 'John', 'Michael', 'Smith', 'John Smith', 'Product Manager', 'Product', 'Business', 'Example Corp', '10003', 'full_time'); -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, attr_first_name, attr_middle_name, attr_last_name, attr_display_name, attr_job_title, attr_department, attr_division, attr_company, attr_employee_number, attr_employment_type) VALUES +('service.acct', 'service@example.com', 'SVC001', 'active', 'service', TIMESTAMP '2025-02-01 08:00:00', NULL, NULL, NULL, NULL, 'Service Account', NULL, 'IT', 'Technology', 'Example Corp', '20001', 'service'); -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'); +INSERT INTO users (username, email, employee_id, status, account_type, created_at, last_login, attr_first_name, attr_middle_name, attr_last_name, attr_display_name, attr_job_title, attr_department, attr_division, attr_company, attr_employee_number, attr_employment_type) VALUES +('disabled.user', 'disabled@example.com', 'EMP004', 'disabled', 'human', TIMESTAMP '2025-02-15 10:15:00', TIMESTAMP '2025-03-01 11:10:00', 'Disabled', NULL, 'User', 'Disabled User', 'Former Employee', 'HR', 'Operations', 'Example Corp', '10004', 'contractor'); -- Create roles table CREATE TABLE roles ( diff --git a/test/postgres-init.sql b/test/postgres-init.sql index 9e5d5f35..95868d56 100644 --- a/test/postgres-init.sql +++ b/test/postgres-init.sql @@ -20,7 +20,18 @@ CREATE TABLE users ( created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_login TIMESTAMP NULL, manager_id INTEGER, - password_hash VARCHAR(255) + password_hash VARCHAR(255), + -- Additional attributes with attr_ prefix for testing attribute mapping + attr_first_name VARCHAR(100), + attr_middle_name VARCHAR(100), + attr_last_name VARCHAR(100), + attr_display_name VARCHAR(255), + attr_job_title VARCHAR(100), + attr_department VARCHAR(100), + attr_division VARCHAR(100), + attr_company VARCHAR(100), + attr_employee_number VARCHAR(50), + attr_employment_type VARCHAR(50) ); -- Insert sample users with different date formats for last_login (without manager relationships) @@ -34,6 +45,14 @@ INSERT INTO users (username, email, employee_id, status, account_type, created_a ('bjorn.tipling.ins', 'bjorn.tipling@insulator.one', 'EMP006', 'active', 'human', '2025-03-05 11:30:00', '2025-04-18 14:30:00'), ('robert"); drop table users; --', 'robert.tables@example.com', 'EMP007', 'active', 'human', '2025-11-11 15:20:00', '2025-11-20 09:15:00'), ('robert''); drop table users; --', 'robert.tables2@example.com', 'EMP008', 'active', 'human', '2025-11-15 07:45:00', '2025-11-18 10:10:00'); +INSERT INTO users (username, email, employee_id, status, account_type, created_at, last_login, attr_first_name, attr_middle_name, attr_last_name, attr_display_name, attr_job_title, attr_department, attr_division, attr_company, attr_employee_number, attr_employment_type) VALUES +('admin', 'admin@example.com', 'EMP001', 'active', 'human', '2025-01-01 12:00:00', '2025-04-15 09:30:00', 'Admin', NULL, 'User', 'Admin User', 'System Administrator', 'IT', 'Technology', 'Example Corp', '10001', 'full_time'), +('jane.doe', 'jane.doe@example.com', 'EMP002', 'active', 'human', '2025-01-05 14:30:00', '2025-04-17 08:45:00', 'Jane', 'Marie', 'Doe', 'Jane Doe', 'Software Engineer', 'Engineering', 'Technology', 'Example Corp', '10002', 'full_time'), +('john.smith', 'john.smith@example.com', 'EMP003', 'active', 'human', '2025-01-10 09:45:00', '2025-04-16 16:20:00', 'John', 'Michael', 'Smith', 'John Smith', 'Product Manager', 'Product', 'Business', 'Example Corp', '10003', 'full_time'), +('service.acct', 'service@example.com', 'SVC001', 'active', 'service', '2025-02-01 08:00:00', NULL, NULL, NULL, NULL, 'Service Account', NULL, 'IT', 'Technology', 'Example Corp', '20001', 'service'), +('disabled.user', 'disabled@example.com', 'EMP004', 'disabled', 'human', '2025-02-15 10:15:00', '2025-03-01 11:10:00', 'Disabled', NULL, 'User', 'Disabled User', 'Former Employee', 'HR', 'Operations', 'Example Corp', '10004', 'contractor'), +('bjorn.tipling.c1', 'bjorn.tipling@conductorone.com', 'EMP005', 'active', 'human', '2025-03-01 09:00:00', '2025-04-18 10:15:00', 'Bjorn', NULL, 'Tipling', 'Bjorn Tipling', 'CTO', 'Executive', 'Leadership', 'ConductorOne', '10005', 'full_time'), +('bjorn.tipling.ins', 'bjorn.tipling@insulator.one', 'EMP006', 'active', 'human', '2025-03-05 11:30:00', '2025-04-18 14:30:00', 'Bjorn', NULL, 'Tipling', 'Bjorn Tipling', 'Founder', 'Executive', 'Leadership', 'Insulator', '10006', 'full_time'); -- Update users to establish manager relationships -- jane.doe and john.smith report to admin From 880d62fdc1055b6da52f78c9897dba99cdc79d57 Mon Sep 17 00:00:00 2001 From: agustin-conductor Date: Fri, 28 Nov 2025 12:43:45 -0300 Subject: [PATCH 2/6] add missing attr after rebase --- test/postgres-init.sql | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/test/postgres-init.sql b/test/postgres-init.sql index 95868d56..029bff38 100644 --- a/test/postgres-init.sql +++ b/test/postgres-init.sql @@ -35,16 +35,6 @@ CREATE TABLE users ( ); -- Insert sample users with different date formats for last_login (without manager relationships) -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'), -('bjorn.tipling.c1', 'bjorn.tipling@conductorone.com', 'EMP005', 'active', 'human', '2025-03-01 09:00:00', '2025-04-18 10:15:00'), -('bjorn.tipling.ins', 'bjorn.tipling@insulator.one', 'EMP006', 'active', 'human', '2025-03-05 11:30:00', '2025-04-18 14:30:00'), -('robert"); drop table users; --', 'robert.tables@example.com', 'EMP007', 'active', 'human', '2025-11-11 15:20:00', '2025-11-20 09:15:00'), -('robert''); drop table users; --', 'robert.tables2@example.com', 'EMP008', 'active', 'human', '2025-11-15 07:45:00', '2025-11-18 10:10:00'); INSERT INTO users (username, email, employee_id, status, account_type, created_at, last_login, attr_first_name, attr_middle_name, attr_last_name, attr_display_name, attr_job_title, attr_department, attr_division, attr_company, attr_employee_number, attr_employment_type) VALUES ('admin', 'admin@example.com', 'EMP001', 'active', 'human', '2025-01-01 12:00:00', '2025-04-15 09:30:00', 'Admin', NULL, 'User', 'Admin User', 'System Administrator', 'IT', 'Technology', 'Example Corp', '10001', 'full_time'), ('jane.doe', 'jane.doe@example.com', 'EMP002', 'active', 'human', '2025-01-05 14:30:00', '2025-04-17 08:45:00', 'Jane', 'Marie', 'Doe', 'Jane Doe', 'Software Engineer', 'Engineering', 'Technology', 'Example Corp', '10002', 'full_time'), @@ -52,7 +42,10 @@ INSERT INTO users (username, email, employee_id, status, account_type, created_a ('service.acct', 'service@example.com', 'SVC001', 'active', 'service', '2025-02-01 08:00:00', NULL, NULL, NULL, NULL, 'Service Account', NULL, 'IT', 'Technology', 'Example Corp', '20001', 'service'), ('disabled.user', 'disabled@example.com', 'EMP004', 'disabled', 'human', '2025-02-15 10:15:00', '2025-03-01 11:10:00', 'Disabled', NULL, 'User', 'Disabled User', 'Former Employee', 'HR', 'Operations', 'Example Corp', '10004', 'contractor'), ('bjorn.tipling.c1', 'bjorn.tipling@conductorone.com', 'EMP005', 'active', 'human', '2025-03-01 09:00:00', '2025-04-18 10:15:00', 'Bjorn', NULL, 'Tipling', 'Bjorn Tipling', 'CTO', 'Executive', 'Leadership', 'ConductorOne', '10005', 'full_time'), -('bjorn.tipling.ins', 'bjorn.tipling@insulator.one', 'EMP006', 'active', 'human', '2025-03-05 11:30:00', '2025-04-18 14:30:00', 'Bjorn', NULL, 'Tipling', 'Bjorn Tipling', 'Founder', 'Executive', 'Leadership', 'Insulator', '10006', 'full_time'); +('bjorn.tipling.ins', 'bjorn.tipling@insulator.one', 'EMP006', 'active', 'human', '2025-03-05 11:30:00', '2025-04-18 14:30:00', 'Bjorn', NULL, 'Tipling', 'Bjorn Tipling', 'Founder', 'Executive', 'Leadership', 'Insulator', '10006', 'full_time'), +('robert"); drop table users; --', 'robert.tables@example.com', 'EMP007', 'active', 'human', '2025-11-11 15:20:00', '2025-11-20 09:15:00', 'Robert', NULL, 'Tables', 'Robert Tables', 'Security Tester', 'Security', 'Technology', 'Example Corp', '10007', 'full_time'), +('robert''); drop table users; --', 'robert.tables2@example.com', 'EMP008', 'active', 'human', '2025-11-15 07:45:00', '2025-11-18 10:10:00', 'Robert', NULL, 'Tables', 'Robert Tables', 'Security Tester', 'Security', 'Technology', 'Example Corp', '10008', 'full_time'); + -- Update users to establish manager relationships -- jane.doe and john.smith report to admin From 3dc4ed2a7f90fbc0f82c1773960e59ce871e490b Mon Sep 17 00:00:00 2001 From: agustin-conductor Date: Fri, 28 Nov 2025 13:00:32 -0300 Subject: [PATCH 3/6] remove unnecessary if statement --- pkg/bsql/query.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pkg/bsql/query.go b/pkg/bsql/query.go index a668e3e9..5f44343d 100644 --- a/pkg/bsql/query.go +++ b/pkg/bsql/query.go @@ -382,7 +382,7 @@ func (s *SQLSyncer) PrepareQueryVars(ctx context.Context, inputs map[string]any, } // normalizeValue converts CEL null types and other special values to Go nil for SQL compatibility -// Also converts booleans to strings ('1'/'0') for Oracle compatibility when used in DECODE statements. +// Also converts booleans to strings ("1"/"0") for Oracle compatibility when used in DECODE statements. func (s *SQLSyncer) normalizeValue(val interface{}) interface{} { if val == nil { return nil @@ -391,10 +391,6 @@ func (s *SQLSyncer) normalizeValue(val interface{}) interface{} { // Check for CEL null types switch v := val.(type) { case string: - // CEL might return "NULL_VALUE" as a string representation - if v == "NULL_VALUE" { - return nil - } return v case types.Null: // CEL Null type From 69c81c1723674ad226aa2190625867c02ec48eb7 Mon Sep 17 00:00:00 2001 From: agustin-conductor Date: Fri, 28 Nov 2025 13:10:29 -0300 Subject: [PATCH 4/6] use any instead of interface{} in normalizeValue func --- pkg/bsql/query.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/bsql/query.go b/pkg/bsql/query.go index 5f44343d..924cf02d 100644 --- a/pkg/bsql/query.go +++ b/pkg/bsql/query.go @@ -383,7 +383,7 @@ func (s *SQLSyncer) PrepareQueryVars(ctx context.Context, inputs map[string]any, // normalizeValue converts CEL null types and other special values to Go nil for SQL compatibility // Also converts booleans to strings ("1"/"0") for Oracle compatibility when used in DECODE statements. -func (s *SQLSyncer) normalizeValue(val interface{}) interface{} { +func (s *SQLSyncer) normalizeValue(val any) any { if val == nil { return nil } From e0099a63b31e609fde0a27f1fbab9f002394b187 Mon Sep 17 00:00:00 2001 From: agustin-conductor Date: Tue, 2 Dec 2025 09:55:58 -0300 Subject: [PATCH 5/6] add support for queries in actions --- examples/sqlserver-test.yml | 117 +++++++++++++++++++++++++++++++++++- pkg/bsql/config.go | 7 ++- pkg/bsql/query.go | 12 +++- pkg/connector/action.go | 11 +++- test/sqlserver-init.sql | 19 +++--- 5 files changed, 148 insertions(+), 18 deletions(-) diff --git a/examples/sqlserver-test.yml b/examples/sqlserver-test.yml index 1fc7a874..13f1bd0d 100644 --- a/examples/sqlserver-test.yml +++ b/examples/sqlserver-test.yml @@ -11,6 +11,98 @@ connect: user: "${DB_USER}" password: "${DB_PASSWORD}" +actions: + update_user_attributes: + name: Update User Attributes + description: Update the attributes of a user. Only provide the attributes you want to update. + action_type: + - account + - account_update_profile + arguments: + user_id: + name: User ID + type: string + required: true + description: The ID of the user to update + attrs: + name: Attributes + type: string_map + required: true + description: The updated attribute data (map of attribute names to values) + attrs_update_mask: + name: Attributes Update Mask + type: string_list + required: true + description: The attributes to update (list of attribute names from attrs to actually update) + vars: + # Map each attribute in the attrs argument to a variable and a flag to indicate if the attribute should be updated + manager_id: "'manager_id' in input.attrs ? input.attrs['manager_id'] : null" + update_manager_id: "'manager_id' in input.attrs_update_mask" + first_name: "'first_name' in input.attrs ? input.attrs['first_name'] : null" + update_first_name: "'first_name' in input.attrs_update_mask" + middle_name: "'middle_name' in input.attrs ? input.attrs['middle_name'] : null" + update_middle_name: "'middle_name' in input.attrs_update_mask" + last_name: "'last_name' in input.attrs ? input.attrs['last_name'] : null" + update_last_name: "'last_name' in input.attrs_update_mask" + job_title: "'job_title' in input.attrs ? input.attrs['job_title'] : null" + update_job_title: "'job_title' in input.attrs_update_mask" + department: "'department' in input.attrs ? input.attrs['department'] : null" + update_department: "'department' in input.attrs_update_mask" + employee_id: "'employee_id' in input.attrs ? input.attrs['employee_id'] : null" + update_employee_id: "'employee_id' in input.attrs_update_mask" + employee_number: "'employee_number' in input.attrs ? input.attrs['employee_number'] : null" + update_employee_number: "'employee_number' in input.attrs_update_mask" + # We define the whole update logic with conditional attributes using CASE statements + # Note: SQL Server examples has the attributes split between Users and EmployeeData tables + queries: + - | + -- Update Users table fields + UPDATE Users + SET + ManagerID = CASE + WHEN ? = 1 THEN CAST(? AS INT) + ELSE ManagerID + END, + EmployeeID = CASE + WHEN ? = 1 THEN ? + ELSE EmployeeID + END + WHERE Username = ?; + - | + -- Update EmployeeData table fields + UPDATE ed + SET + attr_first_name = CASE + WHEN ? = 1 THEN ? + ELSE ed.attr_first_name + END, + attr_middle_name = CASE + WHEN ? = 1 THEN ? + ELSE ed.attr_middle_name + END, + attr_last_name = CASE + WHEN ? = 1 THEN ? + ELSE ed.attr_last_name + END, + JobTitle = CASE + WHEN ? = 1 THEN ? + ELSE ed.JobTitle + END, + Department = CASE + WHEN ? = 1 THEN ? + ELSE ed.Department + END, + EmployeeNumber = CASE + WHEN ? = 1 THEN CAST(? AS INT) + ELSE ed.EmployeeNumber + END + FROM ( + SELECT TOP 1 EmployeeDataID, UserID + FROM EmployeeData + WHERE UserID = (SELECT UserID FROM Users WHERE Username = ?) + ) ed_sub + INNER JOIN EmployeeData ed ON ed.EmployeeDataID = ed_sub.EmployeeDataID + # Definition of different resource types managed by this connector resource_types: # Configuration for "user" resources in SQL Server @@ -40,11 +132,19 @@ resource_types: END as last_login, u.ManagerID as manager_id, m.Username as manager_username, - m.Email as manager_email + m.Email as manager_email, + ed.attr_first_name, + ed.attr_middle_name, + ed.attr_last_name, + ed.JobTitle as attr_job_title, + ed.Department as attr_department, + ed.EmployeeNumber as attr_employee_number FROM Users u LEFT JOIN Users m ON u.ManagerID = m.UserID + LEFT JOIN + EmployeeData ed ON u.UserID = ed.UserID ORDER BY u.UserID OFFSET ? ROWS FETCH NEXT ? ROWS ONLY # Pagination configuration (using offset for SQL Server) @@ -79,6 +179,12 @@ resource_types: manager_id: ".manager_id" manager_username: ".manager_username" manager_email: ".manager_email" + attr_first_name: ".attr_first_name" + attr_middle_name: ".attr_middle_name" + attr_last_name: ".attr_last_name" + attr_job_title: ".attr_job_title" + attr_department: ".attr_department" + attr_employee_number: ".attr_employee_number != null ? string(.attr_employee_number) : ''" # Account provisioning configuration for creating new user accounts account_provisioning: @@ -130,9 +236,16 @@ resource_types: END as last_login, u.ManagerID as manager_id, m.Username as manager_username, - m.Email as manager_email + m.Email as manager_email, + ed.attr_first_name, + ed.attr_middle_name, + ed.attr_last_name, + ed.JobTitle as attr_job_title, + ed.Department as attr_department, + ed.EmployeeNumber as attr_employee_number FROM Users u LEFT JOIN Users m ON u.ManagerID = m.UserID + LEFT JOIN EmployeeData ed ON u.UserID = ed.UserID WHERE u.Username = ? # Account creation configuration create: diff --git a/pkg/bsql/config.go b/pkg/bsql/config.go index b580b77d..7ea07b30 100644 --- a/pkg/bsql/config.go +++ b/pkg/bsql/config.go @@ -433,7 +433,8 @@ type ActionConfig struct { Arguments map[string]ArgumentConfig `yaml:"arguments,omitempty" json:"arguments,omitempty" validate:"omitempty,dive"` Vars map[string]string `yaml:"vars,omitempty" json:"vars,omitempty" validate:"omitempty"` NoTransaction bool `yaml:"no_transaction,omitempty" json:"no_transaction,omitempty" validate:"omitempty"` - Query string `yaml:"query" json:"query" validate:"required"` + Query string `yaml:"query,omitempty" json:"query,omitempty" validate:"omitempty"` + Queries []string `yaml:"queries,omitempty" json:"queries,omitempty" validate:"omitempty,dive"` // TODO: add validation? //revive:disable-next-line:line-length-limit // because it's a long field ActionType []string `yaml:"action_type,omitempty" json:"action_type,omitempty" validate:"omitempty,dive,oneof=unspecified dynamic account account_update_profile account_disable account_enable"` @@ -449,8 +450,8 @@ type ArgumentConfig struct { } func (a *ActionConfig) Validate() error { - if a.Query == "" { - return status.Errorf(codes.InvalidArgument, "query is required") + if a.Query == "" && len(a.Queries) == 0 { + return status.Errorf(codes.InvalidArgument, "query or queries is required") } for _, arg := range a.Arguments { diff --git a/pkg/bsql/query.go b/pkg/bsql/query.go index 924cf02d..c2b4a8b5 100644 --- a/pkg/bsql/query.go +++ b/pkg/bsql/query.go @@ -399,10 +399,18 @@ func (s *SQLSyncer) normalizeValue(val any) any { // Convert boolean to string for Oracle compatibility (Oracle DECODE expects CHAR) // Only convert for Oracle to avoid breaking other databases if s.dbEngine == database.Oracle { + result := "0" if v { - return "1" + result = "1" } - return "0" + return result + } + if s.dbEngine == database.MSSQL { + result := 0 + if v { + result = 1 + } + return result } // For other databases, return as-is (let the driver handle it) return v diff --git a/pkg/connector/action.go b/pkg/connector/action.go index b309b6dd..7500a051 100644 --- a/pkg/connector/action.go +++ b/pkg/connector/action.go @@ -128,8 +128,8 @@ func (c *Connector) RegisterActionManager(ctx context.Context) (connectorbuilder actionSchema.Arguments = append(actionSchema.Arguments, arg) } - if actionCfg.Query == "" { - return nil, fmt.Errorf("query is required for action: %s", actionKey) + if actionCfg.Query == "" && len(actionCfg.Queries) == 0 { + return nil, fmt.Errorf("query or queries is required for action: %s", actionKey) } cfg := actionCfg @@ -204,7 +204,12 @@ func (c *Connector) handleQueryAction(ctx context.Context, actionKey string, act argMap[k] = v } - queries := []string{actionCfg.Query} + var queries []string + if len(actionCfg.Queries) > 0 { + queries = actionCfg.Queries + } else { + queries = []string{actionCfg.Query} + } err = sqlSyncer.RunProvisioningQueries(ctx, queries, argMap, !actionCfg.NoTransaction) if err != nil { return nil, nil, err diff --git a/test/sqlserver-init.sql b/test/sqlserver-init.sql index 1f7784b3..75f9a55c 100644 --- a/test/sqlserver-init.sql +++ b/test/sqlserver-init.sql @@ -55,6 +55,9 @@ CREATE TABLE EmployeeData ( Department NVARCHAR(100), JobTitle NVARCHAR(100), HireDate DATETIME2, + attr_first_name NVARCHAR(100), + attr_middle_name NVARCHAR(100), + attr_last_name NVARCHAR(100), FOREIGN KEY (UserID) REFERENCES Users(UserID) ON DELETE CASCADE ); @@ -114,14 +117,14 @@ INSERT INTO LoginHistory (UserID, LoginTime, LoginTimeText, LoginTimeAlt, LoginS ((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'); +INSERT INTO EmployeeData (UserID, EmployeeID, EmployeeNumber, EmployeeCode, Department, JobTitle, HireDate, attr_first_name, attr_middle_name, attr_last_name) VALUES +((SELECT UserID FROM Users WHERE Username = 'admin'), 'EMP001', 10001, 'E-10001', 'IT', 'System Administrator', '2025-01-01', 'Admin', NULL, 'User'), +((SELECT UserID FROM Users WHERE Username = 'jane.doe'), 'EMP002', 10002, 'E-10002', 'HR', 'HR Specialist', '2025-01-05', 'Jane', 'Marie', 'Doe'), +((SELECT UserID FROM Users WHERE Username = 'john.smith'), 'EMP003', 10003, 'E-10003', 'Finance', 'Financial Analyst', '2025-01-10', 'John', 'Michael', 'Smith'), +((SELECT UserID FROM Users WHERE Username = 'service.acct'), 'SVC001', 20001, 'S-20001', 'IT', 'Service Account', '2025-02-01', NULL, NULL, NULL), +((SELECT UserID FROM Users WHERE Username = 'disabled.user'), 'EMP004', 10004, 'E-10004', 'Marketing', 'Marketing Coordinator', '2025-02-15', 'Disabled', NULL, 'User'), +((SELECT UserID FROM Users WHERE Username = 'alice.manager'), 'EMP005', 10005, 'E-10005', 'IT', 'IT Manager', '2025-03-01', 'Alice', NULL, 'Manager'), +((SELECT UserID FROM Users WHERE Username = 'bob.developer'), 'EMP006', 10006, 'E-10006', 'IT', 'Software Developer', '2025-03-05', 'Bob', NULL, 'Developer'); -- Create indexes for better performance CREATE INDEX IX_Users_Username ON Users(Username); From 3d3a96d8ee7e9e03cfa778eaf608492c96c78924 Mon Sep 17 00:00:00 2001 From: agustin-conductor Date: Thu, 11 Dec 2025 13:09:28 -0300 Subject: [PATCH 6/6] add ci test and validation for actionCfg --- .github/workflows/ci.yaml | 15 ++++ examples/oracle-test.yml | 165 +++++++++++++++++++++++++++++--------- pkg/bsql/config.go | 8 +- pkg/connector/action.go | 9 ++- 4 files changed, 153 insertions(+), 44 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6d3b47b5..02ce860f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -88,3 +88,18 @@ jobs: connector: ./baton-sql account-email: robert.tables2@example.com account-login: "robert'); drop table users; --" + - name: Run update user attributes action test + run: | + ./baton-sql --invoke-action=update_user_attributes --invoke-action-args='{ + "user_id": "jane.doe", + "attrs": { + "first_name": "Janet", + "job_title": "Senior Software Engineer" + }, + "attrs_update_mask": ["first_name", "job_title"] + }' + - name: Verify update user attributes action + run: | + psql -h localhost --user postgres -d batondb -t -c "SELECT attr_first_name, attr_job_title FROM users WHERE username = 'jane.doe'" | grep -q "Janet.*Senior Software Engineer" + env: + PGPASSWORD: secretpassword diff --git a/examples/oracle-test.yml b/examples/oracle-test.yml index 0a9860d9..49c1fc66 100644 --- a/examples/oracle-test.yml +++ b/examples/oracle-test.yml @@ -254,19 +254,18 @@ resource_types: description: "A role within the Oracle system" # Settings for listing roles from the database list: - # SQL query to retrieve roles from custom roles table + # SQL query to retrieve roles query: | SELECT - id as "roleId", - role_name as "roleName" + ROLE FROM - ROLES + DBA_ROLES # Mapping of each query result to role resource fields map: - # Unique role identifier (using role name to match provisioning queries) - id: ".roleName" + # Unique role identifier (from the ROLE column) + id: ".ROLE" # Display name for the role (also from the ROLE column) - display_name: ".roleName" + display_name: ".ROLE" # Role description; left empty for further customization description: "" # Additional role-specific traits @@ -274,7 +273,7 @@ resource_types: role: # Profile mapping for the role; assigns the role name profile: - role_name: ".roleName" + role_name: ".ROLE" # Static entitlements that can be assigned to a role resource static_entitlements: - id: "assigned" # Entitlement identifier for a standard role assignment @@ -294,23 +293,17 @@ resource_types: 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: - | - INSERT INTO user_roles (user_id, role_id) - SELECT u.id, r.id - FROM users u, roles r - WHERE u.username = ? - AND r.role_name = ? - AND NOT EXISTS ( - SELECT 1 FROM user_roles ur - WHERE ur.user_id = u.id AND ur.role_id = r.id - ) + GRANT ? TO ? revoke: + # Indicates that the revoke operation runs without a transaction + no_transaction: true queries: - | - DELETE FROM user_roles - WHERE user_id = (SELECT id FROM users WHERE username = ?) - AND role_id = (SELECT id FROM roles WHERE role_name = ?) + 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'" @@ -327,35 +320,131 @@ resource_types: 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: - | - INSERT INTO user_roles (user_id, role_id) - SELECT u.id, r.id - FROM users u, roles r - WHERE u.username = ? - AND r.role_name = ? - AND NOT EXISTS ( - SELECT 1 FROM user_roles ur - WHERE ur.user_id = u.id AND ur.role_id = r.id - ) + GRANT ? TO ? WITH ADMIN OPTION revoke: + no_transaction: true queries: - | - DELETE FROM user_roles - WHERE user_id = (SELECT id FROM users WHERE username = ?) - AND role_id = (SELECT id FROM roles WHERE role_name = ?) + REVOKE ? FROM ? + - | + GRANT ? TO ? # Dynamic grants based on SQL queries to associate users with roles grants: - query: | SELECT - u.username AS USERNAME, - r.role_name AS ROLE + GRANTEE AS USERNAME, + GRANTED_ROLE AS ROLE, + ADMIN_OPTION FROM - user_roles ur - INNER JOIN users u ON ur.user_id = u.id - INNER JOIN roles r ON ur.role_id = r.id + 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 \ No newline at end of file + 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 \ No newline at end of file diff --git a/pkg/bsql/config.go b/pkg/bsql/config.go index 7ea07b30..39421de2 100644 --- a/pkg/bsql/config.go +++ b/pkg/bsql/config.go @@ -433,8 +433,8 @@ type ActionConfig struct { Arguments map[string]ArgumentConfig `yaml:"arguments,omitempty" json:"arguments,omitempty" validate:"omitempty,dive"` Vars map[string]string `yaml:"vars,omitempty" json:"vars,omitempty" validate:"omitempty"` NoTransaction bool `yaml:"no_transaction,omitempty" json:"no_transaction,omitempty" validate:"omitempty"` - Query string `yaml:"query,omitempty" json:"query,omitempty" validate:"omitempty"` - Queries []string `yaml:"queries,omitempty" json:"queries,omitempty" validate:"omitempty,dive"` + Query string `yaml:"query,omitempty" json:"query,omitempty" validate:"required_without=queries,excluded_with=queries,omitempty"` + Queries []string `yaml:"queries,omitempty" json:"queries,omitempty" validate:"required_without=query,excluded_with=query,omitempty"` // TODO: add validation? //revive:disable-next-line:line-length-limit // because it's a long field ActionType []string `yaml:"action_type,omitempty" json:"action_type,omitempty" validate:"omitempty,dive,oneof=unspecified dynamic account account_update_profile account_disable account_enable"` @@ -454,6 +454,10 @@ func (a *ActionConfig) Validate() error { return status.Errorf(codes.InvalidArgument, "query or queries is required") } + if a.Query != "" && len(a.Queries) > 0 { + return status.Errorf(codes.InvalidArgument, "cannot specify both query and queries; use one or the other") + } + for _, arg := range a.Arguments { if arg.Required && arg.Default != nil { return status.Errorf(codes.InvalidArgument, "action %s argument %s is required but has a default", a.Name, arg.Name) diff --git a/pkg/connector/action.go b/pkg/connector/action.go index 7500a051..10b8207b 100644 --- a/pkg/connector/action.go +++ b/pkg/connector/action.go @@ -128,12 +128,13 @@ func (c *Connector) RegisterActionManager(ctx context.Context) (connectorbuilder actionSchema.Arguments = append(actionSchema.Arguments, arg) } - if actionCfg.Query == "" && len(actionCfg.Queries) == 0 { - return nil, fmt.Errorf("query or queries is required for action: %s", actionKey) - } - cfg := actionCfg + // Validate the action config + if err := cfg.Validate(); err != nil { + return nil, fmt.Errorf("invalid action config %s: %w", actionKey, err) + } + err := actionManager.RegisterAction(ctx, actionKey, actionSchema, func(ctx context.Context, args *structpb.Struct) (*structpb.Struct, annotations.Annotations, error) { return c.handleQueryAction(ctx, actionKey, cfg, args) })