2
0
mirror of https://github.com/VinylDNS/vinyldns synced 2025-08-30 13:58:15 +00:00

Implement domains requiring manual review (via batch change interface) (#779)

* Implement domains requiring review.
* Update configs.
* Update tests.
This commit is contained in:
Michael Ly
2019-08-07 10:33:18 -04:00
committed by GitHub
parent e0456491fb
commit 9014570a37
14 changed files with 238 additions and 48 deletions

View File

@@ -207,6 +207,21 @@ vinyldns {
] ]
} }
# FQDNs / IPs that require manual review upon submission in batch change interface
# domain-list used for all record types except PTR
# ip-list used exclusively for PTR records
manual-review-domains = {
domain-list = [
"needs-review.*"
]
ip-list = [
"192.0.2.254",
"192.0.2.255",
"fd69:27cc:fe91:0:0:0:ffff:1",
"fd69:27cc:fe91:0:0:0:ffff:2"
]
}
# types of unowned records that users can access in shared zones # types of unowned records that users can access in shared zones
shared-approved-types = ["A", "AAAA", "CNAME", "PTR", "TXT"] shared-approved-types = ["A", "AAAA", "CNAME", "PTR", "TXT"]

View File

@@ -9,3 +9,4 @@ $ttl 38400
4.2.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0 IN PTR www.vinyldns. 4.2.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0 IN PTR www.vinyldns.
5.2.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0 IN PTR mail.vinyldns. 5.2.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0 IN PTR mail.vinyldns.
0.0.0.0.f.f.f.f.0.0.0.0.0.0.0.0.0.0.0.0 IN PTR high.value.domain.ip6. 0.0.0.0.f.f.f.f.0.0.0.0.0.0.0.0.0.0.0.0 IN PTR high.value.domain.ip6.
2.0.0.0.f.f.f.f.0.0.0.0.0.0.0.0.0.0.0.0 IN PTR needs.review.domain.ip6.

View File

@@ -12,3 +12,4 @@ $ttl 38400
194 IN CNAME 194.192/30.2.0.192.in-addr.arpa. 194 IN CNAME 194.192/30.2.0.192.in-addr.arpa.
195 IN CNAME 195.192/30.2.0.192.in-addr.arpa. 195 IN CNAME 195.192/30.2.0.192.in-addr.arpa.
253 IN PTR high.value.domain.ip4. 253 IN PTR high.value.domain.ip4.
255 IN PTR needs.review.domain.ip4

View File

@@ -11,7 +11,8 @@ def test_approve_pending_batch_change_success(shared_zone_test_context):
approver = shared_zone_test_context.support_user_client approver = shared_zone_test_context.support_user_client
batch_change_input = { batch_change_input = {
"changes": [ "changes": [
get_change_A_AAAA_json("test-approve-success.not.loaded.", address="4.3.2.1") get_change_A_AAAA_json("test-approve-success.not.loaded.", address="4.3.2.1"),
get_change_A_AAAA_json("needs-review.not.loaded.", address="4.3.2.1"),
], ],
"ownerGroupId": shared_zone_test_context.ok_group['id'] "ownerGroupId": shared_zone_test_context.ok_group['id']
} }
@@ -25,6 +26,8 @@ def test_approve_pending_batch_change_success(shared_zone_test_context):
assert_that(get_batch['approvalStatus'], is_('PendingReview')) assert_that(get_batch['approvalStatus'], is_('PendingReview'))
assert_that(get_batch['changes'][0]['status'], is_('NeedsReview')) assert_that(get_batch['changes'][0]['status'], is_('NeedsReview'))
assert_that(get_batch['changes'][0]['validationErrors'][0]['errorType'], is_('ZoneDiscoveryError')) assert_that(get_batch['changes'][0]['validationErrors'][0]['errorType'], is_('ZoneDiscoveryError'))
assert_that(get_batch['changes'][1]['status'], is_('NeedsReview'))
assert_that(get_batch['changes'][1]['validationErrors'][0]['errorType'], is_('RecordRequiresManualReview'))
# need to create the zone so the change can succeed # need to create the zone so the change can succeed
zone = { zone = {
@@ -43,12 +46,13 @@ def test_approve_pending_batch_change_success(shared_zone_test_context):
to_delete = [(change['zoneId'], change['recordSetId']) for change in completed_batch['changes']] to_delete = [(change['zoneId'], change['recordSetId']) for change in completed_batch['changes']]
assert_that(completed_batch['status'], is_('Complete')) assert_that(completed_batch['status'], is_('Complete'))
assert_that(completed_batch['changes'][0]['status'], is_('Complete')) for change in completed_batch['changes']:
assert_that(change['status'], is_('Complete'))
assert_that(len(change['validationErrors']), is_(0))
assert_that(completed_batch['approvalStatus'], is_('ManuallyApproved')) assert_that(completed_batch['approvalStatus'], is_('ManuallyApproved'))
assert_that(completed_batch['reviewerId'], is_('support-user-id')) assert_that(completed_batch['reviewerId'], is_('support-user-id'))
assert_that(completed_batch['reviewerUserName'], is_('support-user')) assert_that(completed_batch['reviewerUserName'], is_('support-user'))
assert_that(completed_batch, has_key('reviewTimestamp')) assert_that(completed_batch, has_key('reviewTimestamp'))
assert_that(len(completed_batch['changes'][0]['validationErrors']), is_(0))
finally: finally:
clear_zoneid_rsid_tuple_list(to_delete, client) clear_zoneid_rsid_tuple_list(to_delete, client)
if to_disconnect is not None: if to_disconnect is not None:

View File

@@ -882,6 +882,52 @@ def test_create_batch_change_with_high_value_domain_fails(shared_zone_test_conte
assert_that(response[12], is_not(has_key("errors"))) assert_that(response[12], is_not(has_key("errors")))
@pytest.mark.manual_batch_review
def test_create_batch_change_with_domains_requiring_review_succeeds(shared_zone_test_context):
"""
Test creating a batch change with an input name requiring review is accepted
"""
rejecter = shared_zone_test_context.support_user_client
client = shared_zone_test_context.ok_vinyldns_client
batch_change_input = {
"ownerGroupId": shared_zone_test_context.ok_group['id'],
"comments": "this is optional",
"changes": [
get_change_A_AAAA_json("needs-review-add.ok."),
get_change_A_AAAA_json("needs-review-update.ok.", change_type="DeleteRecordSet"),
get_change_A_AAAA_json("needs-review-update.ok."),
get_change_A_AAAA_json("needs-review-delete.ok.", change_type="DeleteRecordSet"),
get_change_PTR_json("192.0.2.254"),
get_change_PTR_json("192.0.2.255", change_type="DeleteRecordSet"), # 255 exists already
get_change_PTR_json("192.0.2.255"),
get_change_PTR_json("192.0.2.255", change_type="DeleteRecordSet"),
get_change_PTR_json("fd69:27cc:fe91:0:0:0:ffff:1"),
get_change_PTR_json("fd69:27cc:fe91:0:0:0:ffff:2", change_type="DeleteRecordSet"), # ffff:2 exists already
get_change_PTR_json("fd69:27cc:fe91:0:0:0:ffff:2"),
get_change_PTR_json("fd69:27cc:fe91:0:0:0:ffff:2", change_type="DeleteRecordSet"),
get_change_A_AAAA_json("i-can-be-touched.ok.", address="1.1.1.1")
]
}
response = None
try:
response = client.create_batch_change(batch_change_input, status=202)
get_batch = client.get_batch_change(response['id'])
assert_that(get_batch['status'], is_('PendingReview'))
assert_that(get_batch['approvalStatus'], is_('PendingReview'))
for i in xrange(1, 11):
assert_that(get_batch['changes'][i]['status'], is_('NeedsReview'))
assert_that(get_batch['changes'][i]['validationErrors'][0]['errorType'], is_('RecordRequiresManualReview'))
assert_that(get_batch['changes'][12]['validationErrors'], empty())
finally:
# Clean up so data doesn't change
if response:
rejecter.reject_batch_change(response['id'], status=200)
def test_create_batch_change_with_invalid_record_type_fails(shared_zone_test_context): def test_create_batch_change_with_invalid_record_type_fails(shared_zone_test_context):
""" """
Test creating a batch change with invalid record type fails Test creating a batch change with invalid record type fails

View File

@@ -113,6 +113,21 @@ vinyldns {
] ]
} }
# FQDNs / IPs that require manual review upon submission in batch change interface
# domain-list used for all record types except PTR
# ip-list used exclusively for PTR records
manual-review-domains = {
domain-list = [
"needs-review.*"
]
ip-list = [
"192.0.2.254",
"192.0.2.255",
"fd69:27cc:fe91:0:0:0:ffff:1",
"fd69:27cc:fe91:0:0:0:ffff:2"
]
}
# types of unowned records that users can access in shared zones # types of unowned records that users can access in shared zones
shared-approved-types = ["A", "AAAA", "CNAME", "PTR", "TXT"] shared-approved-types = ["A", "AAAA", "CNAME", "PTR", "TXT"]

View File

@@ -137,4 +137,11 @@ object VinylDNSConfig {
lazy val scheduledChangesEnabled: Boolean = vinyldnsConfig lazy val scheduledChangesEnabled: Boolean = vinyldnsConfig
.as[Option[Boolean]]("scheduled-changes-enabled") .as[Option[Boolean]]("scheduled-changes-enabled")
.getOrElse(false) .getOrElse(false)
lazy val domainListRequiringManualReview: List[Regex] =
ZoneRecordValidations.toCaseIgnoredRegexList(
getOptionalStringList("manual-review-domains.domain-list"))
lazy val ipListRequiringManualReview: List[IpAddress] =
getOptionalStringList("manual-review-domains.ip-list").flatMap(ip => IpAddress(ip))
} }

View File

@@ -91,7 +91,10 @@ class BatchChangeService(
auth: AuthPrincipal, auth: AuthPrincipal,
allowManualReview: Boolean): BatchResult[BatchChange] = allowManualReview: Boolean): BatchResult[BatchChange] =
for { for {
validationOutput <- applyBatchChangeValidationFlow(batchChangeInput, auth) validationOutput <- applyBatchChangeValidationFlow(
batchChangeInput,
auth,
isApproved = false)
changeForConversion <- buildResponse( changeForConversion <- buildResponse(
batchChangeInput, batchChangeInput,
validationOutput.validatedChanges, validationOutput.validatedChanges,
@@ -107,11 +110,12 @@ class BatchChangeService(
def applyBatchChangeValidationFlow( def applyBatchChangeValidationFlow(
batchChangeInput: BatchChangeInput, batchChangeInput: BatchChangeInput,
auth: AuthPrincipal): BatchResult[BatchValidationFlowOutput] = auth: AuthPrincipal,
isApproved: Boolean): BatchResult[BatchValidationFlowOutput] =
for { for {
existingGroup <- getOwnerGroup(batchChangeInput.ownerGroupId) existingGroup <- getOwnerGroup(batchChangeInput.ownerGroupId)
_ <- validateBatchChangeInput(batchChangeInput, existingGroup, auth) _ <- validateBatchChangeInput(batchChangeInput, existingGroup, auth)
inputValidatedSingleChanges = validateInputChanges(batchChangeInput.changes) inputValidatedSingleChanges = validateInputChanges(batchChangeInput.changes, isApproved)
zoneMap <- getZonesForRequest(inputValidatedSingleChanges).toBatchResult zoneMap <- getZonesForRequest(inputValidatedSingleChanges).toBatchResult
changesWithZones = zoneDiscovery(inputValidatedSingleChanges, zoneMap) changesWithZones = zoneDiscovery(inputValidatedSingleChanges, zoneMap)
recordSets <- getExistingRecordSets(changesWithZones, zoneMap).toBatchResult recordSets <- getExistingRecordSets(changesWithZones, zoneMap).toBatchResult
@@ -152,7 +156,7 @@ class BatchChangeService(
reviewInfo = BatchChangeReviewInfo( reviewInfo = BatchChangeReviewInfo(
authPrincipal.userId, authPrincipal.userId,
approveBatchChangeInput.reviewComment) approveBatchChangeInput.reviewComment)
validationOutput <- applyBatchChangeValidationFlow(asInput, requesterAuth) validationOutput <- applyBatchChangeValidationFlow(asInput, requesterAuth, isApproved = true)
changeForConversion <- buildResponseForApprover( changeForConversion <- buildResponseForApprover(
batchChange, batchChange,
asInput, asInput,

View File

@@ -37,7 +37,9 @@ trait BatchChangeValidationsAlgebra {
existingGroup: Option[Group], existingGroup: Option[Group],
authPrincipal: AuthPrincipal): BatchResult[Unit] authPrincipal: AuthPrincipal): BatchResult[Unit]
def validateInputChanges(input: List[ChangeInput]): ValidatedBatch[ChangeInput] def validateInputChanges(
input: List[ChangeInput],
isApproved: Boolean): ValidatedBatch[ChangeInput]
def validateChangesWithContext( def validateChangesWithContext(
changes: ValidatedBatch[ChangeForValidation], changes: ValidatedBatch[ChangeForValidation],
@@ -143,16 +145,20 @@ class BatchChangeValidations(
/* input validations */ /* input validations */
def validateInputChanges(input: List[ChangeInput]): ValidatedBatch[ChangeInput] = def validateInputChanges(
input: List[ChangeInput],
isApproved: Boolean): ValidatedBatch[ChangeInput] =
input.map { input.map {
case a: AddChangeInput => validateAddChangeInput(a).map(_ => a) case a: AddChangeInput => validateAddChangeInput(a, isApproved).map(_ => a)
case d: DeleteChangeInput => validateInputName(d).map(_ => d) case d: DeleteChangeInput => validateInputName(d, isApproved).map(_ => d)
} }
def validateAddChangeInput(addChangeInput: AddChangeInput): SingleValidation[Unit] = { def validateAddChangeInput(
addChangeInput: AddChangeInput,
isApproved: Boolean): SingleValidation[Unit] = {
val validTTL = addChangeInput.ttl.map(validateTTL(_).asUnit).getOrElse(().valid) val validTTL = addChangeInput.ttl.map(validateTTL(_).asUnit).getOrElse(().valid)
val validRecord = validateRecordData(addChangeInput.record) val validRecord = validateRecordData(addChangeInput.record)
val validInput = validateInputName(addChangeInput) val validInput = validateInputName(addChangeInput, isApproved)
validTTL |+| validRecord |+| validInput validTTL |+| validRecord |+| validInput
} }
@@ -170,7 +176,7 @@ class BatchChangeValidations(
InvalidBatchRecordType(other.toString, SupportedBatchChangeRecordTypes.get).invalidNel[Unit] InvalidBatchRecordType(other.toString, SupportedBatchChangeRecordTypes.get).invalidNel[Unit]
} }
def validateInputName(change: ChangeInput): SingleValidation[Unit] = { def validateInputName(change: ChangeInput, isApproved: Boolean): SingleValidation[Unit] = {
val typedChecks = change.typ match { val typedChecks = change.typ match {
case A | AAAA | MX => case A | AAAA | MX =>
validateHostName(change.inputName).asUnit |+| notInReverseZone(change) validateHostName(change.inputName).asUnit |+| notInReverseZone(change)
@@ -181,7 +187,7 @@ class BatchChangeValidations(
case other => case other =>
InvalidBatchRecordType(other.toString, SupportedBatchChangeRecordTypes.get).invalidNel[Unit] InvalidBatchRecordType(other.toString, SupportedBatchChangeRecordTypes.get).invalidNel[Unit]
} }
typedChecks |+| isNotHighValueDomain(change) typedChecks |+| isNotHighValueDomain(change) |+| doesNotRequireManualReview(change, isApproved)
} }
def validatePtrIp(ip: String): SingleValidation[Unit] = { def validatePtrIp(ip: String): SingleValidation[Unit] = {
@@ -489,6 +495,25 @@ class BatchChangeValidations(
change.inputName) change.inputName)
} }
def doesNotRequireManualReview(
change: ChangeInput,
isApproved: Boolean): SingleValidation[Unit] =
if (isApproved) {
// If we are reviewing, don't need to check whether DNS change needs review
().validNel
} else {
change.typ match {
case RecordType.PTR =>
ZoneRecordValidations.ipDoesNotRequireManualReview(
VinylDNSConfig.ipListRequiringManualReview,
change.inputName)
case _ =>
ZoneRecordValidations.domainDoesNotRequireManualReview(
VinylDNSConfig.domainListRequiringManualReview,
change.inputName)
}
}
def ownerGroupProvidedIfNeeded( def ownerGroupProvidedIfNeeded(
change: AddChangeForValidation, change: AddChangeForValidation,
existingRecord: Option[RecordSet], existingRecord: Option[RecordSet],

View File

@@ -20,7 +20,11 @@ import cats.implicits._
import cats.data._ import cats.data._
import com.comcast.ip4s.IpAddress import com.comcast.ip4s.IpAddress
import com.comcast.ip4s.interop.cats.implicits._ import com.comcast.ip4s.interop.cats.implicits._
import vinyldns.core.domain.{DomainValidationError, HighValueDomainError} import vinyldns.core.domain.{
DomainValidationError,
HighValueDomainError,
RecordRequiresManualReview
}
import vinyldns.core.domain.record.{NSData, RecordSet} import vinyldns.core.domain.record.{NSData, RecordSet}
import scala.util.matching.Regex import scala.util.matching.Regex
@@ -76,4 +80,22 @@ object ZoneRecordValidations {
} else { } else {
HighValueDomainError(ip).invalidNel HighValueDomainError(ip).invalidNel
} }
def domainDoesNotRequireManualReview(
regexList: List[Regex],
fqdn: String): ValidatedNel[DomainValidationError, Unit] =
if (!isStringInRegexList(regexList, fqdn)) {
().validNel
} else {
RecordRequiresManualReview(fqdn).invalidNel
}
def ipDoesNotRequireManualReview(
regexList: List[IpAddress],
ip: String): ValidatedNel[DomainValidationError, Unit] =
if (!isIpInIpList(regexList, ip)) {
().validNel
} else {
RecordRequiresManualReview(ip).invalidNel
}
} }

View File

@@ -41,6 +41,21 @@ vinyldns {
] ]
} }
# FQDNs / IPs that require manual review upon submission in batch change interface
# domain-list used for all record types except PTR
# ip-list used exclusively for PTR records
manual-review-domains = {
domain-list = [
"needs-review.*"
]
ip-list = [
"192.0.2.254",
"192.0.2.255",
"fd69:27cc:fe91:0:0:0:ffff:1",
"fd69:27cc:fe91:0:0:0:ffff:2"
]
}
# types of unowned records that users can access in shared zones # types of unowned records that users can access in shared zones
shared-approved-types = ["A", "AAAA", "CNAME", "PTR", "TXT"] shared-approved-types = ["A", "AAAA", "CNAME", "PTR", "TXT"]

View File

@@ -196,7 +196,7 @@ class BatchChangeValidationsSpec
property("validateInputChanges: should succeed if all inputs are good") { property("validateInputChanges: should succeed if all inputs are good") {
forAll(listOfN(3, validAChangeGen)) { input: List[ChangeInput] => forAll(listOfN(3, validAChangeGen)) { input: List[ChangeInput] =>
val result = validateInputChanges(input) val result = validateInputChanges(input, false)
result.map(_ shouldBe valid) result.map(_ shouldBe valid)
} }
} }
@@ -370,7 +370,9 @@ class BatchChangeValidationsSpec
val invalidIpv6Input = val invalidIpv6Input =
AddChangeInput("testbad.example.com.", RecordType.AAAA, ttl, AAAAData("invalidIpv6:123")) AddChangeInput("testbad.example.com.", RecordType.AAAA, ttl, AAAAData("invalidIpv6:123"))
val result = val result =
validateInputChanges(List(goodInput, goodAAAAInput, invalidDomainNameInput, invalidIpv6Input)) validateInputChanges(
List(goodInput, goodAAAAInput, invalidDomainNameInput, invalidIpv6Input),
false)
result(0) shouldBe valid result(0) shouldBe valid
result(1) shouldBe valid result(1) shouldBe valid
result(2) should haveInvalid[DomainValidationError](InvalidDomainName("invalidDomainName$.")) result(2) should haveInvalid[DomainValidationError](InvalidDomainName("invalidDomainName$."))
@@ -384,9 +386,9 @@ class BatchChangeValidationsSpec
val changeIpV6 = val changeIpV6 =
AddChangeInput("fd69:27cc:fe91:0:0:0:0:ffff", RecordType.PTR, ttl, PTRData("test.")) AddChangeInput("fd69:27cc:fe91:0:0:0:0:ffff", RecordType.PTR, ttl, PTRData("test."))
val resultA = validateInputName(changeA) val resultA = validateInputName(changeA, false)
val resultIpV4 = validateInputName(changeIpV4) val resultIpV4 = validateInputName(changeIpV4, false)
val resultIpV6 = validateInputName(changeIpV6) val resultIpV6 = validateInputName(changeIpV6, false)
resultA should haveInvalid[DomainValidationError]( resultA should haveInvalid[DomainValidationError](
HighValueDomainError("high-value-domain.foo.")) HighValueDomainError("high-value-domain.foo."))
@@ -395,10 +397,33 @@ class BatchChangeValidationsSpec
HighValueDomainError("fd69:27cc:fe91:0:0:0:0:ffff")) HighValueDomainError("fd69:27cc:fe91:0:0:0:0:ffff"))
} }
property("""validateInputName: should fail with a RecordRequiresManualReview
|if inputName is matches domain requiring manual review""".stripMargin) {
val changeA = AddChangeInput("needs-review.foo.", RecordType.A, ttl, AData("1.1.1.1"))
val changeIpV4 = AddChangeInput("192.0.2.254", RecordType.PTR, ttl, PTRData("test."))
val changeIpV6 =
AddChangeInput("fd69:27cc:fe91:0:0:0:ffff:1", RecordType.PTR, ttl, PTRData("test."))
val resultA = validateInputName(changeA, false)
val resultIpV4 = validateInputName(changeIpV4, false)
val resultIpV6 = validateInputName(changeIpV6, false)
resultA should haveInvalid[DomainValidationError](
RecordRequiresManualReview("needs-review.foo."))
resultIpV4 should haveInvalid[DomainValidationError](RecordRequiresManualReview("192.0.2.254"))
resultIpV6 should haveInvalid[DomainValidationError](
RecordRequiresManualReview("fd69:27cc:fe91:0:0:0:ffff:1"))
}
property("doesNotRequireManualReview: should succeed if user is reviewing") {
val changeA = AddChangeInput("needs-review.foo.", RecordType.A, ttl, AData("1.1.1.1"))
validateInputName(changeA, true) should beValid(())
}
property("""validateInputName: should fail with a DomainValidationError for deletes property("""validateInputName: should fail with a DomainValidationError for deletes
|if validateHostName fails for an invalid domain name""".stripMargin) { |if validateHostName fails for an invalid domain name""".stripMargin) {
val change = DeleteChangeInput("invalidDomainName$", RecordType.A) val change = DeleteChangeInput("invalidDomainName$", RecordType.A)
val result = validateInputName(change) val result = validateInputName(change, false)
result should haveInvalid[DomainValidationError](InvalidDomainName("invalidDomainName$.")) result should haveInvalid[DomainValidationError](InvalidDomainName("invalidDomainName$."))
} }
@@ -406,22 +431,22 @@ class BatchChangeValidationsSpec
|if validateHostName fails for an invalid domain name length""".stripMargin) { |if validateHostName fails for an invalid domain name length""".stripMargin) {
val invalidDomainName = Random.alphanumeric.take(256).mkString val invalidDomainName = Random.alphanumeric.take(256).mkString
val change = DeleteChangeInput(invalidDomainName, RecordType.AAAA) val change = DeleteChangeInput(invalidDomainName, RecordType.AAAA)
val result = validateInputName(change) val result = validateInputName(change, false)
result should (haveInvalid[DomainValidationError](InvalidDomainName(s"$invalidDomainName.")) result should haveInvalid[DomainValidationError](InvalidDomainName(s"$invalidDomainName."))
.and(haveInvalid[DomainValidationError](InvalidLength(s"$invalidDomainName.", 2, 255)))) .and(haveInvalid[DomainValidationError](InvalidLength(s"$invalidDomainName.", 2, 255)))
} }
property("""validateInputName: PTR should fail with InvalidIPAddress for deletes property("""validateInputName: PTR should fail with InvalidIPAddress for deletes
|if inputName is not a valid ipv4 or ipv6 address""".stripMargin) { |if inputName is not a valid ipv4 or ipv6 address""".stripMargin) {
val invalidIp = "invalidIp.111" val invalidIp = "invalidIp.111"
val change = DeleteChangeInput(invalidIp, RecordType.PTR) val change = DeleteChangeInput(invalidIp, RecordType.PTR)
val result = validateInputName(change) val result = validateInputName(change, false)
result should haveInvalid[DomainValidationError](InvalidIPAddress(invalidIp)) result should haveInvalid[DomainValidationError](InvalidIPAddress(invalidIp))
} }
property("validateAddChangeInput: should succeed if single addChangeInput is good for A Record") { property("validateAddChangeInput: should succeed if single addChangeInput is good for A Record") {
forAll(validAChangeGen) { input: AddChangeInput => forAll(validAChangeGen) { input: AddChangeInput =>
val result = validateAddChangeInput(input) val result = validateAddChangeInput(input, false)
result shouldBe valid result shouldBe valid
} }
} }
@@ -429,7 +454,7 @@ class BatchChangeValidationsSpec
property( property(
"validateAddChangeInput: should succeed if single addChangeInput is good for AAAA Record") { "validateAddChangeInput: should succeed if single addChangeInput is good for AAAA Record") {
forAll(validAAAAChangeGen) { input: AddChangeInput => forAll(validAAAAChangeGen) { input: AddChangeInput =>
val result = validateAddChangeInput(input) val result = validateAddChangeInput(input, false)
result shouldBe valid result shouldBe valid
} }
} }
@@ -437,7 +462,7 @@ class BatchChangeValidationsSpec
property("""validateAddChangeInput: should fail with a DomainValidationError property("""validateAddChangeInput: should fail with a DomainValidationError
|if validateHostName fails for an invalid domain name""".stripMargin) { |if validateHostName fails for an invalid domain name""".stripMargin) {
val change = AddChangeInput("invalidDomainName$", RecordType.A, ttl, AData("1.1.1.1")) val change = AddChangeInput("invalidDomainName$", RecordType.A, ttl, AData("1.1.1.1"))
val result = validateAddChangeInput(change) val result = validateAddChangeInput(change, false)
result should haveInvalid[DomainValidationError](InvalidDomainName("invalidDomainName$.")) result should haveInvalid[DomainValidationError](InvalidDomainName("invalidDomainName$."))
} }
@@ -445,7 +470,7 @@ class BatchChangeValidationsSpec
|if validateHostName fails for an invalid domain name length""".stripMargin) { |if validateHostName fails for an invalid domain name length""".stripMargin) {
val invalidDomainName = Random.alphanumeric.take(256).mkString val invalidDomainName = Random.alphanumeric.take(256).mkString
val change = AddChangeInput(invalidDomainName, RecordType.A, ttl, AData("1.1.1.1")) val change = AddChangeInput(invalidDomainName, RecordType.A, ttl, AData("1.1.1.1"))
val result = validateAddChangeInput(change) val result = validateAddChangeInput(change, false)
result should haveInvalid[DomainValidationError](InvalidDomainName(s"$invalidDomainName.")) result should haveInvalid[DomainValidationError](InvalidDomainName(s"$invalidDomainName."))
.and(haveInvalid[DomainValidationError](InvalidLength(s"$invalidDomainName.", 2, 255))) .and(haveInvalid[DomainValidationError](InvalidLength(s"$invalidDomainName.", 2, 255)))
} }
@@ -455,7 +480,7 @@ class BatchChangeValidationsSpec
forAll(choose[Long](0, 29)) { invalidTTL: Long => forAll(choose[Long](0, 29)) { invalidTTL: Long =>
val change = val change =
AddChangeInput("test.comcast.com.", RecordType.A, Some(invalidTTL), AData("1.1.1.1")) AddChangeInput("test.comcast.com.", RecordType.A, Some(invalidTTL), AData("1.1.1.1"))
val result = validateAddChangeInput(change) val result = validateAddChangeInput(change, false)
result should haveInvalid[DomainValidationError]( result should haveInvalid[DomainValidationError](
InvalidTTL(invalidTTL, DomainValidations.TTL_MIN_LENGTH, DomainValidations.TTL_MAX_LENGTH)) InvalidTTL(invalidTTL, DomainValidations.TTL_MIN_LENGTH, DomainValidations.TTL_MAX_LENGTH))
} }
@@ -465,7 +490,7 @@ class BatchChangeValidationsSpec
|if validateRecordData fails for an invalid ipv4 address""".stripMargin) { |if validateRecordData fails for an invalid ipv4 address""".stripMargin) {
val invalidIpv4 = "invalidIpv4:123" val invalidIpv4 = "invalidIpv4:123"
val change = AddChangeInput("test.comcast.com.", RecordType.A, ttl, AData(invalidIpv4)) val change = AddChangeInput("test.comcast.com.", RecordType.A, ttl, AData(invalidIpv4))
val result = validateAddChangeInput(change) val result = validateAddChangeInput(change, false)
result should haveInvalid[DomainValidationError](InvalidIpv4Address(invalidIpv4)) result should haveInvalid[DomainValidationError](InvalidIpv4Address(invalidIpv4))
} }
@@ -473,14 +498,14 @@ class BatchChangeValidationsSpec
|if validateRecordData fails for an invalid ipv6 address""".stripMargin) { |if validateRecordData fails for an invalid ipv6 address""".stripMargin) {
val invalidIpv6 = "invalidIpv6:123" val invalidIpv6 = "invalidIpv6:123"
val change = AddChangeInput("test.comcast.com.", RecordType.AAAA, ttl, AAAAData(invalidIpv6)) val change = AddChangeInput("test.comcast.com.", RecordType.AAAA, ttl, AAAAData(invalidIpv6))
val result = validateAddChangeInput(change) val result = validateAddChangeInput(change, false)
result should haveInvalid[DomainValidationError](InvalidIpv6Address(invalidIpv6)) result should haveInvalid[DomainValidationError](InvalidIpv6Address(invalidIpv6))
} }
property("validateAddChangeInput: should fail if A inputName includes a reverse zone address") { property("validateAddChangeInput: should fail if A inputName includes a reverse zone address") {
val invalidInputName = "test.1.2.3.in-addr.arpa." val invalidInputName = "test.1.2.3.in-addr.arpa."
val badAChange = AddChangeInput(invalidInputName, RecordType.A, ttl, AData("1.1.1.1")) val badAChange = AddChangeInput(invalidInputName, RecordType.A, ttl, AData("1.1.1.1"))
val result = validateAddChangeInput(badAChange) val result = validateAddChangeInput(badAChange, false)
result should haveInvalid[DomainValidationError]( result should haveInvalid[DomainValidationError](
RecordInReverseZoneError(invalidInputName, RecordType.A.toString)) RecordInReverseZoneError(invalidInputName, RecordType.A.toString))
} }
@@ -489,7 +514,7 @@ class BatchChangeValidationsSpec
val invalidInputName = "test.1.2.3.ip6.arpa." val invalidInputName = "test.1.2.3.ip6.arpa."
val badAAAAChange = val badAAAAChange =
AddChangeInput(invalidInputName, RecordType.AAAA, ttl, AAAAData("1:2:3:4:5:6:7:8")) AddChangeInput(invalidInputName, RecordType.AAAA, ttl, AAAAData("1:2:3:4:5:6:7:8"))
val result = validateAddChangeInput(badAAAAChange) val result = validateAddChangeInput(badAAAAChange, false)
result should haveInvalid[DomainValidationError]( result should haveInvalid[DomainValidationError](
RecordInReverseZoneError(invalidInputName, RecordType.AAAA.toString)) RecordInReverseZoneError(invalidInputName, RecordType.AAAA.toString))
} }
@@ -499,7 +524,7 @@ class BatchChangeValidationsSpec
val invalidCNAMERecordData = "$$$" val invalidCNAMERecordData = "$$$"
val change = val change =
AddChangeInput("test.comcast.com.", RecordType.CNAME, ttl, CNAMEData(invalidCNAMERecordData)) AddChangeInput("test.comcast.com.", RecordType.CNAME, ttl, CNAMEData(invalidCNAMERecordData))
val result = validateAddChangeInput(change) val result = validateAddChangeInput(change, false)
result should haveInvalid[DomainValidationError](InvalidDomainName(s"$invalidCNAMERecordData.")) result should haveInvalid[DomainValidationError](InvalidDomainName(s"$invalidCNAMERecordData."))
} }
@@ -509,7 +534,7 @@ class BatchChangeValidationsSpec
val invalidCNAMERecordData = "s" * 256 val invalidCNAMERecordData = "s" * 256
val change = val change =
AddChangeInput("test.comcast.com.", RecordType.CNAME, ttl, CNAMEData(invalidCNAMERecordData)) AddChangeInput("test.comcast.com.", RecordType.CNAME, ttl, CNAMEData(invalidCNAMERecordData))
val result = validateAddChangeInput(change) val result = validateAddChangeInput(change, false)
result should haveInvalid[DomainValidationError]( result should haveInvalid[DomainValidationError](
InvalidLength(s"$invalidCNAMERecordData.", 2, 255)) InvalidLength(s"$invalidCNAMERecordData.", 2, 255))
@@ -519,7 +544,7 @@ class BatchChangeValidationsSpec
|if inputName is not a valid ipv4 or ipv6 address""".stripMargin) { |if inputName is not a valid ipv4 or ipv6 address""".stripMargin) {
val invalidIp = "invalidip.111." val invalidIp = "invalidip.111."
val change = AddChangeInput(invalidIp, RecordType.PTR, ttl, PTRData("test.comcast.com")) val change = AddChangeInput(invalidIp, RecordType.PTR, ttl, PTRData("test.comcast.com"))
val result = validateAddChangeInput(change) val result = validateAddChangeInput(change, false)
result should haveInvalid[DomainValidationError](InvalidIPAddress(invalidIp)) result should haveInvalid[DomainValidationError](InvalidIPAddress(invalidIp))
} }
@@ -527,7 +552,7 @@ class BatchChangeValidationsSpec
property("validateAddChangeInput: should fail with InvalidDomainName for invalid PTR record data") { property("validateAddChangeInput: should fail with InvalidDomainName for invalid PTR record data") {
val invalidPTRDname = "*invalidptrdname" val invalidPTRDname = "*invalidptrdname"
val change = AddChangeInput("4.5.6.7", RecordType.PTR, ttl, PTRData(invalidPTRDname)) val change = AddChangeInput("4.5.6.7", RecordType.PTR, ttl, PTRData(invalidPTRDname))
val result = validateAddChangeInput(change) val result = validateAddChangeInput(change, false)
result should haveInvalid[DomainValidationError](InvalidDomainName(s"$invalidPTRDname.")) result should haveInvalid[DomainValidationError](InvalidDomainName(s"$invalidPTRDname."))
} }
@@ -1420,13 +1445,13 @@ class BatchChangeValidationsSpec
property("validateAddChangeInput: should succeed for a valid TXT addChangeInput") { property("validateAddChangeInput: should succeed for a valid TXT addChangeInput") {
val input = AddChangeInput("txt.ok.", RecordType.TXT, ttl, TXTData("test")) val input = AddChangeInput("txt.ok.", RecordType.TXT, ttl, TXTData("test"))
val result = validateAddChangeInput(input) val result = validateAddChangeInput(input, false)
result shouldBe valid result shouldBe valid
} }
property("validateAddChangeInput: should fail for a TXT addChangeInput with empty TXTData") { property("validateAddChangeInput: should fail for a TXT addChangeInput with empty TXTData") {
val input = AddChangeInput("txt.ok.", RecordType.TXT, ttl, TXTData("")) val input = AddChangeInput("txt.ok.", RecordType.TXT, ttl, TXTData(""))
val result = validateAddChangeInput(input) val result = validateAddChangeInput(input, false)
result should haveInvalid[DomainValidationError](InvalidLength("", 1, 64764)) result should haveInvalid[DomainValidationError](InvalidLength("", 1, 64764))
} }
@@ -1434,21 +1459,21 @@ class BatchChangeValidationsSpec
"validateAddChangeInput: should fail for a TXT addChangeInput with TXTData that is too many characters") { "validateAddChangeInput: should fail for a TXT addChangeInput with TXTData that is too many characters") {
val txtData = "x" * 64765 val txtData = "x" * 64765
val input = AddChangeInput("txt.ok.", RecordType.TXT, ttl, TXTData(txtData)) val input = AddChangeInput("txt.ok.", RecordType.TXT, ttl, TXTData(txtData))
val result = validateAddChangeInput(input) val result = validateAddChangeInput(input, false)
result should haveInvalid[DomainValidationError](InvalidLength(txtData, 1, 64764)) result should haveInvalid[DomainValidationError](InvalidLength(txtData, 1, 64764))
} }
property("validateAddChangeInput: should succeed for a valid MX addChangeInput") { property("validateAddChangeInput: should succeed for a valid MX addChangeInput") {
val input = AddChangeInput("mx.ok.", RecordType.MX, ttl, MXData(1, "foo.bar.")) val input = AddChangeInput("mx.ok.", RecordType.MX, ttl, MXData(1, "foo.bar."))
val result = validateAddChangeInput(input) val result = validateAddChangeInput(input, false)
result shouldBe valid result shouldBe valid
} }
property("validateAddChangeInput: should fail for a MX addChangeInput with invalid preference") { property("validateAddChangeInput: should fail for a MX addChangeInput with invalid preference") {
val inputSmall = AddChangeInput("mx.ok.", RecordType.MX, ttl, MXData(-1, "foo.bar.")) val inputSmall = AddChangeInput("mx.ok.", RecordType.MX, ttl, MXData(-1, "foo.bar."))
val inputLarge = AddChangeInput("mx.ok.", RecordType.MX, ttl, MXData(1000000, "foo.bar.")) val inputLarge = AddChangeInput("mx.ok.", RecordType.MX, ttl, MXData(1000000, "foo.bar."))
val resultSmall = validateAddChangeInput(inputSmall) val resultSmall = validateAddChangeInput(inputSmall, false)
val resultLarge = validateAddChangeInput(inputLarge) val resultLarge = validateAddChangeInput(inputLarge, false)
resultSmall should haveInvalid[DomainValidationError]( resultSmall should haveInvalid[DomainValidationError](
InvalidMxPreference( InvalidMxPreference(
@@ -1464,14 +1489,14 @@ class BatchChangeValidationsSpec
property("validateAddChangeInput: should fail for a MX addChangeInput with invalid exchange") { property("validateAddChangeInput: should fail for a MX addChangeInput with invalid exchange") {
val input = AddChangeInput("mx.ok.", RecordType.MX, ttl, MXData(1, "foo$.bar.")) val input = AddChangeInput("mx.ok.", RecordType.MX, ttl, MXData(1, "foo$.bar."))
val result = validateAddChangeInput(input) val result = validateAddChangeInput(input, false)
result should haveInvalid[DomainValidationError](InvalidDomainName("foo$.bar.")) result should haveInvalid[DomainValidationError](InvalidDomainName("foo$.bar."))
} }
property( property(
"validateAddChangeInput: should fail for a MX addChangeInput with invalid preference and exchange") { "validateAddChangeInput: should fail for a MX addChangeInput with invalid preference and exchange") {
val input = AddChangeInput("mx.ok.", RecordType.MX, ttl, MXData(-1, "foo$.bar.")) val input = AddChangeInput("mx.ok.", RecordType.MX, ttl, MXData(-1, "foo$.bar."))
val result = validateAddChangeInput(input) val result = validateAddChangeInput(input, false)
result should haveInvalid[DomainValidationError]( result should haveInvalid[DomainValidationError](
InvalidMxPreference( InvalidMxPreference(
-1, -1,

View File

@@ -172,4 +172,13 @@ final case class NewMultiRecordError(changeName: String, changeType: RecordType)
final case class CnameAtZoneApexError(zoneName: String) extends DomainValidationError { final case class CnameAtZoneApexError(zoneName: String) extends DomainValidationError {
def message: String = s"""CNAME cannot be the same name as zone "$zoneName".""" def message: String = s"""CNAME cannot be the same name as zone "$zoneName"."""
} }
final case class RecordRequiresManualReview(fqdn: String, fatal: Boolean = false)
extends DomainValidationError(fatal) {
def message: String =
s"""Record set with name "$fqdn" is configured to require manual review, but manual review is
|not enabled.""".stripMargin
.replaceAll("\n", " ")
}
// $COVERAGE-ON$ // $COVERAGE-ON$

View File

@@ -34,7 +34,7 @@ object DomainValidationErrorType extends Enumeration {
InvalidBatchRecordType, ZoneDiscoveryError, RecordAlreadyExists, RecordDoesNotExist, InvalidBatchRecordType, ZoneDiscoveryError, RecordAlreadyExists, RecordDoesNotExist,
CnameIsNotUniqueError, UserIsNotAuthorized, RecordNameNotUniqueInBatch, RecordInReverseZoneError, CnameIsNotUniqueError, UserIsNotAuthorized, RecordNameNotUniqueInBatch, RecordInReverseZoneError,
HighValueDomainError, MissingOwnerGroupId, ExistingMultiRecordError, NewMultiRecordError, HighValueDomainError, MissingOwnerGroupId, ExistingMultiRecordError, NewMultiRecordError,
CnameAtZoneApexError = Value CnameAtZoneApexError, RecordRequiresManualReview = Value
// $COVERAGE-OFF$ // $COVERAGE-OFF$
def from(error: DomainValidationError): DomainValidationErrorType = def from(error: DomainValidationError): DomainValidationErrorType =
@@ -66,6 +66,7 @@ object DomainValidationErrorType extends Enumeration {
case _: ExistingMultiRecordError => ExistingMultiRecordError case _: ExistingMultiRecordError => ExistingMultiRecordError
case _: NewMultiRecordError => NewMultiRecordError case _: NewMultiRecordError => NewMultiRecordError
case _: CnameAtZoneApexError => CnameAtZoneApexError case _: CnameAtZoneApexError => CnameAtZoneApexError
case _: RecordRequiresManualReview => RecordRequiresManualReview
} }
// $COVERAGE-ON$ // $COVERAGE-ON$
} }