Merge pull request #2857 from cisagov/es/2729-domain-manager-updates

#2729: Update Add a Domain Manager page - [ES]
This commit is contained in:
Erin Song 2024-10-17 11:53:50 -07:00 committed by GitHub
commit ce7064da2b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 104 additions and 35 deletions

View file

@ -1,4 +1,5 @@
import logging import logging
import random
from faker import Faker from faker import Faker
from django.db import transaction from django.db import transaction
@ -51,23 +52,24 @@ class UserPortfolioPermissionFixture:
user_portfolio_permissions_to_create = [] user_portfolio_permissions_to_create = []
for user in users: for user in users:
for portfolio in portfolios: # Assign a random portfolio to a user
try: portfolio = random.choice(portfolios) # nosec
if not UserPortfolioPermission.objects.filter(user=user, portfolio=portfolio).exists(): try:
user_portfolio_permission = UserPortfolioPermission( if not UserPortfolioPermission.objects.filter(user=user, portfolio=portfolio).exists():
user=user, user_portfolio_permission = UserPortfolioPermission(
portfolio=portfolio, user=user,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN], portfolio=portfolio,
additional_permissions=[UserPortfolioPermissionChoices.EDIT_MEMBERS], roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
) additional_permissions=[UserPortfolioPermissionChoices.EDIT_MEMBERS],
user_portfolio_permissions_to_create.append(user_portfolio_permission) )
else: user_portfolio_permissions_to_create.append(user_portfolio_permission)
logger.info( else:
f"Permission exists for user '{user.username}' " logger.info(
f"on portfolio '{portfolio.organization_name}'." f"Permission exists for user '{user.username}' "
) f"on portfolio '{portfolio.organization_name}'."
except Exception as e: )
logger.warning(e) except Exception as e:
logger.warning(e)
# Bulk create permissions # Bulk create permissions
cls._bulk_create_permissions(user_portfolio_permissions_to_create) cls._bulk_create_permissions(user_portfolio_permissions_to_create)

View file

@ -4,11 +4,31 @@
{% block title %}Add a domain manager | {% endblock %} {% block title %}Add a domain manager | {% endblock %}
{% block domain_content %} {% block domain_content %}
{% block breadcrumb %}
{% url 'domain-users' pk=domain.id as url %}
<nav class="usa-breadcrumb padding-top-0" aria-label="Domain manager breadcrumb">
<ol class="usa-breadcrumb__list">
<li class="usa-breadcrumb__list-item">
<a href="{{ url }}" class="usa-breadcrumb__link"><span>Domain managers</span></a>
</li>
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
<span>Add a domain manager</span>
</li>
</ol>
</nav>
{% endblock breadcrumb %}
<h1>Add a domain manager</h1> <h1>Add a domain manager</h1>
{% if has_organization_feature_flag %}
<p>You can add another user to help manage your domain. They will need to sign <p>
in to the .gov registrar with their Login.gov account. You can add another user to help manage your domain. Users can only be a member of one .gov organization,
and they'll need to sign in with their Login.gov account.
</p> </p>
{% else %}
<p>
You can add another user to help manage your domain. They will need to sign in to the .gov registrar with
their Login.gov account.
</p>
{% endif %}
<form class="usa-form usa-form--large" method="post" novalidate> <form class="usa-form usa-form--large" method="post" novalidate>
{% csrf_token %} {% csrf_token %}

View file

@ -8,8 +8,7 @@
<p> <p>
Domain managers can update all information related to a domain within the Domain managers can update all information related to a domain within the
.gov registrar, including contact details, senior official, security .gov registrar, including including security email and DNS name servers.
email, and DNS name servers.
</p> </p>
<ul class="usa-list"> <ul class="usa-list">
@ -17,7 +16,8 @@
<li>After adding a domain manager, an email invitation will be sent to that user with <li>After adding a domain manager, an email invitation will be sent to that user with
instructions on how to set up an account.</li> instructions on how to set up an account.</li>
<li>All domain managers must keep their contact information updated and be responsive if contacted by the .gov team.</li> <li>All domain managers must keep their contact information updated and be responsive if contacted by the .gov team.</li>
<li>Domains must have at least one domain manager. You cant remove yourself as a domain manager if youre the only one assigned to this domain. Add another domain manager before you remove yourself from this domain.</li> <li>All domain managers will be notified when updates are made to this domain.</li>
<li>Domains must have at least one domain manager. You cant remove yourself as a domain manager if youre the only one assigned to this domain.</li>
</ul> </ul>
{% if domain.permissions %} {% if domain.permissions %}

View file

@ -23,6 +23,15 @@ class InvalidDomainError(ValueError):
pass pass
class OutsideOrgMemberError(ValueError):
"""
Error raised when an org member tries adding a user from a different .gov org.
To be deleted when users can be members of multiple orgs.
"""
pass
class ActionNotAllowed(Exception): class ActionNotAllowed(Exception):
"""User accessed an action that is not """User accessed an action that is not
allowed by the current state""" allowed by the current state"""

View file

@ -21,8 +21,10 @@ from registrar.models import (
DomainRequest, DomainRequest,
DomainInformation, DomainInformation,
DomainInvitation, DomainInvitation,
PortfolioInvitation,
User, User,
UserDomainRole, UserDomainRole,
UserPortfolioPermission,
PublicContact, PublicContact,
) )
from registrar.utility.enums import DefaultEmail from registrar.utility.enums import DefaultEmail
@ -35,9 +37,11 @@ from registrar.utility.errors import (
DsDataErrorCodes, DsDataErrorCodes,
SecurityEmailError, SecurityEmailError,
SecurityEmailErrorCodes, SecurityEmailErrorCodes,
OutsideOrgMemberError,
) )
from registrar.models.utility.contact_error import ContactError from registrar.models.utility.contact_error import ContactError
from registrar.views.utility.permission_views import UserDomainRolePermissionDeleteView from registrar.views.utility.permission_views import UserDomainRolePermissionDeleteView
from registrar.utility.waffle import flag_is_active_for_user
from ..forms import ( from ..forms import (
SeniorOfficialContactForm, SeniorOfficialContactForm,
@ -778,7 +782,18 @@ class DomainAddUserView(DomainFormBaseView):
"""Get an absolute URL for this domain.""" """Get an absolute URL for this domain."""
return self.request.build_absolute_uri(reverse("domain", kwargs={"pk": self.object.id})) return self.request.build_absolute_uri(reverse("domain", kwargs={"pk": self.object.id}))
def _send_domain_invitation_email(self, email: str, requestor: User, add_success=True): def _is_member_of_different_org(self, email, requestor, requested_user):
"""Verifies if an email belongs to a different organization as a member or invited member."""
# Check if user is a already member of a different organization than the requestor's org
requestor_org = UserPortfolioPermission.objects.filter(user=requestor).first().portfolio
existing_org_permission = UserPortfolioPermission.objects.filter(user=requested_user).first()
existing_org_invitation = PortfolioInvitation.objects.filter(email=email).first()
return (existing_org_permission and existing_org_permission.portfolio != requestor_org) or (
existing_org_invitation and existing_org_invitation.portfolio != requestor_org
)
def _send_domain_invitation_email(self, email: str, requestor: User, requested_user=None, add_success=True):
"""Performs the sending of the domain invitation email, """Performs the sending of the domain invitation email,
does not make a domain information object does not make a domain information object
email: string- email to send to email: string- email to send to
@ -803,6 +818,13 @@ class DomainAddUserView(DomainFormBaseView):
) )
return None return None
# Check is user is a member or invited member of a different org from this domain's org
if flag_is_active_for_user(requestor, "organization_feature") and self._is_member_of_different_org(
email, requestor, requested_user
):
add_success = False
raise OutsideOrgMemberError
# Check to see if an invite has already been sent # Check to see if an invite has already been sent
try: try:
invite = DomainInvitation.objects.get(email=email, domain=self.object) invite = DomainInvitation.objects.get(email=email, domain=self.object)
@ -859,16 +881,21 @@ class DomainAddUserView(DomainFormBaseView):
Throws EmailSendingError.""" Throws EmailSendingError."""
requested_email = form.cleaned_data["email"] requested_email = form.cleaned_data["email"]
requestor = self.request.user requestor = self.request.user
email_success = False
# look up a user with that email # look up a user with that email
try: try:
requested_user = User.objects.get(email=requested_email) requested_user = User.objects.get(email=requested_email)
except User.DoesNotExist: except User.DoesNotExist:
# no matching user, go make an invitation # no matching user, go make an invitation
email_success = True
return self._make_invitation(requested_email, requestor) return self._make_invitation(requested_email, requestor)
else: else:
# if user already exists then just send an email # if user already exists then just send an email
try: try:
self._send_domain_invitation_email(requested_email, requestor, add_success=False) self._send_domain_invitation_email(
requested_email, requestor, requested_user=requested_user, add_success=False
)
email_success = True
except EmailSendingError: except EmailSendingError:
logger.warn( logger.warn(
"Could not send email invitation (EmailSendingError)", "Could not send email invitation (EmailSendingError)",
@ -876,6 +903,17 @@ class DomainAddUserView(DomainFormBaseView):
exc_info=True, exc_info=True,
) )
messages.warning(self.request, "Could not send email invitation.") messages.warning(self.request, "Could not send email invitation.")
email_success = True
except OutsideOrgMemberError:
logger.warn(
"Could not send email. Can not invite member of a .gov organization to a different organization.",
self.object,
exc_info=True,
)
messages.error(
self.request,
f"{requested_email} is already a member of another .gov organization.",
)
except Exception: except Exception:
logger.warn( logger.warn(
"Could not send email invitation (Other Exception)", "Could not send email invitation (Other Exception)",
@ -883,17 +921,17 @@ class DomainAddUserView(DomainFormBaseView):
exc_info=True, exc_info=True,
) )
messages.warning(self.request, "Could not send email invitation.") messages.warning(self.request, "Could not send email invitation.")
if email_success:
try:
UserDomainRole.objects.create(
user=requested_user,
domain=self.object,
role=UserDomainRole.Roles.MANAGER,
)
messages.success(self.request, f"Added user {requested_email}.")
except IntegrityError:
messages.warning(self.request, f"{requested_email} is already a manager for this domain")
try:
UserDomainRole.objects.create(
user=requested_user,
domain=self.object,
role=UserDomainRole.Roles.MANAGER,
)
except IntegrityError:
messages.warning(self.request, f"{requested_email} is already a manager for this domain")
else:
messages.success(self.request, f"Added user {requested_email}.")
return redirect(self.get_success_url()) return redirect(self.get_success_url())