mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-29 08:50:01 +02:00
Enable auditlogging
This commit is contained in:
parent
a25dd16094
commit
349659be90
7 changed files with 210 additions and 67 deletions
|
@ -8,6 +8,7 @@ django = "*"
|
||||||
cfenv = "*"
|
cfenv = "*"
|
||||||
pycryptodomex = "*"
|
pycryptodomex = "*"
|
||||||
django-allow-cidr = "*"
|
django-allow-cidr = "*"
|
||||||
|
django-auditlog = "*"
|
||||||
django-csp = "*"
|
django-csp = "*"
|
||||||
environs = {extras=["django"]}
|
environs = {extras=["django"]}
|
||||||
gunicorn = "*"
|
gunicorn = "*"
|
||||||
|
|
|
@ -1,6 +1,25 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.auth.admin import UserAdmin
|
from django.contrib.auth.admin import UserAdmin
|
||||||
from .models import User, UserProfile
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.http.response import HttpResponseRedirect
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from .models import User, UserProfile, DomainApplication, Website
|
||||||
|
|
||||||
|
|
||||||
|
class AuditedAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
|
"""Custom admin to make auditing easier."""
|
||||||
|
|
||||||
|
def history_view(self, request, object_id, extra_context=None):
|
||||||
|
"""On clicking 'History', take admin to the auditlog view for an object."""
|
||||||
|
return HttpResponseRedirect(
|
||||||
|
"{url}?resource_type={content_type}&object_id={object_id}".format(
|
||||||
|
url=reverse("admin:auditlog_logentry_changelist", args=()),
|
||||||
|
content_type=ContentType.objects.get_for_model(self.model).pk,
|
||||||
|
object_id=object_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class UserProfileInline(admin.StackedInline):
|
class UserProfileInline(admin.StackedInline):
|
||||||
|
@ -18,3 +37,5 @@ class MyUserAdmin(UserAdmin):
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(User, MyUserAdmin)
|
admin.site.register(User, MyUserAdmin)
|
||||||
|
admin.site.register(DomainApplication, AuditedAdmin)
|
||||||
|
admin.site.register(Website, AuditedAdmin)
|
||||||
|
|
|
@ -84,6 +84,8 @@ INSTALLED_APPS = [
|
||||||
"django.contrib.staticfiles",
|
"django.contrib.staticfiles",
|
||||||
# application used for integrating with Login.gov
|
# application used for integrating with Login.gov
|
||||||
"djangooidc",
|
"djangooidc",
|
||||||
|
# audit logging of changes to models
|
||||||
|
"auditlog",
|
||||||
# library to simplify form templating
|
# library to simplify form templating
|
||||||
"widget_tweaks",
|
"widget_tweaks",
|
||||||
# library for Finite State Machine statuses
|
# library for Finite State Machine statuses
|
||||||
|
@ -119,6 +121,8 @@ MIDDLEWARE = [
|
||||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
# django-csp: enable use of Content-Security-Policy header
|
# django-csp: enable use of Content-Security-Policy header
|
||||||
"csp.middleware.CSPMiddleware",
|
"csp.middleware.CSPMiddleware",
|
||||||
|
# django-auditlog: obtain the request User for use in logging
|
||||||
|
"auditlog.middleware.AuditlogMiddleware",
|
||||||
]
|
]
|
||||||
|
|
||||||
# application object used by Django’s built-in servers (e.g. `runserver`)
|
# application object used by Django’s built-in servers (e.g. `runserver`)
|
||||||
|
@ -605,7 +609,8 @@ if DEBUG:
|
||||||
# TODO: use settings overrides to ensure this always is True during tests
|
# TODO: use settings overrides to ensure this always is True during tests
|
||||||
INSTALLED_APPS += ("nplusone.ext.django",)
|
INSTALLED_APPS += ("nplusone.ext.django",)
|
||||||
MIDDLEWARE += ("nplusone.ext.django.NPlusOneMiddleware",)
|
MIDDLEWARE += ("nplusone.ext.django.NPlusOneMiddleware",)
|
||||||
NPLUSONE_RAISE = True
|
# turned off for now, because django-auditlog has some issues
|
||||||
|
NPLUSONE_RAISE = False
|
||||||
NPLUSONE_WHITELIST = [
|
NPLUSONE_WHITELIST = [
|
||||||
{"model": "admin.LogEntry", "field": "user"},
|
{"model": "admin.LogEntry", "field": "user"},
|
||||||
{"model": "registrar.UserProfile"},
|
{"model": "registrar.UserProfile"},
|
||||||
|
|
|
@ -30,9 +30,6 @@
|
||||||
"sp": "",
|
"sp": "",
|
||||||
"pc": "",
|
"pc": "",
|
||||||
"cc": "",
|
"cc": "",
|
||||||
"voice": "",
|
|
||||||
"fax": "",
|
|
||||||
"email": "",
|
|
||||||
"user": 1,
|
"user": 1,
|
||||||
"display_name": ""
|
"display_name": ""
|
||||||
}
|
}
|
||||||
|
@ -68,9 +65,6 @@
|
||||||
"sp": "",
|
"sp": "",
|
||||||
"pc": "",
|
"pc": "",
|
||||||
"cc": "",
|
"cc": "",
|
||||||
"voice": "",
|
|
||||||
"fax": "",
|
|
||||||
"email": "",
|
|
||||||
"user": 2,
|
"user": 2,
|
||||||
"display_name": ""
|
"display_name": ""
|
||||||
}
|
}
|
||||||
|
@ -106,9 +100,6 @@
|
||||||
"sp": "",
|
"sp": "",
|
||||||
"pc": "",
|
"pc": "",
|
||||||
"cc": "",
|
"cc": "",
|
||||||
"voice": "",
|
|
||||||
"fax": "",
|
|
||||||
"email": "",
|
|
||||||
"user": 3,
|
"user": 3,
|
||||||
"display_name": ""
|
"display_name": ""
|
||||||
}
|
}
|
||||||
|
|
10
src/registrar/management/commands/loaddata.py
Normal file
10
src/registrar/management/commands/loaddata.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
from django.core.management.commands import loaddata
|
||||||
|
from auditlog.context import disable_auditlog # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
class Command(loaddata.Command):
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
# django-auditlog has some bugs with fixtures
|
||||||
|
# https://github.com/jazzband/django-auditlog/issues/17
|
||||||
|
with disable_auditlog():
|
||||||
|
super(Command, self).handle(*args, **options)
|
|
@ -1,3 +1,17 @@
|
||||||
|
from auditlog.registry import auditlog # type: ignore
|
||||||
|
|
||||||
from .models import User, UserProfile, Contact, Website, DomainApplication
|
from .models import User, UserProfile, Contact, Website, DomainApplication
|
||||||
|
|
||||||
__all__ = ["User", "UserProfile", "Contact", "Website", "DomainApplication"]
|
__all__ = [
|
||||||
|
"Contact",
|
||||||
|
"DomainApplication",
|
||||||
|
"UserProfile",
|
||||||
|
"User",
|
||||||
|
"Website",
|
||||||
|
]
|
||||||
|
|
||||||
|
auditlog.register(Contact)
|
||||||
|
auditlog.register(DomainApplication)
|
||||||
|
auditlog.register(UserProfile)
|
||||||
|
auditlog.register(User)
|
||||||
|
auditlog.register(Website)
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
@ -14,9 +13,12 @@ class User(AbstractUser):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
try:
|
# this info is pulled from Login.gov
|
||||||
return self.userprofile.display_name
|
if self.first_name or self.last_name:
|
||||||
except ObjectDoesNotExist:
|
return f"{self.first_name or ''} {self.last_name or ''}"
|
||||||
|
elif self.email:
|
||||||
|
return self.email
|
||||||
|
else:
|
||||||
return self.username
|
return self.username
|
||||||
|
|
||||||
|
|
||||||
|
@ -60,35 +62,6 @@ class AddressModel(models.Model):
|
||||||
# don't put anything else here, it will be ignored
|
# don't put anything else here, it will be ignored
|
||||||
|
|
||||||
|
|
||||||
class ContactInfo(models.Model):
|
|
||||||
"""
|
|
||||||
An abstract base model that provides common fields
|
|
||||||
for contact information.
|
|
||||||
"""
|
|
||||||
|
|
||||||
voice = models.TextField(blank=True)
|
|
||||||
fax = models.TextField(blank=True)
|
|
||||||
email = models.TextField(blank=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
abstract = True
|
|
||||||
# don't put anything else here, it will be ignored
|
|
||||||
|
|
||||||
|
|
||||||
class UserProfile(TimeStampedModel, ContactInfo, AddressModel):
|
|
||||||
user = models.OneToOneField(User, null=True, on_delete=models.CASCADE)
|
|
||||||
display_name = models.TextField()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
if self.display_name:
|
|
||||||
return self.display_name
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
return self.user.username
|
|
||||||
except ObjectDoesNotExist:
|
|
||||||
return "No username"
|
|
||||||
|
|
||||||
|
|
||||||
class Website(models.Model):
|
class Website(models.Model):
|
||||||
|
|
||||||
"""Keep domain names in their own table so that applications can refer to
|
"""Keep domain names in their own table so that applications can refer to
|
||||||
|
@ -96,7 +69,11 @@ class Website(models.Model):
|
||||||
|
|
||||||
# domain names have strictly limited lengths, 255 characters is more than
|
# domain names have strictly limited lengths, 255 characters is more than
|
||||||
# enough.
|
# enough.
|
||||||
website = models.CharField(max_length=255, null=False, help_text="")
|
website = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
null=False,
|
||||||
|
help_text="",
|
||||||
|
)
|
||||||
|
|
||||||
# a domain name is alphanumeric or hyphen, up to 63 characters, doesn't
|
# a domain name is alphanumeric or hyphen, up to 63 characters, doesn't
|
||||||
# begin or end with a hyphen, followed by a TLD of 2-6 alphabetic characters
|
# begin or end with a hyphen, followed by a TLD of 2-6 alphabetic characters
|
||||||
|
@ -127,12 +104,71 @@ class Contact(models.Model):
|
||||||
|
|
||||||
"""Contact information follows a similar pattern for each contact."""
|
"""Contact information follows a similar pattern for each contact."""
|
||||||
|
|
||||||
first_name = models.TextField(null=True, help_text="First name", db_index=True)
|
first_name = models.TextField(
|
||||||
middle_name = models.TextField(null=True, help_text="Middle name")
|
null=True,
|
||||||
last_name = models.TextField(null=True, help_text="Last name", db_index=True)
|
blank=True,
|
||||||
title = models.TextField(null=True, help_text="Title")
|
help_text="First name",
|
||||||
email = models.TextField(null=True, help_text="Email", db_index=True)
|
db_index=True,
|
||||||
phone = models.TextField(null=True, help_text="Phone", db_index=True)
|
)
|
||||||
|
middle_name = models.TextField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="Middle name",
|
||||||
|
)
|
||||||
|
last_name = models.TextField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="Last name",
|
||||||
|
db_index=True,
|
||||||
|
)
|
||||||
|
title = models.TextField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="Title",
|
||||||
|
)
|
||||||
|
email = models.TextField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="Email",
|
||||||
|
db_index=True,
|
||||||
|
)
|
||||||
|
phone = models.TextField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="Phone",
|
||||||
|
db_index=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if self.first_name or self.last_name:
|
||||||
|
return f"{self.title or ''} {self.first_name or ''} {self.last_name or ''}"
|
||||||
|
elif self.email:
|
||||||
|
return self.email
|
||||||
|
elif self.pk:
|
||||||
|
return str(self.pk)
|
||||||
|
else:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
class UserProfile(TimeStampedModel, Contact, AddressModel):
|
||||||
|
"""User information, unrelated to their login/auth details."""
|
||||||
|
|
||||||
|
user = models.OneToOneField(
|
||||||
|
User,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
)
|
||||||
|
display_name = models.TextField()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
# use info stored in User rather than Contact,
|
||||||
|
# because Contact is user-editable while User
|
||||||
|
# pulls from identity-verified Login.gov
|
||||||
|
if self.user:
|
||||||
|
return str(self.user)
|
||||||
|
else:
|
||||||
|
return "Orphaned account"
|
||||||
|
|
||||||
|
|
||||||
class DomainApplication(TimeStampedModel):
|
class DomainApplication(TimeStampedModel):
|
||||||
|
@ -195,85 +231,150 @@ class DomainApplication(TimeStampedModel):
|
||||||
investigator = models.ForeignKey(
|
investigator = models.ForeignKey(
|
||||||
User,
|
User,
|
||||||
null=True,
|
null=True,
|
||||||
|
blank=True,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
related_name="applications_investigating",
|
related_name="applications_investigating",
|
||||||
)
|
)
|
||||||
|
|
||||||
# ##### data fields from the initial form #####
|
# ##### data fields from the initial form #####
|
||||||
organization_type = models.CharField(
|
organization_type = models.CharField(
|
||||||
max_length=255, choices=ORGANIZATION_CHOICES, help_text="Type of Organization"
|
max_length=255,
|
||||||
|
choices=ORGANIZATION_CHOICES,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="Type of Organization",
|
||||||
)
|
)
|
||||||
|
|
||||||
federal_branch = models.CharField(
|
federal_branch = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
choices=BRANCH_CHOICES,
|
choices=BRANCH_CHOICES,
|
||||||
null=True,
|
null=True,
|
||||||
|
blank=True,
|
||||||
help_text="Branch of federal government",
|
help_text="Branch of federal government",
|
||||||
)
|
)
|
||||||
|
|
||||||
is_election_office = models.BooleanField(
|
is_election_office = models.BooleanField(
|
||||||
null=True, help_text="Is your ogranization an election office?"
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="Is your ogranization an election office?",
|
||||||
)
|
)
|
||||||
|
|
||||||
organization_name = models.TextField(
|
organization_name = models.TextField(
|
||||||
null=True, help_text="Organization name", db_index=True
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="Organization name",
|
||||||
|
db_index=True,
|
||||||
|
)
|
||||||
|
street_address = models.TextField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="Street Address",
|
||||||
|
)
|
||||||
|
unit_type = models.CharField(
|
||||||
|
max_length=15,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="Unit type",
|
||||||
|
)
|
||||||
|
unit_number = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="Unit number",
|
||||||
)
|
)
|
||||||
street_address = models.TextField(null=True, help_text="Street Address")
|
|
||||||
unit_type = models.CharField(max_length=15, null=True, help_text="Unit type")
|
|
||||||
unit_number = models.CharField(max_length=255, null=True, help_text="Unit number")
|
|
||||||
state_territory = models.CharField(
|
state_territory = models.CharField(
|
||||||
max_length=2, null=True, help_text="State/Territory"
|
max_length=2,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="State/Territory",
|
||||||
)
|
)
|
||||||
zip_code = models.CharField(
|
zip_code = models.CharField(
|
||||||
max_length=10, null=True, help_text="ZIP code", db_index=True
|
max_length=10,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="ZIP code",
|
||||||
|
db_index=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
authorizing_official = models.ForeignKey(
|
authorizing_official = models.ForeignKey(
|
||||||
Contact,
|
Contact,
|
||||||
null=True,
|
null=True,
|
||||||
|
blank=True,
|
||||||
related_name="authorizing_official",
|
related_name="authorizing_official",
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
)
|
)
|
||||||
|
|
||||||
# "+" means no reverse relation to lookup applications from Website
|
# "+" means no reverse relation to lookup applications from Website
|
||||||
current_websites = models.ManyToManyField(Website, related_name="current+")
|
current_websites = models.ManyToManyField(
|
||||||
|
Website,
|
||||||
|
blank=True,
|
||||||
|
related_name="current+",
|
||||||
|
)
|
||||||
|
|
||||||
requested_domain = models.ForeignKey(
|
requested_domain = models.ForeignKey(
|
||||||
Website,
|
Website,
|
||||||
null=True,
|
null=True,
|
||||||
|
blank=True,
|
||||||
help_text="The requested domain",
|
help_text="The requested domain",
|
||||||
related_name="requested+",
|
related_name="requested+",
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
)
|
)
|
||||||
alternative_domains = models.ManyToManyField(Website, related_name="alternatives+")
|
alternative_domains = models.ManyToManyField(
|
||||||
|
Website,
|
||||||
|
blank=True,
|
||||||
|
related_name="alternatives+",
|
||||||
|
)
|
||||||
|
|
||||||
# This is the contact information provided by the applicant. The
|
# This is the contact information provided by the applicant. The
|
||||||
# application user who created it is in the `creator` field.
|
# application user who created it is in the `creator` field.
|
||||||
submitter = models.ForeignKey(
|
submitter = models.ForeignKey(
|
||||||
Contact,
|
Contact,
|
||||||
null=True,
|
null=True,
|
||||||
|
blank=True,
|
||||||
related_name="submitted_applications",
|
related_name="submitted_applications",
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
)
|
)
|
||||||
|
|
||||||
purpose = models.TextField(null=True, help_text="Purpose of the domain")
|
purpose = models.TextField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="Purpose of the domain",
|
||||||
|
)
|
||||||
|
|
||||||
other_contacts = models.ManyToManyField(
|
other_contacts = models.ManyToManyField(
|
||||||
Contact, related_name="contact_applications"
|
Contact,
|
||||||
|
blank=True,
|
||||||
|
related_name="contact_applications",
|
||||||
)
|
)
|
||||||
|
|
||||||
security_email = models.CharField(
|
security_email = models.CharField(
|
||||||
max_length=320, null=True, help_text="Security email for public use"
|
max_length=320,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="Security email for public use",
|
||||||
)
|
)
|
||||||
|
|
||||||
anything_else = models.TextField(
|
anything_else = models.TextField(
|
||||||
null=True, help_text="Anything else we should know?"
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="Anything else we should know?",
|
||||||
)
|
)
|
||||||
|
|
||||||
acknowledged_policy = models.BooleanField(
|
acknowledged_policy = models.BooleanField(
|
||||||
null=True, help_text="Acknowledged .gov acceptable use policy"
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="Acknowledged .gov acceptable use policy",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if self.requested_domain and self.requested_domain.website:
|
||||||
|
return self.requested_domain.website
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
return f"{self.status} application created by {self.creator}"
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
|
||||||
@transition(field="status", source=STARTED, target=SUBMITTED)
|
@transition(field="status", source=STARTED, target=SUBMITTED)
|
||||||
def submit(self):
|
def submit(self):
|
||||||
"""Submit an application that is started."""
|
"""Submit an application that is started."""
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue