package businessscope import ( "context" "database/sql" "fmt" "strings" "time" ) // candidateQuery deliberately derives the active lease lifecycle from delivery and // return task facts. vehicle_lease_order_record remains an independent customer // ownership check, but its last_return_time is not authoritative: saving a return // draft currently updates that aggregate field before the return is completed. const candidateQuery = `SELECT dv.vehicle_id, v.id, COALESCE(v.vin, ''), COALESCE(v.plate_number, dv.plate_number, r.plate_number, ''), r.customer_id, ci.id, dv.contract_id, co.id, COALESCE(co.other_customer_id, co.customer_id), COALESCE(co.contract_code, r.contract_code, ''), COALESCE(co.project_name, r.project_name, ''), COALESCE(vs.operation_status, ''), dv.delivery_time, dv.update_time FROM delivery_vehicle dv LEFT JOIN vehicle_info v ON v.id = dv.vehicle_id AND v.del_flag = '0' LEFT JOIN vehicle_lease_order_record r ON r.vehicle_id = dv.vehicle_id AND r.del_flag = '0' LEFT JOIN customer_info ci ON ci.id = r.customer_id AND ci.del_flag = '0' LEFT JOIN vehicle_lease_contract_info co ON co.id = dv.contract_id AND co.del_flag = '0' LEFT JOIN vehicle_status vs ON vs.vehicle_id = dv.vehicle_id AND vs.del_flag = '0' WHERE dv.del_flag = '0' AND dv.delivery_status IN (2, 3) AND dv.vehicle_id IS NOT NULL AND dv.delivery_time IS NOT NULL AND NOT EXISTS ( SELECT 1 FROM delivery_vehicle newer WHERE newer.del_flag = '0' AND newer.vehicle_id = dv.vehicle_id AND newer.vehicle_id IS NOT NULL AND ( COALESCE(newer.delivery_time, '1000-01-01') > COALESCE(dv.delivery_time, '1000-01-01') OR ( COALESCE(newer.delivery_time, '1000-01-01') = COALESCE(dv.delivery_time, '1000-01-01') AND newer.id > dv.id ) ) ) AND NOT EXISTS ( SELECT 1 FROM return_vehicle_task rt WHERE rt.delivery_vehicle_id = dv.id AND rt.del_flag = '0' AND rt.status IN (2, 3, 5) ) ORDER BY dv.vehicle_id, dv.id, r.id` func ReadCandidates(ctx context.Context, db *sql.DB) ([]Candidate, error) { connection, err := db.Conn(ctx) if err != nil { return nil, fmt.Errorf("acquire OneOS connection: %w", err) } defer connection.Close() if err := VerifyReadOnlyGrants(ctx, connection); err != nil { return nil, err } if _, err := connection.ExecContext(ctx, `SET SESSION TRANSACTION READ ONLY`); err != nil { return nil, fmt.Errorf("force OneOS session read only: %w", err) } if _, err := connection.ExecContext(ctx, `SET SESSION MAX_EXECUTION_TIME = 10000`); err != nil { return nil, fmt.Errorf("set OneOS query timeout: %w", err) } tx, err := connection.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelRepeatableRead, ReadOnly: true}) if err != nil { return nil, fmt.Errorf("begin OneOS read-only snapshot: %w", err) } defer tx.Rollback() rows, err := tx.QueryContext(ctx, candidateQuery) if err != nil { return nil, fmt.Errorf("query OneOS customer vehicle candidates: %w", err) } defer rows.Close() candidates := make([]Candidate, 0, 1024) for rows.Next() { var vehicleID, vehicleProfileID, customerID, customerProfileID sql.NullInt64 var contractID, contractProfileID, effectiveCustomerID sql.NullInt64 var vin, plate, contractCode, projectName, operationStatus string var scopeStart time.Time var updatedAt sql.NullTime if err := rows.Scan( &vehicleID, &vehicleProfileID, &vin, &plate, &customerID, &customerProfileID, &contractID, &contractProfileID, &effectiveCustomerID, &contractCode, &projectName, &operationStatus, &scopeStart, &updatedAt, ); err != nil { return nil, fmt.Errorf("scan OneOS scope candidate: %w", err) } candidate := Candidate{ RowNumber: len(candidates) + 1, VehicleID: vehicleID.Int64, VehiclePresent: vehicleID.Valid && vehicleProfileID.Valid, VIN: vin, PlateNumber: plate, CustomerID: customerID.Int64, CustomerPresent: customerID.Valid, CustomerProfileExists: customerProfileID.Valid, ContractID: contractID.Int64, ContractPresent: contractID.Valid, ContractProfileExists: contractProfileID.Valid, EffectiveCustomerID: effectiveCustomerID.Int64, EffectiveCustomerSet: effectiveCustomerID.Valid, ContractCode: contractCode, ProjectName: projectName, OperationStatus: operationStatus, ScopeStartAt: scopeStart, } if updatedAt.Valid { value := updatedAt.Time candidate.SourceUpdatedAt = &value } candidates = append(candidates, candidate) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("iterate OneOS scope candidates: %w", err) } if err := tx.Commit(); err != nil { return nil, fmt.Errorf("finish OneOS read-only snapshot: %w", err) } return candidates, nil } func VerifyReadOnlyGrants(ctx context.Context, connection *sql.Conn) error { rows, err := connection.QueryContext(ctx, `SHOW GRANTS FOR CURRENT_USER`) if err != nil { return fmt.Errorf("inspect OneOS database grants: %w", err) } defer rows.Close() grants := make([]string, 0) for rows.Next() { var grant string if err := rows.Scan(&grant); err != nil { return fmt.Errorf("scan OneOS database grant: %w", err) } grants = append(grants, grant) } if err := rows.Err(); err != nil { return fmt.Errorf("iterate OneOS database grants: %w", err) } if err := ValidateReadOnlyGrants(grants); err != nil { return fmt.Errorf("OneOS database account is not read-only: %w", err) } return nil } func ValidateReadOnlyGrants(grants []string) error { if len(grants) == 0 { return fmt.Errorf("SHOW GRANTS returned no rows") } allowedSelectScopes := map[string]bool{ "LN_ASSET_MANAGEMENT.VEHICLE_LEASE_ORDER_RECORD": true, "LN_ASSET_MANAGEMENT.DELIVERY_VEHICLE": true, "LN_ASSET_MANAGEMENT.RETURN_VEHICLE_TASK": true, "LN_ASSET_MANAGEMENT.VEHICLE_INFO": true, "LN_ASSET_MANAGEMENT.CUSTOMER_INFO": true, "LN_ASSET_MANAGEMENT.VEHICLE_LEASE_CONTRACT_INFO": true, "LN_ASSET_MANAGEMENT.VEHICLE_STATUS": true, } for _, raw := range grants { grant := strings.ToUpper(strings.TrimSpace(raw)) if strings.HasPrefix(grant, "SET DEFAULT ROLE ") { return fmt.Errorf("role-based grants are not accepted; grant SELECT directly to the sync account") } if !strings.HasPrefix(grant, "GRANT ") { return fmt.Errorf("unsupported grant statement") } onIndex := strings.Index(grant, " ON ") if onIndex < 0 { return fmt.Errorf("unsupported role or dynamic grant") } privileges := strings.TrimSpace(strings.TrimPrefix(grant[:onIndex], "GRANT ")) toIndex := strings.Index(grant[onIndex+4:], " TO ") if toIndex < 0 { return fmt.Errorf("unsupported grant scope") } scope := strings.ReplaceAll(strings.TrimSpace(grant[onIndex+4:onIndex+4+toIndex]), "`", "") for _, privilege := range strings.Split(privileges, ",") { privilege = strings.TrimSpace(privilege) switch privilege { case "USAGE": if scope != "*.*" { return fmt.Errorf("USAGE has unsupported scope %q", scope) } case "SELECT": if !allowedSelectScopes[scope] { return fmt.Errorf("SELECT scope %q is not required by the sync query", scope) } default: return fmt.Errorf("disallowed privilege %q", privilege) } } } return nil }