-
Notifications
You must be signed in to change notification settings - Fork 336
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
20 changed files
with
534 additions
and
0 deletions.
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
65
care/security/management/commands/sync_permissions_roles.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
# Generated by Django 5.1.2 on 2024-10-30 10:00 | ||
|
||
import django.db.models.deletion | ||
import uuid | ||
from django.conf import settings | ||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
initial = True | ||
|
||
dependencies = [ | ||
migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name='PermissionModel', | ||
fields=[ | ||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||
('external_id', models.UUIDField(db_index=True, default=uuid.uuid4, unique=True)), | ||
('created_date', models.DateTimeField(auto_now_add=True, db_index=True, null=True)), | ||
('modified_date', models.DateTimeField(auto_now=True, db_index=True, null=True)), | ||
('deleted', models.BooleanField(db_index=True, default=False)), | ||
('slug', models.CharField(db_index=True, max_length=1024, unique=True)), | ||
('name', models.CharField(max_length=1024)), | ||
('description', models.TextField(default='')), | ||
('context', models.CharField(max_length=1024)), | ||
('temp_deleted', models.BooleanField(default=False)), | ||
], | ||
options={ | ||
'abstract': False, | ||
}, | ||
), | ||
migrations.CreateModel( | ||
name='RoleModel', | ||
fields=[ | ||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||
('external_id', models.UUIDField(db_index=True, default=uuid.uuid4, unique=True)), | ||
('created_date', models.DateTimeField(auto_now_add=True, db_index=True, null=True)), | ||
('modified_date', models.DateTimeField(auto_now=True, db_index=True, null=True)), | ||
('deleted', models.BooleanField(db_index=True, default=False)), | ||
('name', models.CharField(max_length=1024)), | ||
('description', models.TextField(default='')), | ||
('context', models.CharField(max_length=1024)), | ||
('is_system', models.BooleanField(default=False)), | ||
('temp_deleted', models.BooleanField(default=False)), | ||
], | ||
options={ | ||
'constraints': [models.UniqueConstraint(fields=('name', 'context'), name='unique_order')], | ||
}, | ||
), | ||
migrations.CreateModel( | ||
name='RoleAssociation', | ||
fields=[ | ||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||
('external_id', models.UUIDField(db_index=True, default=uuid.uuid4, unique=True)), | ||
('created_date', models.DateTimeField(auto_now_add=True, db_index=True, null=True)), | ||
('modified_date', models.DateTimeField(auto_now=True, db_index=True, null=True)), | ||
('deleted', models.BooleanField(db_index=True, default=False)), | ||
('context', models.CharField(max_length=1024)), | ||
('context_id', models.BigIntegerField()), | ||
('expiry', models.DateTimeField(blank=True, null=True)), | ||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), | ||
('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='security.rolemodel')), | ||
], | ||
options={ | ||
'abstract': False, | ||
}, | ||
), | ||
migrations.CreateModel( | ||
name='RolePermission', | ||
fields=[ | ||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||
('external_id', models.UUIDField(db_index=True, default=uuid.uuid4, unique=True)), | ||
('created_date', models.DateTimeField(auto_now_add=True, db_index=True, null=True)), | ||
('modified_date', models.DateTimeField(auto_now=True, db_index=True, null=True)), | ||
('deleted', models.BooleanField(db_index=True, default=False)), | ||
('temp_deleted', models.BooleanField(default=False)), | ||
('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='security.permissionmodel')), | ||
('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='security.rolemodel')), | ||
], | ||
options={ | ||
'abstract': False, | ||
}, | ||
), | ||
] |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
from .permission import * # noqa F403 | ||
from .permission_association import * # noqa F403 | ||
from .role import * # noqa F403 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
from django.db import models | ||
|
||
from care.utils.models.base import BaseModel | ||
|
||
|
||
class PermissionModel(BaseModel): | ||
""" | ||
This model represents a permission in the security system. | ||
A permission allows a certain action to be performed by the user for a given context. | ||
""" | ||
|
||
slug = models.CharField(max_length=1024, unique=True, db_index=True) | ||
name = models.CharField(max_length=1024) | ||
description = models.TextField(default="") | ||
context = models.CharField( | ||
max_length=1024 | ||
) # We can add choices here as well if needed | ||
temp_deleted = models.BooleanField(default=False) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
from django.db import models | ||
|
||
from care.security.models.role import RoleModel | ||
from care.users.models import User | ||
from care.utils.models.base import BaseModel | ||
|
||
|
||
class RoleAssociation(BaseModel): | ||
""" | ||
This model connects roles to users via contexts | ||
Expiry can be used to expire the role allocation after a certain period | ||
""" | ||
|
||
user = models.ForeignKey(User, on_delete=models.CASCADE, null=False, blank=False) | ||
context = models.CharField(max_length=1024) | ||
context_id = models.BigIntegerField() # Store integer id of the context here | ||
role = models.ForeignKey( | ||
RoleModel, on_delete=models.CASCADE, null=False, blank=False | ||
) | ||
expiry = models.DateTimeField(null=True, blank=True) | ||
|
||
# TODO : Index user, context and context_id |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
from django.db import models | ||
from django.db.models import UniqueConstraint | ||
|
||
from care.security.models.permission import PermissionModel | ||
from care.utils.models.base import BaseModel | ||
|
||
|
||
class RoleModel(BaseModel): | ||
""" | ||
This model represents a role in the security system. | ||
A role comprises multiple permissions on the same type. | ||
A role can only be made for a single context. eg, A role can be FacilityAdmin with Facility related permission items | ||
Another role is to be created for other contexts, eg. Asset Admin should only contain Asset related permission items | ||
Roles can be created on the fly, System roles cannot be deleted, but user created roles can be deleted by users | ||
with the permission to delete roles | ||
""" | ||
|
||
name = models.CharField(max_length=1024) | ||
description = models.TextField(default="") | ||
context = models.CharField( | ||
max_length=1024 | ||
) # We can add choices here as well if needed | ||
is_system = models.BooleanField( | ||
default=False | ||
) # Denotes if role was created on the fly | ||
temp_deleted = models.BooleanField(default=False) | ||
|
||
class Meta: | ||
constraints = [ | ||
UniqueConstraint(name="unique_order", fields=["name", "context"]) | ||
] | ||
|
||
|
||
class RolePermission(BaseModel): | ||
""" | ||
Connects a role to a list of permissions | ||
""" | ||
|
||
role = models.ForeignKey( | ||
RoleModel, on_delete=models.CASCADE, null=False, blank=False | ||
) | ||
permission = models.ForeignKey( | ||
PermissionModel, on_delete=models.CASCADE, null=False, blank=False | ||
) | ||
temp_deleted = models.BooleanField(default=False) |
Empty file.
Oops, something went wrong.