Files
rspade_system/app/RSpade/CodeQuality/Rules/PHP/ModelFetchAuthCheck_CodeQualityRule.php
root 84ca3dfe42 Fix code quality violations and rename select input components
Move small tasks from wishlist to todo, update npm packages
Replace #[Auth] attributes with manual auth checks and code quality rule
Remove on_jqhtml_ready lifecycle method from framework
Complete ACL system with 100-based role indexing and /dev/acl tester
WIP: ACL system implementation with debug instrumentation
Convert rsx:check JS linting to RPC socket server
Clean up docs and fix $id→$sid in man pages, remove SSR/FPC feature
Reorganize wishlists: priority order, mark sublayouts complete, add email
Update model_fetch docs: mark MVP complete, fix enum docs, reorganize
Comprehensive documentation overhaul: clarity, compression, and critical rules
Convert Contacts/Projects CRUD to Model.fetch() and add fetch_or_null()
Add JS ORM relationship lazy-loading and fetch array handling
Add JS ORM relationship fetching and CRUD documentation
Fix ORM hydration and add IDE resolution for Base_* model stubs
Rename Json_Tree_Component to JS_Tree_Debug_Component and move to framework
Enhance JS ORM infrastructure and add Json_Tree class name badges

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 21:39:43 +00:00

277 lines
8.7 KiB
PHP
Executable File

<?php
namespace App\RSpade\CodeQuality\Rules\PHP;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
/**
* ModelFetchAuthCheckRule - Validates that model fetch() methods have auth checks
*
* When a model has a fetch() method with the #[Ajax_Endpoint_Model_Fetch] attribute,
* it becomes callable from JavaScript via Orm_Controller. The fetch() method is
* responsible for implementing its own authorization checks.
*
* This rule ensures that fetch() methods in Rsx_Model_Abstract subclasses
* contain auth check patterns when they have the Ajax_Endpoint_Model_Fetch attribute.
*
* Valid auth check patterns:
* - Session::is_logged_in()
* - Session::get_user()
* - Permission::has_permission()
* - Permission::has_role()
* - response_unauthorized()
* - ->site_id check (verifies ownership)
*
* Exemption:
* - Add @auth-exempt comment with reason for public data fetch
*/
class ModelFetchAuthCheck_CodeQualityRule extends CodeQualityRule_Abstract
{
/**
* Get the unique rule identifier
*/
public function get_id(): string
{
return 'PHP-MODEL-FETCH-01';
}
/**
* Get human-readable rule name
*/
public function get_name(): string
{
return 'Model Fetch Authentication Check';
}
/**
* Get rule description
*/
public function get_description(): string
{
return 'Validates that model fetch() methods with #[Ajax_Endpoint_Model_Fetch] have authentication checks';
}
/**
* Get file patterns this rule applies to
*/
public function get_file_patterns(): array
{
return ['*_model.php', '*_Model.php'];
}
/**
* Whether this rule is called during manifest scan
*/
public function is_called_during_manifest_scan(): bool
{
return false; // Only run during rsx:check
}
/**
* Get default severity for this rule
*/
public function get_default_severity(): string
{
return 'high';
}
/**
* Patterns that indicate an auth check is present
*/
private const AUTH_CHECK_PATTERNS = [
'Session::is_logged_in',
'Session::get_user',
'Session::get_user_id',
'Session::get_site_id',
'Permission::has_permission',
'Permission::has_role',
'response_unauthorized',
'->has_permission(',
'->has_role(',
'->site_id', // Checking ownership via site_id
'get_site_id()', // Session site check
];
/**
* Check a file for violations
*/
public function check(string $file_path, string $contents, array $metadata = []): void
{
// Read original file content (not sanitized) for comment checking
$original_contents = file_get_contents($file_path);
// Skip if file-level exception comment is present
if (strpos($original_contents, '@' . $this->get_id() . '-EXCEPTION') !== false) {
return;
}
// Only check model files (must extend Rsx_Model_Abstract)
if (!isset($metadata['extends']) || $metadata['extends'] !== 'Rsx_Model_Abstract') {
return;
}
// Skip archived files
if (str_contains($file_path, '/archive/') || str_contains($file_path, '/archived/')) {
return;
}
// Get the class name
$class_name = $metadata['class'] ?? null;
if (!$class_name) {
return;
}
// Get public static methods from metadata
$methods = $metadata['public_static_methods'] ?? [];
// Check if fetch() method exists
if (!isset($methods['fetch'])) {
return;
}
$fetch_info = $methods['fetch'];
$attributes = $fetch_info['attributes'] ?? [];
// Check if fetch has Ajax_Endpoint_Model_Fetch attribute
$has_fetch_attribute = false;
foreach ($attributes as $attr_name => $attr_data) {
$short_name = basename(str_replace('\\', '/', $attr_name));
if ($short_name === 'Ajax_Endpoint_Model_Fetch') {
$has_fetch_attribute = true;
break;
}
}
// Skip if fetch doesn't have the attribute (not exposed via ORM)
if (!$has_fetch_attribute) {
return;
}
// Get line number for fetch method
$line_number = $fetch_info['line'] ?? 1;
// Check if method has @auth-exempt comment
if ($this->method_has_auth_exempt($original_contents, 'fetch', $line_number)) {
return;
}
// Check if method body has auth check
$method_body = $this->extract_method_body($contents, 'fetch');
if ($method_body && $this->body_has_auth_check($method_body)) {
return;
}
// Violation found - no auth check
$code_snippet = "#[Ajax_Endpoint_Model_Fetch]\npublic static function fetch(\$id)";
$this->add_violation(
$file_path,
$line_number,
"Model fetch() method has no authentication check",
$code_snippet,
$this->build_suggestion($class_name),
'high'
);
}
/**
* Check if a code body has an auth check pattern
*/
private function body_has_auth_check(string $body): bool
{
foreach (self::AUTH_CHECK_PATTERNS as $pattern) {
if (str_contains($body, $pattern)) {
return true;
}
}
return false;
}
/**
* Check if a method has @auth-exempt comment
*/
private function method_has_auth_exempt(string $contents, string $method_name, int $method_line): bool
{
$lines = explode("\n", $contents);
// Check the 10 lines before the method definition for @auth-exempt
$start_line = max(0, $method_line - 11);
$end_line = $method_line - 1;
for ($i = $start_line; $i <= $end_line && $i < count($lines); $i++) {
$line = $lines[$i];
if (str_contains($line, '@auth-exempt')) {
return true;
}
// Stop if we hit another method definition
if (preg_match('/^\s*public\s+static\s+function\s+/', $line) && !str_contains($line, $method_name)) {
break;
}
}
return false;
}
/**
* Extract method body from file contents
*/
private function extract_method_body(string $contents, string $method_name): ?string
{
// Pattern to match method definition
$pattern = '/public\s+static\s+function\s+' . preg_quote($method_name, '/') . '\s*\([^)]*\)[^{]*\{/s';
if (!preg_match($pattern, $contents, $matches, PREG_OFFSET_CAPTURE)) {
return null;
}
$start_pos = $matches[0][1] + strlen($matches[0][0]) - 1;
$brace_count = 1;
$pos = $start_pos + 1;
$length = strlen($contents);
while ($pos < $length && $brace_count > 0) {
$char = $contents[$pos];
if ($char === '{') {
$brace_count++;
} elseif ($char === '}') {
$brace_count--;
}
$pos++;
}
return substr($contents, $start_pos, $pos - $start_pos);
}
/**
* Build suggestion for fixing the violation
*/
private function build_suggestion(string $class_name): string
{
$suggestions = [];
$suggestions[] = "Model fetch() method needs an authentication/authorization check.";
$suggestions[] = "";
$suggestions[] = "Option 1: Check user is logged in and verify ownership:";
$suggestions[] = " #[Ajax_Endpoint_Model_Fetch]";
$suggestions[] = " public static function fetch(\$id)";
$suggestions[] = " {";
$suggestions[] = " if (!Session::is_logged_in()) {";
$suggestions[] = " return null; // or response_unauthorized()";
$suggestions[] = " }";
$suggestions[] = " \$record = static::find(\$id);";
$suggestions[] = " if (!\$record || \$record->site_id !== Session::get_site_id()) {";
$suggestions[] = " return null; // Wrong site or not found";
$suggestions[] = " }";
$suggestions[] = " return \$record;";
$suggestions[] = " }";
$suggestions[] = "";
$suggestions[] = "Option 2: If this is intentionally public data, add @auth-exempt:";
$suggestions[] = " /**";
$suggestions[] = " * @auth-exempt Public reference data (countries, etc.)";
$suggestions[] = " */";
$suggestions[] = " #[Ajax_Endpoint_Model_Fetch]";
$suggestions[] = " public static function fetch(\$id) { ... }";
return implode("\n", $suggestions);
}
}