mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-22 18:56:15 +02:00
Remove UserProfile and add PublicContact
This commit is contained in:
parent
4202775cd0
commit
dee826c5e3
12 changed files with 166 additions and 108 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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 we’ve 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.
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.")
|
||||||
|
|
|
@ -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"]
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
49
src/registrar/models/public_contact.py
Normal file
49
src/registrar/models/public_contact.py
Normal 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}>"
|
|
@ -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"
|
|
|
@ -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
|
|
|
@ -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})
|
|
Loading…
Add table
Add a link
Reference in a new issue