Skip to content

Commit 244f288

Browse files
committed
feat(backends): adds grant on table and sequences to roles
1 parent 6d836b3 commit 244f288

File tree

6 files changed

+159
-31
lines changed

6 files changed

+159
-31
lines changed

db_adapter/db/backends/base/operations.py

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,32 @@
33
from django.db.utils import ProgrammingError
44
from django.utils.functional import cached_property
55

6-
from db_adapter.name_builders import ObjectNameBuilder
6+
from db_adapter.settings import db_settings
77
from db_adapter.utils import enforce_model, enforce_model_fields
88

99

1010
class DatabaseOperations:
11+
# Overrideable SQL statements
1112
sql_create_sequence = None
1213
sql_create_trigger = None
14+
sql_grant = 'GRANT %(privileges)s ON %(name)s TO %(role)s'
1315

14-
name_builder_class = ObjectNameBuilder
16+
# Setting variables
17+
role_name = db_settings.DEFAULT_ROLE_NAME
18+
name_builder_class = db_settings.DEFAULT_NAME_BUILDER_CLASS
19+
default_object_privileges = db_settings.DEFAULT_OBJECT_PRIVILEGES
1520

1621
def autoinc_sql(self, table, column):
1722
if not self.sql_create_sequence and not self.sql_create_trigger:
1823
return None
1924

2025
model, field = self._enforce_model_field_instances(table, column)
26+
sequence_name = self._get_sequence_name(model, field)
27+
trigger_name = self._get_trigger_name(model, field)
2128

2229
args = {
23-
'sq_name': self._get_sequence_name(model, field),
24-
'tr_name': self._get_trigger_name(model, field),
30+
'sq_name': sequence_name,
31+
'tr_name': trigger_name,
2532
'tbl_name': self.quote_name(table),
2633
'col_name': self.quote_name(column),
2734
}
@@ -33,16 +40,30 @@ def autoinc_sql(self, table, column):
3340
pass
3441

3542
try:
36-
sequence_sql = self.sql_create_sequence % args
43+
sequence_sql = self._get_sequence_sql(sequence_name, args)
3744
trigger_sql = self.sql_create_trigger % args
38-
return sequence_sql, trigger_sql
45+
return [*sequence_sql, trigger_sql]
3946
except KeyError as err:
4047
if 'sq_max_value' in err.args:
4148
raise ProgrammingError(
4249
'Cannot retrieve the range of the column type bound to the '
4350
'field %s' % field.name
4451
)
4552

53+
def control_sql(self, name, privileges=None):
54+
if not self.role_name:
55+
return None
56+
57+
privileges = (
58+
self.default_object_privileges if privileges is None else privileges
59+
)
60+
61+
return self.sql_grant % dict(
62+
name=self.quote_name(name),
63+
privileges=', '.join(privileges),
64+
role=self.quote_name(self.role_name),
65+
)
66+
4667
@cached_property
4768
def name_builder(self):
4869
return self.name_builder_class()
@@ -63,3 +84,12 @@ def _get_trigger_name(self, table, column=''):
6384
model, field = self._enforce_model_field_instances(table, column)
6485
name = self.name_builder.process_name(model, [field], type='trigger')
6586
return self.quote_name(name)
87+
88+
def _get_sequence_sql(self, name, args):
89+
sequence_sql = [self.sql_create_sequence % args]
90+
91+
grant_sql = self.control_sql(name, privileges=['SELECT'])
92+
if grant_sql:
93+
sequence_sql.append(grant_sql)
94+
95+
return sequence_sql

