mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-16 09:37:03 +02:00
Merge branch 'main' into za/1292-user-domain-role-search-by-email
This commit is contained in:
commit
091c12151b
28 changed files with 361 additions and 54 deletions
1
.github/workflows/deploy-sandbox.yaml
vendored
1
.github/workflows/deploy-sandbox.yaml
vendored
|
@ -21,6 +21,7 @@ jobs:
|
|||
|| startsWith(github.head_ref, 'dk/')
|
||||
|| startsWith(github.head_ref, 'es/')
|
||||
|| startsWith(github.head_ref, 'ky/')
|
||||
|| startsWith(github.head_ref, 'backup/')
|
||||
outputs:
|
||||
environment: ${{ steps.var.outputs.environment}}
|
||||
runs-on: "ubuntu-latest"
|
||||
|
|
1
.github/workflows/migrate.yaml
vendored
1
.github/workflows/migrate.yaml
vendored
|
@ -16,6 +16,7 @@ on:
|
|||
- stable
|
||||
- staging
|
||||
- development
|
||||
- backup
|
||||
- ky
|
||||
- es
|
||||
- nl
|
||||
|
|
1
.github/workflows/reset-db.yaml
vendored
1
.github/workflows/reset-db.yaml
vendored
|
@ -16,6 +16,7 @@ on:
|
|||
options:
|
||||
- staging
|
||||
- development
|
||||
- backup
|
||||
- ky
|
||||
- es
|
||||
- nl
|
||||
|
|
|
@ -15,9 +15,9 @@ Going into our first production launch we need a plan describing what our releas
|
|||
**Option 1:** Releasing to stable/staging once a sprint
|
||||
Releasing once a sprint would mean that we release the past sprint's work to stable at the end of the current sprint. At the same point, the current sprint's work would be pushed to staging, thus making staging a full sprint ahead of stable. While this is more straight forward, it means our users would have to wait longer to see changes that weren't deemed critical.
|
||||
**Option 2:** Releasing to stable/staging once a week
|
||||
Releasing once a week would follow the same flow but with code being released to staging one week before the same code is released to stable. This would make stable only one week behind staging and would allow us to roll out minor bug fixes and faster with greater speed. The negative side is that we have less time to see if errors occur on staging
|
||||
Releasing once a week would follow the same flow but with code being released to staging one week before the same code is released to stable. This would make stable only one week behind staging and would allow us to roll out minor bug fixes faster. The negative side is that we have less time to see if errors occur on staging.
|
||||
|
||||
In both of the above scenarios the release date would fall on the same day of the week that the sprint starts, which is currently a Wednesday. Additionally, in both scenarios the release commits would eventually be tagged with both a staging and stable tag. Furthermore, critical bugs or features would be exempt from these restrictions based on the product owner's discretion.
|
||||
In both of the above scenarios, the release date would fall on the same day of the week that the sprint starts which is currently a Wednesday. Additionally, in both scenarios the release commits would eventually be tagged with both a staging and stable tag. Furthermore, critical bugs or features would be exempt from these restrictions based on the product owner's discretion.
|
||||
|
||||
## Decision
|
||||
|
||||
|
@ -25,6 +25,6 @@ We decided to go with option 2 and release once a week once in production. This
|
|||
|
||||
## Consequences
|
||||
|
||||
Work not completed by end of the sprint will have to wait to be added to stable. Also, making quick fixes for bugs that are found on stable will be a little more complicated to fix.
|
||||
Work not completed by end of the sprint will have to wait to be added to stable. Also, making quick fixes for bugs that are found on stable will be a little more complicated.
|
||||
|
||||
When first going into production, staging and stable will start with the same code base. The following week a new release will be made to staging, but not stable as no code will have been on staging long enough to warrant another release. Thus just at the start of launch stable will be essentially frozen for 2 weeks, not one.
|
||||
|
|
|
@ -19,7 +19,7 @@ To do this, do the following:
|
|||
3. Click on their username, then scroll down to the `User Permissions` section.
|
||||
4. Under `User Permissions`, see the `Groups` table which has a column for `Available groups` and `Chosen groups`. Select the permission you want from the `Available groups` column and click the right arrow to move it to the `Chosen groups`. Note, if you want this user to be an analyst select `cisa_analysts_group`, otherwise select the `full_access_group`.
|
||||
5. (Optional) If the user needs access to django admin (such as an analyst), then you will also need to make sure "Staff Status" is checked. This can be found in the same `User Permissions` section right below the checkbox for `Active`.
|
||||
6. Click `Save` to apply all changes
|
||||
6. Click `Save` to apply all changes.
|
||||
|
||||
## Removing a user group permission via django-admin
|
||||
|
||||
|
@ -30,7 +30,7 @@ If an employee was given the wrong permissions or has had a change in roles that
|
|||
3. In this table, select the permission you want to remove from the `Chosen groups` and then click the left facing arrow to move the permission to `Available groups`.
|
||||
4. Depending on the scenario you may now need to add the opposite permission group to the `Chosen groups` section, please see the section above for instructions on how to do that.
|
||||
5. If the user should no longer see the admin page, you must ensure that under `User Permissions`, `Staff status` is NOT checked.
|
||||
6. Click `Save` to apply all changes
|
||||
6. Click `Save` to apply all changes.
|
||||
|
||||
## Editing group permissions through code
|
||||
|
||||
|
@ -40,4 +40,4 @@ We can edit and deploy new group permissions by:
|
|||
2. Duplicating migration `0036_create_groups_01`
|
||||
and running migrations (append the name with a version number
|
||||
to help django detect the migration eg 0037_create_groups_02)
|
||||
3. Making sure to update the dependency on the new migration with the previous migration
|
||||
3. Making sure to update the dependency on the new migration with the previous migration.
|
|
@ -55,7 +55,7 @@ In the case where a bug fix or feature needs to be added outside of the normal c
|
|||
|
||||
1. Code will need to be branched NOT off of main, but off of the same commit as the most recent stable commit. This should be the one tagged with the most recent vX.XX.XX value.
|
||||
2. After making the bug fix, the approved PR branch will not be merged yet, instead it will be tagged with a new release tag, incrementing the patch value from the last commit number.
|
||||
3. If main and stable are on the the same commit then merge this branch into the staging using the staging release tag (staging-<the hotfix release number>).
|
||||
3. If main and stable are on the the same commit then merge this branch into staging using the staging release tag (staging-<the hotfix release number>).
|
||||
4. If staging is already ahead stable, you may need to create another branch that is based off of the current staging commit, merge in your code change and then tag that branch with the staging release.
|
||||
5. Wait to merge your original branch until both deploys finish. Once they succeed then merge to main per the usual process.
|
||||
|
||||
|
|
|
@ -112,7 +112,7 @@ base64 -i client.key
|
|||
base64 -i client.crt
|
||||
```
|
||||
|
||||
You'll need to give the new certificate to the registry vendor _before_ rotating it in production. Once it has been accepted by the vender, make sure to update the kdbx file on Google Drive.
|
||||
You'll need to give the new certificate to the registry vendor _before_ rotating it in production. Once it has been accepted by the vendor, make sure to update the kdbx file on Google Drive.
|
||||
|
||||
## REGISTRY_HOSTNAME
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
1. Check the [Pipfile](../../../src/Pipfile) for pinned dependencies and manually adjust the version numbers
|
||||
|
||||
1. Run
|
||||
2. Run
|
||||
|
||||
cd src
|
||||
docker-compose run app bash -c "pipenv lock && pipenv requirements > requirements.txt"
|
||||
|
@ -14,6 +14,6 @@
|
|||
|
||||
The requirements.txt is used by Cloud.gov. It is needed to work around a bug in the CloudFoundry buildpack version of Pipenv that breaks on installing from a git repository.
|
||||
|
||||
1. (optional) Run `docker-compose stop` and `docker-compose build` to build a new image for local development with the updated dependencies.
|
||||
3. (optional) Run `docker-compose stop` and `docker-compose build` to build a new image for local development with the updated dependencies.
|
||||
|
||||
The reason for de-coupling the `build` and `lock` steps is to increase consistency between builds--a run of `build` will always get exactly the dependencies listed in `Pipfile.lock`, nothing more, nothing less.
|
32
ops/manifests/manifest-backup.yaml
Normal file
32
ops/manifests/manifest-backup.yaml
Normal file
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
applications:
|
||||
- name: getgov-backup
|
||||
buildpacks:
|
||||
- python_buildpack
|
||||
path: ../../src
|
||||
instances: 1
|
||||
memory: 512M
|
||||
stack: cflinuxfs4
|
||||
timeout: 180
|
||||
command: ./run.sh
|
||||
health-check-type: http
|
||||
health-check-http-endpoint: /health
|
||||
health-check-invocation-timeout: 40
|
||||
env:
|
||||
# Send stdout and stderr straight to the terminal without buffering
|
||||
PYTHONUNBUFFERED: yup
|
||||
# Tell Django where to find its configuration
|
||||
DJANGO_SETTINGS_MODULE: registrar.config.settings
|
||||
# Tell Django where it is being hosted
|
||||
DJANGO_BASE_URL: https://getgov-backup.app.cloud.gov
|
||||
# Tell Django how much stuff to log
|
||||
DJANGO_LOG_LEVEL: INFO
|
||||
# default public site location
|
||||
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
||||
# Flag to disable/enable features in prod environments
|
||||
IS_PRODUCTION: False
|
||||
routes:
|
||||
- route: getgov-backup.app.cloud.gov
|
||||
services:
|
||||
- getgov-credentials
|
||||
- getgov-backup-database
|
|
@ -2,6 +2,9 @@
|
|||
from django.apps import apps
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.http import JsonResponse
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from registrar.templatetags.url_helpers import public_site_url
|
||||
|
||||
import requests
|
||||
|
||||
|
@ -18,8 +21,13 @@ DOMAIN_API_MESSAGES = {
|
|||
" For example, if you want www.city.gov, you would enter “city”"
|
||||
" (without the quotes).",
|
||||
"extra_dots": "Enter the .gov domain you want without any periods.",
|
||||
"unavailable": "That domain isn’t available. Try entering another one."
|
||||
" Contact us if you need help coming up with a domain.",
|
||||
# message below is considered safe; no user input can be inserted into the message
|
||||
# body; public_site_url() function reads from local app settings and therefore safe
|
||||
"unavailable": mark_safe( # nosec
|
||||
"That domain isn’t available. "
|
||||
"<a class='usa-link' href='{}' target='_blank'>"
|
||||
"Read more about choosing your .gov domain.</a>".format(public_site_url("domains/choosing"))
|
||||
),
|
||||
"invalid": "Enter a domain using only letters, numbers, or hyphens (though we don't recommend using hyphens).",
|
||||
"success": "That domain is available!",
|
||||
"error": "Error finding domain availability.",
|
||||
|
|
|
@ -345,6 +345,12 @@ class UserDomainRoleAdmin(ListHeaderAdmin):
|
|||
class DomainInvitationAdmin(ListHeaderAdmin):
|
||||
"""Custom domain invitation admin class."""
|
||||
|
||||
class Meta:
|
||||
model = models.DomainInvitation
|
||||
fields = "__all__"
|
||||
|
||||
_meta = Meta()
|
||||
|
||||
# Columns
|
||||
list_display = [
|
||||
"email",
|
||||
|
@ -357,6 +363,10 @@ class DomainInvitationAdmin(ListHeaderAdmin):
|
|||
"email",
|
||||
"domain__name",
|
||||
]
|
||||
|
||||
# Filters
|
||||
list_filter = ("status",)
|
||||
|
||||
search_help_text = "Search by email or domain."
|
||||
|
||||
# Mark the FSM field 'status' as readonly
|
||||
|
|
|
@ -115,14 +115,14 @@ function inlineToast(el, id, style, msg) {
|
|||
toast.className = `usa-alert usa-alert--${style} usa-alert--slim`;
|
||||
toastBody.classList.add("usa-alert__body");
|
||||
p.classList.add("usa-alert__text");
|
||||
p.innerText = msg;
|
||||
p.innerHTML = msg;
|
||||
toastBody.appendChild(p);
|
||||
toast.appendChild(toastBody);
|
||||
el.parentNode.insertBefore(toast, el.nextSibling);
|
||||
} else {
|
||||
// update and show the existing message div
|
||||
toast.className = `usa-alert usa-alert--${style} usa-alert--slim`;
|
||||
toast.querySelector("div p").innerText = msg;
|
||||
toast.querySelector("div p").innerHTML = msg;
|
||||
makeVisible(toast);
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -627,6 +627,7 @@ ALLOWED_HOSTS = [
|
|||
"getgov-stable.app.cloud.gov",
|
||||
"getgov-staging.app.cloud.gov",
|
||||
"getgov-development.app.cloud.gov",
|
||||
"getgov-backup.app.cloud.gov",
|
||||
"getgov-ky.app.cloud.gov",
|
||||
"getgov-es.app.cloud.gov",
|
||||
"getgov-nl.app.cloud.gov",
|
||||
|
|
|
@ -67,6 +67,7 @@ class DomainNameserverForm(forms.Form):
|
|||
ip = cleaned_data.get("ip", None)
|
||||
# remove ANY spaces in the ip field
|
||||
ip = ip.replace(" ", "")
|
||||
cleaned_data["ip"] = ip
|
||||
domain = cleaned_data.get("domain", "")
|
||||
|
||||
ip_list = self.extract_ip_list(ip)
|
||||
|
@ -117,8 +118,34 @@ class DomainNameserverForm(forms.Form):
|
|||
self.add_error("ip", str(e))
|
||||
|
||||
|
||||
class BaseNameserverFormset(forms.BaseFormSet):
|
||||
def clean(self):
|
||||
"""
|
||||
Check for duplicate entries in the formset.
|
||||
"""
|
||||
if any(self.errors):
|
||||
# Don't bother validating the formset unless each form is valid on its own
|
||||
return
|
||||
|
||||
data = []
|
||||
duplicates = []
|
||||
|
||||
for form in self.forms:
|
||||
if form.cleaned_data:
|
||||
value = form.cleaned_data["server"]
|
||||
if value in data:
|
||||
form.add_error(
|
||||
"server",
|
||||
NameserverError(code=nsErrorCodes.DUPLICATE_HOST, nameserver=value),
|
||||
)
|
||||
duplicates.append(value)
|
||||
else:
|
||||
data.append(value)
|
||||
|
||||
|
||||
NameserverFormset = formset_factory(
|
||||
DomainNameserverForm,
|
||||
formset=BaseNameserverFormset,
|
||||
extra=1,
|
||||
max_num=13,
|
||||
validate_max=True,
|
||||
|
|
|
@ -212,11 +212,9 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
@Cache
|
||||
def registry_expiration_date(self) -> date:
|
||||
"""Get or set the `ex_date` element from the registry.
|
||||
Additionally, update the expiration date in the registrar"""
|
||||
Additionally, _get_property updates the expiration date in the registrar"""
|
||||
try:
|
||||
self.expiration_date = self._get_property("ex_date")
|
||||
self.save()
|
||||
return self.expiration_date
|
||||
return self._get_property("ex_date")
|
||||
except Exception as e:
|
||||
# exception raised during the save to registrar
|
||||
logger.error(f"error updating expiration date in registrar: {e}")
|
||||
|
@ -880,6 +878,14 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
"""
|
||||
return self.state == self.State.READY
|
||||
|
||||
def is_editable(self) -> bool:
|
||||
"""domain is editable unless state is on hold or deleted"""
|
||||
return self.state in [
|
||||
self.State.UNKNOWN,
|
||||
self.State.DNS_NEEDED,
|
||||
self.State.READY,
|
||||
]
|
||||
|
||||
def transfer(self):
|
||||
"""Going somewhere. Not implemented."""
|
||||
raise NotImplementedError()
|
||||
|
@ -1188,7 +1194,7 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
logger.error(e)
|
||||
logger.error(e.code)
|
||||
raise e
|
||||
if e.code == ErrorCode.OBJECT_DOES_NOT_EXIST:
|
||||
if e.code == ErrorCode.OBJECT_DOES_NOT_EXIST and self.state != Domain.State.DELETED:
|
||||
# avoid infinite loop
|
||||
already_tried_to_create = True
|
||||
self.dns_needed_from_unknown()
|
||||
|
@ -1602,6 +1608,12 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
if old_cache_contacts is not None:
|
||||
cleaned["contacts"] = old_cache_contacts
|
||||
|
||||
# if expiration date from registry does not match what is in db,
|
||||
# update the db
|
||||
if "ex_date" in cleaned and cleaned["ex_date"] != self.expiration_date:
|
||||
self.expiration_date = cleaned["ex_date"]
|
||||
self.save()
|
||||
|
||||
self._cache = cleaned
|
||||
|
||||
except RegistryError as e:
|
||||
|
|
|
@ -4,10 +4,10 @@
|
|||
{% block title %}Add another user{% endblock %}
|
||||
|
||||
{% block domain_content %}
|
||||
<h1>Add another user</h1>
|
||||
<h1>Add a domain manager</h1>
|
||||
|
||||
<p>You can add another user to help manage your domain. They will need to sign
|
||||
into the .gov registrar with their Login.gov account.
|
||||
in to the .gov registrar with their Login.gov account.
|
||||
</p>
|
||||
|
||||
<form class="usa-form usa-form--large" method="post" novalidate>
|
||||
|
|
|
@ -29,30 +29,34 @@
|
|||
|
||||
{% url 'domain-dns-nameservers' pk=domain.id as url %}
|
||||
{% if domain.nameservers|length > 0 %}
|
||||
{% include "includes/summary_item.html" with title='DNS name servers' domains='true' value=domain.nameservers list='true' edit_link=url %}
|
||||
{% include "includes/summary_item.html" with title='DNS name servers' domains='true' value=domain.nameservers list='true' edit_link=url editable=domain.is_editable %}
|
||||
{% else %}
|
||||
{% if domain.is_editable %}
|
||||
<h2 class="margin-top-neg-1"> DNS name servers </h2>
|
||||
<p> No DNS name servers have been added yet. Before your domain can be used we’ll need information about your domain name servers.</p>
|
||||
<a class="usa-button margin-bottom-1" href="{{url}}"> Add DNS name servers </a>
|
||||
{% else %}
|
||||
{% include "includes/summary_item.html" with title='DNS name servers' domains='true' value='' edit_link=url editable=domain.is_editable %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% url 'domain-org-name-address' pk=domain.id as url %}
|
||||
{% include "includes/summary_item.html" with title='Organization name and mailing address' value=domain.domain_info address='true' edit_link=url %}
|
||||
{% include "includes/summary_item.html" with title='Organization name and mailing address' value=domain.domain_info address='true' edit_link=url editable=domain.is_editable %}
|
||||
|
||||
{% url 'domain-authorizing-official' pk=domain.id as url %}
|
||||
{% include "includes/summary_item.html" with title='Authorizing official' value=domain.domain_info.authorizing_official contact='true' edit_link=url %}
|
||||
{% include "includes/summary_item.html" with title='Authorizing official' value=domain.domain_info.authorizing_official contact='true' edit_link=url editable=domain.is_editable %}
|
||||
|
||||
{% url 'domain-your-contact-information' pk=domain.id as url %}
|
||||
{% include "includes/summary_item.html" with title='Your contact information' value=request.user.contact contact='true' edit_link=url %}
|
||||
{% include "includes/summary_item.html" with title='Your contact information' value=request.user.contact contact='true' edit_link=url editable=domain.is_editable %}
|
||||
|
||||
{% url 'domain-security-email' pk=domain.id as url %}
|
||||
{% if security_email is not None and security_email != default_security_email%}
|
||||
{% include "includes/summary_item.html" with title='Security email' value=security_email edit_link=url %}
|
||||
{% include "includes/summary_item.html" with title='Security email' value=security_email edit_link=url editable=domain.is_editable %}
|
||||
{% else %}
|
||||
{% include "includes/summary_item.html" with title='Security email' value='None provided' edit_link=url %}
|
||||
{% include "includes/summary_item.html" with title='Security email' value='None provided' edit_link=url editable=domain.is_editable %}
|
||||
{% endif %}
|
||||
{% url 'domain-users' pk=domain.id as url %}
|
||||
{% include "includes/summary_item.html" with title='Domain managers' users='true' list=True value=domain.permissions.all edit_link=url %}
|
||||
{% include "includes/summary_item.html" with title='Domain managers' users='true' list=True value=domain.permissions.all edit_link=url editable=domain.is_editable %}
|
||||
|
||||
</div>
|
||||
{% endblock %} {# domain_content #}
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
</a>
|
||||
</li>
|
||||
|
||||
{% if domain.is_editable %}
|
||||
<li class="usa-sidenav__item">
|
||||
{% url 'domain-dns' pk=domain.id as url %}
|
||||
<a href="{{ url }}" {% if request.path|startswith:url %}class="usa-current"{% endif %}>
|
||||
|
@ -98,6 +99,7 @@
|
|||
Domain managers
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
|
|
@ -25,8 +25,8 @@
|
|||
{% if domain.permissions %}
|
||||
<section class="section--outlined">
|
||||
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table--stacked dotgov-table">
|
||||
<h2 class> Active users </h2>
|
||||
<caption class="sr-only">Domain users</caption>
|
||||
<h2 class> Domain managers </h2>
|
||||
<caption class="sr-only">Domain managers</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-sortable scope="col" role="columnheader">Email</th>
|
||||
|
@ -53,7 +53,7 @@
|
|||
<a class="usa-button usa-button--unstyled" href="{% url 'domain-users-add' pk=domain.id %}">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
|
||||
</svg><span class="margin-left-05">Add another user</span>
|
||||
</svg><span class="margin-left-05">Add a domain manager</span>
|
||||
</a>
|
||||
</section>
|
||||
|
||||
|
|
|
@ -85,7 +85,7 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if edit_link %}
|
||||
{% if editable and edit_link %}
|
||||
<div class="text-right">
|
||||
<a
|
||||
href="{{ edit_link }}"
|
||||
|
|
|
@ -617,6 +617,7 @@ class MockEppLib(TestCase):
|
|||
common.Status(state="serverTransferProhibited", description="", lang="en"),
|
||||
common.Status(state="inactive", description="", lang="en"),
|
||||
],
|
||||
ex_date=datetime.date(2023, 5, 25),
|
||||
)
|
||||
mockDataInfoContact = mockDataInfoDomain.dummyInfoContactResultData(
|
||||
"123", "123@mail.gov", datetime.datetime(2023, 5, 25, 19, 45, 35), "lastPw"
|
||||
|
@ -859,15 +860,9 @@ class MockEppLib(TestCase):
|
|||
case commands.UpdateDomain:
|
||||
return self.mockUpdateDomainCommands(_request, cleaned)
|
||||
case commands.CreateHost:
|
||||
return MagicMock(
|
||||
res_data=[self.mockDataHostChange],
|
||||
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
|
||||
)
|
||||
return self.mockCreateHostCommands(_request, cleaned)
|
||||
case commands.UpdateHost:
|
||||
return MagicMock(
|
||||
res_data=[self.mockDataHostChange],
|
||||
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
|
||||
)
|
||||
return self.mockUpdateHostCommands(_request, cleaned)
|
||||
case commands.DeleteHost:
|
||||
return MagicMock(
|
||||
res_data=[self.mockDataHostChange],
|
||||
|
@ -882,6 +877,28 @@ class MockEppLib(TestCase):
|
|||
case _:
|
||||
return MagicMock(res_data=[self.mockDataInfoHosts])
|
||||
|
||||
def mockCreateHostCommands(self, _request, cleaned):
|
||||
test_ws_ip = common.Ip(addr="1.1. 1.1")
|
||||
addrs_submitted = getattr(_request, "addrs", [])
|
||||
if test_ws_ip in addrs_submitted:
|
||||
raise RegistryError(code=ErrorCode.PARAMETER_VALUE_RANGE_ERROR)
|
||||
else:
|
||||
return MagicMock(
|
||||
res_data=[self.mockDataHostChange],
|
||||
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
|
||||
)
|
||||
|
||||
def mockUpdateHostCommands(self, _request, cleaned):
|
||||
test_ws_ip = common.Ip(addr="1.1. 1.1")
|
||||
addrs_submitted = getattr(_request, "addrs", [])
|
||||
if test_ws_ip in addrs_submitted:
|
||||
raise RegistryError(code=ErrorCode.PARAMETER_VALUE_RANGE_ERROR)
|
||||
else:
|
||||
return MagicMock(
|
||||
res_data=[self.mockDataHostChange],
|
||||
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
|
||||
)
|
||||
|
||||
def mockUpdateDomainCommands(self, _request, cleaned):
|
||||
if getattr(_request, "name", None) == "dnssec-invalid.gov":
|
||||
raise RegistryError(code=ErrorCode.PARAMETER_VALUE_RANGE_ERROR)
|
||||
|
|
|
@ -8,6 +8,7 @@ from registrar.admin import (
|
|||
DomainAdmin,
|
||||
DomainApplicationAdmin,
|
||||
DomainApplicationAdminForm,
|
||||
DomainInvitationAdmin,
|
||||
ListHeaderAdmin,
|
||||
MyUserAdmin,
|
||||
AuditedAdmin,
|
||||
|
@ -848,6 +849,44 @@ class TestDomainApplicationAdmin(MockEppLib):
|
|||
User.objects.all().delete()
|
||||
|
||||
|
||||
class DomainInvitationAdminTest(TestCase):
|
||||
"""Tests for the DomainInvitation page"""
|
||||
|
||||
def setUp(self):
|
||||
"""Create a client object"""
|
||||
self.client = Client(HTTP_HOST="localhost:8080")
|
||||
self.factory = RequestFactory()
|
||||
self.admin = ListHeaderAdmin(model=DomainInvitationAdmin, admin_site=AdminSite())
|
||||
self.superuser = create_superuser()
|
||||
|
||||
def tearDown(self):
|
||||
"""Delete all DomainInvitation objects"""
|
||||
DomainInvitation.objects.all().delete()
|
||||
|
||||
def test_get_filters(self):
|
||||
"""Ensures that our filters are displaying correctly"""
|
||||
# Have to get creative to get past linter
|
||||
p = "adminpass"
|
||||
self.client.login(username="superuser", password=p)
|
||||
|
||||
response = self.client.get(
|
||||
"/admin/registrar/domaininvitation/",
|
||||
{},
|
||||
follow=True,
|
||||
)
|
||||
|
||||
# Assert that the filters are added
|
||||
self.assertContains(response, "invited", count=4)
|
||||
self.assertContains(response, "retrieved", count=4)
|
||||
|
||||
# Check for the HTML context specificially
|
||||
invited_html = '<a href="?status__exact=invited">invited</a>'
|
||||
retrieved_html = '<a href="?status__exact=retrieved">retrieved</a>'
|
||||
|
||||
self.assertContains(response, invited_html, count=1)
|
||||
self.assertContains(response, retrieved_html, count=1)
|
||||
|
||||
|
||||
class UserDomainRoleAdminTest(TestCase):
|
||||
def setUp(self):
|
||||
self.site = AdminSite()
|
||||
|
|
|
@ -1987,6 +1987,13 @@ class TestExpirationDate(MockEppLib):
|
|||
with self.assertRaises(RegistryError):
|
||||
self.domain_w_error.renew_domain()
|
||||
|
||||
def test_expiration_date_updated_on_info_domain_call(self):
|
||||
"""assert that expiration date in db is updated on info domain call"""
|
||||
# force fetch_cache to be called
|
||||
self.domain.statuses
|
||||
test_date = datetime.date(2023, 5, 25)
|
||||
self.assertEquals(self.domain.expiration_date, test_date)
|
||||
|
||||
|
||||
class TestAnalystClientHold(MockEppLib):
|
||||
"""Rule: Analysts may suspend or restore a domain by using client hold"""
|
||||
|
|
|
@ -50,6 +50,7 @@ class ExportDataTest(TestCase):
|
|||
)
|
||||
|
||||
def tearDown(self):
|
||||
# Dummy push - will remove
|
||||
Domain.objects.all().delete()
|
||||
DomainInformation.objects.all().delete()
|
||||
User.objects.all().delete()
|
||||
|
|
|
@ -1082,6 +1082,8 @@ class TestWithDomainPermissions(TestWithUser):
|
|||
self.domain_with_ip, _ = Domain.objects.get_or_create(name="nameserverwithip.gov")
|
||||
self.domain_just_nameserver, _ = Domain.objects.get_or_create(name="justnameserver.com")
|
||||
self.domain_no_information, _ = Domain.objects.get_or_create(name="noinformation.gov")
|
||||
self.domain_on_hold, _ = Domain.objects.get_or_create(name="on-hold.gov", state=Domain.State.ON_HOLD)
|
||||
self.domain_deleted, _ = Domain.objects.get_or_create(name="deleted.gov", state=Domain.State.DELETED)
|
||||
|
||||
self.domain_dsdata, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov")
|
||||
self.domain_multdsdata, _ = Domain.objects.get_or_create(name="dnssec-multdsdata.gov")
|
||||
|
@ -1096,6 +1098,8 @@ class TestWithDomainPermissions(TestWithUser):
|
|||
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_dnssec_none)
|
||||
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_with_ip)
|
||||
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_just_nameserver)
|
||||
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_on_hold)
|
||||
DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_deleted)
|
||||
|
||||
self.role, _ = UserDomainRole.objects.get_or_create(
|
||||
user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER
|
||||
|
@ -1124,6 +1128,12 @@ class TestWithDomainPermissions(TestWithUser):
|
|||
domain=self.domain_just_nameserver,
|
||||
role=UserDomainRole.Roles.MANAGER,
|
||||
)
|
||||
UserDomainRole.objects.get_or_create(
|
||||
user=self.user, domain=self.domain_on_hold, role=UserDomainRole.Roles.MANAGER
|
||||
)
|
||||
UserDomainRole.objects.get_or_create(
|
||||
user=self.user, domain=self.domain_deleted, role=UserDomainRole.Roles.MANAGER
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
try:
|
||||
|
@ -1177,6 +1187,31 @@ class TestDomainPermissions(TestWithDomainPermissions):
|
|||
response = self.client.get(reverse(view_name, kwargs={"pk": self.domain.id}))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_domain_pages_blocked_for_on_hold_and_deleted(self):
|
||||
"""Test that the domain pages are blocked for on hold and deleted domains"""
|
||||
|
||||
self.client.force_login(self.user)
|
||||
for view_name in [
|
||||
"domain-users",
|
||||
"domain-users-add",
|
||||
"domain-dns",
|
||||
"domain-dns-nameservers",
|
||||
"domain-dns-dnssec",
|
||||
"domain-dns-dnssec-dsdata",
|
||||
"domain-org-name-address",
|
||||
"domain-authorizing-official",
|
||||
"domain-your-contact-information",
|
||||
"domain-security-email",
|
||||
]:
|
||||
for domain in [
|
||||
self.domain_on_hold,
|
||||
self.domain_deleted,
|
||||
]:
|
||||
with self.subTest(view_name=view_name, domain=domain):
|
||||
with less_console_noise():
|
||||
response = self.client.get(reverse(view_name, kwargs={"pk": domain.id}))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
|
||||
class TestDomainOverview(TestWithDomainPermissions, WebTest):
|
||||
def setUp(self):
|
||||
|
@ -1184,6 +1219,8 @@ class TestDomainOverview(TestWithDomainPermissions, WebTest):
|
|||
self.app.set_user(self.user.username)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
|
||||
class TestDomainDetail(TestDomainOverview):
|
||||
def test_domain_detail_link_works(self):
|
||||
home_page = self.app.get("/")
|
||||
self.assertContains(home_page, "igorville.gov")
|
||||
|
@ -1192,7 +1229,7 @@ class TestDomainOverview(TestWithDomainPermissions, WebTest):
|
|||
self.assertContains(detail_page, "igorville.gov")
|
||||
self.assertContains(detail_page, "Status")
|
||||
|
||||
def test_domain_overview_blocked_for_ineligible_user(self):
|
||||
def test_domain_detail_blocked_for_ineligible_user(self):
|
||||
"""We could easily duplicate this test for all domain management
|
||||
views, but a single url test should be solid enough since all domain
|
||||
management pages share the same permissions class"""
|
||||
|
@ -1204,7 +1241,16 @@ class TestDomainOverview(TestWithDomainPermissions, WebTest):
|
|||
response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id}))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_domain_see_just_nameserver(self):
|
||||
def test_domain_detail_allowed_for_on_hold(self):
|
||||
"""Test that the domain overview page displays for on hold domain"""
|
||||
home_page = self.app.get("/")
|
||||
self.assertContains(home_page, "on-hold.gov")
|
||||
|
||||
# View domain overview page
|
||||
detail_page = self.client.get(reverse("domain", kwargs={"pk": self.domain_on_hold.id}))
|
||||
self.assertNotContains(detail_page, "Edit")
|
||||
|
||||
def test_domain_detail_see_just_nameserver(self):
|
||||
home_page = self.app.get("/")
|
||||
self.assertContains(home_page, "justnameserver.com")
|
||||
|
||||
|
@ -1215,7 +1261,7 @@ class TestDomainOverview(TestWithDomainPermissions, WebTest):
|
|||
self.assertContains(detail_page, "ns1.justnameserver.com")
|
||||
self.assertContains(detail_page, "ns2.justnameserver.com")
|
||||
|
||||
def test_domain_see_nameserver_and_ip(self):
|
||||
def test_domain_detail_see_nameserver_and_ip(self):
|
||||
home_page = self.app.get("/")
|
||||
self.assertContains(home_page, "nameserverwithip.gov")
|
||||
|
||||
|
@ -1231,7 +1277,7 @@ class TestDomainOverview(TestWithDomainPermissions, WebTest):
|
|||
self.assertContains(detail_page, "(1.2.3.4,")
|
||||
self.assertContains(detail_page, "2.3.4.5)")
|
||||
|
||||
def test_domain_with_no_information_or_application(self):
|
||||
def test_domain_detail_with_no_information_or_application(self):
|
||||
"""Test that domain management page returns 200 and displays error
|
||||
when no domain information or domain application exist"""
|
||||
# have to use staff user for this test
|
||||
|
@ -1261,12 +1307,12 @@ class TestDomainManagers(TestDomainOverview):
|
|||
def test_domain_managers_add_link(self):
|
||||
"""Button to get to user add page works."""
|
||||
management_page = self.app.get(reverse("domain-users", kwargs={"pk": self.domain.id}))
|
||||
add_page = management_page.click("Add another user")
|
||||
self.assertContains(add_page, "Add another user")
|
||||
add_page = management_page.click("Add a domain manager")
|
||||
self.assertContains(add_page, "Add a domain manager")
|
||||
|
||||
def test_domain_user_add(self):
|
||||
response = self.client.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
|
||||
self.assertContains(response, "Add another user")
|
||||
self.assertContains(response, "Add a domain manager")
|
||||
|
||||
def test_domain_user_add_form(self):
|
||||
"""Adding an existing user works."""
|
||||
|
@ -1462,6 +1508,62 @@ class TestDomainNameservers(TestDomainOverview):
|
|||
status_code=200,
|
||||
)
|
||||
|
||||
def test_domain_nameservers_form_submit_duplicate_host(self):
|
||||
"""Nameserver form catches error when host is duplicated.
|
||||
|
||||
Uses self.app WebTest because we need to interact with forms.
|
||||
"""
|
||||
# initial nameservers page has one server with two ips
|
||||
nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
# attempt to submit the form with duplicate host names of fake.host.com
|
||||
nameservers_page.form["form-0-ip"] = ""
|
||||
nameservers_page.form["form-1-server"] = "fake.host.com"
|
||||
with less_console_noise(): # swallow log warning message
|
||||
result = nameservers_page.form.submit()
|
||||
# form submission was a post with an error, response should be a 200
|
||||
# error text appears twice, once at the top of the page, once around
|
||||
# the required field. remove duplicate entry
|
||||
self.assertContains(
|
||||
result,
|
||||
str(NameserverError(code=NameserverErrorCodes.DUPLICATE_HOST)),
|
||||
count=2,
|
||||
status_code=200,
|
||||
)
|
||||
|
||||
def test_domain_nameservers_form_submit_whitespace(self):
|
||||
"""Nameserver form removes whitespace from ip.
|
||||
|
||||
Uses self.app WebTest because we need to interact with forms.
|
||||
"""
|
||||
nameserver1 = "ns1.igorville.gov"
|
||||
nameserver2 = "ns2.igorville.gov"
|
||||
valid_ip = "1.1. 1.1"
|
||||
# initial nameservers page has one server with two ips
|
||||
# have to throw an error in order to test that the whitespace has been stripped from ip
|
||||
nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
# attempt to submit the form without one host and an ip with whitespace
|
||||
nameservers_page.form["form-0-server"] = nameserver1
|
||||
nameservers_page.form["form-1-ip"] = valid_ip
|
||||
nameservers_page.form["form-1-server"] = nameserver2
|
||||
with less_console_noise(): # swallow log warning message
|
||||
result = nameservers_page.form.submit()
|
||||
# form submission was a post with an ip address which has been stripped of whitespace,
|
||||
# response should be a 302 to success page
|
||||
self.assertEqual(result.status_code, 302)
|
||||
self.assertEqual(
|
||||
result["Location"],
|
||||
reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}),
|
||||
)
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
page = result.follow()
|
||||
# in the event of a generic nameserver error from registry error, there will be a 302
|
||||
# with an error message displayed, so need to follow 302 and test for success message
|
||||
self.assertContains(page, "The name servers for this domain have been updated")
|
||||
|
||||
def test_domain_nameservers_form_submit_glue_record_not_allowed(self):
|
||||
"""Nameserver form catches error when IP is present
|
||||
but host not subdomain.
|
||||
|
@ -1553,7 +1655,7 @@ class TestDomainNameservers(TestDomainOverview):
|
|||
"""
|
||||
nameserver1 = "ns1.igorville.gov"
|
||||
nameserver2 = "ns2.igorville.gov"
|
||||
invalid_ip = "127.0.0.1"
|
||||
valid_ip = "127.0.0.1"
|
||||
# initial nameservers page has one server with two ips
|
||||
nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
|
@ -1562,7 +1664,7 @@ class TestDomainNameservers(TestDomainOverview):
|
|||
# only one has ips
|
||||
nameservers_page.form["form-0-server"] = nameserver1
|
||||
nameservers_page.form["form-1-server"] = nameserver2
|
||||
nameservers_page.form["form-1-ip"] = invalid_ip
|
||||
nameservers_page.form["form-1-ip"] = valid_ip
|
||||
with less_console_noise(): # swallow log warning message
|
||||
result = nameservers_page.form.submit()
|
||||
# form submission was a successful post, response should be a 302
|
||||
|
|
|
@ -68,7 +68,8 @@ class NameserverErrorCodes(IntEnum):
|
|||
- 4 TOO_MANY_HOSTS more than the max allowed host values
|
||||
- 5 MISSING_HOST host is missing for a nameserver
|
||||
- 6 INVALID_HOST host is invalid for a nameserver
|
||||
- 7 BAD_DATA bad data input for nameserver
|
||||
- 7 DUPLICATE_HOST host is a duplicate
|
||||
- 8 BAD_DATA bad data input for nameserver
|
||||
"""
|
||||
|
||||
MISSING_IP = 1
|
||||
|
@ -77,7 +78,8 @@ class NameserverErrorCodes(IntEnum):
|
|||
TOO_MANY_HOSTS = 4
|
||||
MISSING_HOST = 5
|
||||
INVALID_HOST = 6
|
||||
BAD_DATA = 7
|
||||
DUPLICATE_HOST = 7
|
||||
BAD_DATA = 8
|
||||
|
||||
|
||||
class NameserverError(Exception):
|
||||
|
@ -93,6 +95,7 @@ class NameserverError(Exception):
|
|||
NameserverErrorCodes.TOO_MANY_HOSTS: ("Too many hosts provided, you may not have more than 13 nameservers."),
|
||||
NameserverErrorCodes.MISSING_HOST: ("Name server must be provided to enter IP address."),
|
||||
NameserverErrorCodes.INVALID_HOST: ("Enter a name server in the required format, like ns1.example.com"),
|
||||
NameserverErrorCodes.DUPLICATE_HOST: ("Remove duplicate entry"),
|
||||
NameserverErrorCodes.BAD_DATA: (
|
||||
"There’s something wrong with the name server information you provided. "
|
||||
"If you need help email us at help@get.gov."
|
||||
|
|
|
@ -152,6 +152,28 @@ class DomainView(DomainBaseView):
|
|||
context["security_email"] = security_email
|
||||
return context
|
||||
|
||||
def in_editable_state(self, pk):
|
||||
"""Override in_editable_state from DomainPermission
|
||||
Allow detail page to be viewable"""
|
||||
|
||||
requested_domain = None
|
||||
if Domain.objects.filter(id=pk).exists():
|
||||
requested_domain = Domain.objects.get(id=pk)
|
||||
|
||||
# return true if the domain exists, this will allow the detail page to load
|
||||
if requested_domain:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _get_domain(self, request):
|
||||
"""
|
||||
override get_domain for this view so that domain overview
|
||||
always resets the cache for the domain object
|
||||
"""
|
||||
self.session = request.session
|
||||
self.object = self.get_object()
|
||||
self._update_session_with_domain()
|
||||
|
||||
|
||||
class DomainOrgNameAddressView(DomainFormBaseView):
|
||||
"""Organization name and mailing address view"""
|
||||
|
@ -175,7 +197,7 @@ class DomainOrgNameAddressView(DomainFormBaseView):
|
|||
"""The form is valid, save the organization name and mailing address."""
|
||||
form.save()
|
||||
|
||||
messages.success(self.request, "The organization name and mailing address has been updated.")
|
||||
messages.success(self.request, "The organization information has been updated.")
|
||||
|
||||
# superclass has the redirect
|
||||
return super().form_valid(form)
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
|
||||
from registrar.models import (
|
||||
Domain,
|
||||
DomainApplication,
|
||||
DomainInvitation,
|
||||
DomainInformation,
|
||||
|
@ -45,6 +46,10 @@ class DomainPermission(PermissionsLoginMixin):
|
|||
if pk is None:
|
||||
raise ValueError("Primary key is None")
|
||||
|
||||
# test if domain in editable state
|
||||
if not self.in_editable_state(pk):
|
||||
return False
|
||||
|
||||
if self.can_access_other_user_domains(pk):
|
||||
return True
|
||||
|
||||
|
@ -55,6 +60,18 @@ class DomainPermission(PermissionsLoginMixin):
|
|||
# if we need to check more about the nature of role, do it here.
|
||||
return True
|
||||
|
||||
def in_editable_state(self, pk):
|
||||
"""Is the domain in an editable state"""
|
||||
|
||||
requested_domain = None
|
||||
if Domain.objects.filter(id=pk).exists():
|
||||
requested_domain = Domain.objects.get(id=pk)
|
||||
|
||||
# if domain is editable return true
|
||||
if requested_domain and requested_domain.is_editable():
|
||||
return True
|
||||
return False
|
||||
|
||||
def can_access_other_user_domains(self, pk):
|
||||
"""Checks to see if an authorized user (staff or superuser)
|
||||
can access a domain that they did not create or was invited to.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue