mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-20 17:56:11 +02:00
Merge remote-tracking branch 'origin/main' into nl/2470-update-action-needed-email
This commit is contained in:
commit
3a27e1f887
9 changed files with 188 additions and 30 deletions
|
@ -356,9 +356,18 @@ CSP_FORM_ACTION = allowed_sources
|
|||
# strict CSP by allowing scripts to run from their domain
|
||||
# and inline with a nonce, as well as allowing connections back to their domain.
|
||||
# Note: If needed, we can embed chart.js instead of using the CDN
|
||||
CSP_SCRIPT_SRC_ELEM = ["'self'", "https://www.googletagmanager.com/", "https://cdn.jsdelivr.net/npm/chart.js"]
|
||||
CSP_CONNECT_SRC = ["'self'", "https://www.google-analytics.com/"]
|
||||
CSP_INCLUDE_NONCE_IN = ["script-src-elem"]
|
||||
CSP_DEFAULT_SRC = ("'self'",)
|
||||
CSP_STYLE_SRC = ["'self'", "https://www.ssa.gov/accessibility/andi/andi.css"]
|
||||
CSP_SCRIPT_SRC_ELEM = [
|
||||
"'self'",
|
||||
"https://www.googletagmanager.com/",
|
||||
"https://cdn.jsdelivr.net/npm/chart.js",
|
||||
"https://www.ssa.gov",
|
||||
"https://ajax.googleapis.com",
|
||||
]
|
||||
CSP_CONNECT_SRC = ["'self'", "https://www.google-analytics.com/", "https://www.ssa.gov/accessibility/andi/andi.js"]
|
||||
CSP_INCLUDE_NONCE_IN = ["script-src-elem", "style-src"]
|
||||
CSP_IMG_SRC = ["'self'", "https://www.ssa.gov/accessibility/andi/icons/"]
|
||||
|
||||
# Cross-Origin Resource Sharing (CORS) configuration
|
||||
# Sets clients that allow access control to manage.get.gov
|
||||
|
|
|
@ -65,6 +65,11 @@ urlpatterns = [
|
|||
views.PortfolioDomainsView.as_view(),
|
||||
name="domains",
|
||||
),
|
||||
path(
|
||||
"no-organization-domains/",
|
||||
views.PortfolioNoDomainsView.as_view(),
|
||||
name="no-portfolio-domains",
|
||||
),
|
||||
path(
|
||||
"requests/",
|
||||
views.PortfolioDomainRequestsView.as_view(),
|
||||
|
|
|
@ -151,8 +151,7 @@ class CheckPortfolioMiddleware:
|
|||
if request.user.has_domains_portfolio_permission():
|
||||
portfolio_redirect = reverse("domains")
|
||||
else:
|
||||
# View organization is the lowest access
|
||||
portfolio_redirect = reverse("organization")
|
||||
portfolio_redirect = reverse("no-portfolio-domains")
|
||||
|
||||
return HttpResponseRedirect(portfolio_redirect)
|
||||
|
||||
|
|
|
@ -5,16 +5,16 @@
|
|||
{% comment %} Stores the json endpoint in a url for easier access {% endcomment %}
|
||||
{% url 'get_domains_json' as url %}
|
||||
<span id="get_domains_json_url" class="display-none">{{url}}</span>
|
||||
<section class="section--outlined domains{% if not has_domains_portfolio_permission %} margin-top-0{% endif %}" id="domains">
|
||||
<div class="section--outlined__header margin-bottom-3 {% if not has_domains_portfolio_permission %} section--outlined__header--no-portfolio justify-content-space-between{% else %} grid-row{% endif %}">
|
||||
{% if not has_domains_portfolio_permission %}
|
||||
<section class="section--outlined domains{% if not portfolio %} margin-top-0{% endif %}" id="domains">
|
||||
<div class="section--outlined__header margin-bottom-3 {% if not portfolio %} section--outlined__header--no-portfolio justify-content-space-between{% else %} grid-row{% endif %}">
|
||||
{% if not portfolio %}
|
||||
<h2 id="domains-header" class="display-inline-block">Domains</h2>
|
||||
<span class="display-none" id="no-portfolio-js-flag"></span>
|
||||
{% else %}
|
||||
<!-- Embedding the portfolio value in a data attribute -->
|
||||
<span id="portfolio-js-value" data-portfolio="{{ portfolio.id }}"></span>
|
||||
{% endif %}
|
||||
<div class="section--outlined__search {% if has_domains_portfolio_permission %} mobile:grid-col-12 desktop:grid-col-6{% endif %}">
|
||||
<div class="section--outlined__search {% if portfolio %} mobile:grid-col-12 desktop:grid-col-6{% endif %}">
|
||||
<section aria-label="Domains search component" class="margin-top-2">
|
||||
<form class="usa-search usa-search--small" method="POST" role="search">
|
||||
{% csrf_token %}
|
||||
|
@ -43,7 +43,7 @@
|
|||
</section>
|
||||
</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 portfolio %} 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">
|
||||
<a href="{% url 'export_data_type_user' %}" class="usa-button usa-button--unstyled" role="button">
|
||||
<svg class="usa-icon usa-icon--big margin-right-neg-4px" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
|
@ -54,7 +54,7 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if has_domains_portfolio_permission %}
|
||||
{% if portfolio %}
|
||||
<div class="display-flex flex-align-center">
|
||||
<span class="margin-right-2 margin-top-neg-1 usa-prose text-base-darker">Filter by</span>
|
||||
<div class="usa-accordion usa-accordion--select margin-right-2">
|
||||
|
@ -157,7 +157,7 @@
|
|||
<th data-sortable="name" scope="col" role="columnheader">Domain name</th>
|
||||
<th data-sortable="expiration_date" scope="col" role="columnheader">Expires</th>
|
||||
<th data-sortable="state_display" scope="col" role="columnheader">Status</th>
|
||||
{% if has_domains_portfolio_permission and request.user.has_view_suborganization %}
|
||||
{% if portfolio and request.user.has_view_suborganization %}
|
||||
<th data-sortable="suborganization" scope="col" role="columnheader">Suborganization</th>
|
||||
{% endif %}
|
||||
<th
|
||||
|
|
|
@ -13,19 +13,22 @@
|
|||
<img src="{%static 'img/usa-icons/close.svg'%}" role="img" alt="Close" />
|
||||
</button>
|
||||
<ul class="usa-nav__primary usa-accordion">
|
||||
{% if has_domains_portfolio_permission %}
|
||||
<li class="usa-nav__primary-item">
|
||||
<li class="usa-nav__primary-item">
|
||||
{% if has_domains_portfolio_permission %}
|
||||
{% url 'domains' as url %}
|
||||
<a href="{{ url }}" class="usa-nav-link{% if 'domain'|in_path:request.path %} usa-current{% endif %}">
|
||||
Domains
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{%else %}
|
||||
{% url 'no-portfolio-domains' as url %}
|
||||
{% endif %}
|
||||
<a href="{{ url }}" class="usa-nav-link{% if 'domain'|in_path:request.path %} usa-current{% endif %}">
|
||||
Domains
|
||||
</a>
|
||||
</li>
|
||||
<li class="usa-nav__primary-item">
|
||||
<a href="#" class="usa-nav-link">
|
||||
Domain groups
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% if has_domain_requests_portfolio_permission %}
|
||||
<li class="usa-nav__primary-item">
|
||||
{% url 'domain-requests' as url %}
|
||||
|
|
30
src/registrar/templates/no_portfolio_domains.html
Normal file
30
src/registrar/templates/no_portfolio_domains.html
Normal file
|
@ -0,0 +1,30 @@
|
|||
{% extends 'portfolio_base.html' %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% block title %} Domains | {% endblock %}
|
||||
|
||||
{% block portfolio_content %}
|
||||
<h1 id="domains-header">Domains</h1>
|
||||
<section class="section--outlined">
|
||||
<div class="section--outlined__header margin-bottom-3">
|
||||
<h2 id="domains-header" class="display-inline-block">You aren’t managing any domains.</h2>
|
||||
{% if portfolio_administrators %}
|
||||
<p>If you believe you should have access to a domain, reach out to your organization’s administrators.</p>
|
||||
<p>Your organizations administrators:</p>
|
||||
<ul class="margin-top-0">
|
||||
{% for administrator in portfolio_administrators %}
|
||||
{% if administrator.email %}
|
||||
<li>{{ administrator.email }}</li>
|
||||
{% else %}
|
||||
<li>{{ administrator }}</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p><strong>No administrators were found on your organization.</strong></p>
|
||||
<p>If you believe you should have access to a domain, email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -97,8 +97,8 @@ class TestPortfolio(WebTest):
|
|||
self.assertNotContains(portfolio_page, self.portfolio.organization_name)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_middleware_redirects_to_portfolio_organization_page(self):
|
||||
"""Test that user with a portfolio and VIEW_PORTFOLIO is redirected to portfolio organization page"""
|
||||
def test_middleware_redirects_to_portfolio_no_domains_page(self):
|
||||
"""Test that user with a portfolio and VIEW_PORTFOLIO is redirected to the no domains page"""
|
||||
self.app.set_user(self.user.username)
|
||||
self.user.portfolio = self.portfolio
|
||||
self.user.portfolio_additional_permissions = [UserPortfolioPermissionChoices.VIEW_PORTFOLIO]
|
||||
|
@ -110,7 +110,8 @@ class TestPortfolio(WebTest):
|
|||
portfolio_page = self.app.get(reverse("home")).follow()
|
||||
# Assert that we're on the right page
|
||||
self.assertContains(portfolio_page, self.portfolio.organization_name)
|
||||
self.assertContains(portfolio_page, "<h1>Organization</h1>")
|
||||
self.assertContains(portfolio_page, '<h1 id="domains-header">Domains</h1>')
|
||||
self.assertContains(portfolio_page, "You aren’t managing any domains")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_middleware_redirects_to_portfolio_domains_page(self):
|
||||
|
@ -221,8 +222,8 @@ class TestPortfolio(WebTest):
|
|||
self.assertContains(response, 'for="id_city"')
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_navigation_links_hidden_when_user_not_have_permission(self):
|
||||
"""Test that navigation links are hidden when user does not have portfolio permissions"""
|
||||
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)
|
||||
self.user.portfolio = self.portfolio
|
||||
self.user.portfolio_additional_permissions = [
|
||||
|
@ -249,16 +250,29 @@ class TestPortfolio(WebTest):
|
|||
self.user.save()
|
||||
self.user.refresh_from_db()
|
||||
|
||||
# Members should be redirected to the readonly domains page
|
||||
portfolio_page = self.app.get(reverse("home")).follow()
|
||||
|
||||
self.assertContains(portfolio_page, self.portfolio.organization_name)
|
||||
self.assertContains(portfolio_page, "<h1>Organization</h1>")
|
||||
self.assertNotContains(portfolio_page, '<h1 id="domains-header">Domains</h1>')
|
||||
self.assertNotContains(portfolio_page, "<h1>Organization</h1>")
|
||||
self.assertContains(portfolio_page, '<h1 id="domains-header">Domains</h1>')
|
||||
self.assertContains(portfolio_page, "You aren’t managing any domains")
|
||||
self.assertNotContains(portfolio_page, reverse("domains"))
|
||||
self.assertNotContains(portfolio_page, reverse("domain-requests"))
|
||||
|
||||
# The organization page should still be accessible
|
||||
org_page = self.app.get(reverse("organization"))
|
||||
self.assertContains(org_page, self.portfolio.organization_name)
|
||||
self.assertContains(org_page, "<h1>Organization</h1>")
|
||||
|
||||
# Both domain pages should not be accessible
|
||||
domain_page = self.app.get(reverse("domains"), expect_errors=True)
|
||||
self.assertEquals(domain_page.status_code, 403)
|
||||
domain_request_page = self.app.get(reverse("domain-requests"), expect_errors=True)
|
||||
self.assertEquals(domain_request_page.status_code, 403)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_navigation_links_hidden_when_user_not_have_role(self):
|
||||
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)
|
||||
self.user.portfolio = self.portfolio
|
||||
|
@ -282,14 +296,27 @@ class TestPortfolio(WebTest):
|
|||
self.user.save()
|
||||
self.user.refresh_from_db()
|
||||
|
||||
# Members should be redirected to the readonly domains page
|
||||
portfolio_page = self.app.get(reverse("home")).follow()
|
||||
|
||||
self.assertContains(portfolio_page, self.portfolio.organization_name)
|
||||
self.assertContains(portfolio_page, "<h1>Organization</h1>")
|
||||
self.assertNotContains(portfolio_page, '<h1 id="domains-header">Domains</h1>')
|
||||
self.assertNotContains(portfolio_page, "<h1>Organization</h1>")
|
||||
self.assertContains(portfolio_page, '<h1 id="domains-header">Domains</h1>')
|
||||
self.assertContains(portfolio_page, "You aren’t managing any domains")
|
||||
self.assertNotContains(portfolio_page, reverse("domains"))
|
||||
self.assertNotContains(portfolio_page, reverse("domain-requests"))
|
||||
|
||||
# The organization page should still be accessible
|
||||
org_page = self.app.get(reverse("organization"))
|
||||
self.assertContains(org_page, self.portfolio.organization_name)
|
||||
self.assertContains(org_page, "<h1>Organization</h1>")
|
||||
|
||||
# Both domain pages should not be accessible
|
||||
domain_page = self.app.get(reverse("domains"), expect_errors=True)
|
||||
self.assertEquals(domain_page.status_code, 403)
|
||||
domain_request_page = self.app.get(reverse("domain-requests"), expect_errors=True)
|
||||
self.assertEquals(domain_request_page.status_code, 403)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_portfolio_org_name(self):
|
||||
"""Can load portfolio's org name page."""
|
||||
|
@ -355,3 +382,51 @@ class TestPortfolio(WebTest):
|
|||
|
||||
self.assertContains(success_result_page, "6 Downing st")
|
||||
self.assertContains(success_result_page, "London")
|
||||
|
||||
@less_console_noise_decorator
|
||||
@override_flag("organization_feature", active=True)
|
||||
def test_org_member_can_only_see_domains_with_appropriate_permissions(self):
|
||||
"""A user with the role organization_member should not have access to the domains page
|
||||
if they do not have the right permissions.
|
||||
"""
|
||||
|
||||
# A default organization member should not be able to see any domains
|
||||
self.app.set_user(self.user.username)
|
||||
self.user.portfolio = self.portfolio
|
||||
self.user.portfolio_roles = [UserPortfolioRoleChoices.ORGANIZATION_MEMBER]
|
||||
self.user.save()
|
||||
self.user.refresh_from_db()
|
||||
|
||||
self.assertFalse(self.user.has_domains_portfolio_permission())
|
||||
|
||||
response = self.app.get(reverse("no-portfolio-domains"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "You aren’t managing any domains.")
|
||||
|
||||
# Test the domains page - this user should not have access
|
||||
response = self.app.get(reverse("domains"), expect_errors=True)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
# Ensure that this user can see domains with the right permissions
|
||||
self.user.portfolio_additional_permissions = [UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS]
|
||||
self.user.save()
|
||||
self.user.refresh_from_db()
|
||||
|
||||
self.assertTrue(self.user.has_domains_portfolio_permission())
|
||||
|
||||
# Test the domains page - this user should have access
|
||||
response = self.app.get(reverse("domains"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Domain name")
|
||||
|
||||
# Test the managed domains permission
|
||||
self.user.portfolio_additional_permissions = [UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS]
|
||||
self.user.save()
|
||||
self.user.refresh_from_db()
|
||||
|
||||
self.assertTrue(self.user.has_domains_portfolio_permission())
|
||||
|
||||
# Test the domains page - this user should have access
|
||||
response = self.app.get(reverse("domains"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Domain name")
|
||||
|
|
|
@ -4,11 +4,13 @@ from django.shortcuts import render
|
|||
from django.urls import reverse
|
||||
from django.contrib import messages
|
||||
from registrar.forms.portfolio import PortfolioOrgAddressForm, PortfolioSeniorOfficialForm
|
||||
from registrar.models.portfolio import Portfolio
|
||||
from registrar.models import Portfolio, User
|
||||
from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
|
||||
from registrar.views.utility.permission_views import (
|
||||
PortfolioDomainRequestsPermissionView,
|
||||
PortfolioDomainsPermissionView,
|
||||
PortfolioBasePermissionView,
|
||||
NoPortfolioDomainsPermissionView,
|
||||
)
|
||||
from django.views.generic import View
|
||||
from django.views.generic.edit import FormMixin
|
||||
|
@ -38,6 +40,32 @@ class PortfolioDomainRequestsView(PortfolioDomainRequestsPermissionView, View):
|
|||
return render(request, "portfolio_requests.html")
|
||||
|
||||
|
||||
class PortfolioNoDomainsView(NoPortfolioDomainsPermissionView, View):
|
||||
"""Some users have access to the underlying portfolio, but not any domains.
|
||||
This is a custom view which explains that to the user - and denotes who to contact.
|
||||
"""
|
||||
|
||||
model = Portfolio
|
||||
template_name = "no_portfolio_domains.html"
|
||||
|
||||
def get(self, request):
|
||||
return render(request, self.template_name, context=self.get_context_data())
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Add additional context data to the template."""
|
||||
# We can override the base class. This view only needs this item.
|
||||
context = {}
|
||||
portfolio = self.request.user.portfolio if self.request and self.request.user else None
|
||||
if portfolio:
|
||||
context["portfolio_administrators"] = User.objects.filter(
|
||||
portfolio=portfolio,
|
||||
portfolio_roles__overlap=[
|
||||
UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
|
||||
],
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
class PortfolioOrganizationView(PortfolioBasePermissionView, FormMixin):
|
||||
"""
|
||||
View to handle displaying and updating the portfolio's organization details.
|
||||
|
|
|
@ -214,6 +214,15 @@ class PortfolioDomainsPermissionView(PortfolioDomainsPermission, PortfolioBasePe
|
|||
"""
|
||||
|
||||
|
||||
class NoPortfolioDomainsPermissionView(PortfolioBasePermissionView, abc.ABC):
|
||||
"""Abstract base view for a user without access to the
|
||||
portfolio domains views that enforces permissions.
|
||||
|
||||
This abstract view cannot be instantiated. Actual views must specify
|
||||
`template_name`.
|
||||
"""
|
||||
|
||||
|
||||
class PortfolioDomainRequestsPermissionView(PortfolioDomainRequestsPermission, PortfolioBasePermissionView, abc.ABC):
|
||||
"""Abstract base view for portfolio domain request views that enforces permissions.
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue