Security & Permissions ======================= Complete guide to SATHI's security architecture, permissions system, and access control. .. contents:: Table of Contents :local: :depth: 3 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 :doc:`roles_and_permissions_configuration` 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 :doc:`roles_and_permissions_configuration` 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. .. code-block:: python 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. .. code-block:: python 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. .. code-block:: python 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. .. code-block:: python 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. .. code-block:: python 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``: .. code-block:: python 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:** .. code-block:: python 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:** .. code-block:: python 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``: .. code-block:: python # 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:** .. code-block:: html **Caching Strategy:** .. code-block:: javascript // 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:** .. code-block:: bash # .env file DJANGO_SECURED_FIELDS_KEY=your-encryption-key-here **Generate Encryption Key:** .. code-block:: bash python manage.py generate_key **Key Rotation:** .. code-block:: bash # Comma-separated list for key rotation DJANGO_SECURED_FIELDS_KEY=new-key,old-key-1,old-key-2 Encrypted Fields ~~~~~~~~~~~~~~~~ **Patient Model:** .. code-block:: python 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:** .. code-block:: python # 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:** .. code-block:: python # 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:** .. code-block:: python # 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:** .. code-block:: python # 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:** .. code-block:: python 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:** .. code-block:: python PASSWORD_RESET_RATE_LIMIT_COUNT = 2 PASSWORD_RESET_RATE_LIMIT_PERIOD = '10m' reCAPTCHA Integration ~~~~~~~~~~~~~~~~~~~~~ **Configuration:** .. code-block:: python # 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:** .. code-block:: python 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 --------- - `Django Permissions Documentation `_ - `Django Allauth Documentation `_ - `django-secured-fields `_ - `Content Security Policy Guide `_ - `OWASP Security Guidelines `_