diff --git a/docs/architecture/decisions/0012-user-models.md b/docs/architecture/decisions/0012-user-models.md index 3e404106e..434dbb54a 100644 --- a/docs/architecture/decisions/0012-user-models.md +++ b/docs/architecture/decisions/0012-user-models.md @@ -4,7 +4,7 @@ Date: 2022-09-26 ## Status -Proposed +Superseded by [20. User models revisited, plus WHOIS](./0020-user-models-revisited-plus-whois.md) ## Context diff --git a/docs/architecture/decisions/0020-user-models-revisited-plus-whois.md b/docs/architecture/decisions/0020-user-models-revisited-plus-whois.md new file mode 100644 index 000000000..c35bd3507 --- /dev/null +++ b/docs/architecture/decisions/0020-user-models-revisited-plus-whois.md @@ -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. diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 19cf60729..b2d291ce5 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -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.""" - model = models.UserProfile + model = models.Contact class MyUserAdmin(UserAdmin): """Custom user admin class to use our inlines.""" - inlines = [UserProfileInline] + inlines = [UserContactInline] class HostIPInline(admin.StackedInline): diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index c2abd85d4..4043c6991 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -645,7 +645,6 @@ if DEBUG: NPLUSONE_RAISE = False NPLUSONE_WHITELIST = [ {"model": "admin.LogEntry", "field": "user"}, - {"model": "registrar.UserProfile"}, ] # insert the amazing django-debug-toolbar diff --git a/src/registrar/fixtures.py b/src/registrar/fixtures.py index 36c1733c5..9f54b20a7 100644 --- a/src/registrar/fixtures.py +++ b/src/registrar/fixtures.py @@ -4,7 +4,6 @@ from faker import Faker from registrar.models import ( User, - UserProfile, DomainApplication, Domain, Contact, @@ -57,8 +56,6 @@ class UserFixture: user.is_active = True user.save() 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: logger.warning(e) logger.debug("All users loaded.") diff --git a/src/registrar/forms/edit_profile.py b/src/registrar/forms/edit_profile.py deleted file mode 100644 index d3819faa3..000000000 --- a/src/registrar/forms/edit_profile.py +++ /dev/null @@ -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"] diff --git a/src/registrar/models/__init__.py b/src/registrar/models/__init__.py index 1bb9dde84..9b7aba971 100644 --- a/src/registrar/models/__init__.py +++ b/src/registrar/models/__init__.py @@ -6,7 +6,7 @@ from .domain import Domain from .host_ip import HostIP from .host import Host from .nameserver import Nameserver -from .user_profile import UserProfile +from .public_contact import PublicContact from .user import User from .website import Website @@ -17,7 +17,7 @@ __all__ = [ "HostIP", "Host", "Nameserver", - "UserProfile", + "PublicContact", "User", "Website", ] @@ -28,6 +28,6 @@ auditlog.register(Domain) auditlog.register(HostIP) auditlog.register(Host) auditlog.register(Nameserver) -auditlog.register(UserProfile) +auditlog.register(PublicContact) auditlog.register(User) auditlog.register(Website) diff --git a/src/registrar/models/contact.py b/src/registrar/models/contact.py index 6f0b62ea8..d5d32a7ae 100644 --- a/src/registrar/models/contact.py +++ b/src/registrar/models/contact.py @@ -9,6 +9,13 @@ class Contact(TimeStampedModel): """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( null=True, blank=True, diff --git a/src/registrar/models/public_contact.py b/src/registrar/models/public_contact.py new file mode 100644 index 000000000..9c9d627d2 --- /dev/null +++ b/src/registrar/models/public_contact.py @@ -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}>" \ No newline at end of file diff --git a/src/registrar/models/user_profile.py b/src/registrar/models/user_profile.py deleted file mode 100644 index 806124205..000000000 --- a/src/registrar/models/user_profile.py +++ /dev/null @@ -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" diff --git a/src/registrar/models/utility/address_model.py b/src/registrar/models/utility/address_model.py deleted file mode 100644 index c158ce085..000000000 --- a/src/registrar/models/utility/address_model.py +++ /dev/null @@ -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 diff --git a/src/registrar/views/profile.py b/src/registrar/views/profile.py deleted file mode 100644 index 3a1b416c3..000000000 --- a/src/registrar/views/profile.py +++ /dev/null @@ -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})