This commit is contained in:
CocoByte 2024-09-10 14:18:41 -06:00
commit 504fdedf0c
No known key found for this signature in database
GPG key ID: BBFAA2526384C97F
15 changed files with 365 additions and 37 deletions

View file

@ -60,6 +60,17 @@ def add_has_profile_feature_flag_to_context(request):
def portfolio_permissions(request):
"""Make portfolio permissions for the request user available in global context"""
context = {
"has_base_portfolio_permission": False,
"has_domains_portfolio_permission": False,
"has_domain_requests_portfolio_permission": False,
"has_view_members_portfolio_permission": False,
"has_edit_members_portfolio_permission": False,
"has_view_suborganization": False,
"has_edit_suborganization": False,
"portfolio": None,
"has_organization_feature_flag": False,
}
try:
portfolio = request.session.get("portfolio")
if portfolio:
@ -69,32 +80,18 @@ def portfolio_permissions(request):
"has_domain_requests_portfolio_permission": request.user.has_domain_requests_portfolio_permission(
portfolio
),
"has_view_members_portfolio_permission": request.user.has_view_members_portfolio_permission(portfolio),
"has_edit_members_portfolio_permission": request.user.has_edit_members_portfolio_permission(portfolio),
"has_view_suborganization": request.user.has_view_suborganization(portfolio),
"has_edit_suborganization": request.user.has_edit_suborganization(portfolio),
"portfolio": portfolio,
"has_organization_feature_flag": True,
}
return {
"has_base_portfolio_permission": False,
"has_domains_portfolio_permission": False,
"has_domain_requests_portfolio_permission": False,
"has_view_suborganization": False,
"has_edit_suborganization": False,
"portfolio": None,
"has_organization_feature_flag": False,
}
return context
except AttributeError:
# Handles cases where request.user might not exist
return {
"has_base_portfolio_permission": False,
"has_domains_portfolio_permission": False,
"has_domain_requests_portfolio_permission": False,
"has_view_suborganization": False,
"has_edit_suborganization": False,
"portfolio": None,
"has_organization_feature_flag": False,
}
return context
def is_widescreen_mode(request):

View file

@ -1,6 +1,5 @@
import logging
from django.core.management import BaseCommand
from django.db.models.manager import BaseManager
from registrar.management.commands.utility.terminal_helper import PopulateScriptTemplate, TerminalColors
from registrar.models import Domain, TransitionDomain
@ -27,7 +26,7 @@ class Command(BaseCommand, PopulateScriptTemplate):
# check if a transition domain object for this domain name exists,
# or if so whether its first_ready value matches its created_at date
def custom_filter(self, records: BaseManager[Domain]) -> BaseManager[Domain]:
def custom_filter(self, records):
to_include_pks = []
for record in records:
if (

View file

@ -78,7 +78,7 @@ class PopulateScriptTemplate(ABC):
run_summary_header = None
@abstractmethod
def update_record(self, record: Model):
def update_record(self, record):
"""Defines how we update each field.
raises:
@ -86,7 +86,7 @@ class PopulateScriptTemplate(ABC):
"""
raise NotImplementedError
def mass_update_records(self, object_class: Model, filter_conditions, fields_to_update, debug=True, verbose=False):
def mass_update_records(self, object_class, filter_conditions, fields_to_update, debug=True, verbose=False):
"""Loops through each valid "object_class" object - specified by filter_conditions - and
updates fields defined by fields_to_update using update_record.

View file

@ -0,0 +1,66 @@
# Generated by Django 4.2.10 on 2024-09-04 21:29
import django.contrib.postgres.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("registrar", "0122_create_groups_v16"),
]
operations = [
migrations.AlterField(
model_name="portfolioinvitation",
name="portfolio_additional_permissions",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.CharField(
choices=[
("view_all_domains", "View all domains and domain reports"),
("view_managed_domains", "View managed domains"),
("view_members", "View members"),
("edit_members", "Create and edit members"),
("view_all_requests", "View all requests"),
("view_created_requests", "View created requests"),
("edit_requests", "Create and edit requests"),
("view_portfolio", "View organization"),
("edit_portfolio", "Edit organization"),
("view_suborganization", "View suborganization"),
("edit_suborganization", "Edit suborganization"),
],
max_length=50,
),
blank=True,
help_text="Select one or more additional permissions.",
null=True,
size=None,
),
),
migrations.AlterField(
model_name="userportfoliopermission",
name="additional_permissions",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.CharField(
choices=[
("view_all_domains", "View all domains and domain reports"),
("view_managed_domains", "View managed domains"),
("view_members", "View members"),
("edit_members", "Create and edit members"),
("view_all_requests", "View all requests"),
("view_created_requests", "View created requests"),
("edit_requests", "Create and edit requests"),
("view_portfolio", "View organization"),
("edit_portfolio", "Edit organization"),
("view_suborganization", "View suborganization"),
("edit_suborganization", "Edit suborganization"),
],
max_length=50,
),
blank=True,
help_text="Select one or more additional permissions.",
null=True,
size=None,
),
),
]

View file

@ -198,10 +198,40 @@ class User(AbstractUser):
) or self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS)
def has_domain_requests_portfolio_permission(self, portfolio):
# BEGIN
# Note code below is to add organization_request feature
request = HttpRequest()
request.user = self
has_organization_requests_flag = flag_is_active(request, "organization_requests")
if not has_organization_requests_flag:
return False
# END
return self._has_portfolio_permission(
portfolio, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS
) or self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS)
def has_view_members_portfolio_permission(self, portfolio):
# BEGIN
# Note code below is to add organization_request feature
request = HttpRequest()
request.user = self
has_organization_members_flag = flag_is_active(request, "organization_members")
if not has_organization_members_flag:
return False
# END
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_MEMBERS)
def has_edit_members_portfolio_permission(self, portfolio):
# BEGIN
# Note code below is to add organization_request feature
request = HttpRequest()
request.user = self
has_organization_members_flag = flag_is_active(request, "organization_members")
if not has_organization_members_flag:
return False
# END
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_MEMBERS)
def has_view_all_domains_permission(self, portfolio):
"""Determines if the current user can view all available domains in a given portfolio"""
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS)

View file

@ -16,8 +16,8 @@ class UserPortfolioPermission(TimeStampedModel):
PORTFOLIO_ROLE_PERMISSIONS = {
UserPortfolioRoleChoices.ORGANIZATION_ADMIN: [
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
UserPortfolioPermissionChoices.VIEW_MEMBER,
UserPortfolioPermissionChoices.EDIT_MEMBER,
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.EDIT_MEMBERS,
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
UserPortfolioPermissionChoices.EDIT_REQUESTS,
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
@ -28,7 +28,7 @@ class UserPortfolioPermission(TimeStampedModel):
],
UserPortfolioRoleChoices.ORGANIZATION_ADMIN_READ_ONLY: [
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
UserPortfolioPermissionChoices.VIEW_MEMBER,
UserPortfolioPermissionChoices.VIEW_MEMBERS,
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
# Domain: field specific permissions

View file

@ -17,8 +17,8 @@ class UserPortfolioPermissionChoices(models.TextChoices):
VIEW_ALL_DOMAINS = "view_all_domains", "View all domains and domain reports"
VIEW_MANAGED_DOMAINS = "view_managed_domains", "View managed domains"
VIEW_MEMBER = "view_member", "View members"
EDIT_MEMBER = "edit_member", "Create and edit members"
VIEW_MEMBERS = "view_members", "View members"
EDIT_MEMBERS = "edit_members", "Create and edit members"
VIEW_ALL_REQUESTS = "view_all_requests", "View all requests"
VIEW_CREATED_REQUESTS = "view_created_requests", "View created requests"

View file

@ -46,11 +46,11 @@
Domains
</a>
</li>
<li class="usa-nav__primary-item">
<!-- <li class="usa-nav__primary-item">
<a href="#" class="usa-nav-link">
Domain groups
</a>
</li>
</li> -->
{% if has_domain_requests_portfolio_permission %}
<li class="usa-nav__primary-item">
@ -60,11 +60,13 @@
</a>
</li>
{% endif %}
{% if has_view_members_portfolio_permission %}
<li class="usa-nav__primary-item">
<a href="#" class="usa-nav-link">
Members
</a>
</li>
{% endif %}
<li class="usa-nav__primary-item">
{% url 'organization' as url %}
<!-- Move the padding from the a to the span so that the descenders do not get cut off -->

View file

@ -1534,6 +1534,7 @@ class TestUser(TestCase):
self.assertFalse(self.user.has_contact_info())
@less_console_noise_decorator
@override_flag("organization_requests", active=True)
def test_has_portfolio_permission(self):
"""
0. Returns False when user does not have a permission
@ -1555,7 +1556,10 @@ class TestUser(TestCase):
portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
portfolio=portfolio,
user=self.user,
additional_permissions=[UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS],
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
],
)
user_can_view_all_domains = self.user.has_domains_portfolio_permission(portfolio)

View file

@ -1,9 +1,13 @@
from registrar.models import UserDomainRole, Domain, DomainInformation, Portfolio
from django.urls import reverse
from registrar.models.user_portfolio_permission import UserPortfolioPermission
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
from .test_views import TestWithUser
from django_webtest import WebTest # type: ignore
from django.utils.dateparse import parse_date
from api.tests.common import less_console_noise_decorator
from waffle.testutils import override_flag
class GetDomainsJsonTest(TestWithUser, WebTest):
@ -31,6 +35,7 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
def tearDown(self):
UserDomainRole.objects.all().delete()
UserPortfolioPermission.objects.all().delete()
DomainInformation.objects.all().delete()
Portfolio.objects.all().delete()
super().tearDown()
@ -115,8 +120,104 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
self.assertEqual(svg_icon_expected, svg_icons[i])
@less_console_noise_decorator
def test_get_domains_json_with_portfolio(self):
"""Test that an authenticated user gets the list of 2 domains for portfolio."""
@override_flag("organization_feature", active=True)
def test_get_domains_json_with_portfolio_view_managed_domains(self):
"""Test that an authenticated user gets the list of 1 domain for portfolio. The 1 domain
is the domain that they manage within the portfolio."""
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
additional_permissions=[UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS],
)
response = self.app.get(reverse("get_domains_json"), {"portfolio": self.portfolio.id})
self.assertEqual(response.status_code, 200)
data = response.json
# Check pagination info
self.assertEqual(data["page"], 1)
self.assertFalse(data["has_next"])
self.assertFalse(data["has_previous"])
self.assertEqual(data["num_pages"], 1)
# Check the number of domains
self.assertEqual(len(data["domains"]), 1)
# Expected domains
expected_domains = [self.domain3]
# Extract fields from response
domain_ids = [domain["id"] for domain in data["domains"]]
names = [domain["name"] for domain in data["domains"]]
expiration_dates = [domain["expiration_date"] for domain in data["domains"]]
states = [domain["state"] for domain in data["domains"]]
state_displays = [domain["state_display"] for domain in data["domains"]]
get_state_help_texts = [domain["get_state_help_text"] for domain in data["domains"]]
action_urls = [domain["action_url"] for domain in data["domains"]]
action_labels = [domain["action_label"] for domain in data["domains"]]
svg_icons = [domain["svg_icon"] for domain in data["domains"]]
# Check fields for each domain
for i, expected_domain in enumerate(expected_domains):
self.assertEqual(expected_domain.id, domain_ids[i])
self.assertEqual(expected_domain.name, names[i])
self.assertEqual(expected_domain.expiration_date, expiration_dates[i])
self.assertEqual(expected_domain.state, states[i])
# Parsing the expiration date from string to date
parsed_expiration_date = parse_date(expiration_dates[i])
expected_domain.expiration_date = parsed_expiration_date
# Check state_display and get_state_help_text
self.assertEqual(expected_domain.state_display(), state_displays[i])
self.assertEqual(expected_domain.get_state_help_text(), get_state_help_texts[i])
self.assertEqual(reverse("domain", kwargs={"pk": expected_domain.id}), action_urls[i])
# Check action_label
user_domain_role_exists = UserDomainRole.objects.filter(
domain_id=expected_domains[i].id, user=self.user
).exists()
action_label_expected = (
"View"
if not user_domain_role_exists
or expected_domains[i].state
in [
Domain.State.DELETED,
Domain.State.ON_HOLD,
]
else "Manage"
)
self.assertEqual(action_label_expected, action_labels[i])
# Check svg_icon
svg_icon_expected = (
"visibility"
if not user_domain_role_exists
or expected_domains[i].state
in [
Domain.State.DELETED,
Domain.State.ON_HOLD,
]
else "settings"
)
self.assertEqual(svg_icon_expected, svg_icons[i])
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
def test_get_domains_json_with_portfolio_view_all_domains(self):
"""Test that an authenticated user gets the list of 2 domains for portfolio. One is a domain which
they manage within the portfolio. The other is a domain which they don't manage within the
portfolio."""
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
additional_permissions=[UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS],
)
response = self.app.get(reverse("get_domains_json"), {"portfolio": self.portfolio.id})
self.assertEqual(response.status_code, 200)

View file

@ -230,6 +230,7 @@ class TestPortfolio(WebTest):
self.assertContains(response, 'for="id_city"')
@less_console_noise_decorator
@override_flag("organization_requests", active=True)
def test_accessible_pages_when_user_does_not_have_permission(self):
"""Tests which pages are accessible when user does not have portfolio permissions"""
self.app.set_user(self.user.username)
@ -280,6 +281,7 @@ class TestPortfolio(WebTest):
self.assertEquals(domain_request_page.status_code, 403)
@less_console_noise_decorator
@override_flag("organization_requests", active=True)
def test_accessible_pages_when_user_does_not_have_role(self):
"""Test that admin / memmber roles are associated with the right access"""
self.app.set_user(self.user.username)
@ -554,3 +556,99 @@ class TestPortfolio(WebTest):
# Test for widescreen modifier
self.assertContains(response, "--widescreen")
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_requests", active=False)
def test_organization_requests_waffle_flag_off_hides_nav_link_and_restricts_permission(self):
"""Setting the organization_requests waffle off hides the nav link and restricts access to the requests page"""
self.app.set_user(self.user.username)
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS,
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
UserPortfolioPermissionChoices.EDIT_REQUESTS,
],
)
home = self.app.get(reverse("home")).follow()
self.assertContains(home, "Hotel California")
self.assertNotContains(home, "Domain requests")
domain_requests = self.app.get(reverse("domain-requests"), expect_errors=True)
self.assertEqual(domain_requests.status_code, 403)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_requests", active=True)
def test_organization_requests_waffle_flag_on_shows_nav_link_and_allows_permission(self):
"""Setting the organization_requests waffle on shows the nav link and allows access to the requests page"""
self.app.set_user(self.user.username)
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS,
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
UserPortfolioPermissionChoices.EDIT_REQUESTS,
],
)
home = self.app.get(reverse("home")).follow()
self.assertContains(home, "Hotel California")
self.assertContains(home, "Domain requests")
domain_requests = self.app.get(reverse("domain-requests"))
self.assertEqual(domain_requests.status_code, 200)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=False)
def test_organization_members_waffle_flag_off_hides_nav_link(self):
"""Setting the organization_members waffle off hides the nav link"""
self.app.set_user(self.user.username)
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS,
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
UserPortfolioPermissionChoices.EDIT_REQUESTS,
],
)
home = self.app.get(reverse("home")).follow()
self.assertContains(home, "Hotel California")
self.assertNotContains(home, "Members")
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_members", active=True)
def test_organization_members_waffle_flag_on_shows_nav_link(self):
"""Setting the organization_members waffle on shows the nav link"""
self.app.set_user(self.user.username)
UserPortfolioPermission.objects.get_or_create(
user=self.user,
portfolio=self.portfolio,
additional_permissions=[
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
UserPortfolioPermissionChoices.VIEW_MEMBERS,
],
)
home = self.app.get(reverse("home")).follow()
self.assertContains(home, "Hotel California")
self.assertContains(home, "Members")

View file

@ -50,11 +50,15 @@ def get_domain_ids_from_request(request):
"""
portfolio = request.GET.get("portfolio")
if portfolio:
domain_infos = DomainInformation.objects.filter(portfolio=portfolio)
return domain_infos.values_list("domain_id", flat=True)
else:
user_domain_roles = UserDomainRole.objects.filter(user=request.user)
return user_domain_roles.values_list("domain_id", flat=True)
if request.user.is_org_user(request) and request.user.has_view_all_domains_permission(portfolio):
domain_infos = DomainInformation.objects.filter(portfolio=portfolio)
return domain_infos.values_list("domain_id", flat=True)
else:
domain_info_ids = DomainInformation.objects.filter(portfolio=portfolio).values_list("domain_id", flat=True)
user_domain_roles = UserDomainRole.objects.filter(user=request.user).values_list("domain_id", flat=True)
return domain_info_ids.intersection(user_domain_roles)
user_domain_roles = UserDomainRole.objects.filter(user=request.user)
return user_domain_roles.values_list("domain_id", flat=True)
def apply_search(queryset, request):

View file

@ -7,5 +7,6 @@ from .permission_views import (
DomainRequestPermissionWithdrawView,
DomainInvitationPermissionDeleteView,
DomainRequestWizardPermissionView,
PortfolioMembersPermission,
)
from .api_views import get_senior_official_from_federal_agency_json

View file

@ -454,3 +454,20 @@ class PortfolioDomainRequestsPermission(PortfolioBasePermission):
return False
return super().has_permission()
class PortfolioMembersPermission(PortfolioBasePermission):
"""Permission mixin that allows access to portfolio members pages if user
has access, otherwise 403"""
def has_permission(self):
"""Check if this user has access to members for this portfolio.
The user is in self.request.user and the portfolio can be looked
up from the portfolio's primary key in self.kwargs["pk"]"""
portfolio = self.request.session.get("portfolio")
if not self.request.user.has_view_members(portfolio):
return False
return super().has_permission()

View file

@ -18,6 +18,7 @@ from .mixins import (
UserDeleteDomainRolePermission,
UserProfilePermission,
PortfolioBasePermission,
PortfolioMembersPermission,
)
import logging
@ -229,3 +230,11 @@ class PortfolioDomainRequestsPermissionView(PortfolioDomainRequestsPermission, P
This abstract view cannot be instantiated. Actual views must specify
`template_name`.
"""
class PortfolioMembersPermissionView(PortfolioMembersPermission, PortfolioBasePermissionView, abc.ABC):
"""Abstract base view for portfolio domain request views that enforces permissions.
This abstract view cannot be instantiated. Actual views must specify
`template_name`.
"""