Skip to content

Commit 6ddb1d1

Browse files
refactor: remove dangerous role switching features and reorganize code
1 parent 964a91f commit 6ddb1d1

File tree

10 files changed

+67
-202
lines changed

10 files changed

+67
-202
lines changed

README.md

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def api_endpoint(request):
6464
return JsonResponse({'users': list(User.objects.values())})
6565

6666
# Class-Based View Mixin
67-
from django_postgres_anon.decorators import AnonymizedDataMixin
67+
from django_postgres_anon.mixins import AnonymizedDataMixin
6868

6969
class ReportView(AnonymizedDataMixin, ListView):
7070
model = SensitiveModel # ← All queries automatically anonymized!
@@ -89,8 +89,8 @@ POSTGRES_ANON = {'MASKED_GROUP': 'analysts'}
8989

9090
### 🎭 **Dynamic Data Access**
9191

92-
- 🎯 **Context Managers** - `anonymized_data()` and `database_role()` for temporary role switching
93-
- 🎨 **Decorators** - `@use_anonymized_data` and `@database_role_required` for view-level control
92+
- 🎯 **Context Managers** - `anonymized_data()` for temporary role switching
93+
- 🎨 **Decorators** - `@use_anonymized_data` for view-level control
9494
- 🧩 **Class-Based Mixins** - `AnonymizedDataMixin` for automatic anonymization in CBVs
9595
- 🔀 **Smart Middleware** - Group-based automatic role switching for seamless user experience
9696

@@ -275,7 +275,7 @@ python manage.py anon_drop [--confirm]
275275
#### Context Managers
276276

277277
```python
278-
from django_postgres_anon.context_managers import anonymized_data, database_role
278+
from django_postgres_anon.context_managers import anonymized_data
279279

280280
# Use anonymized data in a view
281281
def sensitive_report(request):
@@ -289,17 +289,12 @@ def custom_report(request):
289289
data = SensitiveModel.objects.all()
290290
return JsonResponse({'data': list(data.values())})
291291

292-
# Switch to any database role
293-
def read_only_operation():
294-
with database_role('readonly_user'):
295-
# All queries run as readonly_user
296-
return MyModel.objects.all()
297292
```
298293

299294
#### Decorators
300295

301296
```python
302-
from django_postgres_anon.decorators import use_anonymized_data, database_role_required
297+
from django_postgres_anon.decorators import use_anonymized_data
303298
from django.utils.decorators import method_decorator
304299

305300
# Function-based views
@@ -311,10 +306,6 @@ def api_endpoint(request):
311306
def custom_api_endpoint(request):
312307
return JsonResponse({'data': list(SensitiveModel.objects.values())})
313308

314-
# Require specific database role
315-
@database_role_required('readonly_user')
316-
def read_only_operation():
317-
return MyModel.objects.all()
318309

319310
# Class-based views with method decorator
320311
class SensitiveDataView(View):
@@ -327,7 +318,7 @@ class SensitiveDataView(View):
327318
#### Class-Based View Mixins
328319

329320
```python
330-
from django_postgres_anon.decorators import AnonymizedDataMixin
321+
from django_postgres_anon.mixins import AnonymizedDataMixin
331322
from django.views.generic import ListView, View
332323

333324
# Automatic anonymization for ListView
@@ -672,8 +663,6 @@ with anonymized_data(): # Use default masked role
672663
with anonymized_data('custom_role'): # Use specific role
673664
with anonymized_data('role', False): # Don't auto-create role
674665

675-
# database_role(role_name)
676-
with database_role('readonly_user'): # Switch to any database role
677666
```
678667

679668
### Decorators
@@ -685,12 +674,7 @@ with database_role('readonly_user'): # Switch to any database role
685674
@use_anonymized_data('custom_role') # Use specific role
686675
@use_anonymized_data('role', False) # Don't auto-create role
687676

688-
# @database_role_required(role_name)
689-
@database_role_required('readonly_user') # Require specific role
690677

691-
# Aliases for semantic clarity
692-
@anonymized_view # Alias for @use_anonymized_data
693-
@masked_data # Alternative alias
694678
```
695679

696680
### Mixins

django_postgres_anon/__init__.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
- Django models for managing anonymization rules and presets
66
- Management commands for initializing, applying, and managing anonymization
77
- Middleware for dynamic role switching
8-
- Context managers for temporary role switching (anonymized_data, database_role)
9-
- Decorators for automatic anonymization in views/functions (use_anonymized_data, database_role_required)
8+
- Context managers for temporary role switching (anonymized_data)
9+
- Decorators for automatic anonymization in views/functions (use_anonymized_data)
1010
- Class-based view mixins for anonymized data access (AnonymizedDataMixin)
1111
- Utilities for database schema introspection
1212
- Pre-built presets for common use cases
@@ -157,8 +157,6 @@ def get_available_presets() -> list:
157157
"MaskingPreset",
158158
"MaskingRule",
159159
"anonymized_data",
160-
"database_role",
161-
"database_role_required",
162160
"get_table_columns",
163161
"use_anonymized_data",
164162
"validate_function_syntax",

django_postgres_anon/context_managers.py

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,9 @@
77
from django.db import connection
88

99
from django_postgres_anon.config import get_anon_setting
10-
11-
# ErrorHandler no longer needed
1210
from django_postgres_anon.models import MaskedRole
1311
from django_postgres_anon.utils import reset_role, switch_to_role
1412

15-
# Simplified error handling - no decorators needed
16-
1713
logger = logging.getLogger(__name__)
1814

1915

@@ -145,33 +141,3 @@ def _restore_isolation_level(isolation_level: str) -> None:
145141
"""Restore the original transaction isolation level."""
146142
with connection.cursor() as cursor:
147143
cursor.execute(f"SET transaction_isolation = '{isolation_level}'")
148-
149-
150-
@contextlib.contextmanager
151-
def database_role(role_name: str):
152-
"""
153-
Lower-level context manager for switching to any database role.
154-
155-
Args:
156-
role_name: Name of the database role to switch to
157-
158-
Example:
159-
>>> with database_role('readonly_user'):
160-
... # All queries run as readonly_user
161-
... data = MyModel.objects.all()
162-
"""
163-
role_switched = False
164-
165-
try:
166-
connection.ensure_connection()
167-
168-
if switch_to_role(role_name, auto_create=False):
169-
role_switched = True
170-
else:
171-
raise RuntimeError(f"Database role '{role_name}' does not exist")
172-
173-
yield
174-
175-
finally:
176-
if role_switched:
177-
reset_role()

django_postgres_anon/decorators.py

Lines changed: 0 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -61,65 +61,3 @@ def wrapper(*args, **kwargs):
6161
else:
6262
# Called as @use_anonymized_data() or @use_anonymized_data('role')
6363
return decorator
64-
65-
66-
def database_role_required(role_name: str):
67-
"""
68-
Decorator that ensures a specific database role is active during function execution.
69-
70-
Args:
71-
role_name: Name of the database role to switch to
72-
73-
Example:
74-
>>> @database_role_required('readonly_user')
75-
>>> def read_only_operation():
76-
... return MyModel.objects.all()
77-
78-
Raises:
79-
RuntimeError: If the specified role doesn't exist
80-
"""
81-
82-
def decorator(func: Callable) -> Callable:
83-
@functools.wraps(func)
84-
def wrapper(*args, **kwargs):
85-
from django_postgres_anon.context_managers import database_role
86-
87-
with database_role(role_name):
88-
return func(*args, **kwargs)
89-
90-
return wrapper
91-
92-
return decorator
93-
94-
95-
class AnonymizedDataMixin:
96-
"""
97-
Mixin for class-based views to automatically use anonymized data.
98-
99-
This mixin ensures all database operations within the view use anonymized data
100-
by wrapping the dispatch method.
101-
102-
Example:
103-
>>> class SensitiveReportView(AnonymizedDataMixin, ListView):
104-
... model = User
105-
... template_name = 'sensitive_report.html'
106-
... anonymized_role = 'custom_masked_role' # Optional
107-
108-
>>> class APIView(AnonymizedDataMixin, View):
109-
... def get(self, request):
110-
... users = User.objects.all() # Automatically anonymized
111-
... return JsonResponse({'users': list(users.values())})
112-
"""
113-
114-
anonymized_role: Optional[str] = None
115-
auto_create_role: bool = True
116-
117-
def dispatch(self, request, *args, **kwargs):
118-
"""Override dispatch to use anonymized data context"""
119-
with anonymized_data(role_name=self.anonymized_role, auto_create=self.auto_create_role):
120-
return super().dispatch(request, *args, **kwargs)
121-
122-
123-
# Alias for common usage patterns
124-
anonymized_view = use_anonymized_data # More semantic alias for views
125-
masked_data = use_anonymized_data # Alternative name for clarity

django_postgres_anon/mixins.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""Class-based view mixins for Django PostgreSQL Anonymizer"""
2+
3+
from typing import Optional
4+
5+
from django_postgres_anon.context_managers import anonymized_data
6+
7+
8+
class AnonymizedDataMixin:
9+
"""
10+
Mixin for class-based views to automatically use anonymized data.
11+
12+
This mixin ensures all database operations within the view use anonymized data
13+
by wrapping the dispatch method.
14+
15+
Example:
16+
>>> class SensitiveReportView(AnonymizedDataMixin, ListView):
17+
... model = User
18+
... template_name = 'sensitive_report.html'
19+
... anonymized_role = 'custom_masked_role' # Optional
20+
21+
>>> class APIView(AnonymizedDataMixin, View):
22+
... def get(self, request):
23+
... users = User.objects.all() # Automatically anonymized
24+
... return JsonResponse({'users': list(users.values())})
25+
"""
26+
27+
anonymized_role: Optional[str] = None
28+
auto_create_role: bool = True
29+
30+
def dispatch(self, request, *args, **kwargs):
31+
"""Override dispatch to use anonymized data context"""
32+
with anonymized_data(role_name=self.anonymized_role, auto_create=self.auto_create_role):
33+
return super().dispatch(request, *args, **kwargs)

example_project/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ This example project demonstrates how to integrate and use django-postgres-anony
99
- **Presets**: Loading and using pre-built anonymization presets
1010
- **Admin Integration**: Django admin interface with anonymization actions
1111
- **Interactive Demo**: Web interface to explore anonymization features
12-
- **Context Managers**: Temporary anonymized data access with `anonymized_data()` and `database_role()`
12+
- **Context Managers**: Temporary anonymized data access with `anonymized_data()`
1313
- **Decorators**: Function and class-based view decorators for automatic anonymization
1414
- **Function Validation**: Real-time validation and testing of anonymization functions
1515
- **Data Comparison**: Side-by-side comparison of original vs anonymized data

example_project/sample_app/anonymization_views.py

Lines changed: 10 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414
from django.views.decorators.http import require_http_methods
1515
from django.views.generic import TemplateView
1616

17-
from django_postgres_anon.context_managers import anonymized_data, database_role
18-
from django_postgres_anon.decorators import AnonymizedDataMixin, database_role_required, use_anonymized_data
17+
from django_postgres_anon.context_managers import anonymized_data
18+
from django_postgres_anon.decorators import use_anonymized_data
19+
from django_postgres_anon.mixins import AnonymizedDataMixin
1920
from django_postgres_anon.models import MaskingPreset, MaskingRule
2021
from django_postgres_anon.utils import suggest_anonymization_functions, validate_function_syntax
2122

@@ -37,10 +38,9 @@ def context_manager_demo(request: HttpRequest) -> HttpResponse:
3738
}
3839

3940
try:
40-
# Normal data access - explicitly use default role to bypass middleware
41-
with database_role("sanyamkhurana"): # Use default role to get unmasked data
42-
normal_users = list(User.objects.values("username", "email", "first_name", "last_name")[:5])
43-
context["normal_data"] = normal_users
41+
# Normal data access - direct query without role switching
42+
normal_users = list(User.objects.values("username", "email", "first_name", "last_name")[:5])
43+
context["normal_data"] = normal_users
4444

4545
# Using anonymized_data context manager
4646
try:
@@ -51,14 +51,14 @@ def context_manager_demo(request: HttpRequest) -> HttpResponse:
5151
logger.warning(f"Anonymized data context manager failed: {e}")
5252
context["anonymized_data"] = [{"error": f"Context manager failed: {e}"}]
5353

54-
# Using database_role context manager
54+
# Using anonymized_data with specific role
5555
try:
56-
with database_role("masked_reader"):
56+
with anonymized_data("masked_reader"):
5757
role_users = list(User.objects.values("username", "email", "first_name", "last_name")[:5])
5858
context["role_switched_data"] = role_users
5959
except Exception as e:
60-
logger.warning(f"Database role context manager failed: {e}")
61-
context["role_switched_data"] = [{"error": f"Role switch failed: {e}"}]
60+
logger.warning(f"Anonymized data with custom role failed: {e}")
61+
context["role_switched_data"] = [{"error": f"Custom role failed: {e}"}]
6262

6363
except Exception as e:
6464
logger.error(f"Context manager demo error: {e}")
@@ -80,26 +80,6 @@ def decorated_user_list(request: HttpRequest) -> JsonResponse:
8080
return JsonResponse({"error": str(e)}, status=500)
8181

8282

83-
@database_role_required("masked_reader")
84-
def role_required_view(request: HttpRequest) -> JsonResponse:
85-
"""View using the @database_role_required decorator"""
86-
try:
87-
customers = Customer.objects.select_related("user").values(
88-
"user__username", "user__email", "phone", "ssn", "annual_income"
89-
)[:5]
90-
91-
return JsonResponse(
92-
{
93-
"message": "Data accessed with @database_role_required decorator",
94-
"customers": list(customers),
95-
"required_role": "masked_reader",
96-
}
97-
)
98-
except Exception as e:
99-
logger.error(f"Role required view error: {e}")
100-
return JsonResponse({"error": str(e)}, status=500)
101-
102-
10383
@method_decorator(login_required, name="dispatch")
10484
class AnonymizedCustomerView(AnonymizedDataMixin, TemplateView):
10585
"""Class-based view using AnonymizedDataMixin to show only anonymized data"""

example_project/templates/sample_app/anonymized_customer_view.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ <h5 class="mb-0">
223223
<div class="card-body">
224224
<h6>Class-based View with Mixin</h6>
225225
<div style="position: relative;">
226-
<pre style="background: #f6f8fa; border: 1px solid #e1e4e8; border-radius: 6px; padding: 16px; margin: 0; font-family: 'SFMono-Regular', Consolas, monospace; font-size: 13px; line-height: 1.45;"><code><span style="color: #d73a49; font-weight: 600;">from</span> django_postgres_anon.decorators <span style="color: #d73a49; font-weight: 600;">import</span> AnonymizedDataMixin
226+
<pre style="background: #f6f8fa; border: 1px solid #e1e4e8; border-radius: 6px; padding: 16px; margin: 0; font-family: 'SFMono-Regular', Consolas, monospace; font-size: 13px; line-height: 1.45;"><code><span style="color: #d73a49; font-weight: 600;">from</span> django_postgres_anon.mixins <span style="color: #d73a49; font-weight: 600;">import</span> AnonymizedDataMixin
227227
<span style="color: #d73a49; font-weight: 600;">from</span> django.views.generic <span style="color: #d73a49; font-weight: 600;">import</span> TemplateView
228228

229229
<span style="color: #d73a49; font-weight: 600;">class</span> <span style="color: #6f42c1;">AnonymizedCustomerView</span>(<span style="color: #6f42c1;">AnonymizedDataMixin</span>, <span style="color: #6f42c1;">TemplateView</span>):

example_project/templates/sample_app/context_manager_demo.html

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,11 @@ <h5 class="mb-0">
103103
</h5>
104104
</div>
105105
<div class="card-body">
106-
<p class="small text-muted">Using <code>database_role()</code> context manager:</p>
106+
<p class="small text-muted">Using <code>anonymized_data()</code> with custom role:</p>
107107
<div style="position: relative;">
108-
<pre style="background: #f6f8fa; border: 1px solid #e1e4e8; border-radius: 6px; padding: 16px; margin: 0; font-family: 'SFMono-Regular', Consolas, monospace; font-size: 13px; line-height: 1.45;"><code><span style="color: #d73a49; font-weight: 600;">from</span> django_postgres_anon.context_managers <span style="color: #d73a49; font-weight: 600;">import</span> database_role
108+
<pre style="background: #f6f8fa; border: 1px solid #e1e4e8; border-radius: 6px; padding: 16px; margin: 0; font-family: 'SFMono-Regular', Consolas, monospace; font-size: 13px; line-height: 1.45;"><code><span style="color: #d73a49; font-weight: 600;">from</span> django_postgres_anon.context_managers <span style="color: #d73a49; font-weight: 600;">import</span> anonymized_data
109109

110-
<span style="color: #d73a49; font-weight: 600;">with</span> <span style="color: #6f42c1;">database_role</span>(<span style="color: #032f62;">'masked_reader'</span>):
110+
<span style="color: #d73a49; font-weight: 600;">with</span> <span style="color: #6f42c1;">anonymized_data</span>(<span style="color: #032f62;">'masked_reader'</span>):
111111
User.objects.<span style="color: #6f42c1;">values</span>(<span style="color: #032f62;">'username'</span>, <span style="color: #032f62;">'email'</span>)[<span style="color: #005cc5;">:</span><span style="color: #005cc5;">5</span>]</code></pre>
112112
<button onclick="copyCode(this)" style="position: absolute; top: 8px; right: 8px; background: #0366d6; color: white; border: none; border-radius: 4px; padding: 4px 8px; font-size: 11px; cursor: pointer; opacity: 0.7;" title="Copy code">
113113
Copy
@@ -148,8 +148,8 @@ <h5 class="mb-0">
148148
<div class="card-body">
149149
<p class="mb-3">Ready to use this in your project? Copy and paste this code:</p>
150150
<div style="position: relative;">
151-
<pre style="background: #f6f8fa; border: 1px solid #e1e4e8; border-radius: 6px; padding: 16px; margin: 0; font-family: 'SFMono-Regular', Consolas, monospace; font-size: 13px; line-height: 1.45;"><code><span style="color: #6a737d; font-style: italic;"># Import the context managers</span>
152-
<span style="color: #d73a49; font-weight: 600;">from</span> django_postgres_anon.context_managers <span style="color: #d73a49; font-weight: 600;">import</span> anonymized_data, database_role
151+
<pre style="background: #f6f8fa; border: 1px solid #e1e4e8; border-radius: 6px; padding: 16px; margin: 0; font-family: 'SFMono-Regular', Consolas, monospace; font-size: 13px; line-height: 1.45;"><code><span style="color: #6a737d; font-style: italic;"># Import the context manager</span>
152+
<span style="color: #d73a49; font-weight: 600;">from</span> django_postgres_anon.context_managers <span style="color: #d73a49; font-weight: 600;">import</span> anonymized_data
153153

154154
<span style="color: #6a737d; font-style: italic;"># Basic usage in your views</span>
155155
<span style="color: #d73a49; font-weight: 600;">def</span> <span style="color: #6f42c1;">my_view</span>(request):
@@ -158,7 +158,7 @@ <h5 class="mb-0">
158158
users = User.objects.<span style="color: #6f42c1;">all</span>()
159159

160160
<span style="color: #6a737d; font-style: italic;"># Or use specific role</span>
161-
<span style="color: #d73a49; font-weight: 600;">with</span> <span style="color: #6f42c1;">database_role</span>(<span style="color: #032f62;">'masked_reader'</span>):
161+
<span style="color: #d73a49; font-weight: 600;">with</span> <span style="color: #6f42c1;">anonymized_data</span>(<span style="color: #032f62;">'masked_reader'</span>):
162162
customers = Customer.objects.<span style="color: #6f42c1;">all</span>()
163163

164164
<span style="color: #d73a49; font-weight: 600;">return</span> <span style="color: #6f42c1;">render</span>(request, <span style="color: #032f62;">'template.html'</span>, {<span style="color: #032f62;">'users'</span>: users})</code></pre>

0 commit comments

Comments
 (0)