mirror of
https://github.com/archtechx/tenancy.git
synced 2026-02-05 02:54:03 +00:00
Correct terminology and comments in TableRLSManager
This commit is contained in:
parent
8d37415fc8
commit
9b34a8cdf3
1 changed files with 55 additions and 51 deletions
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue