Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SCHOOL 253. Add RBAC #267

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions config/locale/en/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,19 @@ msgstr " By %(filter_title)s "
#: .\templates\templates\input_filter.html:18
msgid "Remove"
msgstr "Remove"


msgid "creator"
msgstr "creator"


msgid "director"
msgstr "director"


msgid "employee"
msgstr "employee"


msgid "view only"
msgstr "view only"
16 changes: 16 additions & 0 deletions config/locale/ru/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,19 @@ msgstr " От %(filter_title)s "
#: .\templates\templates\input_filter.html:18
msgid "Remove"
msgstr "Удалить"


msgid "creator"
msgstr "создатель"


msgid "director"
msgstr "директор"


msgid "employee"
msgstr "сотрудник"


msgid "view only"
msgstr "только просмотр"
34 changes: 33 additions & 1 deletion open_schools_platform/common/views.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
from operator import attrgetter
from typing import Type, Dict, Union, List, Any

from django.contrib.contenttypes.models import ContentType
from django.views import View
from rest_framework.exceptions import PermissionDenied
from rest_framework.fields import ChoiceField, Field
from rest_framework.serializers import Serializer as RestFrameworkSerializer

from open_schools_platform.common.types import DjangoViewType
from open_schools_platform.organization_management.employees.roles import role_hierarchy


def MultipleViewManager(handlers: Dict[str, Type[DjangoViewType]]) -> Type[DjangoViewType]:
Expand All @@ -30,7 +34,7 @@ def dispatch(self, request, *args, **kwargs):
return BaseManageView


def convert_dict_to_serializer(dictionary: Dict[str, Union[RestFrameworkSerializer, List[str]]])\
def convert_dict_to_serializer(dictionary: Dict[str, Union[RestFrameworkSerializer, List[str]]]) \
-> Type[RestFrameworkSerializer]:
class Serializer(RestFrameworkSerializer): # type: ignore
pass
Expand All @@ -45,3 +49,31 @@ class Serializer(RestFrameworkSerializer): # type: ignore

Serializer._declared_fields = fields
return Serializer


def ensure_role_permission(target_model, relation, target_profile, role):
def decorator(func):
def wrapper(*args, **kwargs):
if len(args) > 1:
request = args[1]
related_entities = {}
related_entities.update(request.data)
related_entities.update(request.query_params)
related_entities.update(request.parser_context.get('kwargs', {}))

pk = related_entities.get(f'{target_model}_id') or related_entities.get(target_model)
if pk:
target_object = ContentType.objects.get(model=target_model).get_object_for_this_type(pk=pk)
retriever = attrgetter(relation)
profile = getattr(request.user, target_profile)
if profile:
relation_object = retriever(target_object).filter(**{target_profile: profile.id}).first()

if hasattr(relation_object, 'role'):
if relation_object.role in role_hierarchy.get(role, ()):
return func(*args, **kwargs)
raise PermissionDenied

return wrapper

return decorator
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
from django.urls import reverse
from rest_framework.test import APIClient
from open_schools_platform.organization_management.circles.tests.utils import create_test_circle
from open_schools_platform.organization_management.employees.tests.utils import create_test_employee
from open_schools_platform.organization_management.organizations.tests.utils import create_test_organization
from open_schools_platform.user_management.users.tests.utils import create_logged_in_user


Expand All @@ -11,7 +13,9 @@ def setUp(self):
self.circle_url = lambda pk: reverse("api:organization-management:circles:circle", args=[pk])

def test_successfully_get_circle(self):
create_logged_in_user(instance=self)
circle = create_test_circle()
user = create_logged_in_user(instance=self)
organization = create_test_organization()
circle = create_test_circle(organization)
create_test_employee(user, organization)
response = self.client.get(self.circle_url(pk=str(circle.id)))
self.assertEqual(200, response.status_code)
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@
from .filters import CircleFilter
from .paginators import ApiCircleListPagination
from .selectors import get_circle, get_circles
from ..employees.roles import EmployeeRole
from ..teachers.selectors import get_teacher_profile
from ..teachers.serializers import CreateCircleInviteTeacherSerializer
from ..teachers.services import create_teacher
from ...common.utils import get_dict_excluding_fields
from ...common.views import convert_dict_to_serializer
from ...common.views import convert_dict_to_serializer, ensure_role_permission
from ...parent_management.families.selectors import get_families
from ...parent_management.parents.services import get_parent_profile_or_create_new_user, \
get_parent_family_or_create_new
Expand Down Expand Up @@ -53,6 +54,7 @@ class CreateCircleApi(ApiAuthMixin, CreateAPIView):
responses={201: convert_dict_to_serializer({"circle": GetCircleSerializer()}), 404: "No such organization"},
tags=[SwaggerTags.ORGANIZATION_MANAGEMENT_CIRCLES],
)
@ensure_role_permission('organization', 'employees', 'employee_profile', EmployeeRole.employee)
def post(self, request):
create_circle_serializer = CreateCircleSerializer(data=request.data)
create_circle_serializer.is_valid(raise_exception=True)
Expand Down Expand Up @@ -101,6 +103,7 @@ class GetCircleApi(ApiAuthMixin, APIView):
tags=[SwaggerTags.ORGANIZATION_MANAGEMENT_CIRCLES],
responses={200: convert_dict_to_serializer({"circle": GetCircleSerializer()})}
)
@ensure_role_permission('circle', 'organization.employees', 'employee_profile', EmployeeRole.view_only)
def get(self, request, circle_id):
circle = get_circle(
filters={"id": str(circle_id)},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 3.2.12 on 2024-06-28 06:36

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('employees', '0008_auto_20230823_0756'),
]

operations = [
migrations.AddField(
model_name='employee',
name='role',
field=models.CharField(blank=True, choices=[('creator', 'creator'), ('director', 'director'), ('employee', 'employee'), ('view_only', 'view only')], default='employee', max_length=50, null=True),
),
migrations.AddField(
model_name='historicalemployee',
name='role',
field=models.CharField(blank=True, choices=[('creator', 'creator'), ('director', 'director'), ('employee', 'employee'), ('view_only', 'view only')], default='employee', max_length=50, null=True),
),
]
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from simple_history.models import HistoricalRecords

from open_schools_platform.common.models import BaseModel, BaseManager
from open_schools_platform.organization_management.employees.roles import EmployeeRole
from open_schools_platform.organization_management.organizations.models import Organization
from open_schools_platform.user_management.users.models import User

Expand Down Expand Up @@ -58,6 +59,8 @@ class Employee(BaseModel):
null=True, default=None, blank=True, on_delete=models.CASCADE)
name = models.CharField(max_length=255)
position = models.CharField(max_length=255, blank=True, default="")
role = models.CharField(choices=EmployeeRole.choices, default=EmployeeRole.employee,
max_length=50, null=True, blank=True)
history = HistoricalRecords()

objects = EmployeeManager() # type: ignore[assignment]
Expand Down
18 changes: 18 additions & 0 deletions open_schools_platform/organization_management/employees/roles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from django.utils.translation import gettext_lazy as _
from django.db import models


class EmployeeRole(models.TextChoices):
creator = 'creator', _('creator')
director = 'director', _('director')
employee = 'employee', _('employee')
view_only = 'view_only', _('view only')


# defines which role can be replaced for a given key
role_hierarchy = {
'creator': ('creator',),
'director': ('director', 'creator'),
'employee': ('employee', 'director', 'creator'),
'view_only': ('view_only', 'employee', 'director', 'creator')
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
from django.utils.translation import gettext_lazy as _


def create_employee(name: str, position: str = "", user: User = None, organization: Organization = None) -> Employee:
def create_employee(name: str,
position: str = "",
user: User = None,
organization: Organization = None,
role=None) -> Employee:
employee_profile = None
if user:
employee_profile = user.employee_profile
Expand All @@ -22,6 +26,7 @@ def create_employee(name: str, position: str = "", user: User = None, organizati
employee_profile=employee_profile,
organization=organization,
position=position,
role=role
)
return employee

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Any, Dict, List

from open_schools_platform.common.filters import SoftCondition
from open_schools_platform.organization_management.employees.roles import EmployeeRole
from open_schools_platform.organization_management.employees.selectors import get_employees
from open_schools_platform.organization_management.employees.services import create_employee

Expand Down Expand Up @@ -79,12 +80,13 @@ def create_test_organizations():
return organizations


def create_test_employee(user: User, organization: Organization = None):
def create_test_employee(user: User, organization: Organization = None, role=None):
employee_data = {
"name": "Andrey",
"position": "Chief director",
"user": user,
"organization": organization
"organization": organization,
"role": role or EmployeeRole.employee
} # type: Dict[Any, Any]
return create_employee(**employee_data)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from open_schools_platform.organization_management.circles.models import Circle
from open_schools_platform.organization_management.circles.paginators import ApiCircleListPagination
from open_schools_platform.organization_management.circles.selectors import get_circle, get_circles
from open_schools_platform.organization_management.employees.roles import EmployeeRole
from open_schools_platform.organization_management.employees.serializers import GetEmployeeSerializer, \
UpdateOrganizationInviteEmployeeSerializer, CreateOrganizationInviteEmployeeSerializer
from open_schools_platform.organization_management.employees.services import create_employee, \
Expand Down Expand Up @@ -71,7 +72,8 @@ def post(self, request, *args, **kwargs):
employee = create_employee(name=request.user.name,
user=request.user,
organization=org,
position="Creator")
position="Creator",
role=EmployeeRole.creator)

return Response({"creator_employee": GetEmployeeSerializer(employee).data},
status=201)
Expand Down
Loading