diff --git a/modules/api/src/main/scala/vinyldns/api/domain/DomainValidations.scala b/modules/api/src/main/scala/vinyldns/api/domain/DomainValidations.scala index 9fc771d37..019b41f91 100644 --- a/modules/api/src/main/scala/vinyldns/api/domain/DomainValidations.scala +++ b/modules/api/src/main/scala/vinyldns/api/domain/DomainValidations.scala @@ -28,6 +28,10 @@ import scala.util.matching.Regex */ object DomainValidations { + val validReverseZoneFQDNRegex: Regex = + """^(?:([0-9a-zA-Z\-\/_]{1,63}|[0-9a-zA-Z\-\/_]{1}[0-9a-zA-Z\-\/_]{0,61}[0-9a-zA-Z\-\/_]{1}|[*.]{2}[0-9a-zA-Z\-\/_]{0,60}[0-9a-zA-Z\-\/_]{1})\.)*$""".r + val validForwardZoneFQDNRegex: Regex = + """^(?:([0-9a-zA-Z_]{1,63}|[0-9a-zA-Z_]{1}[0-9a-zA-Z\-_]{0,61}[0-9a-zA-Z_]{1}|[*.]{2}[0-9a-zA-Z\-_]{0,60}[0-9a-zA-Z_]{1})\.)*$""".r val validFQDNRegex: Regex = """^(?:([0-9a-zA-Z_]{1,63}|[0-9a-zA-Z_]{1}[0-9a-zA-Z\-\/_]{0,61}[0-9a-zA-Z_]{1}|[*.]{2}[0-9a-zA-Z\-\/_]{0,60}[0-9a-zA-Z_]{1})\.)*$""".r val validIpv4Regex: Regex = @@ -68,6 +72,30 @@ object DomainValidations { def validateHostName(name: Fqdn): ValidatedNel[DomainValidationError, Fqdn] = validateHostName(name.fqdn).map(_ => name) + def validateCname(name: Fqdn, isReverse: Boolean): ValidatedNel[DomainValidationError, Fqdn] = + validateCname(name.fqdn, isReverse).map(_ => name) + + def validateCname(name: String, isReverse: Boolean): ValidatedNel[DomainValidationError, String] = { + isReverse match { + case true => + val checkRegex = validReverseZoneFQDNRegex + .findFirstIn(name) + .map(_.validNel) + .getOrElse(InvalidCname(name,isReverse).invalidNel) + val checkLength = validateStringLength(name, Some(HOST_MIN_LENGTH), HOST_MAX_LENGTH) + + checkRegex.combine(checkLength).map(_ => name) + case false => + val checkRegex = validForwardZoneFQDNRegex + .findFirstIn(name) + .map(_.validNel) + .getOrElse(InvalidCname(name,isReverse).invalidNel) + val checkLength = validateStringLength(name, Some(HOST_MIN_LENGTH), HOST_MAX_LENGTH) + + checkRegex.combine(checkLength).map(_ => name) + } + } + def validateHostName(name: String): ValidatedNel[DomainValidationError, String] = { /* Label rules are as follows (from RFC 952; detailed in RFC 1034): @@ -93,6 +121,8 @@ object DomainValidations { checkRegex.combine(checkLength).map(_ => name) } + + def validateIpv4Address(address: String): ValidatedNel[DomainValidationError, String] = validIpv4Regex .findFirstIn(address) diff --git a/modules/api/src/main/scala/vinyldns/api/domain/batch/BatchChangeValidations.scala b/modules/api/src/main/scala/vinyldns/api/domain/batch/BatchChangeValidations.scala index a9d1e9b54..4825be88b 100644 --- a/modules/api/src/main/scala/vinyldns/api/domain/batch/BatchChangeValidations.scala +++ b/modules/api/src/main/scala/vinyldns/api/domain/batch/BatchChangeValidations.scala @@ -211,7 +211,7 @@ class BatchChangeValidations( isApproved: Boolean ): SingleValidation[Unit] = { val validTTL = addChangeInput.ttl.map(validateTTL(_).asUnit).getOrElse(().valid) - val validRecord = validateRecordData(addChangeInput.record) + val validRecord = validateRecordData(addChangeInput.record, addChangeInput) val validInput = validateInputName(addChangeInput, isApproved) validTTL |+| validRecord |+| validInput @@ -222,7 +222,7 @@ class BatchChangeValidations( isApproved: Boolean ): SingleValidation[Unit] = { val validRecord = deleteRRSetChangeInput.record match { - case Some(recordData) => validateRecordData(recordData) + case Some(recordData) => validateRecordData(recordData, deleteRRSetChangeInput) case None => ().validNel } val validInput = validateInputName(deleteRRSetChangeInput, isApproved) @@ -230,11 +230,18 @@ class BatchChangeValidations( validRecord |+| validInput } - def validateRecordData(record: RecordData): SingleValidation[Unit] = + def validateRecordData(record: RecordData,change: ChangeInput): SingleValidation[Unit] = record match { case a: AData => validateIpv4Address(a.address).asUnit case aaaa: AAAAData => validateIpv6Address(aaaa.address).asUnit - case cname: CNAMEData => validateCName(cname.cname).asUnit + case cname: CNAMEData => + /* + To validate the zone is reverse + */ + val isIPv4: Boolean = change.inputName.toLowerCase.endsWith("in-addr.arpa.") + val isIPv6: Boolean = change.inputName.toLowerCase.endsWith("ip6.arpa.") + val isReverse: Boolean = isIPv4 || isIPv6 + validateCname(cname.cname,isReverse).asUnit case ptr: PTRData => validateHostName(ptr.ptrdname).asUnit case txt: TXTData => validateTxtTextLength(txt.text).asUnit case mx: MXData => diff --git a/modules/api/src/main/scala/vinyldns/api/domain/membership/MembershipProtocol.scala b/modules/api/src/main/scala/vinyldns/api/domain/membership/MembershipProtocol.scala index 8853c7d96..02cadc115 100644 --- a/modules/api/src/main/scala/vinyldns/api/domain/membership/MembershipProtocol.scala +++ b/modules/api/src/main/scala/vinyldns/api/domain/membership/MembershipProtocol.scala @@ -171,6 +171,8 @@ final case class GroupNotFoundError(msg: String) extends Throwable(msg) final case class GroupAlreadyExistsError(msg: String) extends Throwable(msg) +final case class GroupValidationError(msg: String) extends Throwable(msg) + final case class UserNotFoundError(msg: String) extends Throwable(msg) final case class InvalidGroupError(msg: String) extends Throwable(msg) diff --git a/modules/api/src/main/scala/vinyldns/api/domain/membership/MembershipService.scala b/modules/api/src/main/scala/vinyldns/api/domain/membership/MembershipService.scala index 50226337d..9345bb4ff 100644 --- a/modules/api/src/main/scala/vinyldns/api/domain/membership/MembershipService.scala +++ b/modules/api/src/main/scala/vinyldns/api/domain/membership/MembershipService.scala @@ -57,6 +57,7 @@ class MembershipService( val adminMembers = inputGroup.adminUserIds val nonAdminMembers = inputGroup.memberIds.diff(adminMembers) for { + _ <- groupValidation(newGroup) _ <- hasMembersAndAdmins(newGroup).toResult _ <- groupWithSameNameDoesNotExist(newGroup.name) _ <- usersExist(newGroup.memberIds) @@ -76,6 +77,7 @@ class MembershipService( for { existingGroup <- getExistingGroup(groupId) newGroup = existingGroup.withUpdates(name, email, description, memberIds, adminUserIds) + _ <- groupValidation(newGroup) _ <- canEditGroup(existingGroup, authPrincipal).toResult addedAdmins = newGroup.adminUserIds.diff(existingGroup.adminUserIds) // new non-admin members ++ admins converted to non-admins @@ -218,7 +220,7 @@ class MembershipService( .map(x => GroupInfo.fromGroup(x, abridged, Some(authPrincipal))) val filtered = allMyGroups - .filter(grp => groupNameFilter.forall(grp.name.contains(_))) + .filter(grp => groupNameFilter.map(_.toLowerCase).forall(grp.name.toLowerCase.contains(_))) .filter(grp => startFrom.forall(grp.id > _)) val nextId = if (filtered.length > maxItems) Some(filtered(maxItems - 1).id) else None @@ -277,6 +279,16 @@ class MembershipService( .orFail(GroupNotFoundError(s"Group with ID $groupId was not found")) .toResult[Group] + // Validate group details. Group name and email cannot be empty + def groupValidation(group: Group): Result[Unit] = { + Option(group) match { + case Some(value) if Option(value.name).forall(_.trim.isEmpty) || Option(value.email).forall(_.trim.isEmpty) => + GroupValidationError(GroupValidationErrorMsg).asLeft + case _ => + ().asRight + } + }.toResult + def groupWithSameNameDoesNotExist(name: String): Result[Unit] = groupRepo .getGroupByName(name) diff --git a/modules/api/src/main/scala/vinyldns/api/route/MembershipRouting.scala b/modules/api/src/main/scala/vinyldns/api/route/MembershipRouting.scala index 620c6d79f..8ec1399e1 100644 --- a/modules/api/src/main/scala/vinyldns/api/route/MembershipRouting.scala +++ b/modules/api/src/main/scala/vinyldns/api/route/MembershipRouting.scala @@ -45,6 +45,7 @@ class MembershipRoute( case GroupNotFoundError(msg) => complete(StatusCodes.NotFound, msg) case NotAuthorizedError(msg) => complete(StatusCodes.Forbidden, msg) case GroupAlreadyExistsError(msg) => complete(StatusCodes.Conflict, msg) + case GroupValidationError(msg) => complete(StatusCodes.BadRequest, msg) case InvalidGroupError(msg) => complete(StatusCodes.BadRequest, msg) case UserNotFoundError(msg) => complete(StatusCodes.NotFound, msg) case InvalidGroupRequestError(msg) => complete(StatusCodes.BadRequest, msg) diff --git a/modules/api/src/test/functional/tests/batch/create_batch_change_test.py b/modules/api/src/test/functional/tests/batch/create_batch_change_test.py index c8d8b5b3a..094e66c9f 100644 --- a/modules/api/src/test/functional/tests/batch/create_batch_change_test.py +++ b/modules/api/src/test/functional/tests/batch/create_batch_change_test.py @@ -1975,7 +1975,7 @@ def test_cname_recordtype_add_checks(shared_zone_test_context): error_messages=['Invalid TTL: "29", must be a number between 30 and 2147483647.', f'Invalid domain name: "bad-ttl-and-invalid-name$.{parent_zone_name}", ' "valid domain names must be letters, numbers, underscores, and hyphens, joined by dots, and terminated with a dot.", - 'Invalid domain name: "also$bad.name.", valid domain names must be letters, numbers, underscores, and hyphens, ' + 'Invalid Cname: "also$bad.name.", valid cnames must be letters, numbers, underscores, and hyphens, ' "joined by dots, and terminated with a dot."]) # zone discovery failure assert_failed_change_in_error_response(response[7], input_name="no.zone.com.", record_type="CNAME", @@ -2138,7 +2138,7 @@ def test_cname_recordtype_update_delete_checks(shared_zone_test_context): error_messages=['Invalid TTL: "20", must be a number between 30 and 2147483647.', 'Invalid domain name: "$another.invalid.host.name.", valid domain names must be letters, numbers, ' 'underscores, and hyphens, joined by dots, and terminated with a dot.', - 'Invalid domain name: "$another.invalid.cname.", valid domain names must be letters, numbers, ' + 'Invalid Cname: "$another.invalid.cname.", valid cnames must be letters, numbers, ' 'underscores, and hyphens, joined by dots, and terminated with a dot.']) # zone discovery failure diff --git a/modules/api/src/test/scala/vinyldns/api/domain/DomainValidationsSpec.scala b/modules/api/src/test/scala/vinyldns/api/domain/DomainValidationsSpec.scala index b101282c5..e447c2772 100644 --- a/modules/api/src/test/scala/vinyldns/api/domain/DomainValidationsSpec.scala +++ b/modules/api/src/test/scala/vinyldns/api/domain/DomainValidationsSpec.scala @@ -19,11 +19,10 @@ package vinyldns.api.domain import cats.scalatest.ValidatedMatchers import org.scalacheck._ import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks -import org.scalatest._ import org.scalatest.propspec.AnyPropSpec import org.scalatest.matchers.should.Matchers import vinyldns.api.ValidationTestImprovements._ -import vinyldns.core.domain.{Fqdn, InvalidDomainName, InvalidLength} +import vinyldns.core.domain.{InvalidDomainName, Fqdn, InvalidCname, InvalidLength} class DomainValidationsSpec extends AnyPropSpec @@ -160,4 +159,52 @@ class DomainValidationsSpec val invalidDesc = "a" * 256 validateStringLength(Some(invalidDesc), None, 255).failWith[InvalidLength] } + + property("Shortest cname should be valid") { + validateCname("a.",true) shouldBe valid + validateCname("a.",false) shouldBe valid + + } + + property("Longest cname should be valid") { + val name = ("a" * 50 + ".") * 5 + validateCname(name,true) shouldBe valid + validateCname(name,false) shouldBe valid + + } + + property("Cnames with underscores should pass property-based testing") { + validateCname("_underscore.domain.name.",true).isValid + validateCname("under_score.domain.name.",true).isValid + validateCname("underscore._domain.name.",true).isValid + validateCname("_underscore.domain.name.",false).isValid + validateCname("under_score.domain.name.",false).isValid + validateCname("underscore._domain.name.",false).isValid + } + + // For wildcard records. '*' can only be in the beginning followed by '.' and domain name + property("Cnames beginning with asterisk should pass property-based testing") { + validateCname("*.domain.name.",true) shouldBe valid + validateCname("aste*risk.domain.name.",true) shouldBe invalid + validateCname("*asterisk.domain.name.",true) shouldBe invalid + validateCname("asterisk*.domain.name.",true) shouldBe invalid + validateCname("asterisk.*domain.name.",true) shouldBe invalid + validateCname("asterisk.domain*.name.",true) shouldBe invalid + validateCname("*.domain.name.",false) shouldBe valid + validateCname("aste*risk.domain.name.",false) shouldBe invalid + validateCname("*asterisk.domain.name.",false) shouldBe invalid + validateCname("asterisk*.domain.name.",false) shouldBe invalid + validateCname("asterisk.*domain.name.",false) shouldBe invalid + validateCname("asterisk.domain*.name.",false) shouldBe invalid + } + property("Cname names with forward slash should pass with reverse zone") { + validateCname("/slash.cname.name.",true).isValid + validateCname("slash./cname.name.",true).isValid + validateCname("slash.cname./name.",true).isValid + } + property("Cname names with forward slash should fail with forward zone") { + validateCname("/slash.cname.name.",false).failWith[InvalidCname] + validateCname("slash./cname.name.",false).failWith[InvalidCname] + validateCname("slash.cname./name.",false).failWith[InvalidCname] + } } diff --git a/modules/api/src/test/scala/vinyldns/api/domain/batch/BatchChangeValidationsSpec.scala b/modules/api/src/test/scala/vinyldns/api/domain/batch/BatchChangeValidationsSpec.scala index 8ce5bf987..35f10af94 100644 --- a/modules/api/src/test/scala/vinyldns/api/domain/batch/BatchChangeValidationsSpec.scala +++ b/modules/api/src/test/scala/vinyldns/api/domain/batch/BatchChangeValidationsSpec.scala @@ -712,7 +712,7 @@ class BatchChangeValidationsSpec ) val result = validateAddChangeInput(change, false) - result should haveInvalid[DomainValidationError](InvalidDomainName(s"$invalidCNAMERecordData.")) + result should haveInvalid[DomainValidationError](InvalidCname(s"$invalidCNAMERecordData.",false)) } property("""validateAddChangeInput: should fail with Invalid CNAME @@ -2681,4 +2681,21 @@ class BatchChangeValidationsSpec result(3) shouldBe valid result(4) shouldBe valid } + + property("validateAddChangeInput: should fail for a CNAME addChangeInput with forward slash for forward zone") { + val cnameWithForwardSlash = AddChangeInput("cname.ok.", RecordType.CNAME, ttl, CNAMEData(Fqdn("cname/"))) + val result = validateAddChangeInput(cnameWithForwardSlash, false) + result should haveInvalid[DomainValidationError](InvalidCname("cname/.",false)) + } + property("validateAddChangeInput: should succeed for a valid CNAME addChangeInput without forward slash for forward zone") { + val cname = AddChangeInput("cname.ok.", RecordType.CNAME, ttl, CNAMEData(Fqdn("cname"))) + val result = validateAddChangeInput(cname, false) + result shouldBe valid + } + property("validateAddChangeInput: should succeed for a valid CNAME addChangeInput with forward slash for reverse zone") { + val cnameWithForwardSlash = AddChangeInput("2.0.192.in-addr.arpa.", RecordType.CNAME, ttl, CNAMEData(Fqdn("cname/"))) + val result = validateAddChangeInput(cnameWithForwardSlash, true) + result shouldBe valid + } + } diff --git a/modules/api/src/test/scala/vinyldns/api/domain/membership/MembershipServiceSpec.scala b/modules/api/src/test/scala/vinyldns/api/domain/membership/MembershipServiceSpec.scala index 9276f12b1..44d178c18 100644 --- a/modules/api/src/test/scala/vinyldns/api/domain/membership/MembershipServiceSpec.scala +++ b/modules/api/src/test/scala/vinyldns/api/domain/membership/MembershipServiceSpec.scala @@ -114,6 +114,7 @@ class MembershipServiceSpec "create a new group" should { "save the group and add the members when the group is valid" in { doReturn(IO.pure(Some(okUser))).when(mockUserRepo).getUser("ok") + doReturn(().toResult).when(underTest).groupValidation(groupInfo) doReturn(().toResult).when(underTest).groupWithSameNameDoesNotExist(groupInfo.name) doReturn(().toResult).when(underTest).usersExist(groupInfo.memberIds) doReturn(IO.pure(okGroup)).when(mockGroupRepo).save(any[DB], any[Group]) @@ -141,6 +142,7 @@ class MembershipServiceSpec "save the groupChange in the groupChangeRepo" in { doReturn(IO.pure(Some(okUser))).when(mockUserRepo).getUser("ok") + doReturn(().toResult).when(underTest).groupValidation(groupInfo) doReturn(().toResult).when(underTest).groupWithSameNameDoesNotExist(groupInfo.name) doReturn(().toResult).when(underTest).usersExist(groupInfo.memberIds) doReturn(IO.pure(okGroup)).when(mockGroupRepo).save(any[DB], any[Group]) @@ -168,7 +170,7 @@ class MembershipServiceSpec adminUserIds = Set(okUserInfo.id, dummyUserInfo.id) ) val expectedMembersAdded = Set(okUserInfo.id, dummyUserInfo.id) - + doReturn(().toResult).when(underTest).groupValidation(info) doReturn(().toResult).when(underTest).groupWithSameNameDoesNotExist(info.name) doReturn(().toResult).when(underTest).usersExist(any[Set[String]]) doReturn(IO.pure(okGroup)).when(mockGroupRepo).save(any[DB], any[Group]) @@ -196,6 +198,7 @@ class MembershipServiceSpec "set the current user as a member" in { val info = groupInfo.copy(memberIds = Set.empty, adminUserIds = Set.empty) doReturn(IO.pure(Some(okUser))).when(mockUserRepo).getUser("ok") + doReturn(().toResult).when(underTest).groupValidation(info) doReturn(().toResult).when(underTest).groupWithSameNameDoesNotExist(info.name) doReturn(().toResult).when(underTest).usersExist(Set(okAuth.userId)) doReturn(IO.pure(okGroup)).when(mockGroupRepo).save(any[DB], any[Group]) @@ -224,6 +227,7 @@ class MembershipServiceSpec "return an error if users do not exist" in { doReturn(IO.pure(Some(okUser))).when(mockUserRepo).getUser("ok") + doReturn(().toResult).when(underTest).groupValidation(groupInfo) doReturn(().toResult).when(underTest).groupWithSameNameDoesNotExist(groupInfo.name) doReturn(result(UserNotFoundError("fail"))) .when(underTest) @@ -239,6 +243,7 @@ class MembershipServiceSpec "return an error if fail while saving the group" in { doReturn(IO.pure(Some(okUser))).when(mockUserRepo).getUser("ok") + doReturn(().toResult).when(underTest).groupValidation(groupInfo) doReturn(().toResult).when(underTest).groupWithSameNameDoesNotExist(groupInfo.name) doReturn(().toResult).when(underTest).usersExist(groupInfo.memberIds) doReturn(IO.raiseError(new RuntimeException("fail"))).when(mockGroupRepo).save(any[DB], any[Group]) @@ -253,6 +258,7 @@ class MembershipServiceSpec "return an error if fail while adding the members" in { doReturn(IO.pure(Some(okUser))).when(mockUserRepo).getUser("ok") + doReturn(().toResult).when(underTest).groupValidation(groupInfo) doReturn(().toResult).when(underTest).groupWithSameNameDoesNotExist(groupInfo.name) doReturn(().toResult).when(underTest).usersExist(groupInfo.memberIds) doReturn(IO.pure(okGroup)).when(mockGroupRepo).save(any[DB], any[Group]) @@ -264,6 +270,20 @@ class MembershipServiceSpec val error = leftResultOf(underTest.createGroup(groupInfo, okAuth).value) error shouldBe a[RuntimeException] } + + "return an error if group name and/or email is empty" in { + doReturn(IO.pure(Some(okUser))).when(mockUserRepo).getUser("ok") + doReturn(result(GroupValidationError("fail"))) + .when(underTest) + .groupValidation(groupInfo.copy(name = "", email = "")) + + val error = leftResultOf(underTest.createGroup(groupInfo.copy(name = "", email = ""), okAuth).value) + error shouldBe a[GroupValidationError] + + verify(mockGroupRepo, never()).save(any[DB], any[Group]) + verify(mockMembershipRepo, never()) + .saveMembers(any[DB], anyString, any[Set[String]], isAdmin = anyBoolean) + } } "update an existing group" should { @@ -388,6 +408,31 @@ class MembershipServiceSpec error shouldBe a[GroupAlreadyExistsError] } + "return an error if group name and/or email is empty" in { + doReturn(IO.pure(Some(existingGroup))) + .when(mockGroupRepo) + .getGroup(existingGroup.id) + doReturn(().toResult).when(underTest).usersExist(any[Set[String]]) + doReturn(result(GroupValidationError("fail"))) + .when(underTest) + .groupValidation(existingGroup.copy(name = "", email = "")) + + val error = leftResultOf( + underTest + .updateGroup( + updatedInfo.id, + name = "", + email = "", + updatedInfo.description, + updatedInfo.memberIds, + updatedInfo.adminUserIds, + okAuth + ) + .value + ) + error shouldBe a[GroupValidationError] + } + "return an error if the group is not found" in { doReturn(IO.pure(None)).when(mockGroupRepo).getGroup(existingGroup.id) @@ -597,6 +642,30 @@ class MembershipServiceSpec ignoreAccess = false ) } + "return only return groups whose name matches the filter, regardless of case" in { + doReturn(IO.pure(listOfDummyGroups.toSet)) + .when(mockGroupRepo) + .getGroups(any[Set[String]]) + val result: ListMyGroupsResponse = rightResultOf( + underTest + .listMyGroups( + groupNameFilter = Some("Name-Dummy01"), + startFrom = None, + maxItems = 100, + listOfDummyGroupsAuth, + false + ) + .value + ) + result shouldBe ListMyGroupsResponse( + groups = listOfDummyGroupInfo.slice(10, 20), + groupNameFilter = Some("Name-Dummy01"), + startFrom = None, + nextId = None, + maxItems = 100, + ignoreAccess = false + ) + } "return only return groups after startFrom" in { doReturn(IO.pure(listOfDummyGroups.toSet)) .when(mockGroupRepo) diff --git a/modules/core/src/main/scala/vinyldns/core/Messages.scala b/modules/core/src/main/scala/vinyldns/core/Messages.scala index b37c9e6eb..0c6929f4f 100644 --- a/modules/core/src/main/scala/vinyldns/core/Messages.scala +++ b/modules/core/src/main/scala/vinyldns/core/Messages.scala @@ -79,4 +79,6 @@ object Messages { val NotAuthorizedErrorMsg = "User \"%s\" is not authorized. Contact %s owner group: %s at %s to make DNS changes." + // Error displayed when group name or email is empty + val GroupValidationErrorMsg = "Group name and email cannot be empty." } diff --git a/modules/core/src/main/scala/vinyldns/core/domain/DomainValidationErrors.scala b/modules/core/src/main/scala/vinyldns/core/domain/DomainValidationErrors.scala index 0590c4225..ad77e2675 100644 --- a/modules/core/src/main/scala/vinyldns/core/domain/DomainValidationErrors.scala +++ b/modules/core/src/main/scala/vinyldns/core/domain/DomainValidationErrors.scala @@ -52,11 +52,23 @@ final case class InvalidDomainName(param: String) extends DomainValidationError "joined by dots, and terminated with a dot." } -final case class InvalidCName(param: String) extends DomainValidationError { +final case class InvalidIPv4CName(param: String) extends DomainValidationError { def message: String = s"""Invalid Cname: "$param", valid cname should not be IP address""" } +final case class InvalidCname(param: String, isReverseZone: Boolean) extends DomainValidationError { + def message: String = + isReverseZone match { + case true => + s"""Invalid Cname: "$param", valid cnames must be letters, numbers, slashes, underscores, and hyphens, """ + + "joined by dots, and terminated with a dot." + case false => + s"""Invalid Cname: "$param", valid cnames must be letters, numbers, underscores, and hyphens, """ + + "joined by dots, and terminated with a dot." + } +} + final case class InvalidLength(param: String, minLengthInclusive: Int, maxLengthInclusive: Int) extends DomainValidationError { def message: String = diff --git a/modules/core/src/main/scala/vinyldns/core/domain/SingleChangeError.scala b/modules/core/src/main/scala/vinyldns/core/domain/SingleChangeError.scala index 99731d7db..862f4ae7e 100644 --- a/modules/core/src/main/scala/vinyldns/core/domain/SingleChangeError.scala +++ b/modules/core/src/main/scala/vinyldns/core/domain/SingleChangeError.scala @@ -30,7 +30,7 @@ object DomainValidationErrorType extends Enumeration { type DomainValidationErrorType = Value // NOTE: once defined, an error code type cannot be changed! val ChangeLimitExceeded, BatchChangeIsEmpty, GroupDoesNotExist, NotAMemberOfOwnerGroup, - InvalidDomainName, InvalidLength, InvalidEmail, InvalidRecordType, InvalidPortNumber, + InvalidDomainName, InvalidCname, InvalidLength, InvalidEmail, InvalidRecordType, InvalidPortNumber, InvalidIpv4Address, InvalidIpv6Address, InvalidIPAddress, InvalidTTL, InvalidMxPreference, InvalidBatchRecordType, ZoneDiscoveryError, RecordAlreadyExists, RecordDoesNotExist, CnameIsNotUniqueError, UserIsNotAuthorized, UserIsNotAuthorizedError, RecordNameNotUniqueInBatch, @@ -46,6 +46,7 @@ object DomainValidationErrorType extends Enumeration { case _: GroupDoesNotExist => GroupDoesNotExist case _: NotAMemberOfOwnerGroup => NotAMemberOfOwnerGroup case _: InvalidDomainName => InvalidDomainName + case _: InvalidCname => InvalidCname case _: InvalidLength => InvalidLength case _: InvalidEmail => InvalidEmail case _: InvalidRecordType => InvalidRecordType diff --git a/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlZoneRepositoryIntegrationSpec.scala b/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlZoneRepositoryIntegrationSpec.scala index 314209155..369c35443 100644 --- a/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlZoneRepositoryIntegrationSpec.scala +++ b/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlZoneRepositoryIntegrationSpec.scala @@ -468,6 +468,27 @@ class MySqlZoneRepositoryIntegrationSpec (f.unsafeRunSync().zones should contain).theSameElementsInOrderAs(expectedZones) } + "support case insensitivity in the zone filter" in { + + val testZones = Seq( + testZone("system-test.", adminGroupId = "foo"), + testZone("system-temp.", adminGroupId = "foo"), + testZone("system-nomatch.", adminGroupId = "bar") + ) + + val expectedZones = Seq(testZones(0), testZones(1)).sortBy(_.name) + + val auth = AuthPrincipal(dummyUser, Seq("foo")) + + val f = + for { + _ <- saveZones(testZones) + retrieved <- repo.listZones(auth, zoneNameFilter = Some("SyStEm*")) + } yield retrieved + + (f.unsafeRunSync().zones should contain).theSameElementsInOrderAs(expectedZones) + } + "support starts with wildcard" in { val testZones = Seq( diff --git a/modules/portal/Gruntfile.js b/modules/portal/Gruntfile.js index 4c4b5c008..7a3123934 100644 --- a/modules/portal/Gruntfile.js +++ b/modules/portal/Gruntfile.js @@ -34,9 +34,11 @@ module.exports = function(grunt) { {expand: true, flatten: true, src: ['node_modules/bootstrap/dist/js/bootstrap.min.js'], dest: 'public/js'}, {expand: true, flatten: true, src: ['node_modules/jquery/dist/jquery.min.js'], dest: 'public/js'}, {expand: true, flatten: true, src: ['node_modules/moment/min/moment.min.js'], dest: 'public/js'}, + {expand: true, flatten: true, src: ['node_modules/jquery-ui-dist/jquery-ui.js'], dest: 'public/js'}, {expand: true, flatten: true, src: ['node_modules/bootstrap/dist/css/bootstrap.min.css'], dest: 'public/css'}, {expand: true, flatten: true, src: ['node_modules/font-awesome/css/font-awesome.min.css'], dest: 'public/css'}, + {expand: true, flatten: true, src: ['node_modules/jquery-ui-dist/jquery-ui.css'], dest: 'public/css'}, // We're picking just the resources we need from the gentelella UI framework and temporarily storing them in mapped/ui/ {expand: true, flatten: true, cwd: 'node_modules/gentelella', dest: 'mapped/ui', src: '**/jquery.{smartWizard,dataTables.min,mousewheel.min}.js'}, diff --git a/modules/portal/app/views/main.scala.html b/modules/portal/app/views/main.scala.html index 99f81d859..ae72212c4 100644 --- a/modules/portal/app/views/main.scala.html +++ b/modules/portal/app/views/main.scala.html @@ -16,10 +16,12 @@ + + @@ -149,6 +151,7 @@ + diff --git a/modules/portal/app/views/zones/zones.scala.html b/modules/portal/app/views/zones/zones.scala.html index 52ab8b759..9c56548df 100644 --- a/modules/portal/app/views/zones/zones.scala.html +++ b/modules/portal/app/views/zones/zones.scala.html @@ -144,6 +144,9 @@ +