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>
This commit is contained in:
316
app/RSpade/CodeQuality/Rules/PHP/EndpointAuthCheck_CodeQualityRule.php
Executable file
316
app/RSpade/CodeQuality/Rules/PHP/EndpointAuthCheck_CodeQualityRule.php
Executable file
@@ -0,0 +1,316 @@
|
||||
<?php
|
||||
|
||||
namespace App\RSpade\CodeQuality\Rules\PHP;
|
||||
|
||||
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
|
||||
|
||||
/**
|
||||
* EndpointAuthCheckRule - Validates that controller endpoints have auth checks
|
||||
*
|
||||
* This rule ensures that all #[Route], #[SPA], and #[Ajax_Endpoint] methods
|
||||
* have authentication checks, either:
|
||||
* - In the method body itself, OR
|
||||
* - In a pre_dispatch() method in the same controller
|
||||
*
|
||||
* Valid auth check patterns:
|
||||
* - Session::is_logged_in()
|
||||
* - Session::get_user()
|
||||
* - Permission::has_permission()
|
||||
* - Permission::has_role()
|
||||
* - response_unauthorized()
|
||||
*
|
||||
* Exemption:
|
||||
* - Add @auth-exempt comment with reason to skip check for public endpoints
|
||||
*/
|
||||
class EndpointAuthCheck_CodeQualityRule extends CodeQualityRule_Abstract
|
||||
{
|
||||
/**
|
||||
* Get the unique rule identifier
|
||||
*/
|
||||
public function get_id(): string
|
||||
{
|
||||
return 'PHP-AUTH-01';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get human-readable rule name
|
||||
*/
|
||||
public function get_name(): string
|
||||
{
|
||||
return 'Endpoint Authentication Check';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get rule description
|
||||
*/
|
||||
public function get_description(): string
|
||||
{
|
||||
return 'Validates that controller endpoints (#[Route], #[SPA], #[Ajax_Endpoint]) have authentication checks';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file patterns this rule applies to
|
||||
*/
|
||||
public function get_file_patterns(): array
|
||||
{
|
||||
return ['*.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',
|
||||
'Permission::has_permission',
|
||||
'Permission::has_role',
|
||||
'Permission::authenticated',
|
||||
'Permission::require_permission',
|
||||
'Permission::require_role',
|
||||
'response_unauthorized',
|
||||
'->has_permission(',
|
||||
'->has_role(',
|
||||
];
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
// Skip if class-level @auth-exempt comment is present (all endpoints public)
|
||||
if (strpos($original_contents, '@auth-exempt') !== false) {
|
||||
// Check if @auth-exempt appears before class definition (in class docblock)
|
||||
// Use regex to find actual class definition, not 'class' in use statements
|
||||
if (preg_match('/^(abstract\s+)?class\s+\w+/m', $original_contents, $matches, PREG_OFFSET_CAPTURE)) {
|
||||
$class_pos = $matches[0][1];
|
||||
$exempt_pos = strpos($original_contents, '@auth-exempt');
|
||||
if ($exempt_pos !== false && $exempt_pos < $class_pos) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only check controller files (must extend Rsx_Controller_Abstract)
|
||||
if (!isset($metadata['extends']) || $metadata['extends'] !== 'Rsx_Controller_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;
|
||||
}
|
||||
|
||||
// Check if pre_dispatch has auth check
|
||||
$pre_dispatch_has_auth = $this->pre_dispatch_has_auth_check($contents, $metadata);
|
||||
|
||||
// Get public static methods from metadata
|
||||
$methods = $metadata['public_static_methods'] ?? [];
|
||||
|
||||
foreach ($methods as $method_name => $method_info) {
|
||||
// Skip pre_dispatch itself
|
||||
if ($method_name === 'pre_dispatch') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if method has endpoint attributes
|
||||
$has_endpoint_attr = false;
|
||||
$endpoint_type = null;
|
||||
$attributes = $method_info['attributes'] ?? [];
|
||||
|
||||
foreach ($attributes as $attr_name => $attr_data) {
|
||||
$short_name = basename(str_replace('\\', '/', $attr_name));
|
||||
if (in_array($short_name, ['Route', 'SPA', 'Ajax_Endpoint'])) {
|
||||
$has_endpoint_attr = true;
|
||||
$endpoint_type = $short_name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Skip methods without endpoint attributes
|
||||
if (!$has_endpoint_attr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get line number for this method
|
||||
$line_number = $method_info['line'] ?? 1;
|
||||
|
||||
// Check if method has @auth-exempt comment
|
||||
if ($this->method_has_auth_exempt($original_contents, $method_name, $line_number)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If pre_dispatch has auth check, this endpoint is covered
|
||||
if ($pre_dispatch_has_auth) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if method body has auth check
|
||||
$method_body = $this->extract_method_body($contents, $method_name);
|
||||
if ($method_body && $this->body_has_auth_check($method_body)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Violation found - no auth check
|
||||
$code_snippet = "#[{$endpoint_type}]\npublic static function {$method_name}(...)";
|
||||
|
||||
$this->add_violation(
|
||||
$file_path,
|
||||
$line_number,
|
||||
"Endpoint '{$method_name}' has no authentication check",
|
||||
$code_snippet,
|
||||
$this->build_suggestion($method_name, $class_name),
|
||||
'high'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if pre_dispatch method has an auth check
|
||||
*/
|
||||
private function pre_dispatch_has_auth_check(string $contents, array $metadata): bool
|
||||
{
|
||||
$methods = $metadata['public_static_methods'] ?? [];
|
||||
|
||||
if (!isset($methods['pre_dispatch'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$pre_dispatch_body = $this->extract_method_body($contents, 'pre_dispatch');
|
||||
if (!$pre_dispatch_body) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->body_has_auth_check($pre_dispatch_body);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 $method_name, string $class_name): string
|
||||
{
|
||||
$suggestions = [];
|
||||
$suggestions[] = "Endpoint '{$method_name}' needs an authentication check.";
|
||||
$suggestions[] = "";
|
||||
$suggestions[] = "Option 1: Add auth check to pre_dispatch() (recommended for all endpoints in controller):";
|
||||
$suggestions[] = " public static function pre_dispatch(Request \$request, array \$params = [])";
|
||||
$suggestions[] = " {";
|
||||
$suggestions[] = " if (!Session::is_logged_in()) {";
|
||||
$suggestions[] = " return response_unauthorized();";
|
||||
$suggestions[] = " }";
|
||||
$suggestions[] = " return null;";
|
||||
$suggestions[] = " }";
|
||||
$suggestions[] = "";
|
||||
$suggestions[] = "Option 2: Add auth check at start of method body:";
|
||||
$suggestions[] = " if (!Session::is_logged_in()) {";
|
||||
$suggestions[] = " return response_unauthorized();";
|
||||
$suggestions[] = " }";
|
||||
$suggestions[] = "";
|
||||
$suggestions[] = "Option 3: Mark as public endpoint with @auth-exempt comment:";
|
||||
$suggestions[] = " /**";
|
||||
$suggestions[] = " * @auth-exempt Public endpoint for webhook receivers";
|
||||
$suggestions[] = " */";
|
||||
$suggestions[] = " #[Ajax_Endpoint]";
|
||||
$suggestions[] = " public static function {$method_name}(...)";
|
||||
|
||||
return implode("\n", $suggestions);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user