Skip to content

Commit

Permalink
Merge branch 'develop' into fixing-investigation-loading
Browse files Browse the repository at this point in the history
  • Loading branch information
DraKen0009 authored Oct 30, 2024
2 parents 02de4de + 67c63d9 commit 5dff175
Show file tree
Hide file tree
Showing 34 changed files with 1,875 additions and 1,133 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:

- uses: actions/setup-python@v5
with:
python-version: "3.12"
python-version: "3.13"
cache: 'pipenv'

- name: Install pipenv
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/linter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v3

- uses: actions/setup-python@v5
with:
python-version: "3.13"

- uses: pre-commit/[email protected]
with:
extra_args: --color=always --from-ref ${{ github.event.pull_request.base.sha }} --to-ref ${{ github.event.pull_request.head.sha }}
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ default_stages: [commit]

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
rev: v5.0.0
hooks:
- id: no-commit-to-branch
args: [--branch, develop, --branch, staging, --branch, production]
Expand All @@ -17,7 +17,7 @@ repos:
- id: check-toml

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.7
rev: v0.7.0
hooks:
- id: ruff
args: [ --fix ]
Expand Down
38 changes: 18 additions & 20 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@ name = "pypi"
[packages]
argon2-cffi = "==23.1.0"
authlib = "==1.3.2"
boto3 = "==1.35.29"
boto3 = "==1.35.49"
celery = "==5.4.0"
django = "==5.1.1"
django = "==5.1.2"
django-environ = "==0.11.2"
django-cors-headers = "==4.4.0"
django-cors-headers = "==4.5.0"
django-filter = "==24.3"
django-maintenance-mode = "==0.21.1"
django-queryset-csv = "==1.1.0"
django-ratelimit = "==4.1.0"
django-redis = "==5.4.0"
django-rest-passwordreset = "==1.4.1"
django-rest-passwordreset = "==1.4.2"
django-simple-history = "==3.7.0"
djangoql = "==0.18.1"
djangorestframework = "==3.15.2"
Expand All @@ -28,44 +28,42 @@ drf-spectacular = "==0.27.2"
gunicorn = "==23.0.0"
healthy-django = "==0.1.0"
jsonschema = "==4.23.0"
jwcrypto = "==1.5.6"
newrelic = "==10.1.0"
pillow = "==10.4.0"
psycopg = { extras = ["c"], version = "==3.2.2" }
pycryptodome = "==3.20.0"
newrelic = "==10.2.0"
pillow = "==11.0.0"
psycopg = { extras = ["c"], version = "==3.2.3" }
pydantic = "==1.10.18" # fix for fhir.resources < 7.0.2
pyjwt = "==2.9.0"
python-slugify = "==8.0.4"
pywebpush = "==2.0.0"
pywebpush = "==2.0.1"
redis = { extras = ["hiredis"], version = "==5.0.8" } # constraint for redis-om
redis-om = "==0.3.1" # > 0.3.1 broken with pydantic < 2
requests = "==2.32.3"
sentry-sdk = "==2.14.0"
sentry-sdk = "==2.17.0"
whitenoise = "==6.7.0"

[dev-packages]
boto3-stubs = { extras = ["s3", "boto3"], version = "==1.35.29" }
coverage = "==7.6.1"
debugpy = "==1.8.6"
boto3-stubs = { extras = ["s3", "boto3"], version = "==1.35.49" }
coverage = "==7.6.4"
debugpy = "==1.8.7"
django-coverage-plugin = "==3.1.0"
django-extensions = "==3.2.3"
django-silk = "==5.2.0"
djangorestframework-stubs = "==3.15.1"
factory-boy = "==3.3.1"
freezegun = "==1.5.1"
ipython = "==8.27.0"
mypy = "==1.11.2"
pre-commit = "==3.8.0"
ipython = "==8.28.0"
mypy = "==1.12.1"
pre-commit = "==4.0.1"
requests-mock = "==1.12.1"
tblib = "==3.0.0"
watchdog = "==5.0.3"
werkzeug = "==3.0.4"
ruff = "==0.6.8"
werkzeug = "==3.0.6"
ruff = "==0.7.0"

[docs]
furo = "==2024.8.6"
sphinx = "==8.0.2"
myst-parser = "==4.0.0"

[requires]
python_version = "3.12"
python_version = "3.13"
2,310 changes: 1,206 additions & 1,104 deletions Pipfile.lock

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions care/facility/migrations/0467_alter_hospitaldoctors_area.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.1.1 on 2024-10-28 13:49

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('facility', '0466_camera_presets'),
]

operations = [
migrations.AlterField(
model_name='hospitaldoctors',
name='area',
field=models.IntegerField(choices=[(1, 'General Medicine'), (2, 'Pulmonology'), (3, 'Intensivist'), (4, 'Pediatrician'), (5, 'Others'), (6, 'Anesthesiologist'), (7, 'Cardiac Surgeon'), (8, 'Cardiologist'), (9, 'Dentist'), (10, 'Dermatologist'), (11, 'Diabetologist'), (12, 'Emergency Medicine Physician'), (13, 'Endocrinologist'), (14, 'Family Physician'), (15, 'Gastroenterologist'), (16, 'General Surgeon'), (17, 'Geriatrician'), (18, 'Hematologist'), (19, 'Immunologist'), (20, 'Infectious Disease Specialist'), (21, 'MBBS doctor'), (22, 'Medical Officer'), (23, 'Nephrologist'), (24, 'Neuro Surgeon'), (25, 'Neurologist'), (26, 'Obstetrician/Gynecologist (OB/GYN)'), (27, 'Oncologist'), (28, 'Oncology Surgeon'), (29, 'Ophthalmologist'), (30, 'Oral and Maxillofacial Surgeon'), (31, 'Orthopedic'), (32, 'Orthopedic Surgeon'), (33, 'Otolaryngologist (ENT)'), (34, 'Palliative care Physician'), (35, 'Pathologist'), (36, 'Pediatric Surgeon'), (37, 'Physician'), (38, 'Plastic Surgeon'), (39, 'Psychiatrist'), (40, 'Pulmonologist'), (41, 'Radio technician'), (42, 'Radiologist'), (43, 'Rheumatologist'), (44, 'Sports Medicine Specialist'), (45, 'Thoraco-Vascular Surgeon'), (46, 'Transfusion Medicine Specialist'), (47, 'Urologist'), (48, 'Nurse'), (49, 'Allergist/Immunologist'), (50, 'Cardiothoracic Surgeon'), (51, 'Gynecologic Oncologist'), (52, 'Hepatologist'), (53, 'Internist'), (54, 'Neonatologist'), (55, 'Pain Management Specialist'), (56, 'Physiatrist (Physical Medicine and Rehabilitation)'), (57, 'Podiatrist'), (58, 'Preventive Medicine Specialist'), (59, 'Radiation Oncologist'), (60, 'Sleep Medicine Specialist'), (61, 'Transplant Surgeon'), (62, 'Trauma Surgeon'), (63, 'Vascular Surgeon'), (64, 'Critical Care Physician')]),
),
]
2 changes: 1 addition & 1 deletion care/facility/models/facility.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ class FacilityFeature(models.IntegerChoices):
(16, "General Surgeon"),
(17, "Geriatrician"),
(18, "Hematologist"),
(29, "Immunologist"),
(19, "Immunologist"),
(20, "Infectious Disease Specialist"),
(21, "MBBS doctor"),
(22, "Medical Officer"),
Expand Down
Empty file added care/security/__init__.py
Empty file.
11 changes: 11 additions & 0 deletions care/security/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _


class SecurityConfig(AppConfig):
name = "care.security"
verbose_name = _("Security Management")

def ready(self):
# import care.security.signals # noqa F401
pass
Empty file.
88 changes: 88 additions & 0 deletions care/security/authorization/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
from care.security.permissions.base import PermissionController


class PermissionDeniedError(Exception):
pass


class AuthorizationHandler:
"""
This is the base class for Authorization Handlers
Authorization handler must define a list of actions that can be performed and define the methods that
actually perform the authorization action.
All Authz methods would be of the signature ( user, obj , **kwargs )
obj refers to the obj which the user is seeking permission to. obj can also be a string or any datatype as long
as the logic can handle the type.
Queries are actions that return a queryset as the response.
"""

actions = []
queries = []

def check_permission(self, user, obj):
if not PermissionController.has_permission(user, obj):
raise PermissionDeniedError

return PermissionController.has_permission(user, obj)


class AuthorizationController:
"""
This class abstracts all security related operations in care
This includes Checking if A has access to resource X,
Filtering query-sets for list based operations and so on.
Security Controller implicitly caches all cachable operations and expects it to be invalidated.
SecurityController maintains a list of override Classes, When present,
The override classes are invoked first and then the predefined classes.
The overridden classes can choose to call the next function in the hierarchy if needed.
"""

override_authz_controllers: list[
AuthorizationHandler
] = [] # The order is important
# Override Security Controllers will be defined from plugs
internal_authz_controllers: list[AuthorizationHandler] = []

cache = {}

@classmethod
def build_cache(cls):
for controller in (
cls.internal_authz_controllers + cls.override_authz_controllers
):
for action in controller.actions:
if "actions" not in cls.cache:
cls.cache["actions"] = {}
cls.cache["actions"][action] = [
*cls.cache["actions"].get(action, []),
controller,
]

@classmethod
def get_action_controllers(cls, action):
return cls.cache["actions"].get(action, [])

@classmethod
def check_action_permission(cls, action, user, obj):
"""
TODO: Add Caching and capability to remove cache at both user and obj level
"""
if not cls.cache:
cls.build_cache()
controllers = cls.get_action_controllers(action)
for controller in controllers:
permission_fn = getattr(controller, action)
result, _continue = permission_fn(user, obj)
if not _continue:
return result
if not result:
return result
return True

@classmethod
def register_internal_controller(cls, controller: AuthorizationHandler):
# TODO : Do some deduplication Logic
cls.internal_authz_controllers.append(controller)
22 changes: 22 additions & 0 deletions care/security/authorization/facility.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from care.abdm.utils.api_call import Facility
from care.facility.models import FacilityUser
from care.security.authorization.base import (
AuthorizationHandler,
PermissionDeniedError,
)


class FacilityAccess(AuthorizationHandler):
actions = ["can_read_facility"]
queries = ["allowed_facilities"]

def can_read_facility(self, user, facility_id):
self.check_permission(user, facility_id)
# Since the old method relied on a facility-user relationship, check that
# This can be removed when the migrations have been completed
if not FacilityUser.objects.filter(facility_id=facility_id, user=user).exists():
raise PermissionDeniedError
return True, True

def allowed_facilities(self, user):
return Facility.objects.filter(users__id__exact=user.id)
Empty file.
Empty file.
65 changes: 65 additions & 0 deletions care/security/management/commands/sync_permissions_roles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from django.core.management import BaseCommand
from django.db import transaction

from care.security.models import PermissionModel, RoleModel, RolePermission
from care.security.permissions.base import PermissionController
from care.security.roles.role import RoleController
from care.utils.lock import Lock


class Command(BaseCommand):
"""
This command syncs roles, permissions and role-permission mapping to the database.
This command should be run after all deployments and plug changes.
This command is idempotent, multiple instances running the same command is automatically blocked with redis.
"""

help = "Syncs permissions and roles to database"

def handle(self, *args, **options):
permissions = PermissionController.get_permissions()
roles = RoleController.get_roles()
with transaction.atomic(), Lock("sync_permissions_roles", 900):
# Create, update permissions and delete old permissions
PermissionModel.objects.all().update(temp_deleted=True)
for permission, metadata in permissions.items():
permission_obj = PermissionModel.objects.filter(slug=permission).first()
if not permission_obj:
permission_obj = PermissionModel(slug=permission)
permission_obj.name = metadata.name
permission_obj.description = metadata.description
permission_obj.context = metadata.context.value
permission_obj.temp_deleted = False
permission_obj.save()
PermissionModel.objects.filter(temp_deleted=True).delete()
# Create, update roles and delete old roles
RoleModel.objects.all().update(temp_deleted=True)
for role in roles:
role_obj = RoleModel.objects.filter(
name=role.name, context=role.context.value
).first()
if not role_obj:
role_obj = RoleModel(name=role.name, context=role.context.value)
role_obj.description = role.description
role_obj.is_system = True
role_obj.temp_deleted = False
role_obj.save()
RoleModel.objects.filter(temp_deleted=True).delete()
# Sync permissions to role
RolePermission.objects.all().update(temp_deleted=True)
role_cache = {}
for permission, metadata in permissions.items():
permission_obj = PermissionModel.objects.filter(slug=permission).first()
for role in metadata.roles:
if role.name not in role_cache:
role_cache[role.name] = RoleModel.objects.get(name=role.name)
obj = RolePermission.objects.filter(
role=role_cache[role.name], permission=permission_obj
).first()
if not obj:
obj = RolePermission(
role=role_cache[role.name], permission=permission_obj
)
obj.temp_deleted = False
obj.save()
RolePermission.objects.filter(temp_deleted=True).delete()
Loading

0 comments on commit 5dff175

Please sign in to comment.