pgalchemy 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pgalchemy-0.1.2/LICENSE +21 -0
- pgalchemy-0.1.2/PKG-INFO +83 -0
- pgalchemy-0.1.2/README.md +67 -0
- pgalchemy-0.1.2/pgalchemy/__init__.py +2 -0
- pgalchemy-0.1.2/pgalchemy/alembic/__init__.py +1 -0
- pgalchemy-0.1.2/pgalchemy/alembic/comparator.py +39 -0
- pgalchemy-0.1.2/pgalchemy/alembic/operations/__init__.py +1 -0
- pgalchemy-0.1.2/pgalchemy/alembic/operations/rls.py +57 -0
- pgalchemy-0.1.2/pgalchemy/policy.py +43 -0
- pgalchemy-0.1.2/pgalchemy/policy_sql.py +56 -0
- pgalchemy-0.1.2/pgalchemy/sqlalchemy.py +66 -0
- pgalchemy-0.1.2/pyproject.toml +21 -0
pgalchemy-0.1.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Matthew Beatty
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pgalchemy-0.1.2/PKG-INFO
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pgalchemy
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary:
|
|
5
|
+
Author: Matthew Beatty
|
|
6
|
+
Author-email: beattyml1@gmail.com
|
|
7
|
+
Requires-Python: >=3.11,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Requires-Dist: alembic (>=1.13.2,<2.0.0)
|
|
12
|
+
Requires-Dist: alembic-utils (>=0.8.4,<0.9.0)
|
|
13
|
+
Requires-Dist: sqlalchemy (>=2.0.32,<3.0.0)
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# pg-rls-sqlalchemy
|
|
17
|
+
|
|
18
|
+
Work in progress.
|
|
19
|
+
|
|
20
|
+
SQLAlchemy and Alembic support for Postgres features like:
|
|
21
|
+
- Row Level Security (RLS)
|
|
22
|
+
- Policies
|
|
23
|
+
|
|
24
|
+
Built on top of alembic_utils but provides a more usable interface and a few missing features
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```shell
|
|
29
|
+
pip install pg-rls-sqlalchemy
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
OR
|
|
33
|
+
|
|
34
|
+
```shell
|
|
35
|
+
poetry add pg-rls-sqlalchemy
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
### Using RLS BaseModel
|
|
41
|
+
Recommended most projects. This is for projects with majority of tables using RLS which will also be almost all new projects using this library.
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
|
|
45
|
+
from sqlalchemy.orm import declarative_base
|
|
46
|
+
from pg_rls import rls_base, policy, Policy, PolicyType, PolicyCommands
|
|
47
|
+
|
|
48
|
+
BaseModel = rls_base(declarative_base())
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@policy(Policy("pol_my_models_select_primary", as_=PolicyType.PERMISSIVE, for_=PolicyCommands.SELECT, using="user_id == auth.uid()"))
|
|
52
|
+
@policy(Policy("pol_my_models_delete_primary", as_=PolicyType.PERMISSIVE, for_=PolicyCommands.DELETE, using="user_id == auth.uid()"))
|
|
53
|
+
@policy(Policy("pol_my_models_update_primary", as_=PolicyType.PERMISSIVE, for_=PolicyCommands.UPDATE, using="user_id == auth.uid()", with_check="user_id == auth.uid()"))
|
|
54
|
+
@policy(Policy("pol_my_models_update_primary", as_=PolicyType.PERMISSIVE, for_=PolicyCommands.INSERT, with_check="user_id == auth.uid()"))
|
|
55
|
+
# Equivalent to:
|
|
56
|
+
# @policy(Policy("pol_my_models_primary", as_=PolicyType.PERMISSIVE, for_=PolicyCommands.ALL, using="user_id == auth.uid()", with_check="user_id == auth.uid()"))
|
|
57
|
+
class MyModel(BaseModel):
|
|
58
|
+
...
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Using RLS Decorator
|
|
62
|
+
Only intended for projects with majority of tables without RLS enabled. Usually only for existing projects with most tables not protected using RLS that are only using RLS for a niche use case
|
|
63
|
+
|
|
64
|
+
This is not recommended for other use cases as it makes it easy for a developer to forget to enable RLS and expose a security vulnerability.
|
|
65
|
+
```python
|
|
66
|
+
|
|
67
|
+
from sqlalchemy.orm import declarative_base
|
|
68
|
+
from pg_rls import rls, policy, Policy, PolicyType, PolicyCommands
|
|
69
|
+
|
|
70
|
+
BaseModel = declarative_base()
|
|
71
|
+
|
|
72
|
+
@rls()
|
|
73
|
+
@policy(Policy("pol_my_models_select_primary", as_=PolicyType.PERMISSIVE, for_=PolicyCommands.SELECT, using="user_id == auth.uid()"))
|
|
74
|
+
@policy(Policy("pol_my_models_delete_primary", as_=PolicyType.PERMISSIVE, for_=PolicyCommands.DELETE, using="user_id == auth.uid()"))
|
|
75
|
+
@policy(Policy("pol_my_models_update_primary", as_=PolicyType.PERMISSIVE, for_=PolicyCommands.UPDATE, using="user_id == auth.uid()", with_check="user_id == auth.uid()"))
|
|
76
|
+
@policy(Policy("pol_my_models_update_primary", as_=PolicyType.PERMISSIVE, for_=PolicyCommands.INSERT, with_check="user_id == auth.uid()"))
|
|
77
|
+
# Equivalent to:
|
|
78
|
+
# @policy(Policy("pol_my_models_primary", as_=PolicyType.PERMISSIVE, for_=PolicyCommands.ALL, using="user_id == auth.uid()", with_check="user_id == auth.uid()"))
|
|
79
|
+
class MyModel(BaseModel):
|
|
80
|
+
...
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# pg-rls-sqlalchemy
|
|
2
|
+
|
|
3
|
+
Work in progress.
|
|
4
|
+
|
|
5
|
+
SQLAlchemy and Alembic support for Postgres features like:
|
|
6
|
+
- Row Level Security (RLS)
|
|
7
|
+
- Policies
|
|
8
|
+
|
|
9
|
+
Built on top of alembic_utils but provides a more usable interface and a few missing features
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```shell
|
|
14
|
+
pip install pg-rls-sqlalchemy
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
OR
|
|
18
|
+
|
|
19
|
+
```shell
|
|
20
|
+
poetry add pg-rls-sqlalchemy
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
### Using RLS BaseModel
|
|
26
|
+
Recommended most projects. This is for projects with majority of tables using RLS which will also be almost all new projects using this library.
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
|
|
30
|
+
from sqlalchemy.orm import declarative_base
|
|
31
|
+
from pg_rls import rls_base, policy, Policy, PolicyType, PolicyCommands
|
|
32
|
+
|
|
33
|
+
BaseModel = rls_base(declarative_base())
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@policy(Policy("pol_my_models_select_primary", as_=PolicyType.PERMISSIVE, for_=PolicyCommands.SELECT, using="user_id == auth.uid()"))
|
|
37
|
+
@policy(Policy("pol_my_models_delete_primary", as_=PolicyType.PERMISSIVE, for_=PolicyCommands.DELETE, using="user_id == auth.uid()"))
|
|
38
|
+
@policy(Policy("pol_my_models_update_primary", as_=PolicyType.PERMISSIVE, for_=PolicyCommands.UPDATE, using="user_id == auth.uid()", with_check="user_id == auth.uid()"))
|
|
39
|
+
@policy(Policy("pol_my_models_update_primary", as_=PolicyType.PERMISSIVE, for_=PolicyCommands.INSERT, with_check="user_id == auth.uid()"))
|
|
40
|
+
# Equivalent to:
|
|
41
|
+
# @policy(Policy("pol_my_models_primary", as_=PolicyType.PERMISSIVE, for_=PolicyCommands.ALL, using="user_id == auth.uid()", with_check="user_id == auth.uid()"))
|
|
42
|
+
class MyModel(BaseModel):
|
|
43
|
+
...
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Using RLS Decorator
|
|
47
|
+
Only intended for projects with majority of tables without RLS enabled. Usually only for existing projects with most tables not protected using RLS that are only using RLS for a niche use case
|
|
48
|
+
|
|
49
|
+
This is not recommended for other use cases as it makes it easy for a developer to forget to enable RLS and expose a security vulnerability.
|
|
50
|
+
```python
|
|
51
|
+
|
|
52
|
+
from sqlalchemy.orm import declarative_base
|
|
53
|
+
from pg_rls import rls, policy, Policy, PolicyType, PolicyCommands
|
|
54
|
+
|
|
55
|
+
BaseModel = declarative_base()
|
|
56
|
+
|
|
57
|
+
@rls()
|
|
58
|
+
@policy(Policy("pol_my_models_select_primary", as_=PolicyType.PERMISSIVE, for_=PolicyCommands.SELECT, using="user_id == auth.uid()"))
|
|
59
|
+
@policy(Policy("pol_my_models_delete_primary", as_=PolicyType.PERMISSIVE, for_=PolicyCommands.DELETE, using="user_id == auth.uid()"))
|
|
60
|
+
@policy(Policy("pol_my_models_update_primary", as_=PolicyType.PERMISSIVE, for_=PolicyCommands.UPDATE, using="user_id == auth.uid()", with_check="user_id == auth.uid()"))
|
|
61
|
+
@policy(Policy("pol_my_models_update_primary", as_=PolicyType.PERMISSIVE, for_=PolicyCommands.INSERT, with_check="user_id == auth.uid()"))
|
|
62
|
+
# Equivalent to:
|
|
63
|
+
# @policy(Policy("pol_my_models_primary", as_=PolicyType.PERMISSIVE, for_=PolicyCommands.ALL, using="user_id == auth.uid()", with_check="user_id == auth.uid()"))
|
|
64
|
+
class MyModel(BaseModel):
|
|
65
|
+
...
|
|
66
|
+
```
|
|
67
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .operations import EnableRlsOp, DisableRlsOp, DropPolicyOp, CreatePolicyOp, AlterPolicyOp, RenamePolicyOp, PolicySql
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from alembic.autogenerate import comparators
|
|
2
|
+
from alembic.operations import Operations, MigrateOperation
|
|
3
|
+
from sqlalchemy import Table
|
|
4
|
+
|
|
5
|
+
from pg_rls.sqlalchemy import RlsData
|
|
6
|
+
from .operations import EnableRlsOp, DisableRlsOp
|
|
7
|
+
from .. import Policy
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@comparators.dispatch_for("table")
|
|
11
|
+
def compare_rls(autogen_context, modify_ops, schemaname, tablename, conn_table, metadata_table: Table):
|
|
12
|
+
rls: RlsData = metadata_table.info.get('rls')
|
|
13
|
+
|
|
14
|
+
db_table, rls_enabled_db = get_table_rls_data(autogen_context, schemaname, tablename)
|
|
15
|
+
if db_table is None:
|
|
16
|
+
return
|
|
17
|
+
|
|
18
|
+
compare_rls_enabled(modify_ops, rls, rls_enabled_db, schemaname, tablename)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def compare_rls_enabled(modify_ops, rls, rls_enabled_db, schemaname, tablename):
|
|
22
|
+
if rls.active is True and rls_enabled_db is False:
|
|
23
|
+
modify_ops.ops.append(
|
|
24
|
+
EnableRlsOp(tablename, schema=schemaname)
|
|
25
|
+
)
|
|
26
|
+
if rls.active is False and rls_enabled_db is True:
|
|
27
|
+
modify_ops.ops.append(
|
|
28
|
+
DisableRlsOp(tablename, schema=schemaname)
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_table_rls_data(autogen_context, schemaname, tablename):
|
|
33
|
+
results = autogen_context.connection.execute(
|
|
34
|
+
'select relname, relrowsecurity, relforcerowsecurity from pg_class where relnamespace = %s and relname = %s;',
|
|
35
|
+
(schemaname, tablename)
|
|
36
|
+
)
|
|
37
|
+
db_table = results.fetchone()
|
|
38
|
+
rls_enabled_db = db_table['relrowsecurity'] if db_table is not None else None
|
|
39
|
+
return db_table, rls_enabled_db
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .rls import DisableRlsOp, EnableRlsOp
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from alembic.operations import MigrateOperation, Operations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@Operations.register_operation("enable_rls")
|
|
5
|
+
class EnableRlsOp(MigrateOperation):
|
|
6
|
+
"""Enable RLS on a table."""
|
|
7
|
+
|
|
8
|
+
def __init__(self, table_name, schema=None):
|
|
9
|
+
self.table_name = table_name
|
|
10
|
+
self.schema = schema
|
|
11
|
+
|
|
12
|
+
@classmethod
|
|
13
|
+
def enable_rls(cls, operations, table_name, **kw):
|
|
14
|
+
"""Issue a "ALTER TABLE ENABLE ROW LEVEL SECURITY" instruction."""
|
|
15
|
+
|
|
16
|
+
op = EnableRlsOp(table_name, **kw)
|
|
17
|
+
return operations.invoke(op)
|
|
18
|
+
|
|
19
|
+
def reverse(self):
|
|
20
|
+
# only needed to support autogenerate
|
|
21
|
+
return DisableRlsOp(self.table_name, schema=self.schema)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@Operations.register_operation("disable_rls")
|
|
25
|
+
class DisableRlsOp(MigrateOperation):
|
|
26
|
+
"""Disable RLS on table."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, table_name, schema=None):
|
|
29
|
+
self.table_name = table_name
|
|
30
|
+
self.schema = schema
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def disable_rls(cls, operations, sequence_name, **kw):
|
|
34
|
+
"""Issue a "ALTER TABLE DISABLE ROW LEVEL SECURITY" instruction."""
|
|
35
|
+
|
|
36
|
+
op = DisableRlsOp(sequence_name, **kw)
|
|
37
|
+
return operations.invoke(op)
|
|
38
|
+
|
|
39
|
+
def reverse(self):
|
|
40
|
+
# only needed to support autogenerate
|
|
41
|
+
return EnableRlsOp(self.sequence_name, schema=self.schema)
|
|
42
|
+
|
|
43
|
+
@Operations.implementation_for(EnableRlsOp)
|
|
44
|
+
def enable_rls(operations, operation: EnableRlsOp):
|
|
45
|
+
if operation.schema is not None:
|
|
46
|
+
name = "%s.%s" % (operation.schema, operation.table_name)
|
|
47
|
+
else:
|
|
48
|
+
name = operation.table_name
|
|
49
|
+
operations.execute("ALTER TABLE %s ENABLE ROW LEVEL SECURITY;" % name)
|
|
50
|
+
|
|
51
|
+
@Operations.implementation_for(DisableRlsOp)
|
|
52
|
+
def disable_rls(operations, operation: DisableRlsOp):
|
|
53
|
+
if operation.schema is not None:
|
|
54
|
+
name = "%s.%s" % (operation.schema, operation.table_name)
|
|
55
|
+
else:
|
|
56
|
+
name = operation.table_name
|
|
57
|
+
operations.execute("ALTER TABLE %s DISABLE ROW LEVEL SECURITY;" % name)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
from alembic_utils.pg_policy import PGPolicy
|
|
4
|
+
from sqlalchemy import Table, BinaryExpression
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PolicyType(Enum):
|
|
8
|
+
PERMISSIVE = 'PERMISSIVE'
|
|
9
|
+
RESTRICTIVE = 'RESTRICTIVE'
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PolicyCommands(Enum):
|
|
13
|
+
ALL = 'ALL'
|
|
14
|
+
SELECT = 'SELECT'
|
|
15
|
+
INSERT = 'INSERT'
|
|
16
|
+
UPDATE = 'UPDATE'
|
|
17
|
+
DELETE = 'DELETE'
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Policy:
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
name: str,
|
|
24
|
+
as_: PolicyType = PolicyType.PERMISSIVE,
|
|
25
|
+
for_: PolicyCommands = PolicyCommands.ALL,
|
|
26
|
+
using: str | BinaryExpression | None = None,
|
|
27
|
+
with_check: str | BinaryExpression | None = None
|
|
28
|
+
):
|
|
29
|
+
self.name = name
|
|
30
|
+
self.with_check = str(with_check)
|
|
31
|
+
self.using = str(using)
|
|
32
|
+
self.for_ = for_
|
|
33
|
+
self.as_ = as_
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def from_pg_policy(cls, row):
|
|
37
|
+
return Policy(
|
|
38
|
+
name=row['policyname'],
|
|
39
|
+
as_=PolicyType(row['permissive']),
|
|
40
|
+
for_=PolicyCommands(row['cmd']),
|
|
41
|
+
using=row['qual'],
|
|
42
|
+
with_check=row['with_check'],
|
|
43
|
+
)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from sqlalchemy import Table
|
|
2
|
+
|
|
3
|
+
from .policy import Policy
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PolicySql:
|
|
7
|
+
def __init__(self, policy: Policy, table_name: str, schema: str):
|
|
8
|
+
self.policy = policy
|
|
9
|
+
self.table_name = table_name
|
|
10
|
+
self.schema = schema
|
|
11
|
+
|
|
12
|
+
def qualify(self, name):
|
|
13
|
+
return f"{self.schema}.{name}"
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def tab(self):
|
|
17
|
+
return self.qualify(self.table_name)
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def pol(self):
|
|
21
|
+
return self.qualify(self.policy.name)
|
|
22
|
+
|
|
23
|
+
# Full
|
|
24
|
+
def rename_sql(self, old_name):
|
|
25
|
+
return f"alter policy {old_name} on {self.tab} rename to {self.pol}"
|
|
26
|
+
|
|
27
|
+
def drop_sql(self):
|
|
28
|
+
return f"drop policy {self.pol} on {self.tab}"
|
|
29
|
+
|
|
30
|
+
def create_sql(self):
|
|
31
|
+
return self._create_fragment() + self._as_fragment() + self._from_fragment() + self._using_fragment() + self._with_check_fragment()
|
|
32
|
+
|
|
33
|
+
def alter_sql(self):
|
|
34
|
+
return self._alter_fragment() + self._using_fragment() + self._with_check_fragment()
|
|
35
|
+
|
|
36
|
+
# Fragments
|
|
37
|
+
def _create_fragment(self):
|
|
38
|
+
return f"create policy {self.pol} on {self.tab}\n"
|
|
39
|
+
|
|
40
|
+
def _alter_fragment(self):
|
|
41
|
+
return f"alter policy {self.pol} on {self.tab}\n"
|
|
42
|
+
|
|
43
|
+
def _as_fragment(self):
|
|
44
|
+
return f"as {self.policy.as_}\n" if self.policy.as_ else ""
|
|
45
|
+
|
|
46
|
+
def _from_fragment(self):
|
|
47
|
+
return f"for {self.policy.for_}\n" if self.policy.for_ else ""
|
|
48
|
+
|
|
49
|
+
def _with_check_fragment(self):
|
|
50
|
+
return f"with check {self.policy.with_check}\n" if self.policy.with_check else ""
|
|
51
|
+
|
|
52
|
+
def _using_fragment(self):
|
|
53
|
+
return f"using {self.policy.using}\n" if self.policy.using else ""
|
|
54
|
+
|
|
55
|
+
def definition_sql(self):
|
|
56
|
+
return self._as_fragment() + self._from_fragment() + self._using_fragment() + self._with_check_fragment()
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from typing import Type, List, Optional
|
|
2
|
+
|
|
3
|
+
from alembic_utils.pg_policy import PGPolicy
|
|
4
|
+
from sqlalchemy import Table
|
|
5
|
+
from sqlalchemy.event import listens_for
|
|
6
|
+
from sqlalchemy.orm import DeclarativeBase, Mapper
|
|
7
|
+
|
|
8
|
+
from .policy import Policy, PolicyType
|
|
9
|
+
from .policy_sql import PolicySql
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RlsData:
|
|
13
|
+
def __init__(self, active):
|
|
14
|
+
self.active = active
|
|
15
|
+
self.policies: List[Policy] = []
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def rls_base(Base: Type[DeclarativeBase], default_active: bool = True):
|
|
19
|
+
class WithRls(Base):
|
|
20
|
+
__rls__ = RlsData(default_active)
|
|
21
|
+
|
|
22
|
+
@listens_for(WithRls, 'after_configured')
|
|
23
|
+
def receive_mapper_configured(mapper: Mapper, class_: Type[WithRls]):
|
|
24
|
+
table: Table = mapper.mapped_table()
|
|
25
|
+
rls = getattr(class_, '__rls__')
|
|
26
|
+
table.info.setdefault('rls', rls)
|
|
27
|
+
if rls.active:
|
|
28
|
+
for policy in rls.policies:
|
|
29
|
+
attach_policy(policy, table)
|
|
30
|
+
|
|
31
|
+
return WithRls
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def rls(enabled=True, policies: Optional[List[Policy]] = None):
|
|
35
|
+
def wrapper(Model: Type[DeclarativeBase]):
|
|
36
|
+
Model.__rls__ = RlsData(enabled)
|
|
37
|
+
Model.__rls__.policies = policies or []
|
|
38
|
+
return Model
|
|
39
|
+
return wrapper
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def rls_for_table(enabled=True, policies: Optional[List[Policy]] = None):
|
|
43
|
+
def wrapper(table: Table):
|
|
44
|
+
data = RlsData(enabled)
|
|
45
|
+
data.policies = policies or []
|
|
46
|
+
table.info.setdefault('rls', data)
|
|
47
|
+
return Table
|
|
48
|
+
return wrapper
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def policy(pol: Policy):
|
|
52
|
+
def wrapper(Model: Type[DeclarativeBase]):
|
|
53
|
+
if not Model.__rls__:
|
|
54
|
+
Model.__rls__ = RlsData(True)
|
|
55
|
+
Model.__rls__.policies.append(pol)
|
|
56
|
+
return Model
|
|
57
|
+
return wrapper
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def attach_policy(policy: Policy, table: Table):
|
|
61
|
+
return PGPolicy(
|
|
62
|
+
on_entity=table.name,
|
|
63
|
+
schema=table.schema,
|
|
64
|
+
signature=policy.name,
|
|
65
|
+
definition=PolicySql(policy, table.name, table.schema)
|
|
66
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "pgalchemy"
|
|
3
|
+
version = "0.1.2"
|
|
4
|
+
description = ""
|
|
5
|
+
authors = ["Matthew Beatty <beattyml1@gmail.com>"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
include = [{path="README.md"}, {path="LICENSE"}]
|
|
8
|
+
packages = [
|
|
9
|
+
{ include = "pgalchemy", from = "." },
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[tool.poetry.dependencies]
|
|
13
|
+
python = "^3.11"
|
|
14
|
+
sqlalchemy = "^2.0.32"
|
|
15
|
+
alembic = "^1.13.2"
|
|
16
|
+
alembic-utils = "^0.8.4"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
[build-system]
|
|
20
|
+
requires = ["poetry-core"]
|
|
21
|
+
build-backend = "poetry.core.masonry.api"
|