Skip to content

Commit

Permalink
Merge branch 'CA-210-fix-access-control-model-in-api' of github.com:i…
Browse files Browse the repository at this point in the history
…ntuitem/ciso-assistant-community into CA-210-fix-access-control-model-in-api
  • Loading branch information
nas-tabchiche committed Feb 23, 2024
2 parents 948d429 + c9420c5 commit 5c75d2a
Show file tree
Hide file tree
Showing 12 changed files with 81 additions and 146 deletions.
4 changes: 2 additions & 2 deletions backend/app_tests/api/test_api_requirement_assessments.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import pytest
from rest_framework.status import HTTP_403_FORBIDDEN
from rest_framework.status import HTTP_403_FORBIDDEN, HTTP_400_BAD_REQUEST
from rest_framework.test import APIClient
from core.models import (
ComplianceAssessment,
Expand Down Expand Up @@ -159,7 +159,7 @@ def test_create_requirement_assessments(self, authenticated_client):
},
base_count=-1,
fails=True,
expected_status=HTTP_403_FORBIDDEN,
expected_status=HTTP_400_BAD_REQUEST
)

def test_update_requirement_assessments(self, authenticated_client):
Expand Down
2 changes: 1 addition & 1 deletion backend/cal/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 5.0.2 on 2024-02-10 02:00
# Generated by Django 5.0.2 on 2024-02-23 00:51

from django.db import migrations, models

Expand Down
46 changes: 22 additions & 24 deletions backend/core/apps.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
from django.apps import AppConfig
from django.db.models.signals import post_migrate
from ciso_assistant.settings import CISO_ASSISTANT_SUPERUSER_EMAIL


def startup():
"""Implement CISO Assistant 1.0 default Roles and User Groups"""
def startup(**kwargs):
"""
Implement CISO Assistant 1.0 default Roles and User Groups during migrate
This makes sure root folder and global groups are defined before any other object is created
Create superuser if CISO_ASSISTANT_SUPERUSER_EMAIL defined
"""

from ciso_assistant.settings import (
CISO_ASSISTANT_SUPERUSER_EMAIL,
)
from django.contrib.auth.models import Permission
from iam.models import Folder, Role, RoleAssignment, User, UserGroup
print("post-migrate handler: initialize database")

auditor_permissions = Permission.objects.filter(
codename__in=[
Expand Down Expand Up @@ -237,14 +241,6 @@ def startup():
]
)

# if superuser defined and does not exist, then create it
if (
CISO_ASSISTANT_SUPERUSER_EMAIL
and not User.objects.filter(email=CISO_ASSISTANT_SUPERUSER_EMAIL).exists()
):
User.objects.create_superuser(
email=CISO_ASSISTANT_SUPERUSER_EMAIL, is_superuser=True
)
# if root folder does not exist, then create it
if not Folder.objects.filter(content_type=Folder.ContentType.ROOT).exists():
Folder.objects.create(
Expand Down Expand Up @@ -309,12 +305,18 @@ def startup():
folder=Folder.get_root_folder(),
)
ra2.perimeter_folders.add(global_validators.folder)
# add any superuser to the global administrors group, in case it is not yet done
for superuser in User.objects.filter(is_superuser=True):
UserGroup.objects.get(name="BI-UG-ADM").user_set.add(superuser)
# fix administrator role, to facilitate migrations
administrator = Role.objects.filter(name="BI-RL-ADM").first()
administrator.permissions.set(administrator_permissions)

# if superuser defined and does not exist, then create it
if (
CISO_ASSISTANT_SUPERUSER_EMAIL
and not User.objects.filter(email=CISO_ASSISTANT_SUPERUSER_EMAIL).exists()
):
try:
User.objects.create_superuser(
email=CISO_ASSISTANT_SUPERUSER_EMAIL, is_superuser=True
)
except Exception as e:
print(e) #NOTE: Add this exception in the logger


class CoreConfig(AppConfig):
Expand All @@ -323,8 +325,4 @@ class CoreConfig(AppConfig):
verbose_name = "Core"

def ready(self):
import os

if os.environ.get("RUN_MAIN"):
"""Only called in main, not during makemigrations or migrate"""
startup()
post_migrate.connect(startup, sender=self)
2 changes: 1 addition & 1 deletion backend/core/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 5.0.2 on 2024-02-10 02:00
# Generated by Django 5.0.2 on 2024-02-23 00:51

