2
0
mirror of https://github.com/VinylDNS/vinyldns synced 2025-09-02 23:35:18 +00:00

Merge branch 'master' into aravindhr/add-zone-sync-scheduler-config

This commit is contained in:
Nicholas Spadaccino
2023-03-15 15:25:01 -04:00
committed by GitHub
23 changed files with 478 additions and 44 deletions

View File

@@ -137,6 +137,9 @@ vinyldns {
from = ${?EMAIL_FROM} from = ${?EMAIL_FROM}
} }
} }
valid-email-config{
email-domains = ["test.com","*dummy.com"]
}
sns { sns {
class-name = "vinyldns.apadi.notifier.sns.SnsNotifierProvider" class-name = "vinyldns.apadi.notifier.sns.SnsNotifierProvider"

View File

@@ -169,7 +169,9 @@ vinyldns {
from = "VinylDNS <do-not-reply@vinyldns.io>" from = "VinylDNS <do-not-reply@vinyldns.io>"
} }
} }
valid-email-config{
email-domains = ["test.com","*dummy.com"]
}
sns { sns {
class-name = "vinyldns.api.notifier.sns.SnsNotifierProvider" class-name = "vinyldns.api.notifier.sns.SnsNotifierProvider"
settings { settings {

View File

@@ -142,7 +142,7 @@ object Boot extends App {
vinyldnsConfig.batchChangeConfig, vinyldnsConfig.batchChangeConfig,
vinyldnsConfig.scheduledChangesConfig vinyldnsConfig.scheduledChangesConfig
) )
val membershipService = MembershipService(repositories) val membershipService = MembershipService(repositories,vinyldnsConfig.validEmailConfig)
val connectionValidator = val connectionValidator =
new ZoneConnectionValidator( new ZoneConnectionValidator(

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2018 Comcast Cable Communications Management, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package vinyldns.api.config
import pureconfig.ConfigReader
case class ValidEmailConfig(
valid_domains : List[String]
)
object ValidEmailConfig {
implicit val configReader: ConfigReader[ValidEmailConfig] =
ConfigReader.forProduct1[ValidEmailConfig,List[String]](
"email-domains"
) {
case valid_domains => ValidEmailConfig(valid_domains)
}
}

View File

@@ -38,6 +38,7 @@ import scala.reflect.ClassTag
final case class VinylDNSConfig( final case class VinylDNSConfig(
serverConfig: ServerConfig, serverConfig: ServerConfig,
limitsconfig: LimitsConfig, limitsconfig: LimitsConfig,
validEmailConfig: ValidEmailConfig,
httpConfig: HttpConfig, httpConfig: HttpConfig,
highValueDomainConfig: HighValueDomainConfig, highValueDomainConfig: HighValueDomainConfig,
manualReviewConfig: ManualReviewConfig, manualReviewConfig: ManualReviewConfig,
@@ -83,6 +84,7 @@ object VinylDNSConfig {
for { for {
config <- IO.delay(ConfigFactory.load()) config <- IO.delay(ConfigFactory.load())
limitsconfig <- loadIO[LimitsConfig](config, "vinyldns.api.limits") //Added Limitsconfig to fetch data from the reference.config and pass to LimitsConfig.config limitsconfig <- loadIO[LimitsConfig](config, "vinyldns.api.limits") //Added Limitsconfig to fetch data from the reference.config and pass to LimitsConfig.config
validEmailConfig <- loadIO[ValidEmailConfig](config, path="vinyldns.valid-email-config")
serverConfig <- loadIO[ServerConfig](config, "vinyldns") serverConfig <- loadIO[ServerConfig](config, "vinyldns")
batchChangeConfig <- loadIO[BatchChangeConfig](config, "vinyldns") batchChangeConfig <- loadIO[BatchChangeConfig](config, "vinyldns")
backendConfigs <- loadIO[BackendConfigs](config, "vinyldns.backend") backendConfigs <- loadIO[BackendConfigs](config, "vinyldns.backend")
@@ -103,6 +105,7 @@ object VinylDNSConfig {
} yield VinylDNSConfig( } yield VinylDNSConfig(
serverConfig, serverConfig,
limitsconfig, limitsconfig,
validEmailConfig,
httpConfig, httpConfig,
hvdConfig, hvdConfig,
manualReviewConfig, manualReviewConfig,

View File

@@ -27,6 +27,7 @@ import scala.util.matching.Regex
Object to house common domain validations Object to house common domain validations
*/ */
object DomainValidations { object DomainValidations {
val validReverseZoneFQDNRegex: Regex = 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 """^(?:([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 = val validForwardZoneFQDNRegex: Regex =
@@ -61,13 +62,20 @@ object DomainValidations {
val MX_PREFERENCE_MIN_VALUE: Int = 0 val MX_PREFERENCE_MIN_VALUE: Int = 0
val MX_PREFERENCE_MAX_VALUE: Int = 65535 val MX_PREFERENCE_MAX_VALUE: Int = 65535
// Cname check - Cname should not be IP address
def validateCname(name: Fqdn, isReverse: Boolean): ValidatedNel[DomainValidationError, Fqdn] =
validateIpv4Address(name.fqdn.dropRight(1)).isValid match {
case true => InvalidIPv4CName(name.toString).invalidNel
case false => validateIsReverseCname(name, isReverse)
}
def validateHostName(name: Fqdn): ValidatedNel[DomainValidationError, Fqdn] = def validateHostName(name: Fqdn): ValidatedNel[DomainValidationError, Fqdn] =
validateHostName(name.fqdn).map(_ => name) validateHostName(name.fqdn).map(_ => name)
def validateCname(name: Fqdn, isReverse: Boolean): ValidatedNel[DomainValidationError, Fqdn] = def validateIsReverseCname(name: Fqdn, isReverse: Boolean): ValidatedNel[DomainValidationError, Fqdn] =
validateCname(name.fqdn, isReverse).map(_ => name) validateIsReverseCname(name.fqdn, isReverse).map(_ => name)
def validateCname(name: String, isReverse: Boolean): ValidatedNel[DomainValidationError, String] = { def validateIsReverseCname(name: String, isReverse: Boolean): ValidatedNel[DomainValidationError, String] = {
isReverse match { isReverse match {
case true => case true =>
val checkRegex = validReverseZoneFQDNRegex val checkRegex = validReverseZoneFQDNRegex

View File

@@ -178,6 +178,8 @@ final case class GroupAlreadyExistsError(msg: String) extends Throwable(msg)
final case class GroupValidationError(msg: String) extends Throwable(msg) final case class GroupValidationError(msg: String) extends Throwable(msg)
final case class EmailValidationError(msg: String) extends Throwable(msg)
final case class UserNotFoundError(msg: String) extends Throwable(msg) final case class UserNotFoundError(msg: String) extends Throwable(msg)
final case class InvalidGroupError(msg: String) extends Throwable(msg) final case class InvalidGroupError(msg: String) extends Throwable(msg)

View File

@@ -20,6 +20,7 @@ import cats.effect.IO
import cats.implicits._ import cats.implicits._
import scalikejdbc.DB import scalikejdbc.DB
import vinyldns.api.Interfaces._ import vinyldns.api.Interfaces._
import vinyldns.api.config.ValidEmailConfig
import vinyldns.api.repository.ApiDataAccessor import vinyldns.api.repository.ApiDataAccessor
import vinyldns.core.domain.auth.AuthPrincipal import vinyldns.core.domain.auth.AuthPrincipal
import vinyldns.core.domain.membership.LockStatus.LockStatus import vinyldns.core.domain.membership.LockStatus.LockStatus
@@ -30,14 +31,15 @@ import vinyldns.core.Messages._
import vinyldns.mysql.TransactionProvider import vinyldns.mysql.TransactionProvider
object MembershipService { object MembershipService {
def apply(dataAccessor: ApiDataAccessor): MembershipService = def apply(dataAccessor: ApiDataAccessor,emailConfig:ValidEmailConfig): MembershipService =
new MembershipService( new MembershipService(
dataAccessor.groupRepository, dataAccessor.groupRepository,
dataAccessor.userRepository, dataAccessor.userRepository,
dataAccessor.membershipRepository, dataAccessor.membershipRepository,
dataAccessor.zoneRepository, dataAccessor.zoneRepository,
dataAccessor.groupChangeRepository, dataAccessor.groupChangeRepository,
dataAccessor.recordSetRepository dataAccessor.recordSetRepository,
emailConfig
) )
} }
@@ -47,7 +49,8 @@ class MembershipService(
membershipRepo: MembershipRepository, membershipRepo: MembershipRepository,
zoneRepo: ZoneRepository, zoneRepo: ZoneRepository,
groupChangeRepo: GroupChangeRepository, groupChangeRepo: GroupChangeRepository,
recordSetRepo: RecordSetRepository recordSetRepo: RecordSetRepository,
validDomains: ValidEmailConfig
) extends MembershipServiceAlgebra with TransactionProvider { ) extends MembershipServiceAlgebra with TransactionProvider {
import MembershipValidations._ import MembershipValidations._
@@ -58,6 +61,7 @@ class MembershipService(
val nonAdminMembers = inputGroup.memberIds.diff(adminMembers) val nonAdminMembers = inputGroup.memberIds.diff(adminMembers)
for { for {
_ <- groupValidation(newGroup) _ <- groupValidation(newGroup)
_ <- emailValidation(newGroup.email)
_ <- hasMembersAndAdmins(newGroup).toResult _ <- hasMembersAndAdmins(newGroup).toResult
_ <- groupWithSameNameDoesNotExist(newGroup.name) _ <- groupWithSameNameDoesNotExist(newGroup.name)
_ <- usersExist(newGroup.memberIds) _ <- usersExist(newGroup.memberIds)
@@ -78,6 +82,7 @@ class MembershipService(
existingGroup <- getExistingGroup(groupId) existingGroup <- getExistingGroup(groupId)
newGroup = existingGroup.withUpdates(name, email, description, memberIds, adminUserIds) newGroup = existingGroup.withUpdates(name, email, description, memberIds, adminUserIds)
_ <- groupValidation(newGroup) _ <- groupValidation(newGroup)
_ <- emailValidation(newGroup.email)
_ <- canEditGroup(existingGroup, authPrincipal).toResult _ <- canEditGroup(existingGroup, authPrincipal).toResult
addedAdmins = newGroup.adminUserIds.diff(existingGroup.adminUserIds) addedAdmins = newGroup.adminUserIds.diff(existingGroup.adminUserIds)
// new non-admin members ++ admins converted to non-admins // new non-admin members ++ admins converted to non-admins
@@ -381,6 +386,29 @@ class MembershipService(
().asRight ().asRight
} }
}.toResult }.toResult
// Validate email details.Email domains details are fetched from the config file.
def emailValidation(email: String): Result[Unit] = {
val emailDomains = validDomains.valid_domains
val splitEmailDomains = emailDomains.mkString(",")
val emailRegex ="""^(?!\.)(?!.*\.$)(?!.*\.\.)[a-zA-Z0-9._]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$""".r
val index = email.indexOf('@');
val emailSplit = if(index != -1){
email.substring(index+1,email.length)}
val wildcardEmailDomains=if(splitEmailDomains.contains("*")){
emailDomains.map(x=>x.replaceAllLiterally("*",""))}
else emailDomains
Option(email) match {
case Some(value) if (emailRegex.findFirstIn(value) != None)=>
if (emailDomains.contains(emailSplit) || emailDomains.isEmpty || wildcardEmailDomains.exists(x => emailSplit.toString.endsWith(x)))
().asRight
else
EmailValidationError(EmailValidationErrorMsg + " " + wildcardEmailDomains.mkString(",")).asLeft
case _ =>
EmailValidationError(InvalidEmailValidationErrorMsg).asLeft
}}.toResult
def groupWithSameNameDoesNotExist(name: String): Result[Unit] = def groupWithSameNameDoesNotExist(name: String): Result[Unit] =
groupRepo groupRepo

View File

@@ -20,6 +20,7 @@ import cats.syntax.either._
import vinyldns.api.Interfaces._ import vinyldns.api.Interfaces._
import vinyldns.api.backend.dns.DnsConversions import vinyldns.api.backend.dns.DnsConversions
import vinyldns.api.config.HighValueDomainConfig import vinyldns.api.config.HighValueDomainConfig
import vinyldns.api.domain.DomainValidations.validateIpv4Address
import vinyldns.api.domain._ import vinyldns.api.domain._
import vinyldns.core.domain.DomainHelpers._ import vinyldns.core.domain.DomainHelpers._
import vinyldns.core.domain.record.RecordType._ import vinyldns.core.domain.record.RecordType._
@@ -236,6 +237,16 @@ object RecordSetValidations {
) )
} }
val isNotIPv4inCname = {
ensuring(
RecordSetValidation(
s"""Invalid CNAME: ${newRecordSet.records.head.toString.dropRight(1)}, valid CNAME record data cannot be an IP address."""
)
)(
validateIpv4Address(newRecordSet.records.head.toString.dropRight(1)).isInvalid
)
}
for { for {
_ <- isNotOrigin( _ <- isNotOrigin(
newRecordSet, newRecordSet,
@@ -243,6 +254,7 @@ object RecordSetValidations {
"CNAME RecordSet cannot have name '@' because it points to zone origin" "CNAME RecordSet cannot have name '@' because it points to zone origin"
) )
_ <- noRecordWithName _ <- noRecordWithName
_ <- isNotIPv4inCname
_ <- RDataWithConsecutiveDots _ <- RDataWithConsecutiveDots
_ <- checkForDot(newRecordSet, zone, existingRecordSet, recordFqdnDoesNotExist, dottedHostZoneConfig, isRecordTypeAndUserAllowed, allowedDotsLimit) _ <- checkForDot(newRecordSet, zone, existingRecordSet, recordFqdnDoesNotExist, dottedHostZoneConfig, isRecordTypeAndUserAllowed, allowedDotsLimit)
} yield () } yield ()

View File

@@ -49,6 +49,7 @@ class MembershipRoute(
case InvalidGroupError(msg) => complete(StatusCodes.BadRequest, msg) case InvalidGroupError(msg) => complete(StatusCodes.BadRequest, msg)
case UserNotFoundError(msg) => complete(StatusCodes.NotFound, msg) case UserNotFoundError(msg) => complete(StatusCodes.NotFound, msg)
case InvalidGroupRequestError(msg) => complete(StatusCodes.BadRequest, msg) case InvalidGroupRequestError(msg) => complete(StatusCodes.BadRequest, msg)
case EmailValidationError(msg) => complete(StatusCodes.BadRequest, msg)
} }
val membershipRoute: Route = path("groups" / Segment) { groupId => val membershipRoute: Route = path("groups" / Segment) { groupId =>

View File

@@ -1938,7 +1938,8 @@ def test_cname_recordtype_add_checks(shared_zone_test_context):
get_change_CNAME_json(existing_forward_fqdn), get_change_CNAME_json(existing_forward_fqdn),
get_change_CNAME_json(existing_cname_fqdn), get_change_CNAME_json(existing_cname_fqdn),
get_change_CNAME_json(f"0.{ip4_zone_name}", cname="duplicate.in.db."), get_change_CNAME_json(f"0.{ip4_zone_name}", cname="duplicate.in.db."),
get_change_CNAME_json(f"user-add-unauthorized.{dummy_zone_name}") get_change_CNAME_json(f"user-add-unauthorized.{dummy_zone_name}"),
get_change_CNAME_json(f"invalid-ipv4-{parent_zone_name}", cname="1.2.3.4")
] ]
} }
@@ -2014,6 +2015,9 @@ def test_cname_recordtype_add_checks(shared_zone_test_context):
assert_failed_change_in_error_response(response[16], input_name=f"user-add-unauthorized.{dummy_zone_name}", assert_failed_change_in_error_response(response[16], input_name=f"user-add-unauthorized.{dummy_zone_name}",
record_type="CNAME", record_data="test.com.", record_type="CNAME", record_data="test.com.",
error_messages=[f"User \"ok\" is not authorized. Contact zone owner group: {dummy_group_name} at test@test.com to make DNS changes."]) error_messages=[f"User \"ok\" is not authorized. Contact zone owner group: {dummy_group_name} at test@test.com to make DNS changes."])
assert_failed_change_in_error_response(response[17], input_name=f"invalid-ipv4-{parent_zone_name}", record_type="CNAME", record_data="1.2.3.4.",
error_messages=[f'Invalid Cname: "Fqdn(1.2.3.4.)", Valid CNAME record data should not be an IP address'])
finally: finally:
clear_recordset_list(to_delete, client) clear_recordset_list(to_delete, client)

View File

@@ -10,7 +10,7 @@ def test_create_group_success(shared_zone_test_context):
try: try:
new_group = { new_group = {
"name": f"test-create-group-success{shared_zone_test_context.partition_id}", "name": "test-create-group-success{shared_zone_test_context.partition_id}",
"email": "test@test.com", "email": "test@test.com",
"description": "this is a description", "description": "this is a description",
"members": [{"id": "ok"}], "members": [{"id": "ok"}],
@@ -32,6 +32,36 @@ def test_create_group_success(shared_zone_test_context):
if result: if result:
client.delete_group(result["id"], status=(200, 404)) client.delete_group(result["id"], status=(200, 404))
def test_create_group_success_wildcard(shared_zone_test_context):
"""
Tests that creating a group works
"""
client = shared_zone_test_context.ok_vinyldns_client
result = None
try:
new_group = {
"name": "test-create-group-success_wildcard{shared_zone_test_context.partition_id}",
"email": "test@ok.dummy.com",
"description": "this is a description",
"members": [{"id": "ok"}],
"admins": [{"id": "ok"}]
}
result = client.create_group(new_group, status=200)
assert_that(result["name"], is_(new_group["name"]))
assert_that(result["email"], is_(new_group["email"]))
assert_that(result["description"], is_(new_group["description"]))
assert_that(result["status"], is_("Active"))
assert_that(result["created"], not_none())
assert_that(result["id"], not_none())
assert_that(result["members"], has_length(1))
assert_that(result["members"][0]["id"], is_("ok"))
assert_that(result["admins"], has_length(1))
assert_that(result["admins"][0]["id"], is_("ok"))
finally:
if result:
client.delete_group(result["id"], status=(200, 404))
def test_creator_is_an_admin(shared_zone_test_context): def test_creator_is_an_admin(shared_zone_test_context):
""" """
@@ -114,8 +144,36 @@ def test_create_group_without_name_or_email(shared_zone_test_context):
"Missing Group.name", "Missing Group.name",
"Missing Group.email" "Missing Group.email"
)) ))
def test_create_group_with_invalid_email_domain(shared_zone_test_context):
"""
Tests that creating a group With Invalid email fails
"""
client = shared_zone_test_context.ok_vinyldns_client
new_group = {
"name": "invalid-email",
"email": "test@abc.com",
"description": "this is a description",
"members": [{"id": "ok"}],
"admins": [{"id": "ok"}]
}
error = client.create_group(new_group, status=400)
assert_that(error, is_("Please enter a valid Email ID. Valid domains should end with test.com,dummy.com"))
def test_create_group_with_invalid_email(shared_zone_test_context):
"""
Tests that creating a group With Invalid email fails
"""
client = shared_zone_test_context.ok_vinyldns_client
new_group = {
"name": "invalid-email",
"email": "test.abc.com",
"description": "this is a description",
"members": [{"id": "ok"}],
"admins": [{"id": "ok"}]
}
error = client.create_group(new_group, status=400)
assert_that(error, is_("Please enter a valid Email ID."))
def test_create_group_without_members_or_admins(shared_zone_test_context): def test_create_group_without_members_or_admins(shared_zone_test_context):
""" """
Tests that creating a group without members or admins fails Tests that creating a group without members or admins fails

View File

@@ -681,6 +681,24 @@ def test_create_dotted_cname_record_succeeds_if_all_dotted_hosts_config_satisfie
client.wait_until_recordset_change_status(delete_result, "Complete") client.wait_until_recordset_change_status(delete_result, "Complete")
def test_create_IPv4_cname_record_fails(shared_zone_test_context):
"""
Test that creating a CNAME record set as IPv4 address records returns an error.
"""
client = shared_zone_test_context.ok_vinyldns_client
zone = shared_zone_test_context.parent_zone
apex_cname_rs = {
"zoneId": zone["id"],
"name": "test_create_cname_with_ipaddress",
"type": "CNAME",
"ttl": 500,
"records": [{"cname": "1.2.3.4"}]
}
error = client.create_recordset(apex_cname_rs, status=400)
assert_that(error, is_(f'Invalid CNAME: 1.2.3.4, valid CNAME record data cannot be an IP address.'))
def test_create_cname_with_multiple_records(shared_zone_test_context): def test_create_cname_with_multiple_records(shared_zone_test_context):
""" """
Test that creating a CNAME record set with multiple records returns an error Test that creating a CNAME record set with multiple records returns an error

View File

@@ -127,7 +127,9 @@ vinyldns {
from = ${?EMAIL_FROM} from = ${?EMAIL_FROM}
} }
} }
valid-email-config{
email-domains = ["test.com","*dummy.com","*ok.com"]
}
sns { sns {
class-name = "vinyldns.apadi.notifier.sns.SnsNotifierProvider" class-name = "vinyldns.apadi.notifier.sns.SnsNotifierProvider"
class-name = ${?SNS_CLASS_NAME} class-name = ${?SNS_CLASS_NAME}

View File

@@ -22,7 +22,7 @@ import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks
import org.scalatest.propspec.AnyPropSpec import org.scalatest.propspec.AnyPropSpec
import org.scalatest.matchers.should.Matchers import org.scalatest.matchers.should.Matchers
import vinyldns.api.ValidationTestImprovements._ import vinyldns.api.ValidationTestImprovements._
import vinyldns.core.domain.{InvalidDomainName, InvalidCname, InvalidLength} import vinyldns.core.domain.{InvalidDomainName, Fqdn, InvalidCname, InvalidLength}
class DomainValidationsSpec class DomainValidationsSpec
extends AnyPropSpec extends AnyPropSpec
@@ -77,6 +77,54 @@ class DomainValidationsSpec
validateHostName("asterisk.domain*.name.") shouldBe invalid validateHostName("asterisk.domain*.name.") shouldBe invalid
} }
property("Shortest fqdn name should be valid") {
val fqdn = Fqdn("a.")
validateCname(fqdn, false) shouldBe valid
}
property("Ip address in cname should be invalid") {
val fqdn = Fqdn("1.2.3.4")
validateCname(fqdn, false) shouldBe invalid
}
property("Longest fqdn name should be valid") {
val fqdn = Fqdn(("a" * 50 + ".") * 5)
validateCname(fqdn, false) shouldBe valid
}
property("fqdn name should pass property-based testing") {
forAll(domainGenerator) { domain: String =>
val domains= Fqdn(domain)
whenever(validateHostName(domains).isValid) {
domains.fqdn.length should be > 0
domains.fqdn.length should be < 256
(domains.fqdn should fullyMatch).regex(validFQDNRegex)
domains.fqdn should endWith(".")
}
}
}
property("fqdn names beginning with invalid characters should fail with InvalidCname") {
validateCname(Fqdn("/slash.domain.name."), false).failWith[InvalidCname]
validateCname(Fqdn("-hyphen.domain.name."), false).failWith[InvalidCname]
}
property("fqdn names with underscores should pass property-based testing") {
validateCname(Fqdn("_underscore.domain.name."), false).isValid
validateCname(Fqdn("under_score.domain.name."), false).isValid
validateCname(Fqdn("underscore._domain.name."), false).isValid
}
// For wildcard records. '*' can only be in the beginning followed by '.' and domain name
property("fqdn names beginning with asterisk should pass property-based testing") {
validateCname(Fqdn("*.domain.name."), false) shouldBe valid
validateCname(Fqdn("aste*risk.domain.name."),false) shouldBe invalid
validateCname(Fqdn("*asterisk.domain.name."),false) shouldBe invalid
validateCname(Fqdn("asterisk*.domain.name."),false) shouldBe invalid
validateCname(Fqdn("asterisk.*domain.name."),false)shouldBe invalid
validateCname(Fqdn("asterisk.domain*.name."),false) shouldBe invalid
}
property("Valid Ipv4 addresses should pass property-based testing") { property("Valid Ipv4 addresses should pass property-based testing") {
forAll(validIpv4Gen) { validIp: String => forAll(validIpv4Gen) { validIp: String =>
val res = validateIpv4Address(validIp) val res = validateIpv4Address(validIp)
@@ -112,50 +160,50 @@ class DomainValidationsSpec
} }
property("Shortest cname should be valid") { property("Shortest cname should be valid") {
validateCname("a.",true) shouldBe valid validateIsReverseCname("a.",true) shouldBe valid
validateCname("a.",false) shouldBe valid validateIsReverseCname("a.",false) shouldBe valid
} }
property("Longest cname should be valid") { property("Longest cname should be valid") {
val name = ("a" * 50 + ".") * 5 val name = ("a" * 50 + ".") * 5
validateCname(name,true) shouldBe valid validateIsReverseCname(name,true) shouldBe valid
validateCname(name,false) shouldBe valid validateIsReverseCname(name,false) shouldBe valid
} }
property("Cnames with underscores should pass property-based testing") { property("Cnames with underscores should pass property-based testing") {
validateCname("_underscore.domain.name.",true).isValid validateIsReverseCname("_underscore.domain.name.",true).isValid
validateCname("under_score.domain.name.",true).isValid validateIsReverseCname("under_score.domain.name.",true).isValid
validateCname("underscore._domain.name.",true).isValid validateIsReverseCname("underscore._domain.name.",true).isValid
validateCname("_underscore.domain.name.",false).isValid validateIsReverseCname("_underscore.domain.name.",false).isValid
validateCname("under_score.domain.name.",false).isValid validateIsReverseCname("under_score.domain.name.",false).isValid
validateCname("underscore._domain.name.",false).isValid validateIsReverseCname("underscore._domain.name.",false).isValid
} }
// For wildcard records. '*' can only be in the beginning followed by '.' and domain name // For wildcard records. '*' can only be in the beginning followed by '.' and domain name
property("Cnames beginning with asterisk should pass property-based testing") { property("Cnames beginning with asterisk should pass property-based testing") {
validateCname("*.domain.name.",true) shouldBe valid validateIsReverseCname("*.domain.name.",true) shouldBe valid
validateCname("aste*risk.domain.name.",true) shouldBe invalid validateIsReverseCname("aste*risk.domain.name.",true) shouldBe invalid
validateCname("*asterisk.domain.name.",true) shouldBe invalid validateIsReverseCname("*asterisk.domain.name.",true) shouldBe invalid
validateCname("asterisk*.domain.name.",true) shouldBe invalid validateIsReverseCname("asterisk*.domain.name.",true) shouldBe invalid
validateCname("asterisk.*domain.name.",true) shouldBe invalid validateIsReverseCname("asterisk.*domain.name.",true) shouldBe invalid
validateCname("asterisk.domain*.name.",true) shouldBe invalid validateIsReverseCname("asterisk.domain*.name.",true) shouldBe invalid
validateCname("*.domain.name.",false) shouldBe valid validateIsReverseCname("*.domain.name.",false) shouldBe valid
validateCname("aste*risk.domain.name.",false) shouldBe invalid validateIsReverseCname("aste*risk.domain.name.",false) shouldBe invalid
validateCname("*asterisk.domain.name.",false) shouldBe invalid validateIsReverseCname("*asterisk.domain.name.",false) shouldBe invalid
validateCname("asterisk*.domain.name.",false) shouldBe invalid validateIsReverseCname("asterisk*.domain.name.",false) shouldBe invalid
validateCname("asterisk.*domain.name.",false) shouldBe invalid validateIsReverseCname("asterisk.*domain.name.",false) shouldBe invalid
validateCname("asterisk.domain*.name.",false) shouldBe invalid validateIsReverseCname("asterisk.domain*.name.",false) shouldBe invalid
} }
property("Cname names with forward slash should pass with reverse zone") { property("Cname names with forward slash should pass with reverse zone") {
validateCname("/slash.cname.name.",true).isValid validateIsReverseCname("/slash.cname.name.",true).isValid
validateCname("slash./cname.name.",true).isValid validateIsReverseCname("slash./cname.name.",true).isValid
validateCname("slash.cname./name.",true).isValid validateIsReverseCname("slash.cname./name.",true).isValid
} }
property("Cname names with forward slash should fail with forward zone") { property("Cname names with forward slash should fail with forward zone") {
validateCname("/slash.cname.name.",false).failWith[InvalidCname] validateIsReverseCname("/slash.cname.name.",false).failWith[InvalidCname]
validateCname("slash./cname.name.",false).failWith[InvalidCname] validateIsReverseCname("slash./cname.name.",false).failWith[InvalidCname]
validateCname("slash.cname./name.",false).failWith[InvalidCname] validateIsReverseCname("slash.cname./name.",false).failWith[InvalidCname]
} }
} }

View File

@@ -717,6 +717,21 @@ class BatchChangeValidationsSpec
result should haveInvalid[DomainValidationError](InvalidCname(s"$invalidCNAMERecordData.",false)) result should haveInvalid[DomainValidationError](InvalidCname(s"$invalidCNAMERecordData.",false))
} }
property("""validateAddChangeInput: should fail with Invalid CNAME
|if validateRecordData fails for IPv4 Address in CNAME record data""".stripMargin) {
val invalidCNAMERecordData = "1.2.3.4"
val change =
AddChangeInput(
"test.comcast.com.",
RecordType.CNAME,
ttl,
CNAMEData(Fqdn(invalidCNAMERecordData))
)
val result = validateAddChangeInput(change, false)
result should haveInvalid[DomainValidationError](InvalidIPv4CName(s"Fqdn($invalidCNAMERecordData.)"))
}
property("""validateAddChangeInput: should fail with InvalidLength property("""validateAddChangeInput: should fail with InvalidLength
|if validateRecordData fails for invalid CNAME record data""".stripMargin) { |if validateRecordData fails for invalid CNAME record data""".stripMargin) {
val invalidCNAMERecordData = "s" * 256 val invalidCNAMERecordData = "s" * 256

View File

@@ -29,6 +29,7 @@ import vinyldns.core.domain.auth.AuthPrincipal
import vinyldns.core.domain.zone.ZoneRepository import vinyldns.core.domain.zone.ZoneRepository
import cats.effect._ import cats.effect._
import scalikejdbc.{ConnectionPool, DB} import scalikejdbc.{ConnectionPool, DB}
import vinyldns.api.config.ValidEmailConfig
import vinyldns.api.domain.zone.NotAuthorizedError import vinyldns.api.domain.zone.NotAuthorizedError
import vinyldns.core.TestMembershipData._ import vinyldns.core.TestMembershipData._
import vinyldns.core.TestZoneData._ import vinyldns.core.TestZoneData._
@@ -48,6 +49,8 @@ class MembershipServiceSpec
private val mockZoneRepo = mock[ZoneRepository] private val mockZoneRepo = mock[ZoneRepository]
private val mockGroupChangeRepo = mock[GroupChangeRepository] private val mockGroupChangeRepo = mock[GroupChangeRepository]
private val mockRecordSetRepo = mock[RecordSetRepository] private val mockRecordSetRepo = mock[RecordSetRepository]
private val mockValidEmailConfig = ValidEmailConfig(valid_domains = List("test.com","*dummy.com"))
private val mockValidEmailConfigNew = ValidEmailConfig(valid_domains = List())
private val backingService = new MembershipService( private val backingService = new MembershipService(
mockGroupRepo, mockGroupRepo,
@@ -55,9 +58,20 @@ class MembershipServiceSpec
mockMembershipRepo, mockMembershipRepo,
mockZoneRepo, mockZoneRepo,
mockGroupChangeRepo, mockGroupChangeRepo,
mockRecordSetRepo mockRecordSetRepo,
mockValidEmailConfig
)
private val backingServiceNew = new MembershipService(
mockGroupRepo,
mockUserRepo,
mockMembershipRepo,
mockZoneRepo,
mockGroupChangeRepo,
mockRecordSetRepo,
mockValidEmailConfigNew
) )
private val underTest = spy(backingService) private val underTest = spy(backingService)
private val underTestNew = spy(backingServiceNew)
private val okUserInfo: UserInfo = UserInfo(okUser) private val okUserInfo: UserInfo = UserInfo(okUser)
private val dummyUserInfo: UserInfo = UserInfo(dummyUser) private val dummyUserInfo: UserInfo = UserInfo(dummyUser)
@@ -82,7 +96,7 @@ class MembershipServiceSpec
// the update will remove users 3 and 4, add users 5 and 6, as well as a new admin user 7 and remove user2 as admin // the update will remove users 3 and 4, add users 5 and 6, as well as a new admin user 7 and remove user2 as admin
private val updatedInfo = Group( private val updatedInfo = Group(
name = "new.name", name = "new.name",
email = "new.email", email = "test@test.com",
description = Some("new desc"), description = Some("new desc"),
id = "id", id = "id",
memberIds = Set("user1", "user2", "user5", "user6", "user7"), memberIds = Set("user1", "user2", "user5", "user6", "user7"),
@@ -282,8 +296,162 @@ class MembershipServiceSpec
verify(mockMembershipRepo, never()) verify(mockMembershipRepo, never())
.saveMembers(any[DB], anyString, any[Set[String]], isAdmin = anyBoolean) .saveMembers(any[DB], anyString, any[Set[String]], isAdmin = anyBoolean)
} }
"return an error if an invalid domain is entered" in {
val error = underTest.createGroup(groupInfo.copy(email = "test@ok.com"), okAuth).value.unsafeRunSync().swap.toOption.get
error shouldBe a[EmailValidationError]
}
"return an error if an invalid email is entered" in {
val error = underTest.createGroup(groupInfo.copy(email = "test.ok.com"), okAuth).value.unsafeRunSync().swap.toOption.get
error shouldBe a[EmailValidationError]
}
"return an error if an invalid email with * is entered" in {
val error = underTest.createGroup(groupInfo.copy(email = "test@*dummy.com"), okAuth).value.unsafeRunSync().swap.toOption.get
error shouldBe a[EmailValidationError]
}
} }
"return an error if an email is invalid test case 1" in {
val error = underTest.emailValidation(email = "test.ok.com").value.unsafeRunSync().swap.toOption.get
error shouldBe a[EmailValidationError]
}
"return an error if a domain is invalid test case 1" in {
val error = underTest.emailValidation(email = "test@ok.com").value.unsafeRunSync().swap.toOption.get
error shouldBe a[EmailValidationError]
}
"return an error if an email is invalid test case 2" in {
val error = underTest.emailValidation(email = "test@.@.test.com").value.unsafeRunSync().swap.toOption.get
error shouldBe a[EmailValidationError]
}
"return an error if an email is invalid test case 3" in {
val error = underTest.emailValidation(email = "test@.@@.test.com").value.unsafeRunSync().swap.toOption.get
error shouldBe a[EmailValidationError]
}
"return an error if an email is invalid test case 4" in {
val error = underTest.emailValidation(email = "@te@st@test.com").value.unsafeRunSync().swap.toOption.get
error shouldBe a[EmailValidationError]
}
"return an error if an email is invalid test case 5" in {
val error = underTest.emailValidation(email = ".test@test.com").value.unsafeRunSync().swap.toOption.get
error shouldBe a[EmailValidationError]
}
"return an error if an email is invalid test case 6" in {
val error = underTest.emailValidation(email = "te.....st@test.com").value.unsafeRunSync().swap.toOption.get
error shouldBe a[EmailValidationError]
}
"return an error if an email is invalid test case 7" in {
val error = underTest.emailValidation(email = "test@test.com.").value.unsafeRunSync().swap.toOption.get
error shouldBe a[EmailValidationError]
}
"Check whether *dummy.com is a valid email" in {
val result = underTest.emailValidation(email = "test@ok.dummy.com").value.unsafeRunSync()
result shouldBe Right(())
}
"Check whether test.com is a valid email" in {
val result = underTest.emailValidation(email = "test@test.com").value.unsafeRunSync()
result shouldBe Right(())
}
"Check whether it is allowing any domain when the config is empty" in {
val result = underTestNew.emailValidation(email = "test@abc.com").value.unsafeRunSync()
result shouldBe Right(())
}
"Create Group when email has domain *dummy.com" 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])
doReturn(IO.pure(Set(okUser.id)))
.when(mockMembershipRepo)
.saveMembers(any[DB], anyString, any[Set[String]], isAdmin = anyBoolean)
doReturn(IO.pure(okGroupChange)).when(mockGroupChangeRepo).save(any[DB], any[GroupChange])
val result = underTest.createGroup(groupInfo.copy(email = "test@ok.dummy.com"), okAuth).value.unsafeRunSync().toOption.get
result shouldBe groupInfo.copy(email = "test@ok.dummy.com")
val groupCaptor = ArgumentCaptor.forClass(classOf[Group])
verify(mockMembershipRepo, times(2))
.saveMembers(any[DB], anyString, any[Set[String]], isAdmin = anyBoolean)
verify(mockGroupRepo).save(any[DB], groupCaptor.capture())
val savedGroup = groupCaptor.getValue
(savedGroup.memberIds should contain).only(okUser.id)
(savedGroup.adminUserIds should contain).only(okUser.id)
savedGroup.name shouldBe groupInfo.name
savedGroup.email shouldBe groupInfo.copy(email = "test@ok.dummy.com").email
savedGroup.description shouldBe groupInfo.description
}
"Create Group when email with any domain when config is empty" in {
doReturn(IO.pure(Some(okUser))).when(mockUserRepo).getUser("ok")
doReturn(().toResult).when(underTestNew).groupValidation(groupInfo)
doReturn(().toResult).when(underTestNew).groupWithSameNameDoesNotExist(groupInfo.name)
doReturn(().toResult).when(underTestNew).usersExist(groupInfo.memberIds)
doReturn(IO.pure(okGroup)).when(mockGroupRepo).save(any[DB], any[Group])
doReturn(IO.pure(Set(okUser.id)))
.when(mockMembershipRepo)
.saveMembers(any[DB], anyString, any[Set[String]], isAdmin = anyBoolean)
doReturn(IO.pure(okGroupChange)).when(mockGroupChangeRepo).save(any[DB], any[GroupChange])
val result = underTestNew.createGroup(groupInfo.copy(email = "test@abc.com"), okAuth).value.unsafeRunSync().toOption.get
result shouldBe groupInfo.copy(email = "test@abc.com")
val groupCaptor = ArgumentCaptor.forClass(classOf[Group])
verify(mockMembershipRepo, times(2))
.saveMembers(any[DB], anyString, any[Set[String]], isAdmin = anyBoolean)
verify(mockGroupRepo).save(any[DB], groupCaptor.capture())
val savedGroup = groupCaptor.getValue
(savedGroup.memberIds should contain).only(okUser.id)
(savedGroup.adminUserIds should contain).only(okUser.id)
savedGroup.name shouldBe groupInfo.name
savedGroup.email shouldBe groupInfo.copy(email = "test@abc.com").email
savedGroup.description shouldBe groupInfo.description
}
"Create Group when email has domain test.com" 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])
doReturn(IO.pure(Set(okUser.id)))
.when(mockMembershipRepo)
.saveMembers(any[DB], anyString, any[Set[String]], isAdmin = anyBoolean)
doReturn(IO.pure(okGroupChange)).when(mockGroupChangeRepo).save(any[DB], any[GroupChange])
val result = underTest.createGroup(groupInfo.copy(email = "test@test.com"), okAuth).value.unsafeRunSync().toOption.get
result shouldBe groupInfo.copy(email = "test@test.com")
val groupCaptor = ArgumentCaptor.forClass(classOf[Group])
verify(mockMembershipRepo, times(2))
.saveMembers(any[DB], anyString, any[Set[String]], isAdmin = anyBoolean)
verify(mockGroupRepo).save(any[DB], groupCaptor.capture())
val savedGroup = groupCaptor.getValue
(savedGroup.memberIds should contain).only(okUser.id)
(savedGroup.adminUserIds should contain).only(okUser.id)
savedGroup.name shouldBe groupInfo.name
savedGroup.email shouldBe groupInfo.copy(email = "test@test.com").email
savedGroup.description shouldBe groupInfo.description
}
"update an existing group" should { "update an existing group" should {
"save the update and add new members and remove deleted members" in { "save the update and add new members and remove deleted members" in {
doReturn(IO.pure(Some(existingGroup))).when(mockGroupRepo).getGroup(any[String]) doReturn(IO.pure(Some(existingGroup))).when(mockGroupRepo).getGroup(any[String])

View File

@@ -1331,7 +1331,7 @@ class RecordSetServiceSpec
doReturn(IO.pure(ListUsersResults(Seq(), None))) doReturn(IO.pure(ListUsersResults(Seq(), None)))
.when(mockUserRepo) .when(mockUserRepo)
.getUsers(Set.empty, None, None) .getUsers(Set.empty, None, None)
val result = val result =
underTest.updateRecordSet(newRecord, auth).map(_.asInstanceOf[RecordSetChange]).value.unsafeRunSync().toOption.get underTest.updateRecordSet(newRecord, auth).map(_.asInstanceOf[RecordSetChange]).value.unsafeRunSync().toOption.get

View File

@@ -486,6 +486,10 @@ class RecordSetValidationsSpec
val error = leftValue(cnameValidations(invalid, List(), okZone, None, true, dottedHostsConfigZonesAllowed.toSet, false)) val error = leftValue(cnameValidations(invalid, List(), okZone, None, true, dottedHostsConfigZonesAllowed.toSet, false))
error shouldBe an[InvalidRequest] error shouldBe an[InvalidRequest]
} }
"return an InvalidRequest if a cname record set fqdn is IPv4 address" in {
val error = leftValue(cnameValidations(cname.copy(records = List(CNAMEData(Fqdn("1.2.3.4")))), List(), okZone, None, true, dottedHostsConfigZonesAllowed.toSet, false))
error shouldBe an[RecordSetValidation]
}
"return an InvalidRequest if a cname record set name is dotted" in { "return an InvalidRequest if a cname record set name is dotted" in {
val error = leftValue(cnameValidations(cname.copy(name = "dot.ted"), List(), okZone, None, true, dottedHostsConfigZonesAllowed.toSet, false)) val error = leftValue(cnameValidations(cname.copy(name = "dot.ted"), List(), okZone, None, true, dottedHostsConfigZonesAllowed.toSet, false))
error shouldBe an[InvalidRequest] error shouldBe an[InvalidRequest]

View File

@@ -81,4 +81,8 @@ object Messages {
// Error displayed when group name or email is empty // Error displayed when group name or email is empty
val GroupValidationErrorMsg = "Group name and email cannot be empty." val GroupValidationErrorMsg = "Group name and email cannot be empty."
val EmailValidationErrorMsg = "Please enter a valid Email ID. Valid domains should end with"
val InvalidEmailValidationErrorMsg = "Please enter a valid Email ID."
} }

View File

@@ -52,6 +52,11 @@ final case class InvalidDomainName(param: String) extends DomainValidationError
"joined by dots, and terminated with a dot." "joined by dots, and terminated with a dot."
} }
final case class InvalidIPv4CName(param: String) extends DomainValidationError {
def message: String =
s"""Invalid Cname: "$param", Valid CNAME record data should not be an IP address"""
}
final case class InvalidCname(param: String, isReverseZone: Boolean) extends DomainValidationError { final case class InvalidCname(param: String, isReverseZone: Boolean) extends DomainValidationError {
def message: String = def message: String =
isReverseZone match { isReverseZone match {

View File

@@ -36,7 +36,7 @@ object DomainValidationErrorType extends Enumeration {
CnameIsNotUniqueError, UserIsNotAuthorized, UserIsNotAuthorizedError, RecordNameNotUniqueInBatch, CnameIsNotUniqueError, UserIsNotAuthorized, UserIsNotAuthorizedError, RecordNameNotUniqueInBatch,
RecordInReverseZoneError, HighValueDomainError, MissingOwnerGroupId, ExistingMultiRecordError, RecordInReverseZoneError, HighValueDomainError, MissingOwnerGroupId, ExistingMultiRecordError,
NewMultiRecordError, CnameAtZoneApexError, RecordRequiresManualReview, UnsupportedOperation, NewMultiRecordError, CnameAtZoneApexError, RecordRequiresManualReview, UnsupportedOperation,
DeleteRecordDataDoesNotExist = Value DeleteRecordDataDoesNotExist, InvalidIPv4CName = Value
// $COVERAGE-OFF$ // $COVERAGE-OFF$
def from(error: DomainValidationError): DomainValidationErrorType = def from(error: DomainValidationError): DomainValidationErrorType =
@@ -74,6 +74,7 @@ object DomainValidationErrorType extends Enumeration {
case _: RecordRequiresManualReview => RecordRequiresManualReview case _: RecordRequiresManualReview => RecordRequiresManualReview
case _: UnsupportedOperation => UnsupportedOperation case _: UnsupportedOperation => UnsupportedOperation
case _: DeleteRecordDataDoesNotExist => DeleteRecordDataDoesNotExist case _: DeleteRecordDataDoesNotExist => DeleteRecordDataDoesNotExist
case _: InvalidIPv4CName => InvalidIPv4CName
} }
// $COVERAGE-ON$ // $COVERAGE-ON$
} }

View File

@@ -483,6 +483,16 @@ sns {
} }
} }
``` ```
### Email Domain Configuration
This configuration setting determines the valid domains which are
allowed in the email fields. `*dummy.com` means it will allow any
subdomain within dummy.com like apac.dummy.com. If email-domains is
left empty then it will accept any domain name.
```yaml
valid-email-config {
email-domains = ["test.com","*dummy.com"]
}
```
### Batch Manual Review Enabled <a id="manual-review" /> ### Batch Manual Review Enabled <a id="manual-review" />
@@ -760,6 +770,11 @@ dotted-hosts = {
} }
} }
# Valid Email Domains
valid-email-config {
email-domains = ["test.com","*dummy.com"]
}
sns { sns {
# Path to notifier provider implementation # Path to notifier provider implementation
class-name = "vinyldns.api.notifier.sns.SnsNotifierProvider" class-name = "vinyldns.api.notifier.sns.SnsNotifierProvider"