Skip to content

Commit 447d340

Browse files
feat: support multiple user groups for anonymized data access
1 parent a243194 commit 447d340

File tree

11 files changed

+98
-45
lines changed

11 files changed

+98
-45
lines changed

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ class ReportView(AnonymizedDataMixin, ListView):
7171

7272
# Automatic Group-Based (Middleware)
7373
# Users in 'analysts' group see anonymized data everywhere
74-
POSTGRES_ANON = {'MASKED_GROUP': 'analysts'}
74+
POSTGRES_ANON = {'MASKED_GROUPS': ['analysts', 'qa_team']}
7575
```
7676

7777
**Installation**: `pip install django-postgres-anonymizer` → Add to `INSTALLED_APPS``python manage.py migrate`
@@ -351,7 +351,7 @@ MIDDLEWARE = [
351351

352352
POSTGRES_ANON = {
353353
'ENABLED': True,
354-
'MASKED_GROUP': 'analysts', # Users in this group see anonymized data
354+
'MASKED_GROUPS': ['analysts', 'qa_team'], # Users in these groups see anonymized data
355355
'DEFAULT_MASKED_ROLE': 'masked_reader',
356356
}
357357
```
@@ -781,7 +781,7 @@ Configuration follows [12-factor app principles](https://12factor.net/config). S
781781
```bash
782782
export POSTGRES_ANON_ENABLED=true
783783
export POSTGRES_ANON_DEFAULT_MASKED_ROLE=masked_reader
784-
export POSTGRES_ANON_MASKED_GROUP=view_masked_data
784+
export POSTGRES_ANON_MASKED_GROUPS=analysts,data_scientists,external_auditors
785785
export POSTGRES_ANON_AUTO_APPLY_RULES=false
786786
export POSTGRES_ANON_VALIDATE_FUNCTIONS=true
787787
export POSTGRES_ANON_ALLOW_CUSTOM_FUNCTIONS=false
@@ -795,7 +795,7 @@ Configuration follows [12-factor app principles](https://12factor.net/config). S
795795
# Core settings
796796
'DEFAULT_MASKED_ROLE': 'masked_reader', # Default role for anonymization
797797
'ANONYMIZED_DATA_ROLE': 'masked_reader', # Role for anonymized_data()
798-
'MASKED_GROUP': 'masked_users', # Django group for middleware
798+
'MASKED_GROUPS': ['analysts', 'data_scientists', 'external_auditors'], # Django groups for middleware
799799

800800
# Behavior settings
801801
'ENABLED': True, # Enable anonymization features

django_postgres_anon/config.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def get_setting(key: str, default=None):
1313
# Default values (moved from constants.py to keep config self-contained)
1414
DEFAULTS = {
1515
"DEFAULT_MASKED_ROLE": "masked_reader",
16-
"MASKED_GROUP": "view_masked_data",
16+
"MASKED_GROUPS": ["view_masked_data"],
1717
"ANONYMIZED_DATA_ROLE": "masked_reader",
1818
"ENABLED": False,
1919
"AUTO_APPLY_RULES": False,
@@ -25,7 +25,7 @@ def get_setting(key: str, default=None):
2525
# Environment variable mappings (12-factor compliant)
2626
ENV_VAR_MAPPING = {
2727
"DEFAULT_MASKED_ROLE": "POSTGRES_ANON_DEFAULT_MASKED_ROLE",
28-
"MASKED_GROUP": "POSTGRES_ANON_MASKED_GROUP",
28+
"MASKED_GROUPS": "POSTGRES_ANON_MASKED_GROUPS",
2929
"ANONYMIZED_DATA_ROLE": "POSTGRES_ANON_ANONYMIZED_DATA_ROLE",
3030
"ENABLED": "POSTGRES_ANON_ENABLED",
3131
"AUTO_APPLY_RULES": "POSTGRES_ANON_AUTO_APPLY_RULES",
@@ -49,6 +49,9 @@ def get_anon_setting(key: str):
4949
# Handle boolean conversion for known boolean settings
5050
if key in ["ENABLED", "AUTO_APPLY_RULES", "VALIDATE_FUNCTIONS", "ALLOW_CUSTOM_FUNCTIONS", "ENABLE_LOGGING"]:
5151
return _parse_env_bool(env_value)
52+
# Handle comma-separated groups
53+
if key == "MASKED_GROUPS":
54+
return [group.strip() for group in env_value.split(",") if group.strip()]
5255
return env_value
5356

5457
# Fall back to Django settings

django_postgres_anon/middleware.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class AnonRoleMiddleware:
1414
"""
1515
Middleware for dynamic role switching based on user permissions.
1616
17-
Users in the ANON_MASKED_GROUP will see anonymized data automatically.
17+
Users in any of the ANON_MASKED_GROUPS will see anonymized data automatically.
1818
"""
1919

2020
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]) -> None:
@@ -26,10 +26,11 @@ def __call__(self, request: HttpRequest) -> HttpResponse:
2626
try:
2727
# Check if user should have data masked - defensive against user access issues
2828
try:
29+
masked_groups = get_anon_setting("MASKED_GROUPS")
2930
should_mask = (
3031
get_anon_setting("ENABLED")
3132
and request.user.is_authenticated
32-
and request.user.groups.filter(name=get_anon_setting("MASKED_GROUP")).exists()
33+
and request.user.groups.filter(name__in=masked_groups).exists()
3334
)
3435
except Exception:
3536
# If there's any issue with user/group access, default to no masking

example_project/.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ ANON_ENABLE_LOGGING=true
2828
ANON_BATCH_SIZE=1000
2929
ANON_USE_TRANSACTIONS=true
3030
ANON_DEFAULT_MASKED_ROLE=masked_reader
31-
ANON_MASKED_GROUP=view_masked_data
31+
ANON_MASKED_GROUPS=view_masked_data,analysts,qa_team
3232
ANONYMIZED_DATA_ROLE=masked_reader
3333

3434
# Logging

example_project/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,10 +239,11 @@ Use different settings for development, staging, and production:
239239

240240
```python
241241
# settings_production.py
242-
DJANGO_POSTGRES_ANON = {
242+
POSTGRES_ANON = {
243243
'AUTO_APPLY_RULES': False, # Never auto-apply in production
244244
'VALIDATE_FUNCTIONS': True, # Always validate
245245
'ALLOW_CUSTOM_FUNCTIONS': False, # Restrict to built-in functions
246+
'MASKED_GROUPS': ['qa_team', 'external_auditors'], # Multiple groups for production
246247
}
247248
```
248249

example_project/example_project/settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@
145145
"ALLOW_CUSTOM_FUNCTIONS": os.getenv("ANON_ALLOW_CUSTOM_FUNCTIONS", "False").lower() in ("true", "1", "yes"),
146146
"ENABLE_LOGGING": os.getenv("ANON_ENABLE_LOGGING", "True").lower() in ("true", "1", "yes"),
147147
"DEFAULT_MASKED_ROLE": os.getenv("ANON_DEFAULT_MASKED_ROLE", "masked_reader"),
148-
"MASKED_GROUP": os.getenv("ANON_MASKED_GROUP", "view_masked_data"),
148+
"MASKED_GROUPS": os.getenv("ANON_MASKED_GROUPS", "view_masked_data,analysts,qa_team").split(","),
149149
"ANONYMIZED_DATA_ROLE": os.getenv("ANONYMIZED_DATA_ROLE", "masked_reader"),
150150
}
151151

tests/settings.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,15 +103,15 @@
103103

104104

105105
# Django PostgreSQL Anonymizer Settings
106-
DJANGO_POSTGRES_ANON = {
106+
POSTGRES_ANON = {
107107
"DEFAULT_MASKED_ROLE": "test_masked_reader",
108+
"MASKED_GROUPS": ["view_masked_data"],
109+
"ANONYMIZED_DATA_ROLE": "test_masked_reader",
110+
"ENABLED": True,
108111
"AUTO_APPLY_RULES": False,
109-
"DEFAULT_PRESET": None,
110-
"ENABLE_LOGGING": True,
111112
"VALIDATE_FUNCTIONS": True,
112113
"ALLOW_CUSTOM_FUNCTIONS": False,
113-
"BATCH_SIZE": 100,
114-
"USE_TRANSACTIONS": True,
114+
"ENABLE_LOGGING": True,
115115
}
116116

117117
# Logging configuration for testing

tests/test_core.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ class TestAnonConfiguration:
275275
"prop",
276276
[
277277
"default_masked_role",
278-
"masked_group",
278+
"masked_groups",
279279
"anonymized_data_role",
280280
"enabled",
281281
"auto_apply_rules",
@@ -294,7 +294,7 @@ def test_config_has_essential_properties(self, prop):
294294
"prop",
295295
[
296296
"default_masked_role",
297-
"masked_group",
297+
"masked_groups",
298298
"anonymized_data_role",
299299
"enabled",
300300
"auto_apply_rules",

tests/test_edge_cases.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ def test_configuration_system_provides_reasonable_defaults():
332332
# Test: Access all configuration properties
333333
essential_properties = [
334334
"default_masked_role",
335-
"masked_group",
335+
"masked_groups",
336336
"anonymized_data_role",
337337
"enabled",
338338
"auto_apply_rules",

tests/test_middleware_database.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def test_middleware_switches_to_masked_role_successfully(
6161
def mock_setting(key):
6262
settings_map = {
6363
"ENABLED": True,
64-
"MASKED_GROUP": "view_masked_data",
64+
"MASKED_GROUPS": ["view_masked_data"],
6565
"DEFAULT_MASKED_ROLE": "masked_reader",
6666
}
6767
return settings_map.get(key)
@@ -90,7 +90,7 @@ def test_middleware_handles_role_switch_failure(self, request_factory, mock_get_
9090
def mock_setting(key):
9191
settings_map = {
9292
"ENABLED": True,
93-
"MASKED_GROUP": "view_masked_data",
93+
"MASKED_GROUPS": ["view_masked_data"],
9494
"DEFAULT_MASKED_ROLE": "masked_reader",
9595
}
9696
return settings_map.get(key)
@@ -123,7 +123,7 @@ def test_middleware_sets_search_path_after_role_switch(
123123
def mock_setting(key):
124124
settings_map = {
125125
"ENABLED": True,
126-
"MASKED_GROUP": "view_masked_data",
126+
"MASKED_GROUPS": ["view_masked_data"],
127127
"DEFAULT_MASKED_ROLE": "masked_reader",
128128
}
129129
return settings_map.get(key)
@@ -155,7 +155,7 @@ def test_middleware_handles_search_path_error(self, request_factory, mock_get_re
155155
def mock_setting(key):
156156
settings_map = {
157157
"ENABLED": True,
158-
"MASKED_GROUP": "view_masked_data",
158+
"MASKED_GROUPS": ["view_masked_data"],
159159
"DEFAULT_MASKED_ROLE": "masked_reader",
160160
}
161161
return settings_map.get(key)
@@ -186,7 +186,7 @@ def test_middleware_resets_role_in_finally_block(self, request_factory, mock_get
186186
def mock_setting(key):
187187
settings_map = {
188188
"ENABLED": True,
189-
"MASKED_GROUP": "view_masked_data",
189+
"MASKED_GROUPS": ["view_masked_data"],
190190
"DEFAULT_MASKED_ROLE": "masked_reader",
191191
}
192192
return settings_map.get(key)
@@ -215,7 +215,7 @@ def test_middleware_handles_role_reset_failure(self, request_factory, mock_get_r
215215
def mock_setting(key):
216216
settings_map = {
217217
"ENABLED": True,
218-
"MASKED_GROUP": "view_masked_data",
218+
"MASKED_GROUPS": ["view_masked_data"],
219219
"DEFAULT_MASKED_ROLE": "masked_reader",
220220
}
221221
return settings_map.get(key)
@@ -250,7 +250,7 @@ def test_middleware_handles_search_path_reset_error(
250250
def mock_setting(key):
251251
settings_map = {
252252
"ENABLED": True,
253-
"MASKED_GROUP": "view_masked_data",
253+
"MASKED_GROUPS": ["view_masked_data"],
254254
"DEFAULT_MASKED_ROLE": "masked_reader",
255255
}
256256
return settings_map.get(key)
@@ -279,7 +279,7 @@ def test_middleware_doesnt_switch_for_user_without_group(
279279
def mock_setting(key):
280280
settings_map = {
281281
"ENABLED": True,
282-
"MASKED_GROUP": "view_masked_data",
282+
"MASKED_GROUPS": ["view_masked_data"],
283283
"DEFAULT_MASKED_ROLE": "masked_reader",
284284
}
285285
return settings_map.get(key)
@@ -306,7 +306,7 @@ def test_middleware_bypasses_when_disabled(self, request_factory, mock_get_respo
306306
def mock_setting(key):
307307
settings_map = {
308308
"ENABLED": False, # Disabled
309-
"MASKED_GROUP": "view_masked_data",
309+
"MASKED_GROUPS": ["view_masked_data"],
310310
"DEFAULT_MASKED_ROLE": "masked_reader",
311311
}
312312
return settings_map.get(key)
@@ -337,7 +337,7 @@ def failing_get_response(_request):
337337
def mock_setting(key):
338338
settings_map = {
339339
"ENABLED": True,
340-
"MASKED_GROUP": "view_masked_data",
340+
"MASKED_GROUPS": ["view_masked_data"],
341341
"DEFAULT_MASKED_ROLE": "masked_reader",
342342
}
343343
return settings_map.get(key)
@@ -367,7 +367,7 @@ def test_middleware_with_custom_masked_role(self, request_factory, mock_get_resp
367367
def mock_setting(key):
368368
settings_map = {
369369
"ENABLED": True,
370-
"MASKED_GROUP": "view_masked_data",
370+
"MASKED_GROUPS": ["view_masked_data"],
371371
"DEFAULT_MASKED_ROLE": "custom_masked_role",
372372
}
373373
return settings_map.get(key)
@@ -401,7 +401,7 @@ def test_middleware_handles_database_connection_error(
401401
def mock_setting(key):
402402
settings_map = {
403403
"ENABLED": True,
404-
"MASKED_GROUP": "view_masked_data",
404+
"MASKED_GROUPS": ["view_masked_data"],
405405
"DEFAULT_MASKED_ROLE": "masked_reader",
406406
}
407407
return settings_map.get(key)
@@ -432,7 +432,7 @@ def test_middleware_handles_user_group_access_error(self, request_factory, mock_
432432
def mock_setting(key):
433433
settings_map = {
434434
"ENABLED": True,
435-
"MASKED_GROUP": "view_masked_data",
435+
"MASKED_GROUPS": ["view_masked_data"],
436436
"DEFAULT_MASKED_ROLE": "masked_reader",
437437
}
438438
return settings_map.get(key)

0 commit comments

Comments
 (0)