Merge branch 'main' into nmb/612-public-site

This commit is contained in:
Neil Martinsen-Burrell 2023-06-01 13:18:44 -05:00
commit 166491673e
No known key found for this signature in database
GPG key ID: 6A3C818CC10D0184
372 changed files with 2035 additions and 19569 deletions

View file

@ -37,3 +37,4 @@ django-webtest = "*"
types-cachetools = "*"
boto3-mocking = "*"
boto3-stubs = "*"
django-model2puml = "*"

171
src/Pipfile.lock generated
View file

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "ebec8b958bcfde525ad74aa1e777b55855e86b2d63264612bc2855bf167070b1"
"sha256": "b6c1a957da6c715c734906059a81da21cb0eb4c4ab04f204eb58a48ddb8f7234"
},
"pipfile-spec": 6,
"requires": {},
@ -24,19 +24,19 @@
},
"boto3": {
"hashes": [
"sha256:38ca632be379963f2a2749b5f63a81fe1679913b954914f470ad282c77674bbc",
"sha256:4d575c180312bec6108852bae12e6396b9d1bb404154d652c57ee849c62fbb83"
"sha256:62285ecee7629a4388d55ae369536f759622d68d5b9a0ced7c58a0c1a409c0f7",
"sha256:8ff0af0b25266a01616396abc19eb34dc3d44bd867fa4158985924128b9034fb"
],
"index": "pypi",
"version": "==1.26.122"
"version": "==1.26.133"
},
"botocore": {
"hashes": [
"sha256:9e4984a9e9777c6b949aa1e98323fa35480d9f99d447af7e179ae611f7ed5af9",
"sha256:c3b41078d235761b9c5dc22f534a76952622ef96787b96bbd10242ec4d73f2a5"
"sha256:7b38e540f73c921d8cb0ac72794072000af9e10758c04ba7f53d5629cc52fa87",
"sha256:b266185d7414a559952569005009a400de50af91fd3da44f05cf05b00946c4a7"
],
"markers": "python_version >= '3.7'",
"version": "==1.29.122"
"version": "==1.29.133"
},
"cachetools": {
"hashes": [
@ -48,11 +48,11 @@
},
"certifi": {
"hashes": [
"sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3",
"sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"
"sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7",
"sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"
],
"markers": "python_version >= '3.6'",
"version": "==2022.12.7"
"version": "==2023.5.7"
},
"cfenv": {
"hashes": [
@ -261,11 +261,11 @@
},
"django": {
"hashes": [
"sha256:ad33ed68db9398f5dfb33282704925bce044bef4261cd4fb59e4e7f9ae505a78",
"sha256:c36e2ab12824e2ac36afa8b2515a70c53c7742f0d6eaefa7311ec379558db997"
"sha256:066b6debb5ac335458d2a713ed995570536c8b59a580005acb0732378d5eb1ee",
"sha256:7efa6b1f781a6119a10ac94b4794ded90db8accbe7802281cd26f8664ffed59c"
],
"index": "pypi",
"version": "==4.2"
"version": "==4.2.1"
},
"django-allow-cidr": {
"hashes": [
@ -338,11 +338,11 @@
},
"faker": {
"hashes": [
"sha256:49060d40e6659e116f53353c5771ad2f2cbcd12b15771f49e3000a3a451f13ec",
"sha256:ac903ba8cb5adbce2cdd15e5536118d484bbe01126f3c774dd9f6df77b61232d"
"sha256:38dbc3b80e655d7301e190426ab30f04b6b7f6ca4764c5dd02772ffde0fa6dcd",
"sha256:f02c6d3fdb5bc781f80b440cf2bdec336ed47ecfb8d620b20c3d4188ed051831"
],
"index": "pypi",
"version": "==18.6.0"
"version": "==18.7.0"
},
"furl": {
"hashes": [
@ -623,19 +623,19 @@
},
"requests": {
"hashes": [
"sha256:e8f3c9be120d3333921d213eef078af392fba3933ab7ed2d1cba3b56f2568c3b",
"sha256:f2e34a75f4749019bb0e3effb66683630e4ffeaf75819fb51bebef1bf5aef059"
"sha256:10e94cc4f3121ee6da529d358cdaeaff2f1c409cd377dbc72b825852f2f7e294",
"sha256:239d7d4458afcb28a692cdd298d87542235f4ca8d36d03a15bfc128a6559a2f4"
],
"index": "pypi",
"version": "==2.29.0"
"version": "==2.30.0"
},
"s3transfer": {
"hashes": [
"sha256:06176b74f3a15f61f1b4f25a1fc29a4429040b7647133a463da8fa5bd28d5ecd",
"sha256:2ed07d3866f523cc561bf4a00fc5535827981b117dd7876f036b0c1aca42c947"
"sha256:3c0da2d074bf35d6870ef157158641178a4204a6e689e82546083e31e0311346",
"sha256:640bb492711f4c0c0905e1f62b6aaeb771881935ad27884852411f8e9cacbca9"
],
"markers": "python_version >= '3.7'",
"version": "==0.6.0"
"version": "==0.6.1"
},
"setuptools": {
"hashes": [
@ -752,11 +752,11 @@
},
"boto3": {
"hashes": [
"sha256:38ca632be379963f2a2749b5f63a81fe1679913b954914f470ad282c77674bbc",
"sha256:4d575c180312bec6108852bae12e6396b9d1bb404154d652c57ee849c62fbb83"
"sha256:62285ecee7629a4388d55ae369536f759622d68d5b9a0ced7c58a0c1a409c0f7",
"sha256:8ff0af0b25266a01616396abc19eb34dc3d44bd867fa4158985924128b9034fb"
],
"index": "pypi",
"version": "==1.26.122"
"version": "==1.26.133"
},
"boto3-mocking": {
"hashes": [
@ -768,27 +768,27 @@
},
"boto3-stubs": {
"hashes": [
"sha256:401e7fe51d88a51b527d883d195ed20c7f57aeb2c0aea24bbb3e911b6d2ad3aa",
"sha256:743a37bfd7d1eed4d67cdf825283abc1d93b7900b81d7426aab7e691e075c897"
"sha256:a921814574761842073822dc5e9fc7ca4f1c5fdeaa53d83cd8831e060dae09c8",
"sha256:cc6a662700e755c1e3dec2383c146b89cd8c70b5921033504bfb8367d03a538f"
],
"index": "pypi",
"version": "==1.26.122"
"version": "==1.26.133"
},
"botocore": {
"hashes": [
"sha256:9e4984a9e9777c6b949aa1e98323fa35480d9f99d447af7e179ae611f7ed5af9",
"sha256:c3b41078d235761b9c5dc22f534a76952622ef96787b96bbd10242ec4d73f2a5"
"sha256:7b38e540f73c921d8cb0ac72794072000af9e10758c04ba7f53d5629cc52fa87",
"sha256:b266185d7414a559952569005009a400de50af91fd3da44f05cf05b00946c4a7"
],
"markers": "python_version >= '3.7'",
"version": "==1.29.122"
"version": "==1.29.133"
},
"botocore-stubs": {
"hashes": [
"sha256:59873a3b535ec3ff0b6bf5f41c9f8a0f8c48032a871bea4d6e4faebbbfc68e8b",
"sha256:e6e6c527a6cac0ec69dd1b755d530c9b2dab01d423ce47bdc636dd01ebb01b1b"
"sha256:5f6f1967d23c45834858a055cbf65b66863f9f28d05f32f57bf52864a13512d9",
"sha256:622c4a5cd740498439008d81c5ded612146f4f0d575341c12591f978edbbe733"
],
"markers": "python_version >= '3.7' and python_version < '4.0'",
"version": "==1.29.122"
"version": "==1.29.130"
},
"click": {
"hashes": [
@ -800,11 +800,11 @@
},
"django": {
"hashes": [
"sha256:ad33ed68db9398f5dfb33282704925bce044bef4261cd4fb59e4e7f9ae505a78",
"sha256:c36e2ab12824e2ac36afa8b2515a70c53c7742f0d6eaefa7311ec379558db997"
"sha256:066b6debb5ac335458d2a713ed995570536c8b59a580005acb0732378d5eb1ee",
"sha256:7efa6b1f781a6119a10ac94b4794ded90db8accbe7802281cd26f8664ffed59c"
],
"index": "pypi",
"version": "==4.2"
"version": "==4.2.1"
},
"django-debug-toolbar": {
"hashes": [
@ -814,6 +814,13 @@
"index": "pypi",
"version": "==4.0.0"
},
"django-model2puml": {
"hashes": [
"sha256:6e773d742e556020a04d3216ce5dee5d3551da162e2d42a997f85b4ed1854771"
],
"index": "pypi",
"version": "==0.4.1"
},
"django-stubs": {
"hashes": [
"sha256:93baff824f0a056e71036b423b942a74f07b909e45e3fa38185b910f597c5c08",
@ -896,35 +903,35 @@
},
"mypy": {
"hashes": [
"sha256:023fe9e618182ca6317ae89833ba422c411469156b690fde6a315ad10695a521",
"sha256:031fc69c9a7e12bcc5660b74122ed84b3f1c505e762cc4296884096c6d8ee140",
"sha256:2de7babe398cb7a85ac7f1fd5c42f396c215ab3eff731b4d761d68d0f6a80f48",
"sha256:2e93a8a553e0394b26c4ca683923b85a69f7ccdc0139e6acd1354cc884fe0128",
"sha256:390bc685ec209ada4e9d35068ac6988c60160b2b703072d2850457b62499e336",
"sha256:3a2d219775a120581a0ae8ca392b31f238d452729adbcb6892fa89688cb8306a",
"sha256:3efde4af6f2d3ccf58ae825495dbb8d74abd6d176ee686ce2ab19bd025273f41",
"sha256:4a99fe1768925e4a139aace8f3fb66db3576ee1c30b9c0f70f744ead7e329c9f",
"sha256:4b41412df69ec06ab141808d12e0bf2823717b1c363bd77b4c0820feaa37249e",
"sha256:4c8d8c6b80aa4a1689f2a179d31d86ae1367ea4a12855cc13aa3ba24bb36b2d8",
"sha256:4d19f1a239d59f10fdc31263d48b7937c585810288376671eaf75380b074f238",
"sha256:4e4a682b3f2489d218751981639cffc4e281d548f9d517addfd5a2917ac78119",
"sha256:695c45cea7e8abb6f088a34a6034b1d273122e5530aeebb9c09626cea6dca4cb",
"sha256:701189408b460a2ff42b984e6bd45c3f41f0ac9f5f58b8873bbedc511900086d",
"sha256:70894c5345bea98321a2fe84df35f43ee7bb0feec117a71420c60459fc3e1eed",
"sha256:8293a216e902ac12779eb7a08f2bc39ec6c878d7c6025aa59464e0c4c16f7eb9",
"sha256:8d26b513225ffd3eacece727f4387bdce6469192ef029ca9dd469940158bc89e",
"sha256:a197ad3a774f8e74f21e428f0de7f60ad26a8d23437b69638aac2764d1e06a6a",
"sha256:bea55fc25b96c53affab852ad94bf111a3083bc1d8b0c76a61dd101d8a388cf5",
"sha256:c9a084bce1061e55cdc0493a2ad890375af359c766b8ac311ac8120d3a472950",
"sha256:d0e9464a0af6715852267bf29c9553e4555b61f5904a4fc538547a4d67617937",
"sha256:d8e9187bfcd5ffedbe87403195e1fc340189a68463903c39e2b63307c9fa0394",
"sha256:eaeaa0888b7f3ccb7bcd40b50497ca30923dba14f385bde4af78fac713d6d6f6",
"sha256:f46af8d162f3d470d8ffc997aaf7a269996d205f9d746124a179d3abe05ac602",
"sha256:f70a40410d774ae23fcb4afbbeca652905a04de7948eaf0b1789c8d1426b72d1",
"sha256:fe91be1c51c90e2afe6827601ca14353bbf3953f343c2129fa1e247d55fd95ba"
"sha256:1c4c42c60a8103ead4c1c060ac3cdd3ff01e18fddce6f1016e08939647a0e703",
"sha256:44797d031a41516fcf5cbfa652265bb994e53e51994c1bd649ffcd0c3a7eccbf",
"sha256:473117e310febe632ddf10e745a355714e771ffe534f06db40702775056614c4",
"sha256:4c99c3ecf223cf2952638da9cd82793d8f3c0c5fa8b6ae2b2d9ed1e1ff51ba85",
"sha256:550a8b3a19bb6589679a7c3c31f64312e7ff482a816c96e0cecec9ad3a7564dd",
"sha256:658fe7b674769a0770d4b26cb4d6f005e88a442fe82446f020be8e5f5efb2fae",
"sha256:6e33bb8b2613614a33dff70565f4c803f889ebd2f859466e42b46e1df76018dd",
"sha256:6e42d29e324cdda61daaec2336c42512e59c7c375340bd202efa1fe0f7b8f8ca",
"sha256:74bc9b6e0e79808bf8678d7678b2ae3736ea72d56eede3820bd3849823e7f305",
"sha256:76ec771e2342f1b558c36d49900dfe81d140361dd0d2df6cd71b3db1be155409",
"sha256:7d23370d2a6b7a71dc65d1266f9a34e4cde9e8e21511322415db4b26f46f6b8c",
"sha256:87df44954c31d86df96c8bd6e80dfcd773473e877ac6176a8e29898bfb3501cb",
"sha256:8c5979d0deb27e0f4479bee18ea0f83732a893e81b78e62e2dda3e7e518c92ee",
"sha256:95d8d31a7713510685b05fbb18d6ac287a56c8f6554d88c19e73f724a445448a",
"sha256:a22435632710a4fcf8acf86cbd0d69f68ac389a3892cb23fbad176d1cddaf228",
"sha256:a8763e72d5d9574d45ce5881962bc8e9046bf7b375b0abf031f3e6811732a897",
"sha256:c1eb485cea53f4f5284e5baf92902cd0088b24984f4209e25981cc359d64448d",
"sha256:c5d2cc54175bab47011b09688b418db71403aefad07cbcd62d44010543fc143f",
"sha256:cbc07246253b9e3d7d74c9ff948cd0fd7a71afcc2b77c7f0a59c26e9395cb152",
"sha256:d0b6c62206e04061e27009481cb0ec966f7d6172b5b936f3ead3d74f29fe3dcf",
"sha256:ddae0f39ca146972ff6bb4399f3b2943884a774b8771ea0a8f50e971f5ea5ba8",
"sha256:e1f4d16e296f5135624b34e8fb741eb0eadedca90862405b1f1fde2040b9bd11",
"sha256:e86c2c6852f62f8f2b24cb7a613ebe8e0c7dc1402c61d36a609174f63e0ff017",
"sha256:ebc95f8386314272bbc817026f8ce8f4f0d2ef7ae44f947c4664efac9adec929",
"sha256:f9dca1e257d4cc129517779226753dbefb4f2266c4eaad610fc15c6a7e14283e",
"sha256:faff86aa10c1aa4a10e1a301de160f3d8fc8703b88c7e98de46b531ff1276a9a"
],
"index": "pypi",
"version": "==1.2.0"
"version": "==1.3.0"
},
"mypy-extensions": {
"hashes": [
@ -968,11 +975,11 @@
},
"platformdirs": {
"hashes": [
"sha256:47692bc24c1958e8b0f13dd727307cff1db103fca36399f457da8e05f222fdc4",
"sha256:7954a68d0ba23558d753f73437c55f89027cf8f5108c19844d4b82e5af396335"
"sha256:412dae91f52a6f84830f39a8078cecd0e866cb72294a5c66808e74d5e88d251f",
"sha256:e2378146f1964972c03c085bb5662ae80b2b8c06226c54b2ff4aa9483e8a13a5"
],
"markers": "python_version >= '3.7'",
"version": "==3.5.0"
"version": "==3.5.1"
},
"pycodestyle": {
"hashes": [
@ -1062,11 +1069,11 @@
},
"s3transfer": {
"hashes": [
"sha256:06176b74f3a15f61f1b4f25a1fc29a4429040b7647133a463da8fa5bd28d5ecd",
"sha256:2ed07d3866f523cc561bf4a00fc5535827981b117dd7876f036b0c1aca42c947"
"sha256:3c0da2d074bf35d6870ef157158641178a4204a6e689e82546083e31e0311346",
"sha256:640bb492711f4c0c0905e1f62b6aaeb771881935ad27884852411f8e9cacbca9"
],
"markers": "python_version >= '3.7'",
"version": "==0.6.0"
"version": "==0.6.1"
},
"six": {
"hashes": [
@ -1118,11 +1125,11 @@
},
"types-awscrt": {
"hashes": [
"sha256:40854d9d7ce055620d5d41e5adc84df11b879aedbd2cf20de84e73f084aa5797",
"sha256:fe38c6fd71199a9f739b69a7c2f3a574585457c4f63730a62830628a7bffc5b0"
"sha256:9e447df3ad46767887d14fa9c856df94f80e8a0a7f0169577ab23b52ee37bcdf",
"sha256:e28fb3f20568ce9e96e33e01e0b87b891822f36b8f368adb582553b016d4aa08"
],
"markers": "python_version >= '3.7' and python_version < '4.0'",
"version": "==0.16.16"
"version": "==0.16.17"
},
"types-cachetools": {
"hashes": [
@ -1148,26 +1155,26 @@
},
"types-requests": {
"hashes": [
"sha256:0d580652ce903f643f8c3b494dd01d29367ea57cea0c7ad7f65cf3169092edb0",
"sha256:cc1aba862575019306b2ed134eb1ea994cab1c887a22e18d3383e6dd42e9789b"
"sha256:c6cf08e120ca9f0dc4fa4e32c3f953c3fba222bcc1db6b97695bce8da1ba9864",
"sha256:dec781054324a70ba64430ae9e62e7e9c8e4618c185a5cb3f87a6738251b5a31"
],
"index": "pypi",
"version": "==2.28.11.17"
"version": "==2.30.0.0"
},
"types-s3transfer": {
"hashes": [
"sha256:40e665643f0647832d51c4a26d8a8275cda9134b02bf22caf28198b79bcad382",
"sha256:d9c669b30fdd61347720434aacb8ecc4645d900712a70b10f495104f9039c07b"
"sha256:6d1ac1dedac750d570428362acdf60fdd4f277b0788855c3894d3226756b2bfb",
"sha256:75ac1d7143d58c1e6af467cfd4a96c67ee058a3adf7c249d9309999e1f5f41e4"
],
"markers": "python_version >= '3.7' and python_version < '4.0'",
"version": "==0.6.0.post7"
"version": "==0.6.1"
},
"types-urllib3": {
"hashes": [
"sha256:04235e792139cf3624b25d38faab593456738fbdb7439634046172e3b1339400",
"sha256:697102ddf4f781eed6f692353f40cee1098643526f5a8b99f49d2ede90fd3754"
"sha256:3300538c9dc11dad32eae4827ac313f5d986b8b21494801f1bf97a1ac6c03ae5",
"sha256:5dbd1d2bef14efee43f5318b5d36d805a489f6600252bb53626d4bfafd95e27c"
],
"version": "==1.26.25.11"
"version": "==1.26.25.13"
},
"typing-extensions": {
"hashes": [

View file

@ -37,7 +37,7 @@ def _domains():
Fetch a file from DOMAIN_FILE_URL, parse the CSV for the domain,
lowercase everything and return the list.
"""
Domain = apps.get_model("registrar.Domain")
DraftDomain = apps.get_model("registrar.DraftDomain")
# 5 second timeout
file_contents = requests.get(DOMAIN_FILE_URL, timeout=5).text
domains = set()
@ -46,7 +46,7 @@ def _domains():
# get the domain before the first comma
domain = line.split(",", 1)[0]
# sanity-check the string we got from the file here
if Domain.string_could_be_domain(domain):
if DraftDomain.string_could_be_domain(domain):
# lowercase everything when we put it in domains
domains.add(domain.lower())
return domains
@ -75,12 +75,12 @@ def available(request, domain=""):
Response is a JSON dictionary with the key "available" and value true or
false.
"""
Domain = apps.get_model("registrar.Domain")
DraftDomain = apps.get_model("registrar.DraftDomain")
# validate that the given domain could be a domain name and fail early if
# not.
if not (
Domain.string_could_be_domain(domain)
or Domain.string_could_be_domain(domain + ".gov")
DraftDomain.string_could_be_domain(domain)
or DraftDomain.string_could_be_domain(domain + ".gov")
):
return JsonResponse(
{"available": False, "message": DOMAIN_API_MESSAGES["invalid"]}

View file

@ -43,10 +43,15 @@ except NameError:
# Attn: these imports should NOT be at the top of the file
try:
from .client import CLIENT, commands
from .errors import RegistryError, ErrorCode
from epplib.models import common
except ImportError:
pass
__all__ = [
"CLIENT",
"commands",
"common",
"ErrorCode",
"RegistryError",
]

View file

@ -67,54 +67,47 @@ class EPPLibWrapper:
def _send(self, command):
"""Helper function used by `send`."""
try:
cmd_type = command.__class__.__name__
with self._connect as wire:
response = wire.send(command)
except (ValueError, ParsingError) as err:
logger.warning(
"%s failed to execute due to some syntax error."
% command.__class__.__name__,
exc_info=True,
)
raise RegistryError() from err
message = "%s failed to execute due to some syntax error."
logger.warning(message, cmd_type, exc_info=True)
raise RegistryError(message) from err
except TransportError as err:
logger.warning(
"%s failed to execute due to a connection error."
% command.__class__.__name__,
exc_info=True,
)
raise RegistryError() from err
message = "%s failed to execute due to a connection error."
logger.warning(message, cmd_type, exc_info=True)
raise RegistryError(message) from err
except LoginError as err:
logger.warning(
"%s failed to execute due to a registry login error."
% command.__class__.__name__,
exc_info=True,
)
raise RegistryError() from err
message = "%s failed to execute due to a registry login error."
logger.warning(message, cmd_type, exc_info=True)
raise RegistryError(message) from err
except Exception as err:
logger.warning(
"%s failed to execute due to an unknown error."
% command.__class__.__name__,
exc_info=True,
)
raise RegistryError() from err
message = "%s failed to execute due to an unknown error."
logger.warning(message, cmd_type, exc_info=True)
raise RegistryError(message) from err
else:
if response.code >= 2000:
raise RegistryError(response.msg)
raise RegistryError(response.msg, code=response.code)
else:
return response
def send(self, command):
def send(self, command, *, cleaned=False):
"""Login, send the command, then close the connection. Tries 3 times."""
# try to prevent use of this method without appropriate safeguards
if not cleaned:
raise ValueError("Please sanitize user input before sending it.")
counter = 0 # we'll try 3 times
while True:
try:
return self._send(command)
except RegistryError as err:
if counter == 3: # don't try again
raise err
else:
if err.should_retry() and counter < 3:
counter += 1
sleep((counter * 50) / 1000) # sleep 50 ms to 150 ms
else: # don't try again
raise err
try:

View file

@ -1,5 +1,77 @@
from enum import IntEnum
class ErrorCode(IntEnum):
"""
Overview of registry response codes from RFC 5730. See RFC 5730 for full text.
- 1000 - 1500 Success
- 2000 - 2308 Registrar did something silly
- 2400 - 2500 Registry did something silly
- 2501 - 2502 Something malicious or abusive may have occurred
"""
COMMAND_COMPLETED_SUCCESSFULLY = 1000
COMMAND_COMPLETED_SUCCESSFULLY_ACTION_PENDING = 1001
COMMAND_COMPLETED_SUCCESSFULLY_NO_MESSAGES = 1300
COMMAND_COMPLETED_SUCCESSFULLY_ACK_TO_DEQUEUE = 1301
COMMAND_COMPLETED_SUCCESSFULLY_ENDING_SESSION = 1500
UNKNOWN_COMMAND = 2000
COMMAND_SYNTAX_ERROR = 2001
COMMAND_USE_ERROR = 2002
REQUIRED_PARAMETER_MISSING = 2003
PARAMETER_VALUE_RANGE_ERROR = 2004
PARAMETER_VALUE_SYNTAX_ERROR = 2005
UNIMPLEMENTED_PROTOCOL_VERSION = 2100
UNIMPLEMENTED_COMMAND = 2101
UNIMPLEMENTED_OPTION = 2102
UNIMPLEMENTED_EXTENSION = 2103
BILLING_FAILURE = 2104
OBJECT_IS_NOT_ELIGIBLE_FOR_RENEWAL = 2105
OBJECT_IS_NOT_ELIGIBLE_FOR_TRANSFER = 2106
AUTHENTICATION_ERROR = 2200
AUTHORIZATION_ERROR = 2201
INVALID_AUTHORIZATION_INFORMATION = 2202
OBJECT_PENDING_TRANSFER = 2300
OBJECT_NOT_PENDING_TRANSFER = 2301
OBJECT_EXISTS = 2302
OBJECT_DOES_NOT_EXIST = 2303
OBJECT_STATUS_PROHIBITS_OPERATION = 2304
OBJECT_ASSOCIATION_PROHIBITS_OPERATION = 2305
PARAMETER_VALUE_POLICY_ERROR = 2306
UNIMPLEMENTED_OBJECT_SERVICE = 2307
DATA_MANAGEMENT_POLICY_VIOLATION = 2308
COMMAND_FAILED = 2400
COMMAND_FAILED_SERVER_CLOSING_CONNECTION = 2500
AUTHENTICATION_ERROR_SERVER_CLOSING_CONNECTION = 2501
SESSION_LIMIT_EXCEEDED_SERVER_CLOSING_CONNECTION = 2502
class RegistryError(Exception):
pass
"""
Overview of registry response codes from RFC 5730. See RFC 5730 for full text.
- 1000 - 1500 Success
- 2000 - 2308 Registrar did something silly
- 2400 - 2500 Registry did something silly
- 2501 - 2502 Something malicious or abusive may have occurred
"""
def __init__(self, *args, code=None, **kwargs):
super().__init__(*args, **kwargs)
self.code = code
def should_retry(self):
return self.code == ErrorCode.COMMAND_FAILED
def is_server_error(self):
return self.code is not None and (self.code >= 2400 and self.code <= 2500)
def is_client_error(self):
return self.code is not None and (self.code >= 2000 and self.code <= 2308)
class LoginError(RegistryError):

View file

@ -77,6 +77,7 @@ h2 {
font-weight: font-weight('semibold');
line-height: line-height('heading', 3);
margin: units(4) 0 units(1);
color: color('primary-darker');
}
.register-form-step > h1 {
@ -431,4 +432,4 @@ abbr[title] {
@include at-media('tablet') {
height: units('mobile');
}
}
}

View file

@ -111,6 +111,8 @@ INSTALLED_APPS = [
"registrar",
# Our internal API application
"api",
# Only for generating documentation, uncomment to run manage.py generate_puml
# "puml_generator",
]
# Middleware are routines for processing web requests.

View file

@ -59,12 +59,12 @@ urlpatterns = [
),
path(
"application/<int:pk>/withdraw",
views.ApplicationWithdraw.as_view(),
views.ApplicationWithdrawConfirmation.as_view(),
name="application-withdraw-confirmation",
),
path(
"application/<int:pk>/withdrawconfirmed",
views.ApplicationWithdraw.updatestatus,
views.ApplicationWithdrawn.as_view(),
name="application-withdrawn",
),
path("health/", views.health),
@ -83,6 +83,16 @@ urlpatterns = [
views.DomainNameserversView.as_view(),
name="domain-nameservers",
),
path(
"domain/<int:pk>/your-contact-information",
views.DomainYourContactInformationView.as_view(),
name="domain-your-contact-information",
),
path(
"domain/<int:pk>/security-email",
views.DomainSecurityEmailView.as_view(),
name="domain-security-email",
),
path(
"domain/<int:pk>/users/add",
views.DomainAddUserView.as_view(),

View file

@ -5,7 +5,7 @@ from faker import Faker
from registrar.models import (
User,
DomainApplication,
Domain,
DraftDomain,
Contact,
Website,
)
@ -216,11 +216,13 @@ class DomainApplicationFixture:
if not da.requested_domain:
if "requested_domain" in app and app["requested_domain"] is not None:
da.requested_domain, _ = Domain.objects.get_or_create(
da.requested_domain, _ = DraftDomain.objects.get_or_create(
name=app["requested_domain"]
)
else:
da.requested_domain = Domain.objects.create(name=cls.fake_dot_gov())
da.requested_domain = DraftDomain.objects.create(
name=cls.fake_dot_gov()
)
@classmethod
def _set_many_to_many_relations(cls, da: DomainApplication, app: dict):

View file

@ -1,2 +1,7 @@
from .application_wizard import *
from .domain import DomainAddUserForm, NameserverFormset
from .domain import (
DomainAddUserForm,
NameserverFormset,
DomainSecurityEmailForm,
ContactForm,
)

View file

@ -11,7 +11,7 @@ from django.utils.safestring import mark_safe
from api.views import DOMAIN_API_MESSAGES
from registrar.models import Contact, DomainApplication, Domain
from registrar.models import Contact, DomainApplication, DraftDomain, Domain
from registrar.utility import errors
logger = logging.getLogger(__name__)
@ -453,7 +453,7 @@ class AlternativeDomainForm(RegistrarForm):
"""Validation code for domain names."""
try:
requested = self.cleaned_data.get("alternative_domain", None)
validated = Domain.validate(requested, blank_ok=True)
validated = DraftDomain.validate(requested, blank_ok=True)
except errors.ExtraDotsError:
raise forms.ValidationError(
DOMAIN_API_MESSAGES["extra_dots"], code="extra_dots"
@ -498,7 +498,7 @@ class BaseAlternativeDomainFormSet(RegistrarFormSet):
@classmethod
def on_fetch(cls, query):
return [{"alternative_domain": domain.sld} for domain in query]
return [{"alternative_domain": Domain.sld(domain.name)} for domain in query]
@classmethod
def from_database(cls, obj):
@ -524,7 +524,7 @@ class DotGovDomainForm(RegistrarForm):
requested_domain.name = f"{domain}.gov"
requested_domain.save()
else:
requested_domain = Domain.objects.create(name=f"{domain}.gov")
requested_domain = DraftDomain.objects.create(name=f"{domain}.gov")
obj.requested_domain = requested_domain
obj.save()
@ -535,14 +535,14 @@ class DotGovDomainForm(RegistrarForm):
values = {}
requested_domain = getattr(obj, "requested_domain", None)
if requested_domain is not None:
values["requested_domain"] = requested_domain.sld
values["requested_domain"] = Domain.sld(requested_domain.name)
return values
def clean_requested_domain(self):
"""Validation code for domain names."""
try:
requested = self.cleaned_data.get("requested_domain", None)
validated = Domain.validate(requested)
validated = DraftDomain.validate(requested)
except errors.BlankValueError:
raise forms.ValidationError(
DOMAIN_API_MESSAGES["required"], code="required"

View file

@ -3,6 +3,10 @@
from django import forms
from django.forms import formset_factory
from phonenumber_field.widgets import RegionalPhoneNumberWidget
from ..models import Contact
class DomainAddUserForm(forms.Form):
@ -22,3 +26,41 @@ NameserverFormset = formset_factory(
DomainNameserverForm,
extra=1,
)
class DomainSecurityEmailForm(forms.Form):
"""Form for adding or editing a security email to a domain."""
security_email = forms.EmailField(label="Security email")
class ContactForm(forms.ModelForm):
"""Form for updating contacts."""
class Meta:
model = Contact
fields = ["first_name", "middle_name", "last_name", "title", "email", "phone"]
widgets = {
"first_name": forms.TextInput,
"middle_name": forms.TextInput,
"last_name": forms.TextInput,
"title": forms.TextInput,
"email": forms.EmailInput,
"phone": RegionalPhoneNumberWidget,
}
# the database fields have blank=True so ModelForm doesn't create
# required fields by default. Use this list in __init__ to mark each
# of these fields as required
required = ["first_name", "last_name", "title", "email", "phone"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# take off maxlength attribute for the phone number field
# which interferes with our input_with_errors template tag
self.fields["phone"].widget.attrs.pop("maxlength", None)
for field_name in self.required:
self.fields[field_name].required = True

View file

@ -54,16 +54,6 @@ class Command(BaseCommand):
domains = []
for row in reader:
name = row["Name"].lower() # we typically use lowercase domains
# Ensure that there is a `Domain` object for each domain name in
# this file and that it is active. There is a uniqueness
# constraint for active Domain objects, so we are going to account
# for that here with this check so that our later bulk_create
# should succeed
if Domain.objects.filter(name=name, is_active=True).exists():
# don't do anything, this domain is here and active
continue
else:
domains.append(Domain(name=name, is_active=True))
domains.append(Domain(name=name))
logger.info("Creating %d new domains", len(domains))
Domain.objects.bulk_create(domains)

View file

@ -0,0 +1,16 @@
# Generated by Django 4.2 on 2023-05-17 17:05
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("registrar", "0019_alter_domainapplication_organization_type"),
]
operations = [
migrations.RemoveField(
model_name="domaininformation",
name="security_email",
),
]

View file

@ -0,0 +1,122 @@
# Generated by Django 4.2.1 on 2023-05-25 15:03
from django.db import migrations, models
import django.db.models.deletion
import registrar.models.public_contact
class Migration(migrations.Migration):
dependencies = [
("registrar", "0020_remove_domaininformation_security_email"),
]
operations = [
migrations.AddField(
model_name="publiccontact",
name="domain",
field=models.ForeignKey(
default=1,
on_delete=django.db.models.deletion.PROTECT,
related_name="contacts",
to="registrar.domain",
),
preserve_default=False,
),
migrations.AddField(
model_name="publiccontact",
name="registry_id",
field=models.CharField(
default=registrar.models.public_contact.get_id,
help_text="Auto generated ID to track this contact in the registry",
max_length=16,
),
),
migrations.AlterField(
model_name="publiccontact",
name="cc",
field=models.TextField(help_text="Contact's country code"),
),
migrations.AlterField(
model_name="publiccontact",
name="city",
field=models.TextField(help_text="Contact's city"),
),
migrations.AlterField(
model_name="publiccontact",
name="contact_type",
field=models.CharField(
choices=[
("registrant", "Registrant"),
("administrative", "Administrative"),
("technical", "Technical"),
("security", "Security"),
],
help_text="For which type of WHOIS contact",
max_length=14,
),
),
migrations.AlterField(
model_name="publiccontact",
name="email",
field=models.TextField(help_text="Contact's email address"),
),
migrations.AlterField(
model_name="publiccontact",
name="fax",
field=models.TextField(
help_text="Contact's fax number (null ok). Must be in ITU.E164.2005 format.",
null=True,
),
),
migrations.AlterField(
model_name="publiccontact",
name="name",
field=models.TextField(help_text="Contact's full name"),
),
migrations.AlterField(
model_name="publiccontact",
name="org",
field=models.TextField(
help_text="Contact's organization (null ok)", null=True
),
),
migrations.AlterField(
model_name="publiccontact",
name="pc",
field=models.TextField(help_text="Contact's postal code"),
),
migrations.AlterField(
model_name="publiccontact",
name="pw",
field=models.TextField(
help_text="Contact's authorization code. 16 characters minimum."
),
),
migrations.AlterField(
model_name="publiccontact",
name="sp",
field=models.TextField(help_text="Contact's state or province"),
),
migrations.AlterField(
model_name="publiccontact",
name="street1",
field=models.TextField(help_text="Contact's street"),
),
migrations.AlterField(
model_name="publiccontact",
name="street2",
field=models.TextField(help_text="Contact's street (null ok)", null=True),
),
migrations.AlterField(
model_name="publiccontact",
name="street3",
field=models.TextField(help_text="Contact's street (null ok)", null=True),
),
migrations.AlterField(
model_name="publiccontact",
name="voice",
field=models.TextField(
help_text="Contact's phone number. Must be in ITU.E164.2005 format"
),
),
]

View file

@ -0,0 +1,66 @@
# Generated by Django 4.2.1 on 2023-05-26 13:14
from django.db import migrations, models
import django.db.models.deletion
import registrar.models.utility.domain_helper
class Migration(migrations.Migration):
dependencies = [
("registrar", "0021_publiccontact_domain_publiccontact_registry_id_and_more"),
]
operations = [
migrations.CreateModel(
name="DraftDomain",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"name",
models.CharField(
default=None,
help_text="Fully qualified domain name",
max_length=253,
),
),
],
options={
"abstract": False,
},
bases=(models.Model, registrar.models.utility.domain_helper.DomainHelper), # type: ignore
),
migrations.AddField(
model_name="domainapplication",
name="approved_domain",
field=models.OneToOneField(
blank=True,
help_text="The approved domain",
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="domain_application",
to="registrar.domain",
),
),
migrations.AlterField(
model_name="domainapplication",
name="requested_domain",
field=models.OneToOneField(
blank=True,
help_text="The requested domain",
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="domain_application",
to="registrar.draftdomain",
),
),
]

View file

@ -0,0 +1,44 @@
# Generated by Django 4.2.1 on 2023-05-31 23:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("registrar", "0022_draftdomain_domainapplication_approved_domain_and_more"),
]
operations = [
migrations.AlterField(
model_name="contact",
name="first_name",
field=models.TextField(
blank=True,
db_index=True,
help_text="First name",
null=True,
verbose_name="first name / given name",
),
),
migrations.AlterField(
model_name="contact",
name="last_name",
field=models.TextField(
blank=True,
db_index=True,
help_text="Last name",
null=True,
verbose_name="last name / family name",
),
),
migrations.AlterField(
model_name="contact",
name="title",
field=models.TextField(
blank=True,
help_text="Title",
null=True,
verbose_name="title or role in your organization",
),
),
]

View file

@ -4,6 +4,7 @@ from .contact import Contact
from .domain_application import DomainApplication
from .domain_information import DomainInformation
from .domain import Domain
from .draft_domain import DraftDomain
from .host_ip import HostIP
from .host import Host
from .domain_invitation import DomainInvitation
@ -18,6 +19,7 @@ __all__ = [
"DomainApplication",
"DomainInformation",
"Domain",
"DraftDomain",
"DomainInvitation",
"HostIP",
"Host",
@ -31,6 +33,7 @@ __all__ = [
auditlog.register(Contact)
auditlog.register(DomainApplication)
auditlog.register(Domain)
auditlog.register(DraftDomain)
auditlog.register(DomainInvitation)
auditlog.register(HostIP)
auditlog.register(Host)

View file

@ -20,6 +20,7 @@ class Contact(TimeStampedModel):
null=True,
blank=True,
help_text="First name",
verbose_name="first name / given name",
db_index=True,
)
middle_name = models.TextField(
@ -31,12 +32,14 @@ class Contact(TimeStampedModel):
null=True,
blank=True,
help_text="Last name",
verbose_name="last name / family name",
db_index=True,
)
title = models.TextField(
null=True,
blank=True,
help_text="Title",
verbose_name="title or role in your organization",
)
email = models.TextField(
null=True,

View file

@ -235,6 +235,19 @@ class Domain(TimeStampedModel):
# nothing.
logger.warn("TODO: Fake setting nameservers to %s", new_nameservers)
def security_email(self) -> str:
"""Get the security email for this domain.
TODO: call EPP to get this info instead of returning fake data.
"""
return "mayor@igorville.gov"
def set_security_email(self, new_security_email: str):
"""Set the security email for this domain."""
# TODO: call EPP to set these values in the registry instead of doing
# nothing.
logger.warn("TODO: Fake setting security email to %s", new_security_email)
@property
def roid(self):
return self._get_property("roid")

View file

@ -400,10 +400,19 @@ class DomainApplication(TimeStampedModel):
related_name="current+",
)
requested_domain = models.OneToOneField(
approved_domain = models.OneToOneField(
"Domain",
null=True,
blank=True,
help_text="The approved domain",
related_name="domain_application",
on_delete=models.PROTECT,
)
requested_domain = models.OneToOneField(
"DraftDomain",
null=True,
blank=True,
help_text="The requested domain",
related_name="domain_application",
on_delete=models.PROTECT,
@ -499,8 +508,8 @@ class DomainApplication(TimeStampedModel):
if self.requested_domain is None:
raise ValueError("Requested domain is missing.")
Domain = apps.get_model("registrar.Domain")
if not Domain.string_could_be_domain(self.requested_domain.name):
DraftDomain = apps.get_model("registrar.DraftDomain")
if not DraftDomain.string_could_be_domain(self.requested_domain.name):
raise ValueError("Requested domain is not a valid domain name.")
# When an application is submitted, we need to send a confirmation email
@ -516,13 +525,16 @@ class DomainApplication(TimeStampedModel):
application into an admin on that domain.
"""
# create the domain if it doesn't exist
# create the domain
Domain = apps.get_model("registrar.Domain")
created_domain, _ = Domain.objects.get_or_create(name=self.requested_domain)
if Domain.objects.filter(name=self.requested_domain.name).exists():
raise ValueError("Cannot approve. Requested domain is already in use.")
created_domain = Domain.objects.create(name=self.requested_domain.name)
self.approved_domain = created_domain
# copy the information from domainapplication into domaininformation
DomainInformation = apps.get_model("registrar.DomainInformation")
DomainInformation.create_from_da(self)
DomainInformation.create_from_da(self, domain=created_domain)
# create the permission for the user
UserDomainRole = apps.get_model("registrar.UserDomainRole")
@ -550,11 +562,13 @@ class DomainApplication(TimeStampedModel):
"""Show this step if the answer to the first question implies it.
This shows for answers that aren't "Federal" or "Interstate".
This also doesnt show if user selected "School District" as well (#524)
"""
user_choice = self.organization_type
excluded = [
DomainApplication.OrganizationChoices.FEDERAL,
DomainApplication.OrganizationChoices.INTERSTATE,
DomainApplication.OrganizationChoices.SCHOOL_DISTRICT,
]
return bool(user_choice and user_choice not in excluded)

View file

@ -200,12 +200,6 @@ class DomainInformation(TimeStampedModel):
blank=True,
help_text="Acknowledged .gov acceptable use policy",
)
security_email = models.EmailField(
max_length=320,
null=True,
blank=True,
help_text="Security email for public use",
)
def __str__(self):
try:
@ -217,24 +211,24 @@ class DomainInformation(TimeStampedModel):
return ""
@classmethod
def create_from_da(cls, domain_application):
def create_from_da(cls, domain_application, domain=None):
"""Takes in a DomainApplication dict and converts it into DomainInformation"""
da_dict = domain_application.to_dict()
# remove the id so one can be assinged on creation
da_id = da_dict.pop("id")
da_id = da_dict.pop("id", None)
# check if we have a record that corresponds with the domain
# application, if so short circuit the create
domain_info = cls.objects.filter(domain_application__id=da_id).first()
if domain_info:
return domain_info
# the following information below is not needed in the domain information:
da_dict.pop("status")
da_dict.pop("current_websites")
da_dict.pop("investigator")
da_dict.pop("alternative_domains")
# use the requested_domain to create information for this domain
da_dict["domain"] = da_dict.pop("requested_domain")
other_contacts = da_dict.pop("other_contacts")
da_dict.pop("status", None)
da_dict.pop("current_websites", None)
da_dict.pop("investigator", None)
da_dict.pop("alternative_domains", None)
da_dict.pop("requested_domain", None)
da_dict.pop("approved_domain", None)
other_contacts = da_dict.pop("other_contacts", [])
domain_info = cls(**da_dict)
domain_info.domain_application = domain_application
# Save so the object now have PK
@ -243,6 +237,8 @@ class DomainInformation(TimeStampedModel):
# Process the remaining "many to many" stuff
domain_info.other_contacts.add(*other_contacts)
if domain:
domain_info.domain = domain
domain_info.save()
return domain_info

View file

@ -0,0 +1,22 @@
import logging
from django.db import models
from .utility.domain_helper import DomainHelper
from .utility.time_stamped_model import TimeStampedModel
logger = logging.getLogger(__name__)
class DraftDomain(TimeStampedModel, DomainHelper):
"""Store domain names which registrants have requested."""
def __str__(self) -> str:
return self.name
name = models.CharField(
max_length=253,
blank=False,
default=None, # prevent saving without a value
help_text="Fully qualified domain name",
)

View file

@ -1,8 +1,21 @@
from datetime import datetime
from random import choices
from string import ascii_uppercase, ascii_lowercase, digits
from django.db import models
from .utility.time_stamped_model import TimeStampedModel
def get_id():
"""Generate a 16 character registry ID with a low probability of collision."""
day = datetime.today().strftime("%A")[:2]
rand = "".join(
choices(ascii_uppercase + ascii_lowercase + digits, k=14) # nosec B311
)
return f"{day}{rand}"
class PublicContact(TimeStampedModel):
"""Contact information intended to be published in WHOIS."""
@ -14,37 +27,126 @@ class PublicContact(TimeStampedModel):
TECHNICAL = "technical", "Technical"
SECURITY = "security", "Security"
contact_type = models.CharField(max_length=14, choices=ContactTypeChoices.choices)
def save(self, *args, **kwargs):
"""Save to the registry and also locally in the registrar database."""
if hasattr(self, "domain"):
match self.contact_type:
case PublicContact.ContactTypeChoices.REGISTRANT:
self.domain.registrant = self
case PublicContact.ContactTypeChoices.ADMINISTRATIVE:
self.domain.administrative_contact = self
case PublicContact.ContactTypeChoices.TECHNICAL:
self.domain.technical_contact = self
case PublicContact.ContactTypeChoices.SECURITY:
self.domain.security_contact = self
super().save(*args, **kwargs)
# contact's full name
name = models.TextField(null=False)
# contact's organization (null ok)
org = models.TextField(null=True)
# contact's street
street1 = models.TextField(null=False)
# contact's street (null ok)
street2 = models.TextField(null=True)
# contact's street (null ok)
street3 = models.TextField(null=True)
# contact's city
city = models.TextField(null=False)
# contact's state or province
sp = models.TextField(null=False)
# contact's postal code
pc = models.TextField(null=False)
# contact's country code
cc = models.TextField(null=False)
# contact's email address
email = models.TextField(null=False)
# contact's phone number
# Must be in ITU.E164.2005 format
voice = models.TextField(null=False)
# contact's fax number (null ok)
# Must be in ITU.E164.2005 format
fax = models.TextField(null=True)
# contact's authorization code
# 16 characters minium
pw = models.TextField(null=False)
contact_type = models.CharField(
max_length=14,
choices=ContactTypeChoices.choices,
help_text="For which type of WHOIS contact",
)
registry_id = models.CharField(
max_length=16,
default=get_id,
null=False,
help_text="Auto generated ID to track this contact in the registry",
)
domain = models.ForeignKey(
"registrar.Domain",
on_delete=models.PROTECT,
related_name="contacts",
)
name = models.TextField(null=False, help_text="Contact's full name")
org = models.TextField(null=True, help_text="Contact's organization (null ok)")
street1 = models.TextField(null=False, help_text="Contact's street")
street2 = models.TextField(null=True, help_text="Contact's street (null ok)")
street3 = models.TextField(null=True, help_text="Contact's street (null ok)")
city = models.TextField(null=False, help_text="Contact's city")
sp = models.TextField(null=False, help_text="Contact's state or province")
pc = models.TextField(null=False, help_text="Contact's postal code")
cc = models.TextField(null=False, help_text="Contact's country code")
email = models.TextField(null=False, help_text="Contact's email address")
voice = models.TextField(
null=False, help_text="Contact's phone number. Must be in ITU.E164.2005 format"
)
fax = models.TextField(
null=True,
help_text="Contact's fax number (null ok). Must be in ITU.E164.2005 format.",
)
pw = models.TextField(
null=False, help_text="Contact's authorization code. 16 characters minimum."
)
@classmethod
def get_default_registrant(cls):
return cls(
contact_type=PublicContact.ContactTypeChoices.REGISTRANT,
registry_id=get_id(),
name="CSD/CB Attn: Cameron Dixon",
org="Cybersecurity and Infrastructure Security Agency",
street1="CISA NGR STOP 0645",
street2="1110 N. Glebe Rd.",
city="Arlington",
sp="VA",
pc="20598-0645",
cc="US",
email="dotgov@cisa.dhs.gov",
voice="+1.8882820870",
pw="thisisnotapassword",
)
@classmethod
def get_default_administrative(cls):
return cls(
contact_type=PublicContact.ContactTypeChoices.ADMINISTRATIVE,
registry_id=get_id(),
name="Program Manager",
org="Cybersecurity and Infrastructure Security Agency",
street1="4200 Wilson Blvd.",
city="Arlington",
sp="VA",
pc="22201",
cc="US",
email="dotgov@cisa.dhs.gov",
voice="+1.8882820870",
pw="thisisnotapassword",
)
@classmethod
def get_default_technical(cls):
return cls(
contact_type=PublicContact.ContactTypeChoices.TECHNICAL,
registry_id=get_id(),
name="Registry Customer Service",
org="Cybersecurity and Infrastructure Security Agency",
street1="4200 Wilson Blvd.",
city="Arlington",
sp="VA",
pc="22201",
cc="US",
email="dotgov@cisa.dhs.gov",
voice="+1.8882820870",
pw="thisisnotapassword",
)
@classmethod
def get_default_security(cls):
return cls(
contact_type=PublicContact.ContactTypeChoices.SECURITY,
registry_id=get_id(),
name="Registry Customer Service",
org="Cybersecurity and Infrastructure Security Agency",
street1="4200 Wilson Blvd.",
city="Arlington",
sp="VA",
pc="22201",
cc="US",
email="dotgov@cisa.dhs.gov",
voice="+1.8882820870",
pw="thisisnotapassword",
)
def __str__(self):
return f"{self.name} <{self.email}>"

View file

@ -0,0 +1,64 @@
import re
from api.views import in_domains
from registrar.utility import errors
class DomainHelper:
"""Utility functions and constants for domain names."""
# a domain name is alphanumeric or hyphen, up to 63 characters, doesn't
# begin or end with a hyphen, followed by a TLD of 2-6 alphabetic characters
DOMAIN_REGEX = re.compile(r"^(?!-)[A-Za-z0-9-]{1,63}(?<!-)\.[A-Za-z]{2,6}$")
# a domain name is alphanumeric or hyphen, has at least 2 dots, doesn't
# begin or end with a hyphen, followed by a TLD of 2-6 alphabetic characters
HOST_REGEX = re.compile(r"^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\.){2,}([A-Za-z]){2,6}$")
# a domain can be no longer than 253 characters in total
MAX_LENGTH = 253
@classmethod
def string_could_be_domain(cls, domain: str | None) -> bool:
"""Return True if the string could be a domain name, otherwise False."""
if not isinstance(domain, str):
return False
return bool(cls.DOMAIN_REGEX.match(domain))
@classmethod
def validate(cls, domain: str | None, blank_ok=False) -> str:
"""Attempt to determine if a domain name could be requested."""
if domain is None:
raise errors.BlankValueError()
if not isinstance(domain, str):
raise ValueError("Domain name must be a string")
domain = domain.lower().strip()
if domain == "":
if blank_ok:
return domain
else:
raise errors.BlankValueError()
if domain.endswith(".gov"):
domain = domain[:-4]
if "." in domain:
raise errors.ExtraDotsError()
if not DomainHelper.string_could_be_domain(domain + ".gov"):
raise ValueError()
if in_domains(domain):
raise errors.DomainUnavailableError()
return domain
@classmethod
def sld(cls, domain: str):
"""
Get the second level domain. Example: `gsa.gov` -> `gsa`.
If no TLD is present, returns the original string.
"""
return domain.split(".")[0]
@classmethod
def tld(cls, domain: str):
"""Get the top level domain. Example: `gsa.gov` -> `gov`."""
parts = domain.rsplit(".")
return parts[-1] if len(parts) > 1 else ""

View file

@ -1,5 +1,3 @@
from django.apps import apps
from django.core.exceptions import ValidationError
from django.db import models
from .utility.time_stamped_model import TimeStampedModel
@ -18,35 +16,5 @@ class Website(TimeStampedModel):
help_text="",
)
@property
def sld(self):
"""Get or set the second level domain string."""
return self.website.split(".")[0]
@sld.setter
def sld(self, value: str):
Domain = apps.get_model("registrar.Domain")
parts = self.website.split(".")
tld = parts[1] if len(parts) > 1 else ""
if Domain.string_could_be_domain(f"{value}.{tld}"):
self.website = f"{value}.{tld}"
else:
raise ValidationError("%s is not a valid second level domain" % value)
@property
def tld(self):
"""Get or set the top level domain string."""
parts = self.website.split(".")
return parts[1] if len(parts) > 1 else ""
@tld.setter
def tld(self, value: str):
Domain = apps.get_model("registrar.Domain")
sld = self.website.split(".")[0]
if Domain.string_could_be_domain(f"{sld}.{value}"):
self.website = f"{sld}.{value}"
else:
raise ValidationError("%s is not a valid top level domain" % value)
def __str__(self) -> str:
return str(self.website)

View file

@ -31,7 +31,7 @@
<p> <b class="review__step__name">Last updated:</b> {{domainapplication.updated_at|date:"F j, Y"}}<br>
<b class="review__step__name">Request #:</b> {{domainapplication.id}}</p>
<p>{% include "includes/domain_application.html" %}</p>
<p><a href="{% url 'application-withdraw-confirmation' domainapplication.id %}" class="usa-button usa-button--outline withdraw_outline">
<p><a href="{% url 'application-withdraw-confirmation' pk=domainapplication.id %}" class="usa-button usa-button--outline withdraw_outline">
Withdraw request</a>
</p>
</div>

View file

@ -61,18 +61,15 @@
<a class="usa-skipnav" href="#main-content">Skip to main content</a>
{% if IS_DEMO_SITE %}
<section
class="usa-site-alert usa-site-alert--emergency usa-site-alert--no-icon"
aria-label="Site alert"
>
<div class="usa-alert">
<div class="usa-alert__body">
<p class="usa-alert__text">
<strong>TEST SITE</strong> - Do not use real personal information. Demo purposes only.
</p>
</div>
<section aria-label="Alert" >
<div class="usa-alert usa-alert--warning usa-alert--no-icon">
<div class="usa-alert__body">
<p class="usa-alert__text">
<strong>BETA SITE:</strong> Were building a new way to get a .gov. Take a look around, but dont rely on this site yet. This site is for testing purposes only. Dont enter real data into any form on this site. To learn about requesting a .gov domain, visit <a href="https://get.gov" class="usa-link">get.gov</a>
</p>
</div>
</section>
</div>
</section>
{% endif %}
<section class="usa-banner" aria-label="Official website of the United States government">

View file

@ -1,6 +1,33 @@
{% extends "domain_base.html" %}
{% load static url_helpers %}
{% block domain_content %}
{{ block.super }}
<p>Active: {% if domain.is_active %}Yes{% else %}No{% endif %}</p>
<div class="margin-top-4 tablet:grid-col-10">
{% url 'domain-nameservers' pk=domain.id as url %}
{% if domain.nameservers %}
{% include "includes/summary_item.html" with title='DNS name servers' value=domain.nameservers list='true' edit_link=url %}
{% else %}
<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 well need information about your domain name servers.</p>
<a class="usa-button margin-bottom-1" href="{{url}}"> Add DNS name servers </a>
{% endif %}
{% url 'todo' as url %}
{% include "includes/summary_item.html" with title='Organization name and mailing address' value=domain.domain_info address='true' edit_link=url %}
{% url 'todo' as url %}
{% include "includes/summary_item.html" with title='Authorizing official' value=domain.domain_info.authorizing_official contact='true' edit_link=url %}
{% 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 %}
{% url 'domain-security-email' pk=domain.id as url %}
{% include "includes/summary_item.html" with title='Security email' value=domain.security_email edit_link=url %}
{% url 'domain-users' pk=domain.id as url %}
{% include "includes/summary_item.html" with title='User management' users='true' list=True value=domain.permissions.all edit_link=url %}
</div>
{% endblock %} {# domain_content #}

View file

@ -0,0 +1,27 @@
{% extends "domain_base.html" %}
{% load static field_helpers %}
{% block title %}Domain security email | {{ domain.name }} | {% endblock %}
{% block domain_content %}
<h1>Domain security email</h1>
<p>We strongly recommend that you provide a security email. This email will allow the public to report observed or suspected security issues on your domain. Security emails are made public and included in the <a href="https://federalist-877ab29f-16f6-4f12-961c-96cf064cf070.sites.pages.cloud.gov/site/cisagov/getgov-home/about/data/">.gov domain data</a> we provide.</p>
<p>A security contact should be capable of evaluating or triaging security reports for your entire domain. Use a team email address, not an individuals email. We recommend using an alias, like security@domain.gov.</p>
{% include "includes/required_fields.html" %}
<form class="usa-form usa-form--large" method="post" novalidate>
{% csrf_token %}
{% input_with_errors form.security_email %}
<button
type="submit"
class="usa-button"
>Add security email</button>
</form>
{% endblock %} {# domain_content #}

View file

@ -8,7 +8,7 @@
<a href="{{ url }}"
{% if request.path == url %}class="usa-current"{% endif %}
>
Domain Overview
Domain overview
</a>
</li>
@ -21,6 +21,15 @@
</a>
</li>
<li class="usa-sidenav__item">
{% url 'todo' as url %}
<a href="{{ url }}"
{% if request.path == url %}class="usa-current"{% endif %}
>
Organization name and mailing address
</a>
</li>
<li class="usa-sidenav__item">
{% url 'todo' as url %}
<a href="{{ url }}"
@ -31,7 +40,7 @@
</li>
<li class="usa-sidenav__item">
{% url 'todo' as url %}
{% url 'domain-your-contact-information' pk=domain.id as url %}
<a href="{{ url }}"
{% if request.path == url %}class="usa-current"{% endif %}
>
@ -40,7 +49,7 @@
</li>
<li class="usa-sidenav__item">
{% url 'todo' as url %}
{% url 'domain-security-email' pk=domain.id as url %}
<a href="{{ url }}"
{% if request.path == url %}class="usa-current"{% endif %}
>

View file

@ -0,0 +1,35 @@
{% extends "domain_base.html" %}
{% load static field_helpers %}
{% block title %}Domain contact information | {{ domain.name }} | {% endblock %}
{% block domain_content %}
<h1>Domain contact information</h1>
<p>If youd like us to use a different name, email, or phone number you can make those changes below. Changing your contact information here wont affect your Login.gov account information.</p>
{% include "includes/required_fields.html" %}
<form class="usa-form usa-form--large" method="post" novalidate id="form-container">
{% csrf_token %}
{% input_with_errors form.first_name %}
{% input_with_errors form.middle_name %}
{% input_with_errors form.last_name %}
{% input_with_errors form.title %}
{% input_with_errors form.email %}
{% input_with_errors form.phone %}
<button
type="submit"
class="usa-button"
>Save</button>
</form>
{% endblock %} {# domain_content #}

View file

@ -22,7 +22,7 @@
<h2>Registered domains</h2>
{% if domains %}
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked">
<caption class="sr-only">Your domain applications</caption>
<caption class="sr-only">Your registered domains</caption>
<thead>
<tr>
<th data-sortable scope="col" role="columnheader">Domain name</th>

View file

@ -1,11 +1,13 @@
{% load static url_helpers %}
<section class="summary-item margin-top-3">
<hr class="" />
<p class="summary-item__title
<hr class="" aria-hidden="true" />
<h2 class="summary-item__title
text-primary-dark text-semibold
margin-top-0 margin-bottom-05"
>
{{ title }}
</p>
>
{{ title }}
</h2>
{% if address %}
{% include "includes/organization_address.html" with organization=value %}
{% elif contact %}
@ -17,7 +19,7 @@
{% for item in value %}
<li>
<p class="text-semibold margin-top-1 margin-bottom-0">
Conatct {{forloop.counter}}
Contact {{forloop.counter}}
</p>
{% include "includes/contact.html" with contact=item %}</li>
{% empty %}
@ -30,11 +32,19 @@
{% endif %}
{% elif list %}
{% if value|length == 1 %}
<p class="margin-top-0">{{ value | first }} </p>
{% if users %}
<p class="margin-top-0">{{ value.0.user.email }} </p>
{% else %}
<p class="margin-top-0">{{ value | first }} </p>
{% endif %}
{% else %}
<ul class="usa-list margin-top-0">
{% for item in value %}
<li>{{ item }}</li>
{% if users %}
<li>{{ item.user.email }}</li>
{% else %}
<li>{{ item }}</li>
{% endif %}
{% empty %}
<li>None</li>
{% endfor %}</ul></p>
@ -45,5 +55,13 @@
{{ value }}
</p>
{% endif %}
{% if edit_link %}
<a
href="{{ edit_link }}"
>
Edit<span class="sr-only"> {{ title }}</span>
</a>
{% endif %}
</section>

View file

@ -5,7 +5,7 @@ from unittest.mock import MagicMock
from django.contrib.auth import get_user_model
from django.test import TestCase
from registrar.models import Contact, Domain, Website, DomainApplication
from registrar.models import Contact, DraftDomain, Website, DomainApplication
import boto3_mocking # type: ignore
@ -28,7 +28,7 @@ class TestEmails(TestCase):
email="testy@town.com",
phone="(555) 555 5555",
)
domain, _ = Domain.objects.get_or_create(name="city.gov")
domain, _ = DraftDomain.objects.get_or_create(name="city.gov")
alt, _ = Website.objects.get_or_create(website="city1.gov")
current, _ = Website.objects.get_or_create(website="city.com")
you, _ = Contact.objects.get_or_create(

View file

@ -8,6 +8,7 @@ from registrar.models import (
User,
Website,
Domain,
DraftDomain,
DomainInvitation,
UserDomainRole,
)
@ -40,7 +41,7 @@ class TestDomainApplication(TestCase):
contact = Contact.objects.create()
com_website, _ = Website.objects.get_or_create(website="igorville.com")
gov_website, _ = Website.objects.get_or_create(website="igorville.gov")
domain, _ = Domain.objects.get_or_create(name="igorville.gov")
domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov")
application = DomainApplication.objects.create(
creator=user,
investigator=user,
@ -100,7 +101,7 @@ class TestDomainApplication(TestCase):
def test_status_fsm_submit_succeed(self):
user, _ = User.objects.get_or_create()
site = Domain.objects.create(name="igorville.gov")
site = DraftDomain.objects.create(name="igorville.gov")
application = DomainApplication.objects.create(
creator=user, requested_domain=site
)
@ -113,7 +114,7 @@ class TestDomainApplication(TestCase):
"""Create an application and submit it and see if email was sent."""
user, _ = User.objects.get_or_create()
contact = Contact.objects.create(email="test@test.gov")
domain, _ = Domain.objects.get_or_create(name="igorville.gov")
domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov")
application = DomainApplication.objects.create(
creator=user,
requested_domain=domain,
@ -135,62 +136,22 @@ class TestDomainApplication(TestCase):
)
class TestDomain(TestCase):
def test_empty_create_fails(self):
"""Can't create a completely empty domain."""
with self.assertRaisesRegex(IntegrityError, "name"):
Domain.objects.create()
def test_minimal_create(self):
"""Can create with just a name."""
domain = Domain.objects.create(name="igorville.gov")
self.assertEqual(domain.is_active, False)
@skip("cannot activate a domain without mock registry")
def test_get_status(self):
"""Returns proper status based on `is_active`."""
domain = Domain.objects.create(name="igorville.gov")
domain.save()
self.assertEqual(None, domain.status)
domain.activate()
domain.save()
self.assertIn("ok", domain.status)
def test_fsm_activate_fail_unique(self):
"""Can't activate domain if name is not unique."""
d1, _ = Domain.objects.get_or_create(name="igorville.gov")
d2, _ = Domain.objects.get_or_create(name="igorville.gov")
d1.activate()
d1.save()
with self.assertRaises(ValueError):
d2.activate()
def test_fsm_activate_fail_unapproved(self):
"""Can't activate domain if application isn't approved."""
d1, _ = Domain.objects.get_or_create(name="igorville.gov")
user, _ = User.objects.get_or_create()
application = DomainApplication.objects.create(creator=user)
d1.domain_application = application
d1.save()
with self.assertRaises(ValueError):
d1.activate()
class TestPermissions(TestCase):
"""Test the User-Domain-Role connection."""
def test_approval_creates_role(self):
domain, _ = Domain.objects.get_or_create(name="igorville.gov")
draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov")
user, _ = User.objects.get_or_create()
application = DomainApplication.objects.create(
creator=user, requested_domain=domain
creator=user, requested_domain=draft_domain
)
# skip using the submit method
application.status = DomainApplication.SUBMITTED
application.approve()
# should be a role for this user
domain = Domain.objects.get(name="igorville.gov")
self.assertTrue(UserDomainRole.objects.get(user=user, domain=domain))
@ -199,16 +160,17 @@ class TestDomainInfo(TestCase):
"""Test creation of Domain Information when approved."""
def test_approval_creates_info(self):
domain, _ = Domain.objects.get_or_create(name="igorville.gov")
draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov")
user, _ = User.objects.get_or_create()
application = DomainApplication.objects.create(
creator=user, requested_domain=domain
creator=user, requested_domain=draft_domain
)
# skip using the submit method
application.status = DomainApplication.SUBMITTED
application.approve()
# should be an information present for this domain
domain = Domain.objects.get(name="igorville.gov")
self.assertTrue(DomainInformation.objects.get(domain=domain))

View file

@ -0,0 +1,54 @@
from django.test import TestCase
from django.db.utils import IntegrityError
from registrar.models import (
DomainApplication,
User,
Domain,
)
from unittest import skip
class TestDomain(TestCase):
def test_empty_create_fails(self):
"""Can't create a completely empty domain."""
with self.assertRaisesRegex(IntegrityError, "name"):
Domain.objects.create()
def test_minimal_create(self):
"""Can create with just a name."""
Domain.objects.create(name="igorville.gov")
# this assertion will not work -- for now, the fact that the
# above command didn't error out is proof enough
# self.assertEquals(domain.state, Domain.State.DRAFTED)
@skip("cannot activate a domain without mock registry")
def test_get_status(self):
"""Returns proper status based on `state`."""
domain = Domain.objects.create(name="igorville.gov")
domain.save()
self.assertEqual(None, domain.status)
domain.activate()
domain.save()
self.assertIn("ok", domain.status)
@skip("cannot activate a domain without mock registry")
def test_fsm_activate_fail_unique(self):
"""Can't activate domain if name is not unique."""
d1, _ = Domain.objects.get_or_create(name="igorville.gov")
d2, _ = Domain.objects.get_or_create(name="igorville.gov")
d1.activate()
d1.save()
with self.assertRaises(ValueError):
d2.activate()
@skip("cannot activate a domain without mock registry")
def test_fsm_activate_fail_unapproved(self):
"""Can't activate domain if application isn't approved."""
d1, _ = Domain.objects.get_or_create(name="igorville.gov")
user, _ = User.objects.get_or_create()
application = DomainApplication.objects.create(creator=user)
d1.domain_application = application
d1.save()
with self.assertRaises(ValueError):
d1.activate()

View file

@ -13,6 +13,8 @@ import boto3_mocking # type: ignore
from registrar.models import (
DomainApplication,
Domain,
DomainInformation,
DraftDomain,
DomainInvitation,
Contact,
Website,
@ -75,7 +77,7 @@ class LoggedInTests(TestWithUser):
def test_home_lists_domain_applications(self):
response = self.client.get("/")
self.assertNotContains(response, "igorville.gov")
site = Domain.objects.create(name="igorville.gov")
site = DraftDomain.objects.create(name="igorville.gov")
application = DomainApplication.objects.create(
creator=self.user, requested_domain=site
)
@ -1029,12 +1031,18 @@ class TestWithDomainPermissions(TestWithUser):
def setUp(self):
super().setUp()
self.domain, _ = Domain.objects.get_or_create(name="igorville.gov")
self.domain_information, _ = DomainInformation.objects.get_or_create(
creator=self.user, domain=self.domain
)
self.role, _ = UserDomainRole.objects.get_or_create(
user=self.user, domain=self.domain, role=UserDomainRole.Roles.ADMIN
)
def tearDown(self):
try:
self.domain_information.delete()
if hasattr(self.domain, "contacts"):
self.domain.contacts.all().delete()
self.domain.delete()
self.role.delete()
except ValueError: # pass if already deleted
@ -1045,48 +1053,43 @@ class TestWithDomainPermissions(TestWithUser):
class TestDomainPermissions(TestWithDomainPermissions):
def test_not_logged_in(self):
"""Not logged in gets a redirect to Login."""
response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id}))
self.assertEqual(response.status_code, 302)
response = self.client.get(
reverse("domain-users", kwargs={"pk": self.domain.id})
)
self.assertEqual(response.status_code, 302)
response = self.client.get(
reverse("domain-users-add", kwargs={"pk": self.domain.id})
)
self.assertEqual(response.status_code, 302)
response = self.client.get(
reverse("domain-nameservers", kwargs={"pk": self.domain.id})
)
self.assertEqual(response.status_code, 302)
for view_name in [
"domain",
"domain-users",
"domain-users-add",
"domain-nameservers",
"domain-your-contact-information",
"domain-security-email",
]:
with self.subTest(view_name=view_name):
response = self.client.get(
reverse(view_name, kwargs={"pk": self.domain.id})
)
self.assertEqual(response.status_code, 302)
def test_no_domain_role(self):
"""Logged in but no role gets 403 Forbidden."""
self.client.force_login(self.user)
self.role.delete() # user no longer has a role on this domain
with less_console_noise():
response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id}))
self.assertEqual(response.status_code, 403)
for view_name in [
"domain",
"domain-users",
"domain-users-add",
"domain-nameservers",
"domain-your-contact-information",
"domain-security-email",
]:
with self.subTest(view_name=view_name):
with less_console_noise():
response = self.client.get(
reverse(view_name, kwargs={"pk": self.domain.id})
)
self.assertEqual(response.status_code, 403)
with less_console_noise():
response = self.client.get(
reverse("domain-users", kwargs={"pk": self.domain.id})
)
self.assertEqual(response.status_code, 403)
with less_console_noise():
response = self.client.get(
reverse("domain-users-add", kwargs={"pk": self.domain.id})
)
self.assertEqual(response.status_code, 403)
with less_console_noise():
response = self.client.get(
reverse("domain-nameservers", kwargs={"pk": self.domain.id})
reverse("domain-security-email", kwargs={"pk": self.domain.id})
)
self.assertEqual(response.status_code, 403)
@ -1125,7 +1128,7 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest):
self.assertContains(response, "Add another user")
def test_domain_user_add_form(self):
"""Adding a user works."""
"""Adding an existing user works."""
other_user, _ = get_user_model().objects.get_or_create(
email="mayor@igorville.gov"
)
@ -1208,6 +1211,22 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest):
with self.assertRaises(DomainInvitation.DoesNotExist):
DomainInvitation.objects.get(id=invitation.id)
def test_domain_invitation_cancel_no_permissions(self):
"""Posting to the delete view as a different user should fail."""
EMAIL = "mayor@igorville.gov"
invitation, _ = DomainInvitation.objects.get_or_create(
domain=self.domain, email=EMAIL
)
other_user = User()
other_user.save()
self.client.force_login(other_user)
with less_console_noise(): # permission denied makes console errors
result = self.client.post(
reverse("invitation-delete", kwargs={"pk": invitation.id})
)
self.assertEqual(result.status_code, 403)
@boto3_mocking.patching
def test_domain_invitation_flow(self):
"""Send an invitation to a new user, log in and load the dashboard."""
@ -1282,11 +1301,60 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest):
# the field.
self.assertContains(result, "This field is required", count=2, status_code=200)
def test_domain_your_contact_information(self):
"""Can load domain's your contact information page."""
page = self.client.get(
reverse("domain-your-contact-information", kwargs={"pk": self.domain.id})
)
self.assertContains(page, "Domain contact information")
def test_domain_your_contact_information_content(self):
"""Logged-in user's contact information appears on the page."""
self.user.contact.first_name = "Testy"
self.user.contact.save()
page = self.app.get(
reverse("domain-your-contact-information", kwargs={"pk": self.domain.id})
)
self.assertContains(page, "Testy")
def test_domain_security_email(self):
"""Can load domain's security email page."""
page = self.client.get(
reverse("domain-security-email", kwargs={"pk": self.domain.id})
)
self.assertContains(page, "Domain security email")
def test_domain_security_email_form(self):
"""Adding a security email works.
Uses self.app WebTest because we need to interact with forms.
"""
security_email_page = self.app.get(
reverse("domain-security-email", kwargs={"pk": self.domain.id})
)
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
security_email_page.form["security_email"] = "mayor@igorville.gov"
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
with less_console_noise(): # swallow log warning message
result = security_email_page.form.submit()
self.assertEqual(result.status_code, 302)
self.assertEqual(
result["Location"],
reverse("domain-security-email", kwargs={"pk": self.domain.id}),
)
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
success_page = result.follow()
self.assertContains(
success_page, "The security email for this domain have been updated"
)
class TestApplicationStatus(TestWithUser, WebTest):
def setUp(self):
super().setUp()
self.app.set_user(self.user.username)
self.client.force_login(self.user)
def _completed_application(
self,
@ -1304,7 +1372,7 @@ class TestApplicationStatus(TestWithUser, WebTest):
email="testy@town.com",
phone="(555) 555 5555",
)
domain, _ = Domain.objects.get_or_create(name="citystatus.gov")
domain, _ = DraftDomain.objects.get_or_create(name="citystatus.gov")
alt, _ = Website.objects.get_or_create(website="city1.gov")
current, _ = Website.objects.get_or_create(website="city.com")
you, _ = Contact.objects.get_or_create(
@ -1400,3 +1468,24 @@ class TestApplicationStatus(TestWithUser, WebTest):
)
home_page = self.app.get("/")
self.assertContains(home_page, "Withdrawn")
def test_application_status_no_permissions(self):
"""Can't access applications without being the creator."""
application = self._completed_application()
other_user = User()
other_user.save()
application.creator = other_user
application.save()
# PermissionDeniedErrors make lots of noise in test output
with less_console_noise():
for url_name in [
"application-status",
"application-withdraw-confirmation",
"application-withdrawn",
]:
with self.subTest(url_name=url_name):
page = self.client.get(
reverse(url_name, kwargs={"pk": application.pk})
)
self.assertEqual(page.status_code, 403)

View file

@ -2,6 +2,8 @@ from .application import *
from .domain import (
DomainView,
DomainNameserversView,
DomainYourContactInformationView,
DomainSecurityEmailView,
DomainUsersView,
DomainAddUserView,
DomainInvitationDeleteView,

View file

@ -6,7 +6,6 @@ from django.shortcuts import redirect, render
from django.urls import resolve, reverse
from django.utils.translation import gettext_lazy as _
from django.views.generic import TemplateView
from django.views import generic
from django.contrib import messages
from registrar.forms import application_wizard as forms
@ -14,7 +13,7 @@ from registrar.models import DomainApplication
from registrar.utility import StrEnum
from registrar.views.utility import StepsHelper
from .utility import DomainPermission
from .utility import DomainApplicationPermissionView
logger = logging.getLogger(__name__)
@ -478,29 +477,31 @@ class Finished(ApplicationWizard):
return render(self.request, self.template_name, context)
class ApplicationStatus(generic.DetailView):
model = DomainApplication
class ApplicationStatus(DomainApplicationPermissionView):
template_name = "application_status.html"
def get_context_data(self, **kwargs):
"""Get context details to process information from application"""
context = super(ApplicationStatus, self).get_context_data(**kwargs)
return context
class ApplicationWithdrawConfirmation(DomainApplicationPermissionView):
"""This page will ask user to confirm if they want to withdraw
class ApplicationWithdraw(LoginRequiredMixin, generic.DetailView, DomainPermission):
model = DomainApplication
template_name = "application_withdraw_confirmation.html"
""" The page above will display asking user to confirm if they want to withdraw;
Note it uses "DomainPermission" from Domain to ensure that the person who
applied only have access to withdraw the request
The DomainApplicationPermissionView restricts access so that only the
`creator` of the application may withdraw it.
"""
def updatestatus(request, pk):
"""If user click on withdraw confirm button, it will be updated to withdraw
and send back to homepage"""
application = DomainApplication.objects.get(id=pk)
template_name = "application_withdraw_confirmation.html"
class ApplicationWithdrawn(DomainApplicationPermissionView):
# this view renders no template
template_name = ""
def get(self, *args, **kwargs):
"""View class that does the actual withdrawing.
If user click on withdraw confirm button, this view updates the status
to withdraw and send back to homepage.
"""
application = DomainApplication.objects.get(id=self.kwargs["pk"])
application.status = "withdrawn"
application.save()
return HttpResponseRedirect(reverse("home"))

View file

@ -1,4 +1,9 @@
"""View for a single Domain."""
"""Views for a single Domain.
Authorization is handled by the `DomainPermissionView`. To ensure that only
authorized users can see information on a domain, every view here should
inherit from `DomainPermissionView` (or DomainInvitationPermissionDeleteView).
"""
import logging
@ -7,35 +12,39 @@ from django.contrib.messages.views import SuccessMessageMixin
from django.db import IntegrityError
from django.shortcuts import redirect
from django.urls import reverse
from django.views.generic import DetailView
from django.views.generic.edit import DeleteView, FormMixin
from django.views.generic.edit import FormMixin
from registrar.models import Domain, DomainInvitation, User, UserDomainRole
from registrar.models import (
DomainInvitation,
User,
UserDomainRole,
)
from ..forms import DomainAddUserForm, NameserverFormset
from ..forms import (
DomainAddUserForm,
NameserverFormset,
DomainSecurityEmailForm,
ContactForm,
)
from ..utility.email import send_templated_email, EmailSendingError
from .utility import DomainPermission
from .utility import DomainPermissionView, DomainInvitationPermissionDeleteView
logger = logging.getLogger(__name__)
class DomainView(DomainPermission, DetailView):
class DomainView(DomainPermissionView):
"""Domain detail overview page."""
model = Domain
template_name = "domain_detail.html"
context_object_name = "domain"
class DomainNameserversView(DomainPermission, FormMixin, DetailView):
class DomainNameserversView(DomainPermissionView, FormMixin):
"""Domain nameserver editing view."""
model = Domain
template_name = "domain_nameservers.html"
context_object_name = "domain"
form_class = NameserverFormset
def get_initial(self):
@ -44,7 +53,7 @@ class DomainNameserversView(DomainPermission, FormMixin, DetailView):
return [{"server": server} for server in domain.nameservers()]
def get_success_url(self):
"""Redirect to the overview page for the domain."""
"""Redirect to the nameservers page for the domain."""
return reverse("domain-nameservers", kwargs={"pk": self.object.pk})
def get_context_data(self, **kwargs):
@ -90,22 +99,103 @@ class DomainNameserversView(DomainPermission, FormMixin, DetailView):
domain.set_nameservers(nameservers)
messages.success(
self.request, "The name servers for this domain have been updated"
self.request, "The name servers for this domain have been updated."
)
# superclass has the redirect
return super().form_valid(formset)
class DomainUsersView(DomainPermission, DetailView):
class DomainYourContactInformationView(DomainPermissionView, FormMixin):
"""Domain your contact information editing view."""
template_name = "domain_your_contact_information.html"
form_class = ContactForm
def get_form_kwargs(self, *args, **kwargs):
"""Add domain_info.submitter instance to make a bound form."""
form_kwargs = super().get_form_kwargs(*args, **kwargs)
form_kwargs["instance"] = self.request.user.contact
return form_kwargs
def get_success_url(self):
"""Redirect to the your contact information for the domain."""
return reverse("domain-your-contact-information", kwargs={"pk": self.object.pk})
def post(self, request, *args, **kwargs):
"""Form submission posts to this view."""
self.object = self.get_object()
form = self.get_form()
if form.is_valid():
# there is a valid email address in the form
return self.form_valid(form)
else:
return self.form_invalid(form)
def form_valid(self, form):
"""The form is valid, call setter in model."""
# Post to DB using values from the form
form.save()
messages.success(
self.request, "Your contact information for this domain has been updated."
)
# superclass has the redirect
return super().form_valid(form)
class DomainSecurityEmailView(DomainPermissionView, FormMixin):
"""Domain security email editing view."""
template_name = "domain_security_email.html"
form_class = DomainSecurityEmailForm
def get_initial(self):
"""The initial value for the form."""
domain = self.get_object()
initial = super().get_initial()
initial["security_email"] = domain.security_email()
return initial
def get_success_url(self):
"""Redirect to the security email page for the domain."""
return reverse("domain-security-email", kwargs={"pk": self.object.pk})
def post(self, request, *args, **kwargs):
"""Form submission posts to this view."""
self.object = self.get_object()
form = self.get_form()
if form.is_valid():
# there is a valid email address in the form
return self.form_valid(form)
else:
return self.form_invalid(form)
def form_valid(self, form):
"""The form is valid, call setter in model."""
# Set the security email from the form
new_email = form.cleaned_data.get("security_email", "")
domain = self.get_object()
domain.set_security_email(new_email)
messages.success(
self.request, "The security email for this domain have been updated."
)
# superclass has the redirect
return redirect(self.get_success_url())
class DomainUsersView(DomainPermissionView):
"""User management page in the domain details."""
model = Domain
template_name = "domain_users.html"
context_object_name = "domain"
class DomainAddUserView(DomainPermission, FormMixin, DetailView):
class DomainAddUserView(DomainPermissionView, FormMixin):
"""Inside of a domain's user management, a form for adding users.
@ -114,7 +204,6 @@ class DomainAddUserView(DomainPermission, FormMixin, DetailView):
"""
template_name = "domain_add_user.html"
model = Domain
form_class = DomainAddUserForm
def get_success_url(self):
@ -194,8 +283,9 @@ class DomainAddUserView(DomainPermission, FormMixin, DetailView):
return redirect(self.get_success_url())
class DomainInvitationDeleteView(SuccessMessageMixin, DeleteView):
model = DomainInvitation
class DomainInvitationDeleteView(
DomainInvitationPermissionDeleteView, SuccessMessageMixin
):
object: DomainInvitation # workaround for type mismatch in DeleteView
def get_success_url(self):

View file

@ -1,3 +1,8 @@
from .steps_helper import StepsHelper
from .always_404 import always_404
from .mixins import DomainPermission
from .permission_views import (
DomainPermissionView,
DomainApplicationPermissionView,
DomainInvitationPermissionDeleteView,
)

View file

@ -2,7 +2,7 @@
from django.contrib.auth.mixins import PermissionRequiredMixin
from registrar.models import UserDomainRole
from registrar.models import UserDomainRole, DomainApplication, DomainInvitation
class PermissionsLoginMixin(PermissionRequiredMixin):
@ -35,3 +35,48 @@ class DomainPermission(PermissionsLoginMixin):
# if we need to check more about the nature of role, do it here.
return True
class DomainApplicationPermission(PermissionsLoginMixin):
"""Does the logged-in user have access to this domain application?"""
def has_permission(self):
"""Check if this user has access to this domain application.
The user is in self.request.user and the domain needs to be looked
up from the domain's primary key in self.kwargs["pk"]
"""
if not self.request.user.is_authenticated:
return False
# user needs to be the creator of the application
# this query is empty if there isn't a domain application with this
# id and this user as creator
if not DomainApplication.objects.filter(
creator=self.request.user, id=self.kwargs["pk"]
).exists():
return False
return True
class DomainInvitationPermission(PermissionsLoginMixin):
"""Does the logged-in user have access to this domain invitation?
A user has access to a domain invitation if they have a role on the
associated domain.
"""
def has_permission(self):
"""Check if this user has a role on the domain of this invitation."""
if not self.request.user.is_authenticated:
return False
if not DomainInvitation.objects.filter(
id=self.kwargs["pk"], domain__permissions__user=self.request.user
).exists():
return False
return True

View file

@ -0,0 +1,69 @@
"""View classes that enforce authorization."""
import abc # abstract base class
from django.views.generic import DetailView, DeleteView
from registrar.models import Domain, DomainApplication, DomainInvitation
from .mixins import (
DomainPermission,
DomainApplicationPermission,
DomainInvitationPermission,
)
class DomainPermissionView(DomainPermission, DetailView, abc.ABC):
"""Abstract base view for domains that enforces permissions.
This abstract view cannot be instantiated. Actual views must specify
`template_name`.
"""
# DetailView property for what model this is viewing
model = Domain
# variable name in template context for the model object
context_object_name = "domain"
# Abstract property enforces NotImplementedError on an attribute.
@property
@abc.abstractmethod
def template_name(self):
raise NotImplementedError
class DomainApplicationPermissionView(DomainApplicationPermission, DetailView, abc.ABC):
"""Abstract base view for domain applications that enforces permissions
This abstract view cannot be instantiated. Actual views must specify
`template_name`.
"""
# DetailView property for what model this is viewing
model = DomainApplication
# variable name in template context for the model object
context_object_name = "domainapplication"
# Abstract property enforces NotImplementedError on an attribute.
@property
@abc.abstractmethod
def template_name(self):
raise NotImplementedError
class DomainInvitationPermissionDeleteView(
DomainInvitationPermission, DeleteView, abc.ABC
):
"""Abstract view for deleting a domain invitation.
This one is fairly specialized, but this is the only thing that we do
right now with domain invitations. We still have the full
`DomainInvitationPermission` class, but here we just pair it with a
DeleteView.
"""
model = DomainInvitation
object: DomainInvitation # workaround for type mismatch in DeleteView

View file

@ -52,6 +52,8 @@
10038 OUTOFSCOPE http://app:8080/users
10038 OUTOFSCOPE http://app:8080/users/add
10038 OUTOFSCOPE http://app:8080/nameservers
10038 OUTOFSCOPE http://app:8080/your-contact-information
10038 OUTOFSCOPE http://app:8080/security-email
10038 OUTOFSCOPE http://app:8080/delete
10038 OUTOFSCOPE http://app:8080/withdraw
10038 OUTOFSCOPE http://app:8080/withdrawconfirmed