Security & Permissions¶
Complete guide to SATHI’s security architecture, permissions system, and access control.
Overview¶
SATHI implements a multi-layered security architecture:
Authentication: Django Allauth with email-based login
Authorization: Role-based access control (RBAC) with Django groups
Data Isolation: Institution-based row-level security
Encryption: Field-level encryption for sensitive data
Session Security: Secure session management with IP/agent binding
Content Security: CSP headers and security middleware
Roles and Permissions Configuration¶
For complete documentation on user roles, permissions, and group configuration, see the dedicated Roles and Permissions Configuration Guide guide.
This includes detailed information on:
Patients Group - Questionnaire access and submission permissions
Healthcare Providers Group - Patient management and clinical data permissions
Patient Registration Staff Group - Patient registration and optional diagnosis/treatment permissions
Questionnaire Creators Group - Questionnaire and item management permissions
REDCap Data Managers Group - Data export and integration permissions
Complete permission matrix and implementation guide
REDCap Permissions¶
For REDCap integration permissions and data export controls, see Roles and Permissions Configuration Guide under the REDCap Data Managers Group section.
Key security points:
Data export restricted to staff users only (
request.user.is_staff)Granular permissions for form mappings, field mappings, and patient ID maps
Permissions checked in both views (backend) and templates (UI visibility)
Institution-Based Access Control¶
SATHI implements row-level security based on institutions to ensure data isolation.
Architecture¶
Key Principles:
Providers: Can only see/manage patients from their institution
Non-providers (superusers, admin): Can see all patients
Patients: Can only see their own data
Implementation Approach:
View-level filtering combined with mixin approach
Clear and explicit behavior
Django-native patterns
Easy to test and debug
Utility Functions¶
Located in patientapp/utils.py:
get_user_institution(user)
Get the institution for the current user if they are a provider.
from patientapp.utils import get_user_institution
institution = get_user_institution(request.user)
if institution:
# User is a provider with an institution
pass
is_provider_user(user)
Check if the user is a provider.
from patientapp.utils import is_provider_user
if is_provider_user(request.user):
# Apply provider-specific logic
pass
filter_patients_by_institution(queryset, user)
Filter a Patient queryset based on user’s institution.
from patientapp.utils import filter_patients_by_institution
patients = Patient.objects.all()
accessible_patients = filter_patients_by_institution(patients, request.user)
check_patient_access(user, patient)
Check if a user can access a specific patient.
from patientapp.utils import check_patient_access
if not check_patient_access(request.user, patient):
raise PermissionDenied
get_accessible_patient_or_404(user, pk)
Get a patient by pk, ensuring the user has access.
from patientapp.utils import get_accessible_patient_or_404
patient = get_accessible_patient_or_404(request.user, patient_id)
Institution Filter Mixin¶
For class-based views, use InstitutionFilterMixin:
from patientapp.utils import InstitutionFilterMixin
from django.views.generic import ListView
class PatientListView(InstitutionFilterMixin, ListView):
model = Patient
template_name = 'patientapp/patient_list.html'
# Mixin automatically filters queryset by institution
Methods:
get_queryset(): Automatically filters by institutionget_object(): Ensures user has access to the object
Protected Views¶
Function-Based Views:
from patientapp.utils import get_accessible_patient_or_404
@login_required
@permission_required('patientapp.view_patient')
def patient_detail(request, pk):
patient = get_accessible_patient_or_404(request.user, pk)
# ... rest of view
Class-Based Views:
from patientapp.utils import InstitutionFilterMixin
class PatientCreateView(InstitutionFilterMixin, CreateView):
model = Patient
# Institution automatically enforced
Security Features¶
Institution Dropdown Filtering:
Providers: Only see their institution
Non-providers: See all institutions
Patient Creation:
Providers: Can only create patients in their institution
Form automatically pre-selects and locks provider’s institution
Patient Updates:
Providers: Cannot move patients to different institutions
Institution field automatically enforced
Error Handling:
404 errors for patients not in user’s institution
Clear error messages
Audit logging
Content Security Policy (CSP)¶
SATHI implements strict CSP headers for security.
Configuration¶
Located in chaviprom/settings.py:
# Content Security Policy
CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'", "'unsafe-inline'", "cdn.jsdelivr.net")
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'", "fonts.googleapis.com")
CSP_FONT_SRC = ("'self'", "fonts.gstatic.com")
CSP_IMG_SRC = ("'self'", "data:", "https:")
CSP_CONNECT_SRC = ("'self'",)
# Service Worker and PWA support
CSP_WORKER_SRC = ("'self'",)
CSP_MANIFEST_SRC = ("'self'",)
# reCAPTCHA support
CSP_SCRIPT_SRC += ("https://www.google.com", "https://www.gstatic.com")
CSP_FRAME_SRC = ("https://www.google.com",)
PWA Service Workers¶
Requirements:
Secure Context: HTTPS or localhost
CSP Permissions:
worker-srcandmanifest-src
Service Worker Registration:
<!-- templates/base.html -->
<link rel="manifest" href="{% url 'manifest' %}">
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/serviceworker.js');
}
</script>
Caching Strategy:
// static/js/serviceworker.js
// Network-first for HTML pages
if (request.destination === 'document') {
return fetch(request)
.then(response => {
cache.put(request, response.clone());
return response;
})
.catch(() => cache.match(request));
}
// Cache-first for static assets
if (request.destination === 'style' ||
request.destination === 'script' ||
request.destination === 'image') {
return cache.match(request)
.then(response => response || fetch(request));
}
Field-Level Encryption¶
SATHI uses django-secured-fields for encrypting sensitive patient data.
Configuration¶
Environment Variable:
# .env file
DJANGO_SECURED_FIELDS_KEY=your-encryption-key-here
Generate Encryption Key:
python manage.py generate_key
Key Rotation:
# Comma-separated list for key rotation
DJANGO_SECURED_FIELDS_KEY=new-key,old-key-1,old-key-2
Encrypted Fields¶
Patient Model:
from secured_fields import EncryptedCharField
class Patient(models.Model):
name = EncryptedCharField(max_length=255)
patient_id = EncryptedCharField(max_length=50, unique=True)
# ... other fields
Usage:
# Encryption/decryption is automatic
patient = Patient.objects.create(
name="John Doe", # Automatically encrypted
patient_id="P12345"
)
# Decryption is automatic when accessing
print(patient.name) # "John Doe" (decrypted)
Searching Encrypted Fields:
# Cannot use __icontains or __startswith
# Must decrypt server-side for search
def search_patients(query):
patients = Patient.objects.all()
results = [
p for p in patients
if query.lower() in p.name.lower()
]
return results
Authentication & Session Security¶
Django Allauth Configuration¶
Email-Based Login:
# settings.py
ACCOUNT_LOGIN_BY_CODE_ENABLED = True
ACCOUNT_LOGIN_BY_CODE_MAX_ATTEMPTS = 3
ACCOUNT_LOGIN_BY_CODE_TIMEOUT = 300 # 5 minutes
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_EMAIL_VERIFICATION = 'mandatory'
Session Security:
# Session settings
SESSION_COOKIE_SECURE = True # HTTPS only
SESSION_COOKIE_HTTPONLY = True # No JavaScript access
SESSION_COOKIE_SAMESITE = 'Lax' # CSRF protection
SESSION_COOKIE_AGE = 3600 # 1 hour
Rate Limiting¶
Login Rate Limiting:
from django_ratelimit.decorators import ratelimit
@ratelimit(key='user_or_ip', rate='5/m', method='POST')
def login_view(request):
# Login logic
pass
Password Reset Rate Limiting:
PASSWORD_RESET_RATE_LIMIT_COUNT = 2
PASSWORD_RESET_RATE_LIMIT_PERIOD = '10m'
reCAPTCHA Integration¶
Configuration:
# settings.py
RECAPTCHA_DEFAULT_ACTION = 'generic'
RECAPTCHA_SCORE_THRESHOLD = 0.5
# Environment variables
RECAPTCHA_PUBLIC_KEY = os.getenv('RECAPTCHA_PUBLIC_KEY')
RECAPTCHA_PRIVATE_KEY = os.getenv('RECAPTCHA_PRIVATE_KEY')
Form Integration:
from django_recaptcha.fields import ReCaptchaField
class LoginForm(forms.Form):
username = forms.CharField()
password = forms.CharField(widget=forms.PasswordInput)
captcha = ReCaptchaField()
Best Practices¶
Permission Management¶
DO:
Use Django groups for role-based access
Apply principle of least privilege
Test permissions thoroughly
Document permission requirements
Use institution-based filtering for providers
DON’T:
Grant delete permissions without careful consideration
Give providers access to patient portal
Allow cross-institution data access
Hardcode permissions in views
Security Checklist¶
Deployment:
[ ] Set
DEBUG = Falsein production[ ] Use strong
SECRET_KEY[ ] Enable HTTPS with valid SSL certificate
[ ] Configure CSP headers
[ ] Set secure cookie flags
[ ] Enable rate limiting
[ ] Configure reCAPTCHA
[ ] Set up encryption keys
[ ] Review all permissions
[ ] Test institution-based access control
Ongoing:
[ ] Regular security audits
[ ] Monitor failed login attempts
[ ] Review access logs
[ ] Update dependencies
[ ] Rotate encryption keys periodically
[ ] Test backup and recovery
Troubleshooting¶
Common Issues¶
Users Can’t Access Patients:
Check user has correct group membership
Verify Provider profile exists and is linked to Institution
Check patient belongs to provider’s institution
Verify permissions are assigned to group
Service Worker Not Registering:
Ensure HTTPS is enabled (or using localhost)
Check CSP headers include
worker-src 'self'Verify manifest URL is correct
Check browser console for errors
Encrypted Field Search Not Working:
Cannot use Django ORM filters on encrypted fields
Must decrypt server-side for searching
Use custom search functions
Consider using Select2 with AJAX for large datasets
Permission Denied Errors:
Check user is authenticated
Verify user has required permissions
Check institution-based access control
Review view decorators and mixins