1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2026-02-05 08:24:05 +00:00

Correct terminology and comments in TableRLSManager

This commit is contained in:
lukinovec 2025-06-10 09:42:51 +02:00
parent 8d37415fc8
commit 9b34a8cdf3

View file

@ -12,7 +12,7 @@ use Stancl\Tenancy\Exceptions\RLSCommentConstraintException;
class TableRLSManager implements RLSPolicyManager class TableRLSManager implements RLSPolicyManager
{ {
/** /**
* When true, all valid foreign keys are considered while generating paths for RLS policies, * When true, all valid constraints are considered while generating paths for RLS policies,
* unless explicitly marked with 'no-rls' comment. * unless explicitly marked with 'no-rls' comment.
* *
* When false, only columns explicitly marked with 'rls' or 'rls table.column' comments are considered. * When false, only columns explicitly marked with 'rls' or 'rls table.column' comments are considered.
@ -111,9 +111,9 @@ class TableRLSManager implements RLSPolicyManager
* The 'steps' key contains the path steps returned by shortestPaths(). * The 'steps' key contains the path steps returned by shortestPaths().
* The 'dead_end' and 'recursive_relationship' keys are just internal metadata. * The 'dead_end' and 'recursive_relationship' keys are just internal metadata.
* *
* @param bool $deadEnd Whether the path is a dead end (no valid foreign keys leading to tenants table) * @param bool $deadEnd Whether the path is a dead end (no valid constraints leading to tenants table)
* @param bool $recursive Whether the path has recursive relationships * @param bool $recursive Whether the path has recursive relationships
* @param array $steps The steps in the path, each step being an array of formatted foreign keys * @param array $steps Steps to the tenants table, each step being a formatted constraint
*/ */
protected function buildPath(bool $deadEnd = false, bool $recursive = false, array $steps = []): array protected function buildPath(bool $deadEnd = false, bool $recursive = false, array $steps = []): array
{ {
@ -125,14 +125,14 @@ class TableRLSManager implements RLSPolicyManager
} }
/** /**
* Formats the retrieved foreign key to a more readable format. * Formats the retrieved constraint to a more readable format.
* *
* Also provides internal metadata about * Also provides internal metadata about
* - the foreign key's nullability (the 'nullable' key), * - the constraint's nullability (the 'nullable' key),
* - the foreign key's comment * - the constraint's comment
* *
* These internal details are then omitted * These internal details are then omitted
* from the foreign keys (or the "path steps") * from the constraints (or the "path steps")
* before returning the shortest paths in shortestPath(). * before returning the shortest paths in shortestPath().
* *
* [ * [
@ -140,12 +140,12 @@ class TableRLSManager implements RLSPolicyManager
* 'foreignTable' => 'tenants', * 'foreignTable' => 'tenants',
* 'foreignId' => 'id', * 'foreignId' => 'id',
* 'comment' => 'no-rls', // Used to explicitly enable/disable RLS or to create a comment constraint (internal metadata) * 'comment' => 'no-rls', // Used to explicitly enable/disable RLS or to create a comment constraint (internal metadata)
* 'nullable' => false, // Used to determine if the foreign key is nullable (internal metadata) * 'nullable' => false, // Used to determine if the constraint is nullable (internal metadata)
* ]. * ].
*/ */
protected function formatForeignKey(array $foreignKey, string $table): array protected function formatConstraint(array $constraint, string $table): array
{ {
$foreignKeyName = $foreignKey['columns'][0]; $foreignKeyName = $constraint['columns'][0];
$comment = collect($this->database->getSchemaBuilder()->getColumns($table)) $comment = collect($this->database->getSchemaBuilder()->getColumns($table))
->filter(fn ($column) => $column['name'] === $foreignKeyName) ->filter(fn ($column) => $column['name'] === $foreignKeyName)
@ -158,8 +158,8 @@ class TableRLSManager implements RLSPolicyManager
return [ return [
'foreignKey' => $foreignKeyName, 'foreignKey' => $foreignKeyName,
'foreignTable' => $foreignKey['foreign_table'], 'foreignTable' => $constraint['foreign_table'],
'foreignId' => $foreignKey['foreign_columns'][0], 'foreignId' => $constraint['foreign_columns'][0],
// Internal metadata omitted in shortestPaths() // Internal metadata omitted in shortestPaths()
'comment' => $comment, 'comment' => $comment,
'nullable' => $columnIsNullable, 'nullable' => $columnIsNullable,
@ -176,7 +176,7 @@ class TableRLSManager implements RLSPolicyManager
* @param string $table The table to find a path from * @param string $table The table to find a path from
* @param array &$cachedPaths Reference to array where discovered shortest paths are cached (including dead ends) * @param array &$cachedPaths Reference to array where discovered shortest paths are cached (including dead ends)
* @param array $visitedTables Already visited tables (used for detecting recursive relationships) * @param array $visitedTables Already visited tables (used for detecting recursive relationships)
* @return array Paths with 'steps' (arrays of formatted foreign keys), 'dead_end' flag (bool), and 'recursive_relationship' flag (bool). * @return array Paths with 'steps' (arrays of formatted constraints), 'dead_end' flag (bool), and 'recursive_relationship' flag (bool).
*/ */
protected function shortestPathToTenantsTable( protected function shortestPathToTenantsTable(
string $table, string $table,
@ -195,57 +195,58 @@ class TableRLSManager implements RLSPolicyManager
return $cachedPaths[$table]; return $cachedPaths[$table];
} }
$foreignKeys = $this->getForeignKeys($table); $constraints = $this->getConstraints($table);
if (empty($foreignKeys)) { if (empty($constraints)) {
// Dead end // Dead end
$cachedPaths[$table] = $this->buildPath(deadEnd: true); $cachedPaths[$table] = $this->buildPath(deadEnd: true);
return $cachedPaths[$table]; return $cachedPaths[$table];
} }
return $this->determineShortestPath($table, $foreignKeys, $cachedPaths, $visitedTables); return $this->determineShortestPath($table, $constraints, $cachedPaths, $visitedTables);
} }
/** /**
* Get all valid foreign key relationships for a table. * Get all valid relationship constraints for a table.
* Combines both standard foreign key constraints and comment-based constraints. *
* Combines both standard foreign key constraints and comment constraints.
*/ */
protected function getForeignKeys(string $table): array protected function getConstraints(string $table): array
{ {
$constraints = array_merge( $constraints = array_merge(
$this->database->getSchemaBuilder()->getForeignKeys($table), $this->database->getSchemaBuilder()->getForeignKeys($table),
$this->getCommentConstraints($table) $this->getCommentConstraints($table)
); );
$foreignKeys = []; $validConstraints = [];
foreach ($constraints as $constraint) { foreach ($constraints as $constraint) {
$formatted = $this->formatForeignKey($constraint, $table); $formattedConstraint = $this->formatConstraint($constraint, $table);
if (! $this->shouldSkipPathLeadingThrough($formatted)) { if (! $this->shouldSkipPathLeadingThrough($formattedConstraint)) {
$foreignKeys[] = $formatted; $validConstraints[] = $formattedConstraint;
} }
} }
return $foreignKeys; return $validConstraints;
} }
/** /**
* Determine if a path leading through the passed foreign key * Determine if a path leading through the passed constraint
* should be excluded from choosing the shortest path * should be excluded from choosing the shortest path
* based on the foreign key's comment. * based on the constraint's comment.
* *
* If static::$scopeByDefault is true, only skip paths leading through foreign keys flagged with the 'no-rls' comment. * If static::$scopeByDefault is true, only skip paths leading through constraints flagged with the 'no-rls' comment.
* If static::$scopeByDefault is false, skip paths leading through any foreign key, unless the key has explicit 'rls' or 'rls table.column' comments. * If static::$scopeByDefault is false, skip paths leading through any constraint, unless the key has explicit 'rls' or 'rls table.column' comments.
* *
* @param array $foreignKey Formatted foreign key (has to have the 'comment' key) * @param array $constraint Formatted constraint (has to have the 'comment' key)
*/ */
protected function shouldSkipPathLeadingThrough(array $foreignKey): bool protected function shouldSkipPathLeadingThrough(array $constraint): bool
{ {
$comment = $foreignKey['comment'] ?? null; $comment = $constraint['comment'] ?? null;
// Always skip foreign keys with the 'no-rls' comment // Always skip constraints with the 'no-rls' comment
if ($comment === 'no-rls') { if ($comment === 'no-rls') {
return true; return true;
} }
@ -254,7 +255,7 @@ class TableRLSManager implements RLSPolicyManager
return false; return false;
} }
// When scopeByDefault is false, skip every foreign key // When scopeByDefault is false, skip every constraint
// with a comment that doesn't start with 'rls'. // with a comment that doesn't start with 'rls'.
if (! is_string($comment)) { if (! is_string($comment)) {
return true; return true;
@ -264,21 +265,24 @@ class TableRLSManager implements RLSPolicyManager
} }
/** /**
* Retrieve table's comment-based constraints. These are columns with comments * Retrieve table's comment constraints.
*
* Comment constraints are columns with comments
* formatted like "rls <foreign_table>.<foreign_column>". * formatted like "rls <foreign_table>.<foreign_column>".
* *
* Returns the constraints as unformatted foreign key arrays, ready to be formatted by formatForeignKey(). * Returns the comment constraints as unformatted constraint arrays,
* ready to be formatted by formatConstraint().
*/ */
protected function getCommentConstraints(string $tableName): array protected function getCommentConstraints(string $tableName): array
{ {
$commentConstraintColumns = array_filter($this->database->getSchemaBuilder()->getColumns($tableName), function ($column) { $commentConstraints = array_filter($this->database->getSchemaBuilder()->getColumns($tableName), function ($column) {
return (isset($column['comment']) && is_string($column['comment'])) return (isset($column['comment']) && is_string($column['comment']))
&& Str::startsWith($column['comment'], 'rls '); && Str::startsWith($column['comment'], 'rls ');
}); });
return array_map( return array_map(
fn ($column) => $this->parseCommentConstraint($column['comment'], $tableName, $column['name']), fn ($column) => $this->parseCommentConstraint($column['comment'], $tableName, $column['name']),
$commentConstraintColumns $commentConstraints
); );
} }
@ -287,7 +291,7 @@ class TableRLSManager implements RLSPolicyManager
* *
* This method validates that the table and column referenced * This method validates that the table and column referenced
* in the comment exist, and returns the constraint in a format corresponding to the * in the comment exist, and returns the constraint in a format corresponding to the
* standardly retrieved foreign keys (ready to be formatted using formatForeignKey()). * standardly retrieved constraints (ready to be formatted using formatConstraint()).
* *
* @throws RLSCommentConstraintException When comment format is invalid or references don't exist * @throws RLSCommentConstraintException When comment format is invalid or references don't exist
*/ */
@ -393,7 +397,7 @@ class TableRLSManager implements RLSPolicyManager
/** /**
* Find the optimal path from a table to the tenants table. * Find the optimal path from a table to the tenants table.
* *
* Gathers table's constraints (both foreign keys and comment-based constraints) * Gathers table's constraints (both foreign key constraints and comment constraints)
* and recursively finds paths through each constraint while tracking both * and recursively finds paths through each constraint while tracking both
* the overall shortest path and the shortest non-nullable * the overall shortest path and the shortest non-nullable
* path (non-nullable paths are preferred for reliability). * path (non-nullable paths are preferred for reliability).
@ -406,14 +410,14 @@ class TableRLSManager implements RLSPolicyManager
* falling back to the overall shortest path. * falling back to the overall shortest path.
* *
* @param string $table The table to find a path from * @param string $table The table to find a path from
* @param array $foreignKeys Array of foreign key relationships to explore * @param array $constraints Array of formatted constraints
* @param array &$cachedPaths Reference to caching array for memoization * @param array &$cachedPaths Reference to caching array for memoization
* @param array $visitedTables Tables already visited in this path (used for detecting recursion) * @param array $visitedTables Tables already visited in this path (used for detecting recursion)
* @return array Path with 'steps' array, 'dead_end' flag, and 'recursive_relationship' flag * @return array Path with 'steps' array, 'dead_end' flag, and 'recursive_relationship' flag
*/ */
protected function determineShortestPath( protected function determineShortestPath(
string $table, string $table,
array $foreignKeys, array $constraints,
array &$cachedPaths, array &$cachedPaths,
array $visitedTables array $visitedTables
): array { ): array {
@ -422,31 +426,31 @@ class TableRLSManager implements RLSPolicyManager
$hasRecursiveRelationships = false; $hasRecursiveRelationships = false;
$hasValidPaths = false; $hasValidPaths = false;
foreach ($foreignKeys as $foreign) { foreach ($constraints as $constraint) {
// Check if this specific foreign key would lead to recursion // Check if the constraint would lead to recursion
if (in_array($foreign['foreignTable'], $visitedTables)) { if (in_array($constraint['foreignTable'], $visitedTables)) {
// This foreign key leads to a table we're already visiting - skip it // This constraint leads to a table we've already visited - skip it
$hasRecursiveRelationships = true; $hasRecursiveRelationships = true;
continue; continue;
} }
// Recursive call // Recursive call
$foreignPath = $this->shortestPathToTenantsTable( $pathThroughConstraint = $this->shortestPathToTenantsTable(
$foreign['foreignTable'], $constraint['foreignTable'],
$cachedPaths, $cachedPaths,
$visitedTables $visitedTables
); );
if ($foreignPath['recursive_relationship']) { if ($pathThroughConstraint['recursive_relationship']) {
$hasRecursiveRelationships = true; $hasRecursiveRelationships = true;
continue; continue;
} }
if (! $foreignPath['dead_end']) { if (! $pathThroughConstraint['dead_end']) {
$hasValidPaths = true; $hasValidPaths = true;
// Build the full path with the current foreign key as the first step // Build the full path with the current constraint as the first step
$path = $this->buildPath(steps: array_merge([$foreign], $foreignPath['steps'])); $path = $this->buildPath(steps: array_merge([$constraint], $pathThroughConstraint['steps']));
if ($this->isPathPreferable($path, $shortestPath)) { if ($this->isPathPreferable($path, $shortestPath)) {
$shortestPath = $path; $shortestPath = $path;