unit tests, add cisa_analyst permission in the cisa_analysts_group for better grannular hasPerm testing in admin.py

This commit is contained in:
Rachid Mrad 2023-09-29 13:40:06 -04:00
parent cd14eb2584
commit 155baa0200
No known key found for this signature in database
GPG key ID: EF38E4CEC4A8F3CF
11 changed files with 142 additions and 64 deletions

View file

@ -9,13 +9,8 @@ our `user_group` model and run in a migration.
## Editing group permissions through code ## Editing group permissions through code
We can edit and deploy new group permissions by We can edit and deploy new group permissions by:
editing `user_group` then:
- Duplicating migration `0036_create_groups` 1. editing `user_group` then:
and running migrations (RECOMMENDED METHOD), or 2. Duplicating migration `0036_create_groups`
and running migrations
- Fake the previous migration to run an existing create groups migration:
- step 1: docker-compose exec app ./manage.py migrate --fake registrar 0035_contenttypes_permissions
- step 2: docker-compose exec app ./manage.py migrate registrar 0036_create_groups
- step 3: fake run the latest migration in the migrations list

View file

@ -205,10 +205,10 @@ class MyUserAdmin(BaseUserAdmin):
# which is equivalent to superuser. The other group we use to manage # which is equivalent to superuser. The other group we use to manage
# perms is cisa_analysts_group. cisa_analysts_group will never contain # perms is cisa_analysts_group. cisa_analysts_group will never contain
# full_access_permission # full_access_permission
if request.user.has_perm('registrar.full_access_permission'): if request.user.has_perm("registrar.full_access_permission"):
# Use the default list display for all access users # Use the default list display for all access users
return super().get_list_display(request) return super().get_list_display(request)
# Customize the list display for analysts # Customize the list display for analysts
return ( return (
"email", "email",
@ -220,17 +220,23 @@ class MyUserAdmin(BaseUserAdmin):
) )
def get_fieldsets(self, request, obj=None): def get_fieldsets(self, request, obj=None):
if request.user.has_perm('registrar.full_access_permission'): if request.user.has_perm("registrar.full_access_permission"):
# Show all fields for all access users # Show all fields for all access users
return super().get_fieldsets(request, obj) return super().get_fieldsets(request, obj)
elif request.user.has_perm("registrar.analyst_access_permission"):
# show analyst_fieldsets for analysts # show analyst_fieldsets for analysts
return self.analyst_fieldsets return self.analyst_fieldsets
else:
# any admin user should belong to either full_access_group
# or cisa_analyst_group
return []
def get_readonly_fields(self, request, obj=None): def get_readonly_fields(self, request, obj=None):
if request.user.has_perm('registrar.full_access_permission'): if request.user.has_perm("registrar.full_access_permission"):
return () # No read-only fields for all access users return () # No read-only fields for all access users
return self.analyst_readonly_fields # Read-only fields for analysts # Return restrictive Read-only fields for analysts and
# users who might not belong to groups
return self.analyst_readonly_fields
class HostIPInline(admin.StackedInline): class HostIPInline(admin.StackedInline):
@ -409,11 +415,12 @@ class DomainInformationAdmin(ListHeaderAdmin):
readonly_fields = list(self.readonly_fields) readonly_fields = list(self.readonly_fields)
if request.user.has_perm('registrar.full_access_permission'): if request.user.has_perm("registrar.full_access_permission"):
return readonly_fields
else:
readonly_fields.extend([field for field in self.analyst_readonly_fields])
return readonly_fields return readonly_fields
# Return restrictive Read-only fields for analysts and
# users who might not belong to groups
readonly_fields.extend([field for field in self.analyst_readonly_fields])
return readonly_fields # Read-only fields for analysts
class DomainApplicationAdminForm(forms.ModelForm): class DomainApplicationAdminForm(forms.ModelForm):
@ -627,11 +634,12 @@ class DomainApplicationAdmin(ListHeaderAdmin):
["current_websites", "other_contacts", "alternative_domains"] ["current_websites", "other_contacts", "alternative_domains"]
) )
if request.user.has_perm('registrar.full_access_permission'): if request.user.has_perm("registrar.full_access_permission"):
return readonly_fields
else:
readonly_fields.extend([field for field in self.analyst_readonly_fields])
return readonly_fields return readonly_fields
# Return restrictive Read-only fields for analysts and
# users who might not belong to groups
readonly_fields.extend([field for field in self.analyst_readonly_fields])
return readonly_fields
def display_restricted_warning(self, request, obj): def display_restricted_warning(self, request, obj):
if obj and obj.creator.status == models.User.RESTRICTED: if obj and obj.creator.status == models.User.RESTRICTED:
@ -702,7 +710,7 @@ class DomainAdmin(ListHeaderAdmin):
search_fields = ["name"] search_fields = ["name"]
search_help_text = "Search by domain name." search_help_text = "Search by domain name."
change_form_template = "django/admin/domain_change_form.html" change_form_template = "django/admin/domain_change_form.html"
readonly_fields = ["state"] # readonly_fields = ["state"]
def response_change(self, request, obj): def response_change(self, request, obj):
# Create dictionary of action functions # Create dictionary of action functions

View file

@ -10,9 +10,6 @@ from registrar.models import (
Website, Website,
) )
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
fake = Faker() fake = Faker()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View file

@ -128,7 +128,7 @@ class UserFixture:
"email": "nicolle.leclair@ecstech.com", "email": "nicolle.leclair@ecstech.com",
}, },
] ]
def load_users(cls, users, group_name): def load_users(cls, users, group_name):
logger.info(f"Going to load {len(users)} users in group {group_name}") logger.info(f"Going to load {len(users)} users in group {group_name}")
for user_data in users: for user_data in users:
@ -153,4 +153,3 @@ class UserFixture:
def load(cls): def load(cls):
cls.load_users(cls, cls.ADMINS, "full_access_group") cls.load_users(cls, cls.ADMINS, "full_access_group")
cls.load_users(cls, cls.STAFF, "cisa_analysts_group") cls.load_users(cls, cls.STAFF, "cisa_analysts_group")

View file

@ -13,6 +13,7 @@ class Migration(migrations.Migration):
name="user", name="user",
options={ options={
"permissions": [ "permissions": [
("analyst_access_permission", "Analyst Access Permission"),
("full_access_permission", "Full Access Permission"), ("full_access_permission", "Full Access Permission"),
] ]
}, },

View file

@ -1,12 +1,13 @@
# From mithuntnt's answer on: # From mithuntnt's answer on:
# https://stackoverflow.com/questions/26464838/getting-model-contenttype-in-migration-django-1-7 # https://stackoverflow.com/questions/26464838/getting-model-contenttype-in-migration-django-1-7
# The problem is that ContentType and Permission objects are not already created # The problem is that ContentType and Permission objects are not already created
# while we're still running migrations, so we'll go ahead and speen up that process # while we're still running migrations, so we'll go ahead and speed up that process
# a bit before we attempt to create groups which require Permissions and ContentType. # a bit before we attempt to create groups which require Permissions and ContentType.
from django.conf import settings from django.conf import settings
from django.db import migrations from django.db import migrations
def create_all_contenttypes(**kwargs): def create_all_contenttypes(**kwargs):
from django.apps import apps from django.apps import apps
from django.contrib.contenttypes.management import create_contenttypes from django.contrib.contenttypes.management import create_contenttypes
@ -14,6 +15,7 @@ def create_all_contenttypes(**kwargs):
for app_config in apps.get_app_configs(): for app_config in apps.get_app_configs():
create_contenttypes(app_config, **kwargs) create_contenttypes(app_config, **kwargs)
def create_all_permissions(**kwargs): def create_all_permissions(**kwargs):
from django.contrib.auth.management import create_permissions from django.contrib.auth.management import create_permissions
from django.apps import apps from django.apps import apps
@ -21,20 +23,21 @@ def create_all_permissions(**kwargs):
for app_config in apps.get_app_configs(): for app_config in apps.get_app_configs():
create_permissions(app_config, **kwargs) create_permissions(app_config, **kwargs)
def forward(apps, schema_editor): def forward(apps, schema_editor):
create_all_contenttypes() create_all_contenttypes()
create_all_permissions() create_all_permissions()
def backward(apps, schema_editor): def backward(apps, schema_editor):
pass pass
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('contenttypes', '0002_remove_content_type_name'), ("contenttypes", "0002_remove_content_type_name"),
("registrar", "0034_alter_user_options"), ("registrar", "0034_alter_user_options"),
] ]
operations = [ operations = [migrations.RunPython(forward, backward)]
migrations.RunPython(forward, backward)
]

View file

@ -1,22 +1,30 @@
# This migration creates the create_full_access_group and create_cisa_analyst_group groups # This migration creates the create_full_access_group and create_cisa_analyst_group groups
# It is dependent on 0035 (which populates ContentType and Permissions)
# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS # If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS
# in the user_group model then: # in the user_group model then:
# step 1: docker-compose exec app ./manage.py migrate --fake registrar 0035_contenttypes_permissions # step 1: docker-compose exec app ./manage.py migrate --fake registrar 0035_contenttypes_permissions
# step 2: docker-compose exec app ./manage.py migrate registrar 0036_create_groups # step 2: docker-compose exec app ./manage.py migrate registrar 0036_create_groups
# step 3: fake run the latest migration in the migrations list # step 3: fake run the latest migration in the migrations list
# Alternatively: # Alternatively:
# Only step: duplicate the migtation that loads data and run: docker-compose exec app ./manage.py migrate # Only step: duplicate the migtation that loads data and run: docker-compose exec app ./manage.py migrate
from django.db import migrations from django.db import migrations
from registrar.models import UserGroup from registrar.models import UserGroup
def create_groups():
UserGroup.create_cisa_analyst_group()
UserGroup.create_full_access_group()
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("registrar", "0035_contenttypes_permissions"), ("registrar", "0035_contenttypes_permissions"),
] ]
operations = [ operations = [
migrations.RunPython(UserGroup.create_cisa_analyst_group, reverse_code=migrations.RunPython.noop, atomic=True), migrations.RunPython(
migrations.RunPython(UserGroup.create_full_access_group, reverse_code=migrations.RunPython.noop, atomic=True), create_groups, # noqa
reverse_code=migrations.RunPython.noop,
atomic=True,
),
] ]

View file

@ -675,7 +675,7 @@ class Domain(TimeStampedModel, DomainHelper):
max_length=21, max_length=21,
choices=State.choices, choices=State.choices,
default=State.UNKNOWN, default=State.UNKNOWN,
protected=True, # cannot change state directly, particularly in Django admin protected=False, # cannot change state directly, particularly in Django admin
help_text="Very basic info about the lifecycle of this domain object", help_text="Very basic info about the lifecycle of this domain object",
) )

View file

@ -81,8 +81,9 @@ class User(AbstractUser):
logger.warn( logger.warn(
"Failed to retrieve invitation %s", invitation, exc_info=True "Failed to retrieve invitation %s", invitation, exc_info=True
) )
class Meta: class Meta:
permissions = [ permissions = [
("analyst_access_permission", "Analyst Access Permission"),
("full_access_permission", "Full Access Permission"), ("full_access_permission", "Full Access Permission"),
] ]

View file

@ -3,14 +3,15 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class UserGroup(Group):
class UserGroup(Group):
class Meta: class Meta:
verbose_name = "User group" verbose_name = "User group"
verbose_name_plural = "User groups" verbose_name_plural = "User groups"
def create_cisa_analyst_group(apps, schema_editor): def create_cisa_analyst_group(apps, schema_editor):
"""This method gets run from a data migration."""
# Hard to pass self to these methods as the calls from migrations # Hard to pass self to these methods as the calls from migrations
# are only expecting apps and schema_editor, so we'll just define # are only expecting apps and schema_editor, so we'll just define
# apps, schema_editor in the local scope instead # apps, schema_editor in the local scope instead
@ -20,7 +21,11 @@ class UserGroup(Group):
"model": "logentry", "model": "logentry",
"permissions": ["view_logentry"], "permissions": ["view_logentry"],
}, },
{"app_label": "registrar", "model": "contact", "permissions": ["view_contact"]}, {
"app_label": "registrar",
"model": "contact",
"permissions": ["view_contact"],
},
{ {
"app_label": "registrar", "app_label": "registrar",
"model": "domaininformation", "model": "domaininformation",
@ -31,16 +36,24 @@ class UserGroup(Group):
"model": "domainapplication", "model": "domainapplication",
"permissions": ["change_domainapplication"], "permissions": ["change_domainapplication"],
}, },
{"app_label": "registrar", "model": "domain", "permissions": ["view_domain"]}, {
"app_label": "registrar",
"model": "domain",
"permissions": ["view_domain"],
},
{ {
"app_label": "registrar", "app_label": "registrar",
"model": "draftdomain", "model": "draftdomain",
"permissions": ["change_draftdomain"], "permissions": ["change_draftdomain"],
}, },
{"app_label": "registrar", "model": "user", "permissions": ["change_user"]}, {
"app_label": "registrar",
"model": "user",
"permissions": ["analyst_access_permission", "change_user"],
},
] ]
# Avoid error: You can't execute queries until the end # Avoid error: You can't execute queries until the end
# of the 'atomic' block. # of the 'atomic' block.
# From django docs: # From django docs:
# https://docs.djangoproject.com/en/4.2/topics/migrations/#data-migrations # https://docs.djangoproject.com/en/4.2/topics/migrations/#data-migrations
@ -49,15 +62,15 @@ class UserGroup(Group):
ContentType = apps.get_model("contenttypes", "ContentType") ContentType = apps.get_model("contenttypes", "ContentType")
Permission = apps.get_model("auth", "Permission") Permission = apps.get_model("auth", "Permission")
UserGroup = apps.get_model("registrar", "UserGroup") UserGroup = apps.get_model("registrar", "UserGroup")
logger.info("Going to create the Analyst Group") logger.info("Going to create the Analyst Group")
try: try:
cisa_analysts_group, _ = UserGroup.objects.get_or_create( cisa_analysts_group, _ = UserGroup.objects.get_or_create(
name="cisa_analysts_group", name="cisa_analysts_group",
) )
cisa_analysts_group.permissions.clear() cisa_analysts_group.permissions.clear()
for permission in CISA_ANALYST_GROUP_PERMISSIONS: for permission in CISA_ANALYST_GROUP_PERMISSIONS:
app_label = permission["app_label"] app_label = permission["app_label"]
model_name = permission["model"] model_name = permission["model"]
@ -67,19 +80,17 @@ class UserGroup(Group):
content_type = ContentType.objects.get( content_type = ContentType.objects.get(
app_label=app_label, model=model_name app_label=app_label, model=model_name
) )
# Retrieve the permissions based on their codenames # Retrieve the permissions based on their codenames
permissions = Permission.objects.filter( permissions = Permission.objects.filter(
content_type=content_type, codename__in=permissions content_type=content_type, codename__in=permissions
) )
# Assign the permissions to the group # Assign the permissions to the group
cisa_analysts_group.permissions.add(*permissions) cisa_analysts_group.permissions.add(*permissions)
# Convert the permissions QuerySet to a list of codenames # Convert the permissions QuerySet to a list of codenames
permission_list = list( permission_list = list(permissions.values_list("codename", flat=True))
permissions.values_list("codename", flat=True)
)
logger.debug( logger.debug(
app_label app_label
@ -92,14 +103,18 @@ class UserGroup(Group):
) )
cisa_analysts_group.save() cisa_analysts_group.save()
logger.debug("CISA Analyt permissions added to group " + cisa_analysts_group.name) logger.debug(
"CISA Analyt permissions added to group " + cisa_analysts_group.name
)
except Exception as e: except Exception as e:
logger.error(f"Error creating analyst permissions group: {e}") logger.error(f"Error creating analyst permissions group: {e}")
def create_full_access_group(apps, schema_editor): def create_full_access_group(apps, schema_editor):
"""This method gets run from a data migration."""
Permission = apps.get_model("auth", "Permission") Permission = apps.get_model("auth", "Permission")
UserGroup = apps.get_model("registrar", "UserGroup") UserGroup = apps.get_model("registrar", "UserGroup")
logger.info("Going to create the Full Access Group") logger.info("Going to create the Full Access Group")
try: try:
full_access_group, _ = UserGroup.objects.get_or_create( full_access_group, _ = UserGroup.objects.get_or_create(
@ -107,10 +122,10 @@ class UserGroup(Group):
) )
# Get all available permissions # Get all available permissions
all_permissions = Permission.objects.all() all_permissions = Permission.objects.all()
# Assign all permissions to the group # Assign all permissions to the group
full_access_group.permissions.add(*all_permissions) full_access_group.permissions.add(*all_permissions)
full_access_group.save() full_access_group.save()
logger.debug("All permissions added to group " + full_access_group.name) logger.debug("All permissions added to group " + full_access_group.name)
except Exception as e: except Exception as e:

View file

@ -0,0 +1,51 @@
from django.test import TestCase
from registrar.models import (
UserGroup,
)
import logging
logger = logging.getLogger(__name__)
class TestGroups(TestCase):
def test_groups_created(self):
"""The test enviroment contains data that was created in migration,
so we are able to test groups and permissions.
- Test cisa_analysts_group and full_access_group created
- Test permissions on full_access_group
"""
# Get the UserGroup objects
cisa_analysts_group = UserGroup.objects.get(name="cisa_analysts_group")
full_access_group = UserGroup.objects.get(name="full_access_group")
# Assert that the cisa_analysts_group exists in the database
self.assertQuerysetEqual(
UserGroup.objects.filter(name="cisa_analysts_group"), [cisa_analysts_group]
)
# Assert that the full_access_group exists in the database
self.assertQuerysetEqual(
UserGroup.objects.filter(name="full_access_group"), [full_access_group]
)
# Test permissions for cisa_analysts)group
# Define the expected permission codenames
expected_permissions = [
"view_logentry",
"view_contact",
"view_domain",
"change_domainapplication",
"change_domaininformation",
"change_draftdomain",
"analyst_access_permission",
"change_user",
]
# Get the codenames of actual permissions associated with the group
actual_permissions = [p.codename for p in cisa_analysts_group.permissions.all()]
# Assert that the actual permissions match the expected permissions
self.assertListEqual(actual_permissions, expected_permissions)