Merge pull request #2517 from cisagov/za/2348-csv-export-org-member-domain-export

(on getgov-za) Ticket #2348: Handle portfolio permissions for csv export
This commit is contained in:
zandercymatics 2024-08-09 11:46:13 -06:00 committed by GitHub
commit 20626539c9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 103 additions and 11 deletions

View file

@ -4,6 +4,7 @@ from django.contrib.auth.models import AbstractUser
from django.db import models from django.db import models
from django.db.models import Q from django.db.models import Q
from registrar.models.domain_information import DomainInformation
from registrar.models.user_domain_role import UserDomainRole from registrar.models.user_domain_role import UserDomainRole
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
@ -265,6 +266,10 @@ class User(AbstractUser):
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS
) or self._has_portfolio_permission(UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS) ) or self._has_portfolio_permission(UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS)
def has_view_all_domains_permission(self):
"""Determines if the current user can view all available domains in a given portfolio"""
return self._has_portfolio_permission(UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS)
@classmethod @classmethod
def needs_identity_verification(cls, email, uuid): def needs_identity_verification(cls, email, uuid):
"""A method used by our oidc classes to test whether a user needs email/uuid verification """A method used by our oidc classes to test whether a user needs email/uuid verification
@ -406,3 +411,10 @@ class User(AbstractUser):
def is_org_user(self, request): def is_org_user(self, request):
has_organization_feature_flag = flag_is_active(request, "organization_feature") has_organization_feature_flag = flag_is_active(request, "organization_feature")
return has_organization_feature_flag and self.has_base_portfolio_permission() return has_organization_feature_flag and self.has_base_portfolio_permission()
def get_user_domain_ids(self, request):
"""Returns either the domains ids associated with this user on UserDomainRole or Portfolio"""
if self.is_org_user(request) and self.has_view_all_domains_permission():
return DomainInformation.objects.filter(portfolio=self.portfolio).values_list("domain_id", flat=True)
else:
return UserDomainRole.objects.filter(user=self).values_list("domain_id", flat=True)

View file

@ -29,7 +29,7 @@
</a> </a>
</p> </p>
{% include "includes/domains_table.html" %} {% include "includes/domains_table.html" with user_domain_count=user_domain_count %}
{% include "includes/domain_requests_table.html" %} {% include "includes/domain_requests_table.html" %}
</div> </div>

View file

@ -37,6 +37,7 @@
</form> </form>
</section> </section>
</div> </div>
{% if user_domain_count and user_domain_count > 0 %}
<div class="section--outlined__utility-button mobile-lg:padding-right-105 {% if has_domains_portfolio_permission %} mobile:grid-col-12 desktop:grid-col-6 desktop:padding-left-3{% endif %}"> <div class="section--outlined__utility-button mobile-lg:padding-right-105 {% if has_domains_portfolio_permission %} mobile:grid-col-12 desktop:grid-col-6 desktop:padding-left-3{% endif %}">
<section aria-label="Domains report component" class="mobile-lg:margin-top-205"> <section aria-label="Domains report component" class="mobile-lg:margin-top-205">
<a href="{% url 'export_data_type_user' %}" class="usa-button usa-button--unstyled" role="button"> <a href="{% url 'export_data_type_user' %}" class="usa-button usa-button--unstyled" role="button">
@ -46,6 +47,7 @@
</a> </a>
</section> </section>
</div> </div>
{% endif %}
</div> </div>
{% if has_domains_portfolio_permission %} {% if has_domains_portfolio_permission %}
<div class="display-flex flex-align-center"> <div class="display-flex flex-align-center">

View file

@ -6,5 +6,5 @@
{% block portfolio_content %} {% block portfolio_content %}
<h1 id="domains-header">Domains</h1> <h1 id="domains-header">Domains</h1>
{% include "includes/domains_table.html" with portfolio=portfolio %} {% include "includes/domains_table.html" with portfolio=portfolio user_domain_count=user_domain_count %}
{% endblock %} {% endblock %}

View file

@ -6,6 +6,8 @@ from registrar.models import (
Domain, Domain,
UserDomainRole, UserDomainRole,
) )
from registrar.models import Portfolio
from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
from registrar.utility.csv_export import ( from registrar.utility.csv_export import (
DomainDataFull, DomainDataFull,
DomainDataType, DomainDataType,
@ -32,6 +34,7 @@ from registrar.utility.s3_bucket import S3ClientError, S3ClientErrorCodes # typ
from django.utils import timezone from django.utils import timezone
from api.tests.common import less_console_noise_decorator from api.tests.common import less_console_noise_decorator
from .common import MockDbForSharedTests, MockDbForIndividualTests, MockEppLib, less_console_noise, get_time_aware_date from .common import MockDbForSharedTests, MockDbForIndividualTests, MockEppLib, less_console_noise, get_time_aware_date
from waffle.testutils import override_flag
class CsvReportsTest(MockDbForSharedTests): class CsvReportsTest(MockDbForSharedTests):
@ -311,6 +314,80 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
self.maxDiff = None self.maxDiff = None
self.assertEqual(csv_content, expected_content) self.assertEqual(csv_content, expected_content)
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
def test_domain_data_type_user_with_portfolio(self):
"""Tests DomainDataTypeUser export with portfolio permissions"""
# Create a portfolio and assign it to the user
portfolio = Portfolio.objects.create(creator=self.user, organization_name="Test Portfolio")
self.user.portfolio = portfolio
self.user.save()
UserDomainRole.objects.create(user=self.user, domain=self.domain_2)
UserDomainRole.objects.filter(user=self.user, domain=self.domain_1).delete()
UserDomainRole.objects.filter(user=self.user, domain=self.domain_3).delete()
# Add portfolios to the first and third domains
self.domain_1.domain_info.portfolio = portfolio
self.domain_3.domain_info.portfolio = portfolio
self.domain_1.domain_info.save()
self.domain_3.domain_info.save()
# Set up user permissions
self.user.portfolio_roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
self.user.save()
self.user.refresh_from_db()
# Create a request object
factory = RequestFactory()
request = factory.get("/")
request.user = self.user
# Get the csv content
csv_content = self._run_domain_data_type_user_export(request)
# We expect only domains associated with the user's portfolio
self.assertIn(self.domain_1.name, csv_content)
self.assertIn(self.domain_3.name, csv_content)
self.assertNotIn(self.domain_2.name, csv_content)
# Test the output for readonly admin
self.user.portfolio_roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN_READ_ONLY]
self.user.save()
self.assertIn(self.domain_1.name, csv_content)
self.assertIn(self.domain_3.name, csv_content)
self.assertNotIn(self.domain_2.name, csv_content)
# Get the csv content
self.user.portfolio_roles = [UserPortfolioRoleChoices.ORGANIZATION_MEMBER]
self.user.save()
csv_content = self._run_domain_data_type_user_export(request)
self.assertNotIn(self.domain_1.name, csv_content)
self.assertNotIn(self.domain_3.name, csv_content)
self.assertIn(self.domain_2.name, csv_content)
self.domain_1.delete()
self.domain_2.delete()
self.domain_3.delete()
portfolio.delete()
def _run_domain_data_type_user_export(self, request):
"""Helper function to run the export_data_to_csv function on DomainDataTypeUser"""
# Create a CSV file in memory
csv_file = StringIO()
# Call the export functions
DomainDataTypeUser.export_data_to_csv(csv_file, request=request)
# Reset the CSV file's position to the beginning
csv_file.seek(0)
# Read the content into a variable
csv_content = csv_file.read()
return csv_content
@less_console_noise_decorator @less_console_noise_decorator
def test_domain_data_full(self): def test_domain_data_full(self):
"""Shows security contacts, filtered by state""" """Shows security contacts, filtered by state"""

View file

@ -578,10 +578,9 @@ class DomainDataTypeUser(DomainDataType):
if request is None or not hasattr(request, "user") or not request.user: if request is None or not hasattr(request, "user") or not request.user:
# Return nothing # Return nothing
return Q(id__in=[]) return Q(id__in=[])
else:
user_domain_roles = UserDomainRole.objects.filter(user=request.user) # Get all domains the user is associated with
domain_ids = user_domain_roles.values_list("domain_id", flat=True) return Q(domain__id__in=request.user.get_user_domain_ids(request))
return Q(domain__id__in=domain_ids)
class DomainDataFull(DomainExport): class DomainDataFull(DomainExport):

View file

@ -1,13 +1,11 @@
import logging import logging
from django.http import JsonResponse from django.http import JsonResponse
from django.core.paginator import Paginator from django.core.paginator import Paginator
from registrar.models import UserDomainRole, Domain from registrar.models import UserDomainRole, Domain, DomainInformation
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.urls import reverse from django.urls import reverse
from django.db.models import Q from django.db.models import Q
from registrar.models.domain_information import DomainInformation
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View file

@ -5,8 +5,9 @@ def index(request):
"""This page is available to anyone without logging in.""" """This page is available to anyone without logging in."""
context = {} context = {}
if request.user.is_authenticated: if request and request.user and request.user.is_authenticated:
# This controls the creation of a new domain request in the wizard # This controls the creation of a new domain request in the wizard
request.session["new_request"] = True request.session["new_request"] = True
context["user_domain_count"] = request.user.get_user_domain_ids(request).count()
return render(request, "home.html", context) return render(request, "home.html", context)

View file

@ -22,7 +22,10 @@ class PortfolioDomainsView(PortfolioDomainsPermissionView, View):
template_name = "portfolio_domains.html" template_name = "portfolio_domains.html"
def get(self, request): def get(self, request):
return render(request, "portfolio_domains.html") context = {}
if self.request and self.request.user and self.request.user.is_authenticated:
context["user_domain_count"] = self.request.user.get_user_domain_ids(request).count()
return render(request, "portfolio_domains.html", context)
class PortfolioDomainRequestsView(PortfolioDomainRequestsPermissionView, View): class PortfolioDomainRequestsView(PortfolioDomainRequestsPermissionView, View):