import core.validators
import uuid
Expand Down
22 changes: 20 additions & 2 deletions backend/core/migrations/0002_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 5.0.2 on 2024-02-10 02:00
# Generated by Django 5.0.2 on 2024-02-23 00:51

import django.db.models.deletion
import iam.models
Expand Down Expand Up @@ -52,6 +52,11 @@ class Migration(migrations.Migration):
name='framework',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.framework', verbose_name='Framework'),
),
migrations.AddField(
model_name='library',
name='dependencies',
field=models.ManyToManyField(blank=True, to='core.library', verbose_name='Dependencies'),
),
migrations.AddField(
model_name='library',
name='folder',
Expand All @@ -75,7 +80,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='requirementassessment',
name='compliance_assessment',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.complianceassessment', verbose_name='Compliance assessment'),
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='requirement_assessments', to='core.complianceassessment', verbose_name='Compliance assessment'),
),
migrations.AddField(
model_name='requirementassessment',
Expand Down Expand Up @@ -207,6 +212,19 @@ class Migration(migrations.Migration):
name='security_measures',
field=models.ManyToManyField(blank=True, related_name='requirement_assessments', to='core.securitymeasure', verbose_name='Security measures'),
),
migrations.CreateModel(
name='Policy',
fields=[
],
options={
'verbose_name': 'Policy',
'verbose_name_plural': 'Policies',
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('core.securitymeasure',),
),
migrations.AddField(
model_name='threat',
name='folder',
Expand Down
24 changes: 0 additions & 24 deletions backend/core/migrations/0003_library_dependencies_and_more.py

This file was deleted.

26 changes: 0 additions & 26 deletions backend/core/migrations/0004_policy.py

This file was deleted.

27 changes: 5 additions & 22 deletions backend/core/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,9 @@ class RBACPermissions(permissions.DjangoObjectPermissions):
}

def has_permission(self, request: Request, view) -> bool:
if not request.user or (
not request.user.is_authenticated and self.authenticated_users_only
):
return False
if not request.method:
return False
# Read access is filtered at the queryset level
if request.method in permissions.SAFE_METHODS:
return True
# Workaround to ensure DjangoModelPermissions are not applied
# to the root view when using DefaultRouter.
if getattr(view, "_ignore_model_permissions", False):
return True

queryset = self._queryset(view)
perms = self.get_required_permissions(request.method, queryset.model)
if not perms:
return False
_codename = perms[0].split(".")[1]
return RoleAssignment.has_permission(user=request.user, codename=_codename)
""" we don't need this check, as we have queryset for list and serializers for create
see https://www.django-rest-framework.org/api-guide/permissions/ """
return True

def has_object_permission(self, request: Request, view, obj):
if not request.method:
Expand All @@ -49,8 +32,8 @@ def has_object_permission(self, request: Request, view, obj):
if not perms:
return False
_codename = perms[0].split(".")[1]
if queryset.model == User:
return RoleAssignment.has_permission(user=request.user, codename=_codename)
# if queryset.model == User:
# return RoleAssignment.has_permission(user=request.user, codename=_codename)
return RoleAssignment.is_access_allowed(
user=request.user,
perm=Permission.objects.get(codename=_codename),
Expand Down
1 change: 1 addition & 0 deletions backend/core/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def update(self, instance: models.Model, validated_data: Any) -> models.Model:
def create(self, validated_data: Any):
logger.debug("validated data", **validated_data)
folder = Folder.get_folder(validated_data)
folder = folder if folder else Folder.get_root_folder()
can_create_in_folder = RoleAssignment.is_access_allowed(
user=self.context["request"].user,
perm=Permission.objects.get(
Expand Down
35 changes: 18 additions & 17 deletions backend/iam/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 5.0.2 on 2024-02-10 02:00
# Generated by Django 5.0.2 on 2024-02-23 00:51

import django.db.models.deletion
import django.utils.timezone
Expand All @@ -17,6 +17,22 @@ class Migration(migrations.Migration):
]

operations = [
migrations.CreateModel(
name='Folder',
fields=[
('name', models.CharField(max_length=200, verbose_name='Name')),
('description', models.TextField(blank=True, null=True, verbose_name='Description')),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('content_type', models.CharField(choices=[('GL', 'GLOBAL'), ('DO', 'DOMAIN')], default='DO', max_length=2)),
('builtin', models.BooleanField(default=False)),
('parent_folder', models.ForeignKey(default=iam.models._get_root_folder, null=True, on_delete=django.db.models.deletion.CASCADE, to='iam.folder', verbose_name='parent folder')),
],
options={
'verbose_name': 'Folder',
'verbose_name_plural': 'Folders',
},
),
migrations.CreateModel(
name='User',
fields=[
Expand All @@ -30,6 +46,7 @@ class Migration(migrations.Migration):
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('folder', models.ForeignKey(default=iam.models._get_root_folder, on_delete=django.db.models.deletion.CASCADE, to='iam.folder', verbose_name='Folder')),
],
options={
'verbose_name': 'user',
Expand All @@ -40,22 +57,6 @@ class Migration(migrations.Migration):
('objects', iam.models.UserManager()),
],
),
migrations.CreateModel(
name='Folder',
fields=[
('name', models.CharField(max_length=200, verbose_name='Name')),
('description', models.TextField(blank=True, null=True, verbose_name='Description')),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('content_type', models.CharField(choices=[('GL', 'GLOBAL'), ('DO', 'DOMAIN')], default='DO', max_length=2)),
('builtin', models.BooleanField(default=False)),
('parent_folder', models.ForeignKey(default=iam.models._get_root_folder, null=True, on_delete=django.db.models.deletion.CASCADE, to='iam.folder', verbose_name='parent folder')),
],
options={
'verbose_name': 'Folder',
'verbose_name_plural': 'Folders',
},
),
migrations.CreateModel(
name='Role',
fields=[
Expand Down
26 changes: 11 additions & 15 deletions backend/iam/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ def _create_user(self, email, password, mailing=True, **extra_fields):
last_name=extra_fields.get("last_name", ""),
is_superuser=extra_fields.get("is_superuser", False),
is_active=extra_fields.get("is_active", True),
folder=_get_root_folder()
)
user.user_groups.set(extra_fields.get("user_groups", []))
user.password = make_password(password if password else str(uuid.uuid4()))
Expand All @@ -277,6 +278,7 @@ def _create_user(self, email, password, mailing=True, **extra_fields):
subject=_("Welcome to Ciso Assistant!"),
)
except Exception as exception:
print(f"sending email to {email} failed")
raise exception
return user

Expand All @@ -292,14 +294,9 @@ def create_superuser(self, email, password=None, **extra_fields):
extra_fields.setdefault("is_superuser", True)
if extra_fields.get("is_superuser") is not True:
raise ValueError("Superuser must have is_superuser=True.")
if not (EMAIL_HOST or EMAIL_HOST_RESCUE):
extra_fields.setdefault("mailing", False)
extra_fields.setdefault("mailing", not(password) and (EMAIL_HOST or EMAIL_HOST_RESCUE))
superuser = self._create_user(email, password, **extra_fields)
# when possible, add superuser to admin group
try:
UserGroup.objects.get(name="BI-UG-ADM").user_set.add(superuser)
except ObjectDoesNotExist:
pass
UserGroup.objects.get(name="BI-UG-ADM").user_set.add(superuser)
return superuser


Expand Down Expand Up @@ -338,6 +335,13 @@ class User(AbstractBaseUser):
),
)
objects = UserManager()
folder = models.ForeignKey(
Folder,
on_delete=models.CASCADE,
verbose_name=_("Folder"),
default=_get_root_folder
)

except:
logger.debug("Exception kludge")

Expand Down Expand Up @@ -684,14 +688,6 @@ def get_permissions(user: AbstractBaseUser | AnonymousUser):

return permissions

@staticmethod
def has_permission(user: AbstractBaseUser | AnonymousUser, codename: str):
"""Determines if a user has a specific permission. To be used cautiously with proper commenting"""
for ra in RoleAssignment.get_role_assignments(user):
for perm in ra.role.permissions.all():
if perm.codename == codename:
return True
return False

@staticmethod
def has_role(user: AbstractBaseUser | AnonymousUser, role: Role):
Expand Down
12 changes: 0 additions & 12 deletions backend/library/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,21 +40,9 @@ class LibraryViewSet(BaseModelViewSet):
model = Library

def list(self, request, *args, **kwargs):
if not RoleAssignment.has_permission(
user=request.user, codename="view_library"
):
return Response(
status=status.HTTP_403_FORBIDDEN,
)
return Response({"results": get_available_libraries()})

def retrieve(self, request, *args, pk, **kwargs):
if not RoleAssignment.has_permission(
user=request.user, codename="view_library"
):
return Response(
status=status.HTTP_403_FORBIDDEN,
)
library = get_library(pk)
return Response(library)

Expand Down

0 comments on commit 5c75d2a

Please sign in to comment.