Refactor groups and permissions: divide fixtures in 2 files, one for users and one for data, load groups in migrations (using methods defined in user_groups model), use hasperm in admin to test for 'superuser'

This commit is contained in:
Rachid Mrad 2023-09-28 17:34:53 -04:00
parent fd860998fb
commit cd14eb2584
No known key found for this signature in database
GPG key ID: EF38E4CEC4A8F3CF
15 changed files with 667 additions and 559 deletions

View file

@ -80,7 +80,7 @@ The endpoint /admin can be used to view and manage site content, including but n
1. Login via login.gov 1. Login via login.gov
2. Go to the home page and make sure you can see the part where you can submit an application 2. Go to the home page and make sure you can see the part where you can submit an application
3. Go to /admin and it will tell you that UUID is not authorized, copy that UUID for use in 4 3. Go to /admin and it will tell you that UUID is not authorized, copy that UUID for use in 4
4. in src/registrar/fixtures.py add to the `ADMINS` list in that file by adding your UUID as your username along with your first and last name. See below: 4. in src/registrar/fixtures_users.py add to the `ADMINS` list in that file by adding your UUID as your username along with your first and last name. See below:
``` ```
ADMINS = [ ADMINS = [
@ -102,7 +102,7 @@ Analysts are a variant of the admin role with limited permissions. The process f
1. Login via login.gov (if you already exist as an admin, you will need to create a separate login.gov account for this: i.e. first.last+1@email.com) 1. Login via login.gov (if you already exist as an admin, you will need to create a separate login.gov account for this: i.e. first.last+1@email.com)
2. Go to the home page and make sure you can see the part where you can submit an application 2. Go to the home page and make sure you can see the part where you can submit an application
3. Go to /admin and it will tell you that UUID is not authorized, copy that UUID for use in 4 (this will be a different UUID than the one obtained from creating an admin) 3. Go to /admin and it will tell you that UUID is not authorized, copy that UUID for use in 4 (this will be a different UUID than the one obtained from creating an admin)
4. in src/registrar/fixtures.py add to the `STAFF` list in that file by adding your UUID as your username along with your first and last name. See below: 4. in src/registrar/fixtures_users.py add to the `STAFF` list in that file by adding your UUID as your username along with your first and last name. See below:
``` ```
STAFF = [ STAFF = [
@ -145,7 +145,7 @@ You can change the logging verbosity, if needed. Do a web search for "django log
## Mock data ## Mock data
There is a `post_migrate` signal in [signals.py](../../src/registrar/signals.py) that will load the fixtures from [fixtures.py](../../src/registrar/fixtures.py), giving you some test data to play with while developing. There is a `post_migrate` signal in [signals.py](../../src/registrar/signals.py) that will load the fixtures from [fixtures_user.py](../../src/registrar/fixtures_users.py) and [fixtures_applications.py](../../src/registrar/fixtures_applications.py), giving you some test data to play with while developing.
See the [database-access README](./database-access.md) for information on how to pull data to update these fixtures. See the [database-access README](./database-access.md) for information on how to pull data to update these fixtures.

View file

@ -48,3 +48,7 @@ future, as we add additional roles that our product vision calls for
(read-only? editing only some information?), we need to add conditional (read-only? editing only some information?), we need to add conditional
behavior in the permission mixin, or additional mixins that more clearly behavior in the permission mixin, or additional mixins that more clearly
express what is allowed for those new roles. express what is allowed for those new roles.
# Admin User Permissions
Refre to [Django Admin Roles](../django-admin/roles.md)

View file

@ -1,21 +1,21 @@
# Django admin user roles # Django admin user roles
Roles other than superuser should be defined in authentication and authorization groups in django admin For our MVP, we create and maintain 2 admin roles:
Full access and CISA analyst. Both have the role `staff`.
Permissions on these roles are set through groups:
`full_access_group` and `cisa_analysts_group`. These
groups and the methods to create them are defined in
our `user_group` model and run in a migration.
## Superuser ## Editing group permissions through code
Full access We can edit and deploy new group permissions by
editing `user_group` then:
## CISA analyst - Duplicating migration `0036_create_groups`
and running migrations (RECOMMENDED METHOD), or
### Basic permission level - 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
Staff - step 2: docker-compose exec app ./manage.py migrate registrar 0036_create_groups
- step 3: fake run the latest migration in the migrations list
### Additional group permissions
auditlog | log entry | can view log entry
registrar | contact | can view contact
registrar | domain application | can change domain application
registrar | domain | can view domain
registrar | user | can view user

View file

@ -89,7 +89,8 @@ command in the running Cloud.gov container. For example, to run our Django
admin command that loads test fixture data: admin command that loads test fixture data:
``` ```
cf run-task getgov-{environment} --command "./manage.py load" --name fixtures cf run-task getgov-{environment} --command "./manage.py load" --name fixtures--users
cf run-task getgov-{environment} --command "./manage.py load" --name fixtures--applications
``` ```
However, this task runs asynchronously in the background without any command However, this task runs asynchronously in the background without any command

View file

@ -161,6 +161,9 @@ class MyUserAdmin(BaseUserAdmin):
("Important dates", {"fields": ("last_login", "date_joined")}), ("Important dates", {"fields": ("last_login", "date_joined")}),
) )
# Hide Username (uuid), Groups and Permissions
# Q: Now that we're using Groups and Permissions,
# do we expose those to analysts to view?
analyst_fieldsets = ( analyst_fieldsets = (
( (
None, None,
@ -180,6 +183,8 @@ class MyUserAdmin(BaseUserAdmin):
("Important dates", {"fields": ("last_login", "date_joined")}), ("Important dates", {"fields": ("last_login", "date_joined")}),
) )
# NOT all fields are readonly for admin, otherwise we would have
# set this at the permissions level. The exception is 'status'
analyst_readonly_fields = [ analyst_readonly_fields = [
"password", "password",
"Personal Info", "Personal Info",
@ -196,8 +201,15 @@ class MyUserAdmin(BaseUserAdmin):
] ]
def get_list_display(self, request): def get_list_display(self, request):
if request.user.groups.filter(name='cisa_analysts_group').exists(): # The full_access_permission perm will load onto the full_access_group
# Customize the list display for staff users # which is equivalent to superuser. The other group we use to manage
# perms is cisa_analysts_group. cisa_analysts_group will never contain
# full_access_permission
if request.user.has_perm('registrar.full_access_permission'):
# Use the default list display for all access users
return super().get_list_display(request)
# Customize the list display for analysts
return ( return (
"email", "email",
"first_name", "first_name",
@ -207,22 +219,18 @@ class MyUserAdmin(BaseUserAdmin):
"status", "status",
) )
# Use the default list display for non-staff users
return super().get_list_display(request)
def get_fieldsets(self, request, obj=None): def get_fieldsets(self, request, obj=None):
if request.user.groups.filter(name='cisa_analysts_group').exists(): if request.user.has_perm('registrar.full_access_permission'):
# If the user doesn't have permission to change the model, # Show all fields for all access users
# show a read-only fieldset
return self.analyst_fieldsets
# If the user has permission to change the model, show all fields
return super().get_fieldsets(request, obj) return super().get_fieldsets(request, obj)
# show analyst_fieldsets for analysts
return self.analyst_fieldsets
def get_readonly_fields(self, request, obj=None): def get_readonly_fields(self, request, obj=None):
if request.user.groups.filter(name='cisa_analysts_group').exists(): if request.user.has_perm('registrar.full_access_permission'):
return () # No read-only fields for all access users
return self.analyst_readonly_fields # Read-only fields for analysts return self.analyst_readonly_fields # Read-only fields for analysts
return () # No read-only fields for other users
class HostIPInline(admin.StackedInline): class HostIPInline(admin.StackedInline):
@ -401,7 +409,7 @@ class DomainInformationAdmin(ListHeaderAdmin):
readonly_fields = list(self.readonly_fields) readonly_fields = list(self.readonly_fields)
if request.user.groups.filter(name='full_access_group').exists(): if request.user.has_perm('registrar.full_access_permission'):
return readonly_fields return readonly_fields
else: else:
readonly_fields.extend([field for field in self.analyst_readonly_fields]) readonly_fields.extend([field for field in self.analyst_readonly_fields])
@ -619,7 +627,7 @@ class DomainApplicationAdmin(ListHeaderAdmin):
["current_websites", "other_contacts", "alternative_domains"] ["current_websites", "other_contacts", "alternative_domains"]
) )
if request.user.groups.filter(name='full_access_group').exists(): if request.user.has_perm('registrar.full_access_permission'):
return readonly_fields return readonly_fields
else: else:
readonly_fields.extend([field for field in self.analyst_readonly_fields]) readonly_fields.extend([field for field in self.analyst_readonly_fields])

View file

@ -1,511 +0,0 @@
import logging
import random
from faker import Faker
from registrar.models import (
User,
UserGroup,
DomainApplication,
DraftDomain,
Contact,
Website,
)
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
fake = Faker()
logger = logging.getLogger(__name__)
class UserFixture:
"""
Load users into the database.
Make sure this class' `load` method is called from `handle`
in management/commands/load.py, then use `./manage.py load`
to run this code.
"""
ADMINS = [
{
"username": "5f283494-31bd-49b5-b024-a7e7cae00848",
"first_name": "Rachid",
"last_name": "Mrad",
},
# {
# "username": "eb2214cd-fc0c-48c0-9dbd-bc4cd6820c74",
# "first_name": "Alysia",
# "last_name": "Broddrick",
# },
# {
# "username": "8f8e7293-17f7-4716-889b-1990241cbd39",
# "first_name": "Katherine",
# "last_name": "Osos",
# },
# {
# "username": "70488e0a-e937-4894-a28c-16f5949effd4",
# "first_name": "Gaby",
# "last_name": "DiSarli",
# },
# {
# "username": "83c2b6dd-20a2-4cac-bb40-e22a72d2955c",
# "first_name": "Cameron",
# "last_name": "Dixon",
# },
# {
# "username": "0353607a-cbba-47d2-98d7-e83dcd5b90ea",
# "first_name": "Ryan",
# "last_name": "Brooks",
# },
# {
# "username": "30001ee7-0467-4df2-8db2-786e79606060",
# "first_name": "Zander",
# "last_name": "Adkinson",
# },
# {
# "username": "2bf518c2-485a-4c42-ab1a-f5a8b0a08484",
# "first_name": "Paul",
# "last_name": "Kuykendall",
# },
# {
# "username": "2a88a97b-be96-4aad-b99e-0b605b492c78",
# "first_name": "Rebecca",
# "last_name": "Hsieh",
# },
# {
# "username": "fa69c8e8-da83-4798-a4f2-263c9ce93f52",
# "first_name": "David",
# "last_name": "Kennedy",
# },
# {
# "username": "f14433d8-f0e9-41bf-9c72-b99b110e665d",
# "first_name": "Nicolle",
# "last_name": "LeClair",
# },
]
STAFF = [
{
"username": "319c490d-453b-43d9-bc4d-7d6cd8ff6844",
"first_name": "Rachid-Analyst",
"last_name": "Mrad-Analyst",
"email": "rachid.mrad@gmail.com",
},
# {
# "username": "b6a15987-5c88-4e26-8de2-ca71a0bdb2cd",
# "first_name": "Alysia-Analyst",
# "last_name": "Alysia-Analyst",
# },
# {
# "username": "91a9b97c-bd0a-458d-9823-babfde7ebf44",
# "first_name": "Katherine-Analyst",
# "last_name": "Osos-Analyst",
# "email": "kosos@truss.works",
# },
# {
# "username": "2cc0cde8-8313-4a50-99d8-5882e71443e8",
# "first_name": "Zander-Analyst",
# "last_name": "Adkinson-Analyst",
# },
# {
# "username": "57ab5847-7789-49fe-a2f9-21d38076d699",
# "first_name": "Paul-Analyst",
# "last_name": "Kuykendall-Analyst",
# },
# {
# "username": "e474e7a9-71ca-449d-833c-8a6e094dd117",
# "first_name": "Rebecca-Analyst",
# "last_name": "Hsieh-Analyst",
# },
# {
# "username": "5dc6c9a6-61d9-42b4-ba54-4beff28bac3c",
# "first_name": "David-Analyst",
# "last_name": "Kennedy-Analyst",
# },
# {
# "username": "0eb6f326-a3d4-410f-a521-aa4c1fad4e47",
# "first_name": "Gaby-Analyst",
# "last_name": "DiSarli-Analyst",
# "email": "gaby@truss.works",
# },
# {
# "username": "cfe7c2fc-e24a-480e-8b78-28645a1459b3",
# "first_name": "Nicolle-Analyst",
# "last_name": "LeClair-Analyst",
# "email": "nicolle.leclair@ecstech.com",
# },
]
CISA_ANALYST_GROUP_PERMISSIONS = [
{
"app_label": "auditlog",
"model": "logentry",
"permissions": ["view_logentry"],
},
{"app_label": "registrar", "model": "contact", "permissions": ["view_contact"]},
{
"app_label": "registrar",
"model": "domaininformation",
"permissions": ["change_domaininformation"],
},
{
"app_label": "registrar",
"model": "domainapplication",
"permissions": ["change_domainapplication"],
},
{"app_label": "registrar", "model": "domain", "permissions": ["view_domain"]},
{
"app_label": "registrar",
"model": "draftdomain",
"permissions": ["change_draftdomain"],
},
{"app_label": "registrar", "model": "user", "permissions": ["change_user"]},
]
@classmethod
def load(cls):
logger.info("Going to load %s groups" % str(len(cls.ADMINS)))
try:
cisa_analysts_group, cisa_analysts_group_created = UserGroup.objects.get_or_create(
name="cisa_analysts_group",
)
full_access_group, full_access_group_created = UserGroup.objects.get_or_create(
name="full_access_group",
)
except Exception as e:
logger.warning(e)
if cisa_analysts_group_created:
for permission in cls.CISA_ANALYST_GROUP_PERMISSIONS:
try:
app_label = permission["app_label"]
model_name = permission["model"]
permissions = permission["permissions"]
# Retrieve the content type for the app and model
content_type = ContentType.objects.get(
app_label=app_label, model=model_name
)
# Retrieve the permissions based on their codenames
permissions = Permission.objects.filter(
content_type=content_type, codename__in=permissions
)
# Assign the permissions to the group
cisa_analysts_group.permissions.add(*permissions)
# Convert the permissions QuerySet to a list of codenames
permission_list = list(
permissions.values_list("codename", flat=True)
)
logger.debug(
app_label
+ " | "
+ model_name
+ " | "
+ ", ".join(permission_list)
+ " added to group "
+ cisa_analysts_group.name
)
cisa_analysts_group.save()
logger.debug("CISA Analyt permissions added to group " + cisa_analysts_group.name)
except Exception as e:
logger.warning(e)
else:
logger.warning(cisa_analysts_group.name + " was not created successfully.")
if full_access_group_created:
try:
# Get all available permissions
all_permissions = Permission.objects.all()
# Assign all permissions to the group
full_access_group.permissions.add(*all_permissions)
full_access_group.save()
logger.debug("All permissions added to group " + full_access_group.name)
except Exception as e:
logger.warning(e)
else:
logger.warning(full_access_group.name + " was not created successfully.")
logger.info("%s groups loaded." % str(len(cls.ADMINS)))
logger.info("Going to load %s superusers" % str(len(cls.ADMINS)))
for admin in cls.ADMINS:
try:
user, _ = User.objects.get_or_create(
username=admin["username"],
)
user.is_superuser = False
user.first_name = admin["first_name"]
user.last_name = admin["last_name"]
if "email" in admin.keys():
user.email = admin["email"]
user.is_staff = True
user.is_active = True
user.groups.add(full_access_group)
user.save()
logger.debug("User object created for %s" % admin["first_name"])
except Exception as e:
logger.warning(e)
logger.info("All superusers loaded.")
logger.info("Going to load %s CISA analysts (staff)" % str(len(cls.STAFF)))
for staff in cls.STAFF:
try:
user, _ = User.objects.get_or_create(
username=staff["username"],
)
user.is_superuser = False
user.first_name = staff["first_name"]
user.last_name = staff["last_name"]
if "email" in admin.keys():
user.email = admin["email"]
user.is_staff = True
user.is_active = True
user.groups.add(cisa_analysts_group)
user.save()
logger.debug("User object created for %s" % staff["first_name"])
except Exception as e:
logger.warning(e)
logger.info("All CISA analysts (staff) loaded.")
class DomainApplicationFixture:
"""
Load domain applications into the database.
Make sure this class' `load` method is called from `handle`
in management/commands/load.py, then use `./manage.py load`
to run this code.
"""
# any fields not specified here will be filled in with fake data or defaults
# NOTE BENE: each fixture must have `organization_name` for uniqueness!
# Here is a more complete example as a template:
# {
# "status": "started",
# "organization_name": "Example - Just started",
# "organization_type": "federal",
# "federal_agency": None,
# "federal_type": None,
# "address_line1": None,
# "address_line2": None,
# "city": None,
# "state_territory": None,
# "zipcode": None,
# "urbanization": None,
# "purpose": None,
# "anything_else": None,
# "is_policy_acknowledged": None,
# "authorizing_official": None,
# "submitter": None,
# "other_contacts": [],
# "current_websites": [],
# "alternative_domains": [],
# },
DA = [
{
"status": "started",
"organization_name": "Example - Finished but not Submitted",
},
{
"status": "submitted",
"organization_name": "Example - Submitted but pending Investigation",
},
{
"status": "in review",
"organization_name": "Example - In Investigation",
},
{
"status": "in review",
"organization_name": "Example - Approved",
},
{
"status": "withdrawn",
"organization_name": "Example - Withdrawn",
},
{
"status": "action needed",
"organization_name": "Example - Action Needed",
},
{
"status": "rejected",
"organization_name": "Example - Rejected",
},
]
@classmethod
def fake_contact(cls):
return {
"first_name": fake.first_name(),
"middle_name": None,
"last_name": fake.last_name(),
"title": fake.job(),
"email": fake.ascii_safe_email(),
"phone": "201-555-5555",
}
@classmethod
def fake_dot_gov(cls):
return f"{fake.slug()}.gov"
@classmethod
def _set_non_foreign_key_fields(cls, da: DomainApplication, app: dict):
"""Helper method used by `load`."""
da.status = app["status"] if "status" in app else "started"
da.organization_type = (
app["organization_type"] if "organization_type" in app else "federal"
)
da.federal_agency = (
app["federal_agency"]
if "federal_agency" in app
# Random choice of agency for selects, used as placeholders for testing.
else random.choice(DomainApplication.AGENCIES) # nosec
)
da.federal_type = (
app["federal_type"]
if "federal_type" in app
else random.choice(["executive", "judicial", "legislative"]) # nosec
)
da.address_line1 = (
app["address_line1"] if "address_line1" in app else fake.street_address()
)
da.address_line2 = app["address_line2"] if "address_line2" in app else None
da.city = app["city"] if "city" in app else fake.city()
da.state_territory = (
app["state_territory"] if "state_territory" in app else fake.state_abbr()
)
da.zipcode = app["zipcode"] if "zipcode" in app else fake.postalcode()
da.urbanization = app["urbanization"] if "urbanization" in app else None
da.purpose = app["purpose"] if "purpose" in app else fake.paragraph()
da.anything_else = app["anything_else"] if "anything_else" in app else None
da.is_policy_acknowledged = (
app["is_policy_acknowledged"] if "is_policy_acknowledged" in app else True
)
@classmethod
def _set_foreign_key_fields(cls, da: DomainApplication, app: dict, user: User):
"""Helper method used by `load`."""
if not da.investigator:
da.investigator = (
User.objects.get(username=user.username)
if "investigator" in app
else None
)
if not da.authorizing_official:
if (
"authorizing_official" in app
and app["authorizing_official"] is not None
):
da.authorizing_official, _ = Contact.objects.get_or_create(
**app["authorizing_official"]
)
else:
da.authorizing_official = Contact.objects.create(**cls.fake_contact())
if not da.submitter:
if "submitter" in app and app["submitter"] is not None:
da.submitter, _ = Contact.objects.get_or_create(**app["submitter"])
else:
da.submitter = Contact.objects.create(**cls.fake_contact())
if not da.requested_domain:
if "requested_domain" in app and app["requested_domain"] is not None:
da.requested_domain, _ = DraftDomain.objects.get_or_create(
name=app["requested_domain"]
)
else:
da.requested_domain = DraftDomain.objects.create(
name=cls.fake_dot_gov()
)
@classmethod
def _set_many_to_many_relations(cls, da: DomainApplication, app: dict):
"""Helper method used by `load`."""
if "other_contacts" in app:
for contact in app["other_contacts"]:
da.other_contacts.add(Contact.objects.get_or_create(**contact)[0])
elif not da.other_contacts.exists():
other_contacts = [
Contact.objects.create(**cls.fake_contact())
for _ in range(random.randint(0, 3)) # nosec
]
da.other_contacts.add(*other_contacts)
if "current_websites" in app:
for website in app["current_websites"]:
da.current_websites.add(
Website.objects.get_or_create(website=website)[0]
)
elif not da.current_websites.exists():
current_websites = [
Website.objects.create(website=fake.uri())
for _ in range(random.randint(0, 3)) # nosec
]
da.current_websites.add(*current_websites)
if "alternative_domains" in app:
for domain in app["alternative_domains"]:
da.alternative_domains.add(
Website.objects.get_or_create(website=domain)[0]
)
elif not da.alternative_domains.exists():
alternative_domains = [
Website.objects.create(website=cls.fake_dot_gov())
for _ in range(random.randint(0, 3)) # nosec
]
da.alternative_domains.add(*alternative_domains)
@classmethod
def load(cls):
"""Creates domain applications for each user in the database."""
logger.info("Going to load %s domain applications" % len(cls.DA))
try:
users = list(User.objects.all()) # force evaluation to catch db errors
except Exception as e:
logger.warning(e)
return
for user in users:
logger.debug("Loading domain applications for %s" % user)
for app in cls.DA:
try:
da, _ = DomainApplication.objects.get_or_create(
creator=user,
organization_name=app["organization_name"],
)
cls._set_non_foreign_key_fields(da, app)
cls._set_foreign_key_fields(da, app, user)
da.save()
cls._set_many_to_many_relations(da, app)
except Exception as e:
logger.warning(e)
class DomainFixture(DomainApplicationFixture):
"""Create one domain and permissions on it for each user."""
@classmethod
def load(cls):
try:
users = list(User.objects.all()) # force evaluation to catch db errors
except Exception as e:
logger.warning(e)
return
for user in users:
# approve one of each users in review status domains
application = DomainApplication.objects.filter(
creator=user, status=DomainApplication.IN_REVIEW
).last()
logger.debug(f"Approving {application} for {user}")
application.approve()
application.save()

View file

@ -0,0 +1,253 @@
import logging
import random
from faker import Faker
from registrar.models import (
User,
DomainApplication,
DraftDomain,
Contact,
Website,
)
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
fake = Faker()
logger = logging.getLogger(__name__)
class DomainApplicationFixture:
"""
Load domain applications into the database.
Make sure this class' `load` method is called from `handle`
in management/commands/load.py, then use `./manage.py load`
to run this code.
"""
# any fields not specified here will be filled in with fake data or defaults
# NOTE BENE: each fixture must have `organization_name` for uniqueness!
# Here is a more complete example as a template:
# {
# "status": "started",
# "organization_name": "Example - Just started",
# "organization_type": "federal",
# "federal_agency": None,
# "federal_type": None,
# "address_line1": None,
# "address_line2": None,
# "city": None,
# "state_territory": None,
# "zipcode": None,
# "urbanization": None,
# "purpose": None,
# "anything_else": None,
# "is_policy_acknowledged": None,
# "authorizing_official": None,
# "submitter": None,
# "other_contacts": [],
# "current_websites": [],
# "alternative_domains": [],
# },
DA = [
{
"status": "started",
"organization_name": "Example - Finished but not Submitted",
},
{
"status": "submitted",
"organization_name": "Example - Submitted but pending Investigation",
},
{
"status": "in review",
"organization_name": "Example - In Investigation",
},
{
"status": "in review",
"organization_name": "Example - Approved",
},
{
"status": "withdrawn",
"organization_name": "Example - Withdrawn",
},
{
"status": "action needed",
"organization_name": "Example - Action Needed",
},
{
"status": "rejected",
"organization_name": "Example - Rejected",
},
]
@classmethod
def fake_contact(cls):
return {
"first_name": fake.first_name(),
"middle_name": None,
"last_name": fake.last_name(),
"title": fake.job(),
"email": fake.ascii_safe_email(),
"phone": "201-555-5555",
}
@classmethod
def fake_dot_gov(cls):
return f"{fake.slug()}.gov"
@classmethod
def _set_non_foreign_key_fields(cls, da: DomainApplication, app: dict):
"""Helper method used by `load`."""
da.status = app["status"] if "status" in app else "started"
da.organization_type = (
app["organization_type"] if "organization_type" in app else "federal"
)
da.federal_agency = (
app["federal_agency"]
if "federal_agency" in app
# Random choice of agency for selects, used as placeholders for testing.
else random.choice(DomainApplication.AGENCIES) # nosec
)
da.federal_type = (
app["federal_type"]
if "federal_type" in app
else random.choice(["executive", "judicial", "legislative"]) # nosec
)
da.address_line1 = (
app["address_line1"] if "address_line1" in app else fake.street_address()
)
da.address_line2 = app["address_line2"] if "address_line2" in app else None
da.city = app["city"] if "city" in app else fake.city()
da.state_territory = (
app["state_territory"] if "state_territory" in app else fake.state_abbr()
)
da.zipcode = app["zipcode"] if "zipcode" in app else fake.postalcode()
da.urbanization = app["urbanization"] if "urbanization" in app else None
da.purpose = app["purpose"] if "purpose" in app else fake.paragraph()
da.anything_else = app["anything_else"] if "anything_else" in app else None
da.is_policy_acknowledged = (
app["is_policy_acknowledged"] if "is_policy_acknowledged" in app else True
)
@classmethod
def _set_foreign_key_fields(cls, da: DomainApplication, app: dict, user: User):
"""Helper method used by `load`."""
if not da.investigator:
da.investigator = (
User.objects.get(username=user.username)
if "investigator" in app
else None
)
if not da.authorizing_official:
if (
"authorizing_official" in app
and app["authorizing_official"] is not None
):
da.authorizing_official, _ = Contact.objects.get_or_create(
**app["authorizing_official"]
)
else:
da.authorizing_official = Contact.objects.create(**cls.fake_contact())
if not da.submitter:
if "submitter" in app and app["submitter"] is not None:
da.submitter, _ = Contact.objects.get_or_create(**app["submitter"])
else:
da.submitter = Contact.objects.create(**cls.fake_contact())
if not da.requested_domain:
if "requested_domain" in app and app["requested_domain"] is not None:
da.requested_domain, _ = DraftDomain.objects.get_or_create(
name=app["requested_domain"]
)
else:
da.requested_domain = DraftDomain.objects.create(
name=cls.fake_dot_gov()
)
@classmethod
def _set_many_to_many_relations(cls, da: DomainApplication, app: dict):
"""Helper method used by `load`."""
if "other_contacts" in app:
for contact in app["other_contacts"]:
da.other_contacts.add(Contact.objects.get_or_create(**contact)[0])
elif not da.other_contacts.exists():
other_contacts = [
Contact.objects.create(**cls.fake_contact())
for _ in range(random.randint(0, 3)) # nosec
]
da.other_contacts.add(*other_contacts)
if "current_websites" in app:
for website in app["current_websites"]:
da.current_websites.add(
Website.objects.get_or_create(website=website)[0]
)
elif not da.current_websites.exists():
current_websites = [
Website.objects.create(website=fake.uri())
for _ in range(random.randint(0, 3)) # nosec
]
da.current_websites.add(*current_websites)
if "alternative_domains" in app:
for domain in app["alternative_domains"]:
da.alternative_domains.add(
Website.objects.get_or_create(website=domain)[0]
)
elif not da.alternative_domains.exists():
alternative_domains = [
Website.objects.create(website=cls.fake_dot_gov())
for _ in range(random.randint(0, 3)) # nosec
]
da.alternative_domains.add(*alternative_domains)
@classmethod
def load(cls):
"""Creates domain applications for each user in the database."""
logger.info("Going to load %s domain applications" % len(cls.DA))
try:
users = list(User.objects.all()) # force evaluation to catch db errors
except Exception as e:
logger.warning(e)
return
for user in users:
logger.debug("Loading domain applications for %s" % user)
for app in cls.DA:
try:
da, _ = DomainApplication.objects.get_or_create(
creator=user,
organization_name=app["organization_name"],
)
cls._set_non_foreign_key_fields(da, app)
cls._set_foreign_key_fields(da, app, user)
da.save()
cls._set_many_to_many_relations(da, app)
except Exception as e:
logger.warning(e)
class DomainFixture(DomainApplicationFixture):
"""Create one domain and permissions on it for each user."""
@classmethod
def load(cls):
try:
users = list(User.objects.all()) # force evaluation to catch db errors
except Exception as e:
logger.warning(e)
return
for user in users:
# approve one of each users in review status domains
application = DomainApplication.objects.filter(
creator=user, status=DomainApplication.IN_REVIEW
).last()
logger.debug(f"Approving {application} for {user}")
application.approve()
application.save()

View file

@ -0,0 +1,156 @@
import logging
from faker import Faker
from registrar.models import (
User,
UserGroup,
)
fake = Faker()
logger = logging.getLogger(__name__)
class UserFixture:
"""
Load users into the database.
Make sure this class' `load` method is called from `handle`
in management/commands/load.py, then use `./manage.py load`
to run this code.
"""
ADMINS = [
{
"username": "5f283494-31bd-49b5-b024-a7e7cae00848",
"first_name": "Rachid",
"last_name": "Mrad",
},
{
"username": "eb2214cd-fc0c-48c0-9dbd-bc4cd6820c74",
"first_name": "Alysia",
"last_name": "Broddrick",
},
{
"username": "8f8e7293-17f7-4716-889b-1990241cbd39",
"first_name": "Katherine",
"last_name": "Osos",
},
{
"username": "70488e0a-e937-4894-a28c-16f5949effd4",
"first_name": "Gaby",
"last_name": "DiSarli",
},
{
"username": "83c2b6dd-20a2-4cac-bb40-e22a72d2955c",
"first_name": "Cameron",
"last_name": "Dixon",
},
{
"username": "0353607a-cbba-47d2-98d7-e83dcd5b90ea",
"first_name": "Ryan",
"last_name": "Brooks",
},
{
"username": "30001ee7-0467-4df2-8db2-786e79606060",
"first_name": "Zander",
"last_name": "Adkinson",
},
{
"username": "2bf518c2-485a-4c42-ab1a-f5a8b0a08484",
"first_name": "Paul",
"last_name": "Kuykendall",
},
{
"username": "2a88a97b-be96-4aad-b99e-0b605b492c78",
"first_name": "Rebecca",
"last_name": "Hsieh",
},
{
"username": "fa69c8e8-da83-4798-a4f2-263c9ce93f52",
"first_name": "David",
"last_name": "Kennedy",
},
{
"username": "f14433d8-f0e9-41bf-9c72-b99b110e665d",
"first_name": "Nicolle",
"last_name": "LeClair",
},
]
STAFF = [
{
"username": "319c490d-453b-43d9-bc4d-7d6cd8ff6844",
"first_name": "Rachid-Analyst",
"last_name": "Mrad-Analyst",
"email": "rachid.mrad@gmail.com",
},
{
"username": "b6a15987-5c88-4e26-8de2-ca71a0bdb2cd",
"first_name": "Alysia-Analyst",
"last_name": "Alysia-Analyst",
},
{
"username": "91a9b97c-bd0a-458d-9823-babfde7ebf44",
"first_name": "Katherine-Analyst",
"last_name": "Osos-Analyst",
"email": "kosos@truss.works",
},
{
"username": "2cc0cde8-8313-4a50-99d8-5882e71443e8",
"first_name": "Zander-Analyst",
"last_name": "Adkinson-Analyst",
},
{
"username": "57ab5847-7789-49fe-a2f9-21d38076d699",
"first_name": "Paul-Analyst",
"last_name": "Kuykendall-Analyst",
},
{
"username": "e474e7a9-71ca-449d-833c-8a6e094dd117",
"first_name": "Rebecca-Analyst",
"last_name": "Hsieh-Analyst",
},
{
"username": "5dc6c9a6-61d9-42b4-ba54-4beff28bac3c",
"first_name": "David-Analyst",
"last_name": "Kennedy-Analyst",
},
{
"username": "0eb6f326-a3d4-410f-a521-aa4c1fad4e47",
"first_name": "Gaby-Analyst",
"last_name": "DiSarli-Analyst",
"email": "gaby@truss.works",
},
{
"username": "cfe7c2fc-e24a-480e-8b78-28645a1459b3",
"first_name": "Nicolle-Analyst",
"last_name": "LeClair-Analyst",
"email": "nicolle.leclair@ecstech.com",
},
]
def load_users(cls, users, group_name):
logger.info(f"Going to load {len(users)} users in group {group_name}")
for user_data in users:
try:
user, _ = User.objects.get_or_create(username=user_data["username"])
user.is_superuser = False
user.first_name = user_data["first_name"]
user.last_name = user_data["last_name"]
if "email" in user_data:
user.email = user_data["email"]
user.is_staff = True
user.is_active = True
group = UserGroup.objects.get(name=group_name)
user.groups.add(group)
user.save()
logger.debug(f"User object created for {user_data['first_name']}")
except Exception as e:
logger.warning(e)
logger.info(f"All users in group {group_name} loaded.")
@classmethod
def load(cls):
cls.load_users(cls, cls.ADMINS, "full_access_group")
cls.load_users(cls, cls.STAFF, "cisa_analysts_group")

View file

@ -4,7 +4,8 @@ from django.core.management.base import BaseCommand
from auditlog.context import disable_auditlog # type: ignore from auditlog.context import disable_auditlog # type: ignore
from registrar.fixtures import UserFixture, DomainApplicationFixture, DomainFixture from registrar.fixtures_users import UserFixture
from registrar.fixtures_applications import DomainApplicationFixture, DomainFixture
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View file

@ -8,7 +8,7 @@ import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("auth", "0012_alter_user_first_name_max_length"), ("auth", "0012_alter_user_first_name_max_length"),
("registrar", "0031_transitiondomain_and_more"), ("registrar", "0032_alter_transitiondomain_status"),
] ]
operations = [ operations = [

View file

@ -0,0 +1,20 @@
# Generated by Django 4.2.1 on 2023-09-27 18:53
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("registrar", "0033_usergroup"),
]
operations = [
migrations.AlterModelOptions(
name="user",
options={
"permissions": [
("full_access_permission", "Full Access Permission"),
]
},
),
]

View file

@ -0,0 +1,40 @@
# From mithuntnt's answer on:
# 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
# while we're still running migrations, so we'll go ahead and speen up that process
# a bit before we attempt to create groups which require Permissions and ContentType.
from django.conf import settings
from django.db import migrations
def create_all_contenttypes(**kwargs):
from django.apps import apps
from django.contrib.contenttypes.management import create_contenttypes
for app_config in apps.get_app_configs():
create_contenttypes(app_config, **kwargs)
def create_all_permissions(**kwargs):
from django.contrib.auth.management import create_permissions
from django.apps import apps
for app_config in apps.get_app_configs():
create_permissions(app_config, **kwargs)
def forward(apps, schema_editor):
create_all_contenttypes()
create_all_permissions()
def backward(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('contenttypes', '0002_remove_content_type_name'),
("registrar", "0034_alter_user_options"),
]
operations = [
migrations.RunPython(forward, backward)
]

View file

@ -0,0 +1,22 @@
# This migration creates the create_full_access_group and create_cisa_analyst_group groups
# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS
# in the user_group model then:
# 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
# Alternatively:
# Only step: duplicate the migtation that loads data and run: docker-compose exec app ./manage.py migrate
from django.db import migrations
from registrar.models import UserGroup
class Migration(migrations.Migration):
dependencies = [
("registrar", "0035_contenttypes_permissions"),
]
operations = [
migrations.RunPython(UserGroup.create_cisa_analyst_group, reverse_code=migrations.RunPython.noop, atomic=True),
migrations.RunPython(UserGroup.create_full_access_group, reverse_code=migrations.RunPython.noop, atomic=True),
]

View file

@ -81,3 +81,8 @@ 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:
permissions = [
("full_access_permission", "Full Access Permission"),
]

View file

@ -1,8 +1,117 @@
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
import logging
logger = logging.getLogger(__name__)
class UserGroup(Group): class UserGroup(Group):
# Add custom fields or methods specific to your group model here
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):
# Hard to pass self to these methods as the calls from migrations
# are only expecting apps and schema_editor, so we'll just define
# apps, schema_editor in the local scope instead
CISA_ANALYST_GROUP_PERMISSIONS = [
{
"app_label": "auditlog",
"model": "logentry",
"permissions": ["view_logentry"],
},
{"app_label": "registrar", "model": "contact", "permissions": ["view_contact"]},
{
"app_label": "registrar",
"model": "domaininformation",
"permissions": ["change_domaininformation"],
},
{
"app_label": "registrar",
"model": "domainapplication",
"permissions": ["change_domainapplication"],
},
{"app_label": "registrar", "model": "domain", "permissions": ["view_domain"]},
{
"app_label": "registrar",
"model": "draftdomain",
"permissions": ["change_draftdomain"],
},
{"app_label": "registrar", "model": "user", "permissions": ["change_user"]},
]
# Avoid error: You can't execute queries until the end
# of the 'atomic' block.
# From django docs:
# https://docs.djangoproject.com/en/4.2/topics/migrations/#data-migrations
# We cant import the Person model directly as it may be a newer
# version than this migration expects. We use the historical version.
ContentType = apps.get_model("contenttypes", "ContentType")
Permission = apps.get_model("auth", "Permission")
UserGroup = apps.get_model("registrar", "UserGroup")
logger.info("Going to create the Analyst Group")
try:
cisa_analysts_group, _ = UserGroup.objects.get_or_create(
name="cisa_analysts_group",
)
cisa_analysts_group.permissions.clear()
for permission in CISA_ANALYST_GROUP_PERMISSIONS:
app_label = permission["app_label"]
model_name = permission["model"]
permissions = permission["permissions"]
# Retrieve the content type for the app and model
content_type = ContentType.objects.get(
app_label=app_label, model=model_name
)
# Retrieve the permissions based on their codenames
permissions = Permission.objects.filter(
content_type=content_type, codename__in=permissions
)
# Assign the permissions to the group
cisa_analysts_group.permissions.add(*permissions)
# Convert the permissions QuerySet to a list of codenames
permission_list = list(
permissions.values_list("codename", flat=True)
)
logger.debug(
app_label
+ " | "
+ model_name
+ " | "
+ ", ".join(permission_list)
+ " added to group "
+ cisa_analysts_group.name
)
cisa_analysts_group.save()
logger.debug("CISA Analyt permissions added to group " + cisa_analysts_group.name)
except Exception as e:
logger.error(f"Error creating analyst permissions group: {e}")
def create_full_access_group(apps, schema_editor):
Permission = apps.get_model("auth", "Permission")
UserGroup = apps.get_model("registrar", "UserGroup")
logger.info("Going to create the Full Access Group")
try:
full_access_group, _ = UserGroup.objects.get_or_create(
name="full_access_group",
)
# Get all available permissions
all_permissions = Permission.objects.all()
# Assign all permissions to the group
full_access_group.permissions.add(*all_permissions)
full_access_group.save()
logger.debug("All permissions added to group " + full_access_group.name)
except Exception as e:
logger.error(f"Error creating full access group: {e}")