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/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..49c1fc66 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: @@ -324,4 +447,4 @@ resource_types: - 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: "admin" # Apply the 'admin' entitlement when administrative rights are present \ 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/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/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/config.go b/pkg/bsql/config.go index b580b77d..39421de2 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:"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"` @@ -449,8 +450,12 @@ 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") + } + + 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 { 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..c2b4a8b5 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,61 @@ 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 any) any { + if val == nil { + return nil + } + + // Check for CEL null types + switch v := val.(type) { + case string: + 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 { + result := "0" + if v { + result = "1" + } + 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 + } + + // 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..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 == "" { - return nil, fmt.Errorf("query 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) }) @@ -188,7 +189,28 @@ func (c *Connector) handleQueryAction(ctx context.Context, actionKey string, act } } - queries := []string{actionCfg.Query} + // 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 + } + + 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/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..029bff38 100644 --- a/test/postgres-init.sql +++ b/test/postgres-init.sql @@ -20,20 +20,32 @@ 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) -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'), +('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'), +('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 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);