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 institution

  • get_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:

  1. Secure Context: HTTPS or localhost

  2. CSP Permissions: worker-src and manifest-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 = False in 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:

  1. Check user has correct group membership

  2. Verify Provider profile exists and is linked to Institution

  3. Check patient belongs to provider’s institution

  4. Verify permissions are assigned to group

Service Worker Not Registering:

  1. Ensure HTTPS is enabled (or using localhost)

  2. Check CSP headers include worker-src 'self'

  3. Verify manifest URL is correct

  4. Check browser console for errors

Encrypted Field Search Not Working:

  1. Cannot use Django ORM filters on encrypted fields

  2. Must decrypt server-side for searching

  3. Use custom search functions

  4. Consider using Select2 with AJAX for large datasets

Permission Denied Errors:

  1. Check user is authenticated

  2. Verify user has required permissions

  3. Check institution-based access control

  4. Review view decorators and mixins

Resources