db_adapter/db/backends/base/schema.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ class DatabaseSchemaEditor:
1717
renaming, index fiddling, and so on.
1818
"""
1919

20-
# Deferred column SQL order
2120
deferred_sql_order = (
2221
'PRIMARY KEY',
2322
'UNIQUE',
2423
'FOREIGN KEY',
2524
'CHECK',
2625
'INDEX',
2726
'COMMENT',
27+
'CONTROL',
2828
'SEQUENCE',
2929
'TRIGGER',
3030
)
@@ -47,6 +47,7 @@ class DatabaseSchemaEditor:
4747
'_idx': 'index',
4848
}
4949

50+
# Setting variables
5051
name_builder_class = db_settings.DEFAULT_NAME_BUILDER_CLASS
5152

5253
def __init__(self, *args, **kwargs):
@@ -144,8 +145,8 @@ def column_sql(
144145
model._meta.db_table, field.column
145146
)
146147
if autoinc_sql:
147-
sequence_sql, trigger_sql = autoinc_sql
148-
self.deferred_column_sql['SEQUENCE'].append(sequence_sql)
148+
*sequence_sql, trigger_sql = autoinc_sql
149+
self.deferred_column_sql['SEQUENCE'].extend(sequence_sql)
149150
self.deferred_column_sql['TRIGGER'].append(trigger_sql)
150151

151152
# Comment columns for fields with help_text
@@ -191,6 +192,11 @@ def table_sql(self, model: Model) -> Tuple[str, list]:
191192
# _remake_table needs it)
192193
self.deferred_table_sql['INDEX'].extend(self._model_indexes_sql(model))
193194

195+
# Grant/Revoke object privileges
196+
control_sql = self.connection.ops.control_sql(model._meta.db_table)
197+
if control_sql:
198+
self.deferred_table_sql['CONTROL'].append(control_sql)
199+
194200
return sql, params
195201

196202
def create_model(self, model: Model):

db_adapter/settings.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,17 @@
3636
'DEFAULT_UNIQUE_NAME': '{table}_{columns}_uniq',
3737
'DEFAULT_CHECK_NAME': '{table}_{columns}{qualifier}_check',
3838

39+
# Grant options
40+
'DEFAULT_ROLE_NAME': '',
41+
3942
# Base policies
4043
'DEFAULT_NAME_BUILDER_CLASS': 'db_adapter.name_builders.ObjectNameBuilder',
44+
'DEFAULT_OBJECT_PRIVILEGES': [
45+
'SELECT',
46+
'INSERT',
47+
'UPDATE',
48+
'DELETE',
49+
]
4150
}
4251
# fmt: on
4352

tests/connection.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def quote_name(self, name: str) -> str:
2020
return name
2121

2222

23-
class TestDatabaseOperationsWithSqls(TestDatabaseOperations):
23+
class TestDatabaseOperationsAutoincSql(TestDatabaseOperations):
2424
integer_field_ranges = {
2525
'SmallIntegerField': (-99999999999, 99999999999),
2626
'IntegerField': (-99999999999, 99999999999),
@@ -52,7 +52,11 @@ class TestDatabaseOperationsWithSqls(TestDatabaseOperations):
5252
'''
5353

5454

55-
class TestDatabaseOperationsOptionalRange(TestDatabaseOperationsWithSqls):
55+
class TestDatabaseOperationsControlSql(TestDatabaseOperationsAutoincSql):
56+
role_name = 'rl_tests'
57+
58+
59+
class TestDatabaseOperationsOptionalRange(TestDatabaseOperationsAutoincSql):
5660
sql_create_sequence = '''
5761
DECLARE
5862
i INTEGER;
@@ -71,7 +75,7 @@ class TestDatabaseSchemaEditor(DatabaseSchemaEditor, BaseDatabaseSchemaEditor):
7175

7276

7377
class TestDatabaseWrapper(DatabaseWrapper):
74-
ops_class = TestDatabaseOperationsWithSqls
78+
ops_class = TestDatabaseOperationsAutoincSql
7579
SchemaEditorClass = TestDatabaseSchemaEditor
7680

7781
data_types = {
@@ -119,4 +123,9 @@ class TestDatabaseWrapper(DatabaseWrapper):
119123
}
120124

121125

122-
test_connection = TestDatabaseWrapper(settings_dict={}, alias=DEFAULT_DB_ALIAS)
126+
class TestDatabaseWrapperControlSql(TestDatabaseWrapper):
127+
ops_class = TestDatabaseOperationsControlSql
128+
129+
130+
test_connection = TestDatabaseWrapper({}, DEFAULT_DB_ALIAS)
131+
test_control_connection = TestDatabaseWrapperControlSql({}, DEFAULT_DB_ALIAS)

tests/test_backends/test_base/test_operations.py

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,45 @@
33

44
from tests.connection import (
55
TestDatabaseOperations,
6+
TestDatabaseOperationsAutoincSql,
7+
TestDatabaseOperationsControlSql,
68
TestDatabaseOperationsOptionalRange,
7-
TestDatabaseOperationsWithSqls,
89
test_connection,
10+
test_control_connection,
911
)
1012

1113

14+
class SqlControlTests(TestCase):
15+
def test_control_sql(self):
16+
ops = TestDatabaseOperationsAutoincSql(test_connection)
17+
control_ops = TestDatabaseOperationsControlSql(test_control_connection)
18+
19+
sql_none = ops.control_sql('tbl_article')
20+
sql_default_privileges = control_ops.control_sql('tbl_article')
21+
sql_custom_privileges = control_ops.control_sql(
22+
'tbl_article_sq', privileges=['SELECT']
23+
)
24+
25+
self.assertIsNone(sql_none)
26+
self.assertEqual(
27+
sql_default_privileges,
28+
'GRANT SELECT, INSERT, UPDATE, DELETE ON tbl_article TO rl_tests',
29+
)
30+
self.assertEqual(
31+
sql_custom_privileges,
32+
'GRANT SELECT ON tbl_article_sq TO rl_tests',
33+
)
34+
35+
1236
class SqlAutoincTests(TestCase):
1337
def test_autoinc_sql_without_overwritten_sqls(self):
14-
ops = TestDatabaseOperations(connection=test_connection)
38+
ops = TestDatabaseOperations(test_connection)
1539
autoinc_sql = ops.autoinc_sql('tbl_article', 'article_id')
1640

1741
self.assertIsNone(autoinc_sql)
1842

1943
def test_autoinc_sql_with_overwritten_sqls(self):
20-
ops = TestDatabaseOperationsWithSqls(connection=test_connection)
44+
ops = TestDatabaseOperationsAutoincSql(test_connection)
2145
autoinc_sql = ops.autoinc_sql('tbl_article', 'article_id')
2246

2347
self.assertEqual(len(autoinc_sql), 2)
@@ -61,11 +85,11 @@ def test_autoinc_sql_for_required_integer_field_range(self):
6185
)
6286

6387
with self.assertRaises(ProgrammingError, msg=msg):
64-
ops = TestDatabaseOperationsWithSqls(connection=test_connection)
88+
ops = TestDatabaseOperationsAutoincSql(test_connection)
6589
ops.autoinc_sql('tbl_article', 'active')
6690

6791
def test_autoinc_sql_for_optional_integer_field_range(self):
68-
ops = TestDatabaseOperationsOptionalRange(connection=test_connection)
92+
ops = TestDatabaseOperationsOptionalRange(test_connection)
6993
autoinc_sql = ops.autoinc_sql('tbl_article', 'active')
7094

7195
self.assertEqual(len(autoinc_sql), 2)
@@ -84,3 +108,14 @@ def test_autoinc_sql_for_optional_integer_field_range(self):
84108
END IF;
85109
END''',
86110
)
111+
112+
def test_autoinc_sql_grant_sequence(self):
113+
ops = TestDatabaseOperationsControlSql(test_control_connection)
114+
autoinc_sql = ops.autoinc_sql('tbl_article', 'article_id')
115+
116+
self.assertEqual(len(autoinc_sql), 3)
117+
118+
_, grant_sequence_sql, _ = autoinc_sql
119+
self.assertEqual(
120+
grant_sequence_sql, 'GRANT SELECT ON tbl_article_sq TO rl_tests'
121+
)

0 commit comments

Comments
 (0)