mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-05 09:21:54 +02:00
Merge branch 'main' into nmb/612-public-site
This commit is contained in:
commit
166491673e
372 changed files with 2035 additions and 19569 deletions
|
@ -37,3 +37,4 @@ django-webtest = "*"
|
|||
types-cachetools = "*"
|
||||
boto3-mocking = "*"
|
||||
boto3-stubs = "*"
|
||||
django-model2puml = "*"
|
||||
|
|
171
src/Pipfile.lock
generated
171
src/Pipfile.lock
generated
|
@ -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": [
|
||||
|
|
|
@ -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"]}
|
||||
|
|
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -1,2 +1,7 @@
|
|||
from .application_wizard import *
|
||||
from .domain import DomainAddUserForm, NameserverFormset
|
||||
from .domain import (
|
||||
DomainAddUserForm,
|
||||
NameserverFormset,
|
||||
DomainSecurityEmailForm,
|
||||
ContactForm,
|
||||
)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
),
|
||||
]
|
|
@ -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"
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
22
src/registrar/models/draft_domain.py
Normal file
22
src/registrar/models/draft_domain.py
Normal 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",
|
||||
)
|
|
@ -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}>"
|
||||
|
|
64
src/registrar/models/utility/domain_helper.py
Normal file
64
src/registrar/models/utility/domain_helper.py
Normal 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 ""
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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> We’re building a new way to get a .gov. Take a look around, but don’t rely on this site yet. This site is for testing purposes only. Don’t 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">
|
||||
|
|
|
@ -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 we’ll 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 #}
|
||||
|
|
27
src/registrar/templates/domain_security_email.html
Normal file
27
src/registrar/templates/domain_security_email.html
Normal 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 individual’s 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 #}
|
|
@ -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 %}
|
||||
>
|
||||
|
|
35
src/registrar/templates/domain_your_contact_information.html
Normal file
35
src/registrar/templates/domain_your_contact_information.html
Normal 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 you’d like us to use a different name, email, or phone number you can make those changes below. Changing your contact information here won’t 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 #}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
||||
|
|
54
src/registrar/tests/test_models_domain.py
Normal file
54
src/registrar/tests/test_models_domain.py
Normal 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()
|
|
@ -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)
|
||||
|
|
|
@ -2,6 +2,8 @@ from .application import *
|
|||
from .domain import (
|
||||
DomainView,
|
||||
DomainNameserversView,
|
||||
DomainYourContactInformationView,
|
||||
DomainSecurityEmailView,
|
||||
DomainUsersView,
|
||||
DomainAddUserView,
|
||||
DomainInvitationDeleteView,
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
69
src/registrar/views/utility/permission_views.py
Normal file
69
src/registrar/views/utility/permission_views.py
Normal 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
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue