Add UserDomainRole table and helpers

This commit is contained in:
Neil Martinsen-Burrell 2023-03-06 12:03:29 -06:00
parent 22eb49c004
commit 49b4f078e8
No known key found for this signature in database
GPG key ID: 6A3C818CC10D0184
9 changed files with 211 additions and 7 deletions

View file

@ -0,0 +1,66 @@
# Generated by Django 4.1.6 on 2023-03-06 17:26
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("registrar", "0011_remove_domainapplication_security_email"),
]
operations = [
migrations.RemoveField(
model_name="domain",
name="owners",
),
migrations.CreateModel(
name="UserDomainRole",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("role", models.TextField(choices=[("admin", "Admin")])),
(
"domain",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="permissions",
to="registrar.domain",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="permissions",
to=settings.AUTH_USER_MODEL,
),
),
],
),
migrations.AddField(
model_name="user",
name="domains",
field=models.ManyToManyField(
related_name="users",
through="registrar.UserDomainRole",
to="registrar.domain",
),
),
migrations.AddConstraint(
model_name="userdomainrole",
constraint=models.UniqueConstraint(
fields=("user", "domain"), name="unique_user_domain_role"
),
),
]

View file

@ -6,6 +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_domain_role import UserDomainRole
from .user_profile import UserProfile from .user_profile import UserProfile
from .user import User from .user import User
from .website import Website from .website import Website
@ -17,6 +18,7 @@ __all__ = [
"HostIP", "HostIP",
"Host", "Host",
"Nameserver", "Nameserver",
"UserDomainRole",
"UserProfile", "UserProfile",
"User", "User",
"Website", "Website",
@ -28,6 +30,7 @@ auditlog.register(Domain)
auditlog.register(HostIP) auditlog.register(HostIP)
auditlog.register(Host) auditlog.register(Host)
auditlog.register(Nameserver) auditlog.register(Nameserver)
auditlog.register(UserDomainRole)
auditlog.register(UserProfile) auditlog.register(UserProfile)
auditlog.register(User) auditlog.register(User)
auditlog.register(Website) auditlog.register(Website)

View file

@ -276,9 +276,8 @@ class Domain(TimeStampedModel):
help_text="Domain is live in the registry", help_text="Domain is live in the registry",
) )
# TODO: determine the relationship between this field # ForeignKey on UserDomainRole creates a "permissions" member for
# and the domain application's `creator` and `submitter` # all of the user-roles that are in place for this domain
owners = models.ManyToManyField(
"registrar.User", # ManyToManyField on User creates a "users" member for all of the
help_text="", # users who have some role on this domain
)

View file

@ -502,6 +502,25 @@ class DomainApplication(TimeStampedModel):
# This is a side-effect of the state transition # This is a side-effect of the state transition
self._send_confirmation_email() self._send_confirmation_email()
@transition(field="status", source=[SUBMITTED, INVESTIGATING], target=APPROVED)
def approve(self):
"""Approve an application that has been submitted.
This has substantial side-effects because it creates another database
object for the approved Domain and makes the user who created the
application into an admin on that domain.
"""
# create the domain if it doesn't exist
Domain = apps.get_model("registrar.Domain")
created_domain, _ = Domain.objects.get_or_create(name=self.requested_domain)
# create the permission for the user
UserDomainRole = apps.get_model("registrar.UserDomainRole")
UserDomainRole.objects.get_or_create(
user=self.creator, domain=created_domain, role=UserDomainRole.Roles.ADMIN
)
# ## Form policies ### # ## Form policies ###
# #
# These methods control what questions need to be answered by applicants # These methods control what questions need to be answered by applicants

View file

@ -1,4 +1,5 @@
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser): class User(AbstractUser):
@ -7,6 +8,12 @@ class User(AbstractUser):
but can be customized later. but can be customized later.
""" """
domains = models.ManyToManyField(
"registrar.Domain",
through="registrar.UserDomainRole",
related_name="users",
)
def __str__(self): def __str__(self):
# this info is pulled from Login.gov # this info is pulled from Login.gov
if self.first_name or self.last_name: if self.first_name or self.last_name:

View file

