2
0
mirror of https://github.com/VinylDNS/vinyldns synced 2025-08-30 22:05:21 +00:00

Merge branch 'master' into disallow_ipv4_in_cname_manage_records

This commit is contained in:
Nicholas Spadaccino
2023-03-15 15:09:19 -04:00
committed by GitHub
13 changed files with 328 additions and 9 deletions

View File

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

View File

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

View File

@@ -142,7 +142,7 @@ object Boot extends App {
vinyldnsConfig.batchChangeConfig,
vinyldnsConfig.scheduledChangesConfig
)
val membershipService = MembershipService(repositories)
val membershipService = MembershipService(repositories,vinyldnsConfig.validEmailConfig)
val connectionValidator =
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(
serverConfig: ServerConfig,
limitsconfig: LimitsConfig,
validEmailConfig: ValidEmailConfig,
httpConfig: HttpConfig,
highValueDomainConfig: HighValueDomainConfig,
manualReviewConfig: ManualReviewConfig,
@@ -83,6 +84,7 @@ object VinylDNSConfig {
for {
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
validEmailConfig <- loadIO[ValidEmailConfig](config, path="vinyldns.valid-email-config")
serverConfig <- loadIO[ServerConfig](config, "vinyldns")
batchChangeConfig <- loadIO[BatchChangeConfig](config, "vinyldns")
backendConfigs <- loadIO[BackendConfigs](config, "vinyldns.backend")
@@ -103,6 +105,7 @@ object VinylDNSConfig {
} yield VinylDNSConfig(
serverConfig,
limitsconfig,
validEmailConfig,
httpConfig,
hvdConfig,
manualReviewConfig,

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 EmailValidationError(msg: String) extends Throwable(msg)
final case class UserNotFoundError(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 scalikejdbc.DB
import vinyldns.api.Interfaces._
import vinyldns.api.config.ValidEmailConfig
import vinyldns.api.repository.ApiDataAccessor
import vinyldns.core.domain.auth.AuthPrincipal
import vinyldns.core.domain.membership.LockStatus.LockStatus
@@ -30,14 +31,15 @@ import vinyldns.core.Messages._
import vinyldns.mysql.TransactionProvider
object MembershipService {
def apply(dataAccessor: ApiDataAccessor): MembershipService =
def apply(dataAccessor: ApiDataAccessor,emailConfig:ValidEmailConfig): MembershipService =
new MembershipService(
dataAccessor.groupRepository,
dataAccessor.userRepository,
dataAccessor.membershipRepository,
dataAccessor.zoneRepository,
dataAccessor.groupChangeRepository,
dataAccessor.recordSetRepository
dataAccessor.recordSetRepository,
emailConfig
)
}
@@ -47,7 +49,8 @@ class MembershipService(
membershipRepo: MembershipRepository,
zoneRepo: ZoneRepository,
groupChangeRepo: GroupChangeRepository,
recordSetRepo: RecordSetRepository
recordSetRepo: RecordSetRepository,
validDomains: ValidEmailConfig
) extends MembershipServiceAlgebra with TransactionProvider {
import MembershipValidations._
@@ -58,6 +61,7 @@ class MembershipService(
val nonAdminMembers = inputGroup.memberIds.diff(adminMembers)
for {
_ <- groupValidation(newGroup)
_ <- emailValidation(newGroup.email)
_ <- hasMembersAndAdmins(newGroup).toResult
_ <- groupWithSameNameDoesNotExist(newGroup.name)
_ <- usersExist(newGroup.memberIds)
@@ -78,6 +82,7 @@ class MembershipService(
existingGroup <- getExistingGroup(groupId)
newGroup = existingGroup.withUpdates(name, email, description, memberIds, adminUserIds)
_ <- groupValidation(newGroup)
_ <- emailValidation(newGroup.email)
_ <- canEditGroup(existingGroup, authPrincipal).toResult
addedAdmins = newGroup.adminUserIds.diff(existingGroup.adminUserIds)
// new non-admin members ++ admins converted to non-admins
@@ -381,6 +386,29 @@ class MembershipService(
().asRight
}
}.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] =
groupRepo

View File

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

View File

@@ -10,7 +10,7 @@ def test_create_group_success(shared_zone_test_context):
try:
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",
"description": "this is a description",
"members": [{"id": "ok"}],
@@ -32,6 +32,36 @@ def test_create_group_success(shared_zone_test_context):
if result:
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):
"""
@@ -114,8 +144,36 @@ def test_create_group_without_name_or_email(shared_zone_test_context):
"Missing Group.name",
"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):
"""
Tests that creating a group without members or admins fails

View File

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

View File

@@ -29,6 +29,7 @@ import vinyldns.core.domain.auth.AuthPrincipal
import vinyldns.core.domain.zone.ZoneRepository
import cats.effect._
import scalikejdbc.{ConnectionPool, DB}
import vinyldns.api.config.ValidEmailConfig
import vinyldns.api.domain.zone.NotAuthorizedError
import vinyldns.core.TestMembershipData._
import vinyldns.core.TestZoneData._
@@ -48,6 +49,8 @@ class MembershipServiceSpec
private val mockZoneRepo = mock[ZoneRepository]
private val mockGroupChangeRepo = mock[GroupChangeRepository]
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(
mockGroupRepo,
@@ -55,9 +58,20 @@ class MembershipServiceSpec
mockMembershipRepo,
mockZoneRepo,
mockGroupChangeRepo,
mockRecordSetRepo
mockRecordSetRepo,
mockValidEmailConfig
)
private val backingServiceNew = new MembershipService(
mockGroupRepo,
mockUserRepo,
mockMembershipRepo,
mockZoneRepo,
mockGroupChangeRepo,
mockRecordSetRepo,
mockValidEmailConfigNew
)
private val underTest = spy(backingService)
private val underTestNew = spy(backingServiceNew)
private val okUserInfo: UserInfo = UserInfo(okUser)
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
private val updatedInfo = Group(
name = "new.name",
email = "new.email",
email = "test@test.com",
description = Some("new desc"),
id = "id",
memberIds = Set("user1", "user2", "user5", "user6", "user7"),
@@ -282,8 +296,162 @@ class MembershipServiceSpec
verify(mockMembershipRepo, never())
.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 {
"save the update and add new members and remove deleted members" in {
doReturn(IO.pure(Some(existingGroup))).when(mockGroupRepo).getGroup(any[String])

View File

@@ -81,4 +81,8 @@ object Messages {
// Error displayed when group name or email is 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

@@ -475,6 +475,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" />
@@ -752,6 +762,11 @@ dotted-hosts = {
}
}
# Valid Email Domains
valid-email-config {
email-domains = ["test.com","*dummy.com"]
}
sns {
# Path to notifier provider implementation
class-name = "vinyldns.api.notifier.sns.SnsNotifierProvider"