Remove UserProfile and add PublicContact

This commit is contained in:
Seamus Johnston 2023-03-06 15:49:11 -06:00
parent 4202775cd0
commit dee826c5e3
No known key found for this signature in database
GPG key ID: 2F21225985069105
12 changed files with 166 additions and 108 deletions

View file

@ -4,7 +4,7 @@ Date: 2022-09-26
## Status ## Status
Proposed Superseded by [20. User models revisited, plus WHOIS](./0020-user-models-revisited-plus-whois.md)
## Context ## Context

View file

@ -0,0 +1,103 @@
# 20. User models revisited, plus WHOIS
Date: 2022-03-01
## Status
Accepted
## Context
In the process of thinking through the details of registry implementation and role-based access control, it has become clear that the registrar has 3 types of contacts:
1. Those for the purpose of allowing CISA to verify the authenticity of a request. In other words, is the organization an eligible U.S.-based government and is the requestor duly authorized to make the request on behalf of their government?
1. Those for the purpose of managing a domain or its DNS configuration.
* There is ambiguous overlap remaining between use case 1 and 2.
1. Those for the purpose of publishing publicly in WHOIS.
Additionally, there are two mental models of contacts that impact the permissions associated with them and how they can be updated:
1. A contact represents a person: changes made in one part of the system will update in all parts of the system; people are not allowed to make updates unless they are authorized.
1. A contact represents information filled out on a sheet of paper: changes on one “copy” of the information will not update other “copies” of the information; people are allowed to make updates based on their authorization to access and edit the “sheet of paper”.
## Decision
To have a custom `User` model containing un-editable data derived from Login.gov and updated automatically each time a user logs in. In role-based access control, User is the model to which roles attach.
To have a `Contact` model which stores name and contact data. The presence of a foreign key from Contact to User indicates that that contact data has been associated with a Login.gov user account. If a User is deleted, the foreign key column is set to null.
User and Contact follow the “person” mental model.
To have a `PublicContact` model which stores WHOIS data. Domains will be created with the following default values.
PublicContact follows the “sheet of paper” mental model.
### Registrant default values
| Field | Value |
|---|---|
|name | CSD/CB Attn: Cameron Dixon
|org | Cybersecurity and Infrastructure Security Agency
|street1 | CISA NGR STOP 0645
|street2 | 1110 N. Glebe Rd.
|city | Arlington
|sp | VA
|pc | 20598-0645
|cc | US
### Administrative default values
| Field | Value |
|---|---|
|name | Program Manager
|org | Cybersecurity and Infrastructure Security Agency
|street1 | 4200 Wilson Blvd.
|city | Arlington
|sp | VA
|pc | 22201
|cc | US
|voice | +1.8882820870
|email | dotgov@cisa.dhs.gov
### Technical default values
Whether this contact will be created by default or not is yet to be determined.
| Field | Value |
|---|---|
|name | Registry Customer Service
|org | Cybersecurity and Infrastructure Security Agency
|street1 | 4200 Wilson Blvd.
|city | Arlington
|sp | VA
|pc | 22201
|cc | US
|voice | +1.8882820870
|email | registrar@dotgov.gov
### Security default values
Whether this contact will be created by default or not is yet to be determined.
The EPP “disclose tags” feature might be used to publish only the email address.
| Field | Value |
|---|---|
|name | Registry Customer Service
|org | Cybersecurity and Infrastructure Security Agency
|street1 | 4200 Wilson Blvd.
|city | Arlington
|sp | VA
|pc | 22201
|cc | US
|voice | +1.8882820870
|email | registrar@dotgov.gov
## Consequences
This has minimal impact on the code weve developed so far.
By having PublicContact be an entirely separate model, it ensures that (for better or worse) WHOIS contact data must be updated separately from general Contacts. At present, CISA intends to allow registrants to edit only one contact: security, so this is a minor point of low impact.
In a future state where CISA allows more to be published, it is easy to imagine a set of checkboxes on a contact update form: “[ ] publish this as my technical contact for example.gov”, etc.

View file

@ -22,18 +22,18 @@ class AuditedAdmin(admin.ModelAdmin):
) )
class UserProfileInline(admin.StackedInline): class UserContactInline(admin.StackedInline):
"""Edit a user's profile on the user page.""" """Edit a user's profile on the user page."""
model = models.UserProfile model = models.Contact
class MyUserAdmin(UserAdmin): class MyUserAdmin(UserAdmin):
"""Custom user admin class to use our inlines.""" """Custom user admin class to use our inlines."""
inlines = [UserProfileInline] inlines = [UserContactInline]
class HostIPInline(admin.StackedInline): class HostIPInline(admin.StackedInline):

View file

@ -645,7 +645,6 @@ if DEBUG:
NPLUSONE_RAISE = False NPLUSONE_RAISE = False
NPLUSONE_WHITELIST = [ NPLUSONE_WHITELIST = [
{"model": "admin.LogEntry", "field": "user"}, {"model": "admin.LogEntry", "field": "user"},
{"model": "registrar.UserProfile"},
] ]
# insert the amazing django-debug-toolbar # insert the amazing django-debug-toolbar

View file

@ -4,7 +4,6 @@ from faker import Faker
from registrar.models import ( from registrar.models import (
User, User,
UserProfile,
DomainApplication, DomainApplication,
Domain, Domain,
Contact, Contact,
@ -57,8 +56,6 @@ class UserFixture:
user.is_active = True user.is_active = True
user.save() user.save()
logger.debug("User object created for %s" % admin["first_name"]) logger.debug("User object created for %s" % admin["first_name"])
UserProfile.objects.get_or_create(user=user)
logger.debug("Profile object created for %s" % admin["first_name"])
except Exception as e: except Exception as e:
logger.warning(e) logger.warning(e)
logger.debug("All users loaded.") logger.debug("All users loaded.")

View file

@ -1,21 +0,0 @@
from django import forms
from ..models import UserProfile
class EditProfileForm(forms.ModelForm):
"""Custom form class for editing a UserProfile.
We can add whatever fields we want to this form and customize how they
are displayed. The form is rendered into a template `profile.html` by a
view called `edit_profile` in `profile.py`.
"""
display_name = forms.CharField(
widget=forms.TextInput(attrs={"class": "usa-input"}), label="Display Name"
)
class Meta:
model = UserProfile
fields = ["display_name"]

View file

@ -6,7 +6,7 @@ from .domain import Domain
from .host_ip import HostIP from .host_ip import HostIP
from .host import Host from .host import Host
from .nameserver import Nameserver from .nameserver import Nameserver
from .user_profile import UserProfile from .public_contact import PublicContact
from .user import User from .user import User
from .website import Website from .website import Website
@ -17,7 +17,7 @@ __all__ = [
"HostIP", "HostIP",
"Host", "Host",
"Nameserver", "Nameserver",
"UserProfile", "PublicContact",
"User", "User",
"Website", "Website",
] ]
@ -28,6 +28,6 @@ auditlog.register(Domain)
auditlog.register(HostIP) auditlog.register(HostIP)
auditlog.register(Host) auditlog.register(Host)
auditlog.register(Nameserver) auditlog.register(Nameserver)
auditlog.register(UserProfile) auditlog.register(PublicContact)
auditlog.register(User) auditlog.register(User)
auditlog.register(Website) auditlog.register(Website)

View file

@ -9,6 +9,13 @@ class Contact(TimeStampedModel):
"""Contact information follows a similar pattern for each contact.""" """Contact information follows a similar pattern for each contact."""
user = models.OneToOneField(
"registrar.User",
null=True,
blank=True,
on_delete=models.SET_NULL,
)
first_name = models.TextField( first_name = models.TextField(
null=True, null=True,
blank=True, blank=True,

View file

@ -0,0 +1,49 @@
from django.db import models
from .utility.time_stamped_model import TimeStampedModel
class PublicContact(TimeStampedModel):
"""Contact information intended to be published in WHOIS."""
class ContactTypeChoices(models.TextChoices):
"""These are the types of contacts accepted by the registry."""
REGISTRANT = "registrant", "Registrant"
ADMINISTRATIVE = "administrative", "Administrative"
TECHNICAL = "technical", "Technical"
SECURITY = "security", "Security"
contact_type = models.CharField(choices=ContactTypeChoices)
# contact's full name
name = models.TextField(null=False)
# contact's organization (null ok)
org = models.TextField(null=True)
# contact's street
street1 = models.TextField(null=False)
# contact's street (null ok)
street2 = models.TextField(null=True)
# contact's street (null ok)
street3 = models.TextField(null=True)
# contact's city
city = models.TextField(null=False)
# contact's state or province
sp = models.TextField(null=False)
# contact's postal code
pc = models.TextField(null=False)
# contact's country code
cc = models.TextField(null=False)
# contact's email address
email = models.TextField(null=False)
# contact's phone number
# Must be in ITU.E164.2005 format
voice = models.TextField(null=False)
# contact's fax number (null ok)
# Must be in ITU.E164.2005 format
fax = models.TextField(null=True)
# contact's authorization code
# 16 characters minium
pw = models.TextField(null=False)
def __str__(self):
return f"{self.name} <{self.email}>"

View file

@ -1,28 +0,0 @@
from django.db import models
from .utility.time_stamped_model import TimeStampedModel
from .utility.address_model import AddressModel
from .contact import Contact
class UserProfile(Contact, TimeStampedModel, AddressModel):
"""User information, unrelated to their login/auth details."""
user = models.OneToOneField(
"registrar.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
try:
return str(self.user)
except Exception:
return "Orphaned account"

View file

@ -1,27 +0,0 @@
from django.db import models
class AddressModel(models.Model):
"""
An abstract base model that provides common fields
for postal addresses.
"""
# contact's street (null ok)
street1 = models.TextField(blank=True)
# contact's street (null ok)
street2 = models.TextField(blank=True)
# contact's street (null ok)
street3 = models.TextField(blank=True)
# contact's city
city = models.TextField(blank=True)
# contact's state or province (null ok)
sp = models.TextField(blank=True)
# contact's postal code (null ok)
pc = models.TextField(blank=True)
# contact's country code
cc = models.TextField(blank=True)
class Meta:
abstract = True
# don't put anything else here, it will be ignored

View file

@ -1,21 +0,0 @@
from django.shortcuts import redirect, render
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from registrar.forms import EditProfileForm
@login_required
def edit_profile(request):
"""View for a profile editing page."""
if request.method == "POST":
# post to this view when changes are made
profile_form = EditProfileForm(request.POST, instance=request.user.userprofile)
if profile_form.is_valid():
profile_form.save()
messages.success(request, "Your profile is updated successfully")
return redirect(to="edit-profile")
else:
profile_form = EditProfileForm(instance=request.user.userprofile)
return render(request, "profile.html", {"profile_form": profile_form})