@ -0,0 +1,49 @@
from django.db import models
from .utility.time_stamped_model import TimeStampedModel
class UserDomainRole(TimeStampedModel):
"""This is a linking table that connects a user with a role on a domain."""
class Roles(models.TextChoices):
"""The possible roles are listed here.
Implementation of the named roles for allowing particular operations happens
elsewhere.
"""
ADMIN = "admin"
user = models.ForeignKey(
"registrar.User",
null=False,
on_delete=models.CASCADE, # when a user is deleted, their permissions will be too
related_name="permissions",
)
domain = models.ForeignKey(
"registrar.Domain",
null=False,
on_delete=models.CASCADE, # when a domain is deleted, permissions are too
related_name="permissions"
)
role = models.TextField(
choices=Roles.choices,
null=False,
blank=False,
)
def __str__(self):
return "User {} is {} on domain {}".format(self.user, self.role, self.domain)
class Meta:
constraints = [
# a user can have only one role on a given domain, that is, there can
# be only a single row with a certain (user, domain) pair.
models.UniqueConstraint(
fields=['user', 'domain'], name='unique_user_domain_role'
)
]

View file

@ -16,7 +16,41 @@
<section class="dashboard tablet:grid-col-11 desktop:grid-col-10"> <section class="dashboard tablet:grid-col-11 desktop:grid-col-10">
<h2>Registered domains</h2> <h2>Registered domains</h2>
{% if domains %}
<table class="usa-table usa-table--borderless usa-table--stacked">
<caption class="sr-only">Your domain applications</caption>
<thead>
<tr>
<th data-sortable scope="col" role="columnheader">Domain name</th>
<th data-sortable scope="col" role="columnheader">Date created</th>
<th data-sortable scope="col" role="columnheader">Status</th>
<th scope="col" role="columnheader"><span class="usa-sr-only">Action</span></th>
</tr>
</thead>
<tbody>
{% for domain in domains %}
<tr>
<th th scope="row" role="rowheader" data-label="Domain name">
{{ domain.name }}
</th>
<td data-sort-value="{{ domain.created_time|date:"U" }}" data-label="Date created">{{ domain.created_time|date }}</td>
<td data-label="Status">{{ domain.application_status|title }}</td>
<td>
<a href="{% url "todo" %}">
Edit <span class="usa-sr-only">{{ domain.name }} </span>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div
class="usa-sr-only usa-table__announcement-region"
aria-live="polite"
></div>
{% else %}
<p>You don't have any registered domains yet</p> <p>You don't have any registered domains yet</p>
{% endif %}
</section> </section>
<section class="dashboard tablet:grid-col-11 desktop:grid-col-10"> <section class="dashboard tablet:grid-col-11 desktop:grid-col-10">

View file

@ -1,7 +1,7 @@
from django.test import TestCase from django.test import TestCase
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
from registrar.models import Contact, DomainApplication, User, Website, Domain from registrar.models import Contact, DomainApplication, User, Website, Domain, UserDomainRole
from unittest import skip from unittest import skip
import boto3_mocking # type: ignore import boto3_mocking # type: ignore
@ -139,6 +139,24 @@ class TestDomain(TestCase):
d1.activate() d1.activate()
class TestPermissions(TestCase):
"""Test the User-Domain-Role connection."""
def test_approval_creates_role(self):
domain, _ = Domain.objects.get_or_create(name="igorville.gov")
user, _ = User.objects.get_or_create()
application = DomainApplication.objects.create(creator=user,
requested_domain=domain)
# skip using the submit method
application.status = DomainApplication.SUBMITTED
application.approve()
# should be a role for this user
self.assertTrue(UserDomainRole.objects.get(user=user, domain=domain))
@skip("Not implemented yet.") @skip("Not implemented yet.")
class TestDomainApplicationLifeCycle(TestCase): class TestDomainApplicationLifeCycle(TestCase):
def test_application_approval(self): def test_application_approval(self):

View file

@ -1,3 +1,4 @@
from django.db.models import F
from django.shortcuts import render from django.shortcuts import render
from registrar.models import DomainApplication from registrar.models import DomainApplication
@ -9,4 +10,12 @@ def index(request):
if request.user.is_authenticated: if request.user.is_authenticated:
applications = DomainApplication.objects.filter(creator=request.user) applications = DomainApplication.objects.filter(creator=request.user)
context["domain_applications"] = applications context["domain_applications"] = applications
domains = request.user.permissions.values(
"role",
name=F("domain__name"),
created_time=F("domain__created_at"),
application_status=F("domain__domain_application__status"),
)
context["domains"] = domains
return render(request, "home.html", context) return render(request, "home.html", context)