2
0
mirror of https://github.com/VinylDNS/vinyldns synced 2025-08-22 02:02:14 +00:00

Merge branch 'master' into aravindhr/change-to-java-time

This commit is contained in:
Aravindh R 2022-11-08 10:15:51 +05:30 committed by GitHub
commit 22ea714d9d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
104 changed files with 4923 additions and 691 deletions

View File

@ -36,14 +36,17 @@ in any way, but do not see your name here, please open a PR to add yourself (in
- Joshulyne Park
- Nathan Pierce
- Michael Pilquist
- Aravindh Raju
- Sriram Ramakrishnan
- Khalid Reid
- Timo Schmid
- Trent Schmidt
- Nick Spadaccino
- Ghafar Shah
- Rebecca Star
- Jess Stodola
- Juan Valencia
- Jayaraj Velkumar
- Anastasia Vishnyakova
- Jim Wakemen
- Fei Wan

View File

@ -10,6 +10,7 @@
* [Portal](#portal)
* [Documentation](#documentation)
- [Running VinylDNS Locally](#running-vinyldns-locally)
* [Support for M1 Macs](#support-for-m1-macs)
* [Starting the API Server](#starting-the-api-server)
* [Starting the Portal](#starting-the-portal)
- [Testing](#testing)
@ -161,6 +162,61 @@ settings for the microsite are also configured in `build.sbt` of the project roo
VinylDNS can be started in the background by running the [quickstart instructions](README.md#quickstart) located in the
README. However, VinylDNS can also be run in the foreground.
### Support for M1 Macs
If you are using a Mac running macOS with one of the new Apple M1 chips, you will need to update some dependencies to
newer versions before attempting to run VinylDNS locally. To verify whether your computer has one of these chips,
go to About This Mac in the Apple menu in the top-left corner of your screen. If next to Chip you see Apple M1,
or any later chip such as the Apple M1 Pro or Apple M1 Max, then you will need to apply these changes to the code.
#### build.sbt
Update protoc from version 2.6.1:
```shell
PB.targets in Compile := Seq(PB.gens.java("2.6.1") -> (sourceManaged in Compile).value),
PB.protocVersion := "-v261"
```
to version 3.21.7:
```shell
PB.targets in Compile := Seq(PB.gens.java("3.21.7") -> (sourceManaged in Compile).value),
PB.protocVersion := "-v3.21.7"
```
#### project/build.properties
Update `sbt.version=1.4.0` to `sbt.version=1.7.2`
#### project/Dependencies.scala
Update protobuf from version 2.6.1:
```shell
"com.google.protobuf" % "protobuf-java" % "2.6.1",
```
to version 3.21.7:
```shell
"com.google.protobuf" % "protobuf-java" % "3.21.7",
```
#### project/plugins.sbt
Update the sbt-protoc plugin from version 0.99.18:
```shell
addSbtPlugin("com.thesamet" % "sbt-protoc" % "0.99.18")
```
to version 1.0.6:
```shell
addSbtPlugin("com.thesamet" % "sbt-protoc" % "1.0.6")
```
### Starting the API Server
Before starting the API service, you can start the dependencies for local development:
@ -267,7 +323,7 @@ Additionally, you can pass `--interactive` to `make run` or `make run-local` to
From there you can run tests with the `/functional_test/run.sh` command. This allows for finer-grained control over the
test execution process as well as easier inspection of logs.
You can run a specific test by name by running `make run -- -k <name of test function>`. Any arguments after
You can run a specific test by name by running `make build` and `make run -- -k <name of test function>`. Any arguments after
`make run --` will be passed to the test runner [`test/api/functional/run.sh`](test/api/functional/run.sh).
Finally, you can execute `make run-deps-bg` to all of the dependencies for the functional test, but not run the tests.

View File

@ -163,6 +163,28 @@ vinyldns {
"ns1.parent.com4."
]
# approved zones, individual users, users in groups, record types and no.of.dots that are allowed for dotted hosts
dotted-hosts = {
# for local testing
allowed-settings = [
{
zone = "*mmy."
user-list = ["testuser"]
group-list = ["dummy-group"]
record-types = ["AAAA"]
dots-limit = 3
},
{
# for wildcard zones. Settings will be applied to all matching zones
zone = "parent.com."
user-list = ["professor", "testuser"]
group-list = ["testing-group"]
record-types = ["A", "CNAME"]
dots-limit = 3
}
]
}
# Note: This MUST match the Portal or strange errors will ensue, NoOpCrypto should not be used for production
crypto {
type = "vinyldns.core.crypto.NoOpCrypto"

View File

@ -33,7 +33,6 @@ import vinyldns.api.domain.access.AccessValidations
import vinyldns.api.domain.zone._
import vinyldns.api.engine.TestMessageQueue
import vinyldns.mysql.TransactionProvider
import vinyldns.core.TestMembershipData._
import vinyldns.core.TestZoneData.testConnection
import vinyldns.core.domain.{Fqdn, HighValueDomainError}
import vinyldns.core.domain.auth.AuthPrincipal
@ -65,14 +64,24 @@ class RecordSetServiceIntegrationSpec
private var testRecordSetService: RecordSetServiceAlgebra = _
private val user = User("live-test-user", "key", "secret")
private val testUser = User("testuser", "key", "secret")
private val user2 = User("shared-record-test-user", "key-shared", "secret-shared")
private val group = Group(s"test-group", "test@test.com", adminUserIds = Set(user.id))
private val dummyGroup = Group(s"dummy-group", "test@test.com", adminUserIds = Set(testUser.id))
private val group2 = Group(s"test-group", "test@test.com", adminUserIds = Set(user.id, user2.id))
private val sharedGroup =
Group(s"test-shared-group", "test@test.com", adminUserIds = Set(user.id, user2.id))
private val auth = AuthPrincipal(user, Seq(group.id, sharedGroup.id))
private val auth2 = AuthPrincipal(user2, Seq(sharedGroup.id, group2.id))
val dummyAuth: AuthPrincipal = AuthPrincipal(testUser, Seq(dummyGroup.id))
private val dummyZone = Zone(
s"dummy.",
"test@test.com",
status = ZoneStatus.Active,
connection = testConnection,
adminGroupId = dummyGroup.id
)
private val zone = Zone(
s"live-zone-test.",
"test@test.com",
@ -101,6 +110,16 @@ class RecordSetServiceIntegrationSpec
None,
List(AAAAData("fd69:27cc:fe91::60"))
)
private val dottedTestRecord = RecordSet(
dummyZone.id,
"test.dotted",
AAAA,
38400,
RecordSetStatus.Active,
DateTime.now,
None,
List(AAAAData("fd69:27cc:fe91::60"))
)
private val subTestRecordA = RecordSet(
zone.id,
"a-record",
@ -255,8 +274,8 @@ class RecordSetServiceIntegrationSpec
groupRepo.save(db, group)
}
List(group, group2, sharedGroup).traverse(g => saveGroupData(groupRepo, g).void).unsafeRunSync()
List(zone, zoneTestNameConflicts, zoneTestAddRecords, sharedZone)
List(group, group2, sharedGroup, dummyGroup).traverse(g => saveGroupData(groupRepo, g).void).unsafeRunSync()
List(zone, dummyZone, zoneTestNameConflicts, zoneTestAddRecords, sharedZone)
.traverse(
z => zoneRepo.save(z)
)
@ -274,6 +293,7 @@ class RecordSetServiceIntegrationSpec
val zoneRecords = List(
apexTestRecordA,
apexTestRecordAAAA,
dottedTestRecord,
subTestRecordA,
subTestRecordAAAA,
subTestRecordNS,
@ -301,6 +321,7 @@ class RecordSetServiceIntegrationSpec
mockBackendResolver,
false,
vinyldnsConfig.highValueDomainConfig,
vinyldnsConfig.dottedHostsConfig,
vinyldnsConfig.serverConfig.approvedNameServers,
useRecordSetCache = true
)
@ -339,6 +360,93 @@ class RecordSetServiceIntegrationSpec
.name shouldBe "zone-test-add-records."
}
"create dotted record fails if it doesn't satisfy dotted hosts config" in {
val newRecord = RecordSet(
zoneTestAddRecords.id,
"test.dot",
A,
38400,
RecordSetStatus.Active,
DateTime.now,
None,
List(AData("10.1.1.1"))
)
val result =
testRecordSetService
.addRecordSet(newRecord, auth)
.value
.unsafeRunSync()
leftValue(result) shouldBe a[InvalidRequest]
}
"create dotted record succeeds if it satisfies all dotted hosts config" in {
val newRecord = RecordSet(
dummyZone.id,
"testing.dotted",
AAAA,
38400,
RecordSetStatus.Active,
DateTime.now,
None,
List(AAAAData("fd69:27cc:fe91::60"))
)
// succeeds as zone, user and record type is allowed as defined in application.conf
val result =
testRecordSetService
.addRecordSet(newRecord, dummyAuth)
.value
.unsafeRunSync()
rightValue(result)
.asInstanceOf[RecordSetChange]
.recordSet
.name shouldBe "testing.dotted"
}
"fail creating dotted record if it satisfies all dotted hosts config except dots-limit for the zone" in {
val newRecord = RecordSet(
dummyZone.id,
"test.dotted.more.dots.than.allowed",
AAAA,
38400,
RecordSetStatus.Active,
DateTime.now,
None,
List(AAAAData("fd69:27cc:fe91::60"))
)
// The number of dots allowed in the record name for this zone as defined in the config is 3.
// Creating with 4 dots results in an error
val result =
testRecordSetService
.addRecordSet(newRecord, dummyAuth)
.value
.unsafeRunSync()
leftValue(result) shouldBe a[InvalidRequest]
}
"update dotted record succeeds if it satisfies all dotted hosts config" in {
val newRecord = dottedTestRecord.copy(ttl = 37000)
val result = testRecordSetService
.updateRecordSet(newRecord, dummyAuth)
.value
.unsafeRunSync()
val change = rightValue(result).asInstanceOf[RecordSetChange]
change.recordSet.name shouldBe "test.dotted"
change.recordSet.ttl shouldBe 37000
}
"update dotted record name fails as updating a record name is not allowed" in {
val newRecord = dottedTestRecord.copy(name = "trial.dotted")
val result = testRecordSetService
.updateRecordSet(newRecord, dummyAuth)
.value
.unsafeRunSync()
// We get an "InvalidRequest: Cannot update RecordSet's name."
leftValue(result) shouldBe a[InvalidRequest]
}
"update apex A record and add trailing dot" in {
val newRecord = apexTestRecordA.copy(ttl = 200)
val result = testRecordSetService
@ -550,6 +658,15 @@ class RecordSetServiceIntegrationSpec
Some(group2.id)
}
"delete dotted host record successfully for user in record owner group" in {
val result = testRecordSetService
.deleteRecordSet(dottedTestRecord.id, dottedTestRecord.zoneId, dummyAuth)
.value
.unsafeRunSync()
result should be(right)
}
"fail deleting for user not in record owner group in shared zone" in {
val result = leftResultOf(
testRecordSetService

View File

@ -165,6 +165,19 @@ vinyldns {
"ns1.parent.com4."
]
# approved zones, individual users, users in groups, record types and no.of.dots that are allowed for dotted hosts
dotted-hosts = {
allowed-settings = [
{
zone = "zonenamehere."
user-list = []
group-list = []
record-types = []
dots-limit = 0
}
]
}
# Note: This MUST match the Portal or strange errors will ensue, NoOpCrypto should not be used for production
crypto {
type = "vinyldns.core.crypto.NoOpCrypto"

View File

@ -90,6 +90,29 @@ vinyldns {
"ns1.parent.com."
]
# approved zones, individual users, users in groups, record types and no.of.dots that are allowed for dotted hosts
dotted-hosts = {
# for local testing
allowed-settings = [
{
# for wildcard zones. Settings will be applied to all matching zones
zone = "*ent.com*."
user-list = ["ok"]
group-list = ["dummy-group"]
record-types = ["CNAME"]
dots-limit = 3
},
{
# for wildcard zones. Settings will be applied to all matching zones
zone = "dummy*."
user-list = ["sharedZoneUser"]
group-list = ["history-group1"]
record-types = ["A"]
dots-limit = 3
}
]
}
# color should be green or blue, used in order to do blue/green deployment
color = "green"
@ -111,7 +134,7 @@ vinyldns {
batchchange-routing-max-items-limit = 100
membership-routing-default-max-items = 100
membership-routing-max-items-limit = 1000
membership-routing-max-groups-list-limit = 1500
membership-routing-max-groups-list-limit = 3000
recordset-routing-default-max-items= 100
zone-routing-default-max-items = 100
zone-routing-max-items-limit = 100

View File

@ -139,6 +139,7 @@ object Boot extends App {
backendResolver,
vinyldnsConfig.serverConfig.validateRecordLookupAgainstDnsBackend,
vinyldnsConfig.highValueDomainConfig,
vinyldnsConfig.dottedHostsConfig,
vinyldnsConfig.serverConfig.approvedNameServers,
vinyldnsConfig.serverConfig.useRecordSetCache
)

View File

@ -279,7 +279,7 @@ trait DnsConversions {
def fromSPFRecord(r: DNS.SPFRecord, zoneName: DNS.Name, zoneId: String): RecordSet =
fromDnsRecord(r, zoneName, zoneId) { data =>
List(SPFData(data.getStrings.asScala.mkString(",")))
List(SPFData(data.getStrings.asScala.mkString))
}
def fromSRVRecord(r: DNS.SRVRecord, zoneName: DNS.Name, zoneId: String): RecordSet =
@ -394,7 +394,8 @@ trait DnsConversions {
new DNS.SSHFPRecord(recordName, DNS.DClass.IN, ttl, algorithm, typ, Hex.decodeHex(fingerprint.toCharArray()))
case SPFData(text) =>
new DNS.SPFRecord(recordName, DNS.DClass.IN, ttl, text)
val texts = text.grouped(255).toList
new DNS.SPFRecord(recordName, DNS.DClass.IN, ttl, texts.asJava)
case TXTData(text) =>
val texts = text.grouped(255).toList

View File

@ -0,0 +1,31 @@
/*
* 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
import pureconfig.generic.auto._
final case class ZoneAuthConfigs(zone: String, userList: List[String], groupList: List[String], recordTypes: List[String], dotsLimit: Int)
final case class DottedHostsConfig(zoneAuthConfigs: List[ZoneAuthConfigs])
object DottedHostsConfig {
implicit val configReader: ConfigReader[DottedHostsConfig] =
ConfigReader.forProduct1[DottedHostsConfig, List[ZoneAuthConfigs]](
"allowed-settings",
)(zoneAuthConfigs =>
DottedHostsConfig(zoneAuthConfigs))
}

View File

@ -47,6 +47,7 @@ final case class VinylDNSConfig(
notifierConfigs: List[NotifierConfig],
dataStoreConfigs: List[DataStoreConfig],
backendConfigs: BackendConfigs,
dottedHostsConfig: DottedHostsConfig,
configuredDnsConnections: ConfiguredDnsConnections,
apiMetricSettings: APIMetricsSettings,
crypto: CryptoAlgebra,
@ -85,6 +86,7 @@ object VinylDNSConfig {
serverConfig <- loadIO[ServerConfig](config, "vinyldns")
batchChangeConfig <- loadIO[BatchChangeConfig](config, "vinyldns")
backendConfigs <- loadIO[BackendConfigs](config, "vinyldns.backend")
dottedHostsConfig <- loadIO[DottedHostsConfig](config, "vinyldns.dotted-hosts")
httpConfig <- loadIO[HttpConfig](config, "vinyldns.rest")
hvdConfig <- loadIO[HighValueDomainConfig](config, "vinyldns.high-value-domains")
scheduledChangesConfig <- loadIO[ScheduledChangesConfig](config, "vinyldns")
@ -110,6 +112,7 @@ object VinylDNSConfig {
notifierConfigs,
dataStoreConfigs,
backendConfigs,
dottedHostsConfig,
connections,
metricSettings,
crypto,

View File

@ -27,6 +27,10 @@ import scala.util.matching.Regex
Object to house common domain validations
*/
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 =
@ -60,6 +64,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):
@ -85,6 +113,8 @@ object DomainValidations {
checkRegex.combine(checkLength).map(_ => name)
}
def validateIpv4Address(address: String): ValidatedNel[DomainValidationError, String] =
validIpv4Regex
.findFirstIn(address)

View File

@ -28,12 +28,15 @@ import vinyldns.api.domain.record.RecordSetChangeGenerator
import vinyldns.core.domain.record._
import vinyldns.core.domain.zone.Zone
import vinyldns.core.domain.batch._
import vinyldns.core.domain.record.RecordType.RecordType
import vinyldns.core.domain.record.RecordType.{RecordType, UNKNOWN}
import vinyldns.core.queue.MessageQueue
class BatchChangeConverter(batchChangeRepo: BatchChangeRepository, messageQueue: MessageQueue)
extends BatchChangeConverterAlgebra {
private val notExistCompletedMessage: String = "This record does not exist." +
"No further action is required."
private val failedMessage: String = "Error queueing RecordSetChange for processing"
private val logger = LoggerFactory.getLogger(classOf[BatchChangeConverter])
def sendBatchForProcessing(
@ -68,15 +71,20 @@ class BatchChangeConverter(batchChangeRepo: BatchChangeRepository, messageQueue:
recordSetChanges: List[RecordSetChange]
): BatchResult[Unit] = {
val convertedIds = recordSetChanges.flatMap(_.singleBatchChangeIds).toSet
singleChanges.find(ch => !convertedIds.contains(ch.id)) match {
case Some(change) => BatchConversionError(change).toLeftBatchResult
case None =>
// Each single change has a corresponding recordset id
// If they're not equal, then there's a delete request for a record that doesn't exist. So we allow this to process
case Some(_) if singleChanges.map(_.id).length != recordSetChanges.map(_.id).length && !singleChanges.map(_.typ).contains(UNKNOWN) =>
logger.info(s"Successfully converted SingleChanges [${singleChanges
.map(_.id)}] to RecordSetChanges [${recordSetChanges.map(_.id)}]")
().toRightBatchResult
case Some(change) => BatchConversionError(change).toLeftBatchResult
case None =>
logger.info(s"Successfully converted SingleChanges [${singleChanges
.map(_.id)}] to RecordSetChanges [${recordSetChanges.map(_.id)}]")
().toRightBatchResult
}
}
}
def putChangesOnQueue(
recordSetChanges: List[RecordSetChange],
@ -105,7 +113,6 @@ class BatchChangeConverter(batchChangeRepo: BatchChangeRepository, messageQueue:
val idsMap = recordSetChanges.flatMap { rsChange =>
rsChange.singleBatchChangeIds.map(batchId => (batchId, rsChange.id))
}.toMap
val withStatus = batchChange.changes.map { change =>
idsMap
.get(change.id)
@ -114,19 +121,26 @@ class BatchChangeConverter(batchChangeRepo: BatchChangeRepository, messageQueue:
change
}
.getOrElse {
// failure here means there was a message queue issue for this change
change.withFailureMessage("Error queueing RecordSetChange for processing")
// Match and check if it's a delete change for a record that doesn't exists.
change match {
case _: SingleDeleteRRSetChange if change.recordSetId.isEmpty =>
// Mark as Complete since we don't want to throw it as an error
change.withDoesNotExistMessage(notExistCompletedMessage)
case _ =>
// Failure here means there was a message queue issue for this change
change.withFailureMessage(failedMessage)
}
}
}
batchChange.copy(changes = withStatus)
}
def storeQueuingFailures(batchChange: BatchChange): BatchResult[Unit] = {
val failedChanges = batchChange.changes.collect {
case change if change.status == SingleChangeStatus.Failed => change
// Update if Single change is Failed or if a record that does not exist is deleted
val failedAndNotExistsChanges = batchChange.changes.collect {
case change if change.status == SingleChangeStatus.Failed || change.systemMessage.contains(notExistCompletedMessage) => change
}
batchChangeRepo.updateSingleChanges(failedChanges).as(())
batchChangeRepo.updateSingleChanges(failedAndNotExistsChanges).as(())
}.toBatchResult
def createRecordSetChangesForBatch(

View File

@ -33,19 +33,8 @@ import vinyldns.core.domain.auth.AuthPrincipal
import vinyldns.core.domain.batch.BatchChangeApprovalStatus.BatchChangeApprovalStatus
import vinyldns.core.domain.batch._
import vinyldns.core.domain.batch.BatchChangeApprovalStatus._
import vinyldns.core.domain.{
CnameAtZoneApexError,
SingleChangeError,
UserIsNotAuthorizedError,
ZoneDiscoveryError
}
import vinyldns.core.domain.membership.{
Group,
GroupRepository,
ListUsersResults,
User,
UserRepository
}
import vinyldns.core.domain.{CnameAtZoneApexError, SingleChangeError, UserIsNotAuthorizedError, ZoneDiscoveryError}
import vinyldns.core.domain.membership.{Group, GroupRepository, ListUsersResults, User, UserRepository}
import vinyldns.core.domain.record.RecordType._
import vinyldns.core.domain.record.RecordSetRepository
import vinyldns.core.domain.zone.ZoneRepository

View File

@ -19,15 +19,9 @@ package vinyldns.api.domain.batch
import java.net.InetAddress
import java.time.Instant
import java.time.temporal.ChronoUnit
import cats.data._
import cats.implicits._
import vinyldns.api.config.{
BatchChangeConfig,
HighValueDomainConfig,
ManualReviewConfig,
ScheduledChangesConfig
}
import vinyldns.api.config.{BatchChangeConfig, HighValueDomainConfig, ManualReviewConfig, ScheduledChangesConfig}
import vinyldns.api.domain.DomainValidations._
import vinyldns.api.domain.access.AccessValidationsAlgebra
import vinyldns.core.domain.auth.AuthPrincipal
@ -36,7 +30,7 @@ import vinyldns.api.domain.batch.BatchTransformations._
import vinyldns.api.domain.zone.ZoneRecordValidations
import vinyldns.core.domain.record._
import vinyldns.core.domain._
import vinyldns.core.domain.batch.{BatchChange, BatchChangeApprovalStatus, OwnerType, RecordKey}
import vinyldns.core.domain.batch.{BatchChange, BatchChangeApprovalStatus, OwnerType, RecordKey, RecordKeyData}
import vinyldns.core.domain.membership.Group
trait BatchChangeValidationsAlgebra {
@ -53,10 +47,10 @@ trait BatchChangeValidationsAlgebra {
): ValidatedBatch[ChangeInput]
def validateChangesWithContext(
groupedChanges: ChangeForValidationMap,
auth: AuthPrincipal,
isApproved: Boolean,
batchOwnerGroupId: Option[String]
groupedChanges: ChangeForValidationMap,
auth: AuthPrincipal,
isApproved: Boolean,
batchOwnerGroupId: Option[String]
): ValidatedBatch[ChangeForValidation]
def canGetBatchChange(
@ -213,7 +207,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
@ -224,7 +218,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)
@ -232,11 +226,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 => validateHostName(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 =>
@ -274,17 +275,17 @@ class BatchChangeValidations(
/* context validations */
def validateChangesWithContext(
groupedChanges: ChangeForValidationMap,
auth: AuthPrincipal,
isApproved: Boolean,
batchOwnerGroupId: Option[String]
groupedChanges: ChangeForValidationMap,
auth: AuthPrincipal,
isApproved: Boolean,
batchOwnerGroupId: Option[String]
): ValidatedBatch[ChangeForValidation] =
// Updates are a combination of an add and delete for a record with the same name and type in a zone.
// Updates are a combination of an add and delete for a record with the same name and type in a zone.
groupedChanges.changes.mapValid {
case add: AddChangeForValidation
if groupedChanges
.getLogicalChangeType(add.recordKey)
.contains(LogicalChangeType.Add) =>
if groupedChanges
.getLogicalChangeType(add.recordKey)
.contains(LogicalChangeType.Add) =>
validateAddWithContext(add, groupedChanges, auth, isApproved, batchOwnerGroupId)
case addUpdate: AddChangeForValidation =>
validateAddUpdateWithContext(addUpdate, groupedChanges, auth, isApproved, batchOwnerGroupId)
@ -349,7 +350,7 @@ class BatchChangeValidations(
userCanDeleteRecordSet(change, auth, rs.ownerGroupId, rs.records) |+|
zoneDoesNotRequireManualReview(change, isApproved) |+|
ensureRecordExists(change, groupedChanges)
case None => RecordDoesNotExist(change.inputChange.inputName).invalidNel
case None => RecordDoesNotExist(change.inputChange.inputName).validNel
}
validations.map(_ => change)
}
@ -403,18 +404,18 @@ class BatchChangeValidations(
zoneDoesNotRequireManualReview(change, isApproved) |+|
ensureRecordExists(change, groupedChanges)
case None =>
RecordDoesNotExist(change.inputChange.inputName).invalidNel
RecordDoesNotExist(change.inputChange.inputName).validNel
}
validations.map(_ => change)
}
def validateAddWithContext(
change: AddChangeForValidation,
groupedChanges: ChangeForValidationMap,
auth: AuthPrincipal,
isApproved: Boolean,
ownerGroupId: Option[String]
change: AddChangeForValidation,
groupedChanges: ChangeForValidationMap,
auth: AuthPrincipal,
isApproved: Boolean,
ownerGroupId: Option[String]
): SingleValidation[ChangeForValidation] = {
val typedValidations = change.inputChange.typ match {
case A | AAAA | MX =>
@ -437,11 +438,12 @@ class BatchChangeValidations(
change.recordName,
change.inputChange.inputName,
change.inputChange.typ,
groupedChanges
change.inputChange.record,
groupedChanges,
isApproved
) |+|
ownerGroupProvidedIfNeeded(change, None, ownerGroupId) |+|
zoneDoesNotRequireManualReview(change, isApproved)
validations.map(_ => change)
}
@ -479,11 +481,16 @@ class BatchChangeValidations(
recordName: String,
inputName: String,
typ: RecordType,
groupedChanges: ChangeForValidationMap
): SingleValidation[Unit] =
groupedChanges.getExistingRecordSet(RecordKey(zoneId, recordName, typ)) match {
case Some(_) => RecordAlreadyExists(inputName).invalidNel
case None => ().validNel
recordData: RecordData,
groupedChanges: ChangeForValidationMap,
isApproved: Boolean
): SingleValidation[Unit] = {
val record = groupedChanges.getExistingRecordSetData(RecordKeyData(zoneId, recordName, typ, recordData))
if(record.isDefined) {
record.get.records.contains(recordData) match {
case true => ().validNel
case false => RecordAlreadyExists(inputName, recordData, isApproved).invalidNel}
} else ().validNel
}
def noIncompatibleRecordExists(

View File

@ -68,6 +68,9 @@ object BatchTransformations {
def get(recordKey: RecordKey): Option[RecordSet] =
get(recordKey.zoneId, recordKey.recordName, recordKey.recordType)
def get(recordKeyData: RecordKeyData): Option[RecordSet] =
get(recordKeyData.zoneId, recordKeyData.recordName, recordKeyData.recordType)
def getRecordSetMatch(zoneId: String, name: String): List[RecordSet] =
recordSetMap.getOrElse((zoneId, name.toLowerCase), List())
}
@ -171,6 +174,9 @@ object BatchTransformations {
def getExistingRecordSet(recordKey: RecordKey): Option[RecordSet] =
existingRecordSets.get(recordKey)
def getExistingRecordSetData(recordKeyData: RecordKeyData): Option[RecordSet] =
existingRecordSets.get(recordKeyData)
def getProposedAdds(recordKey: RecordKey): Set[RecordData] =
innerMap.get(recordKey).map(_.proposedAdds).toSet.flatten

View File

@ -60,7 +60,9 @@ final case class GroupChangeInfo(
userId: String,
oldGroup: Option[GroupInfo] = None,
id: String = UUID.randomUUID().toString,
created: Instant = Instant.now.truncatedTo(ChronoUnit.MILLIS)
created: Instant = Instant.now.truncatedTo(ChronoUnit.MILLIS),
userName: String,
groupChangeMessage: String
)
object GroupChangeInfo {
@ -70,7 +72,9 @@ object GroupChangeInfo {
userId = groupChange.userId,
oldGroup = groupChange.oldGroup.map(GroupInfo.apply),
id = groupChange.id,
created = groupChange.created
created = groupChange.created,
userName = groupChange.userName.getOrElse("unknown user"),
groupChangeMessage = groupChange.groupChangeMessage.getOrElse("")
)
}
@ -172,6 +176,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)

View File

@ -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
@ -214,12 +216,18 @@ class MembershipService(
): ListMyGroupsResponse = {
val allMyGroups = allGroups
.filter(_.status == GroupStatus.Active)
.sortBy(_.id)
.sortBy(_.name.toLowerCase)
.map(x => GroupInfo.fromGroup(x, abridged, Some(authPrincipal)))
val filtered = allMyGroups
.filter(grp => groupNameFilter.forall(grp.name.contains(_)))
.filter(grp => startFrom.forall(grp.id > _))
val filtered = if(startFrom.isDefined){
val prevPageGroup = allMyGroups.filter(_.id == startFrom.get).head.name
allMyGroups
.filter(grp => groupNameFilter.map(_.toLowerCase).forall(grp.name.toLowerCase.contains(_)))
.filter(grp => grp.name.toLowerCase > prevPageGroup.toLowerCase)
} else {
allMyGroups
.filter(grp => groupNameFilter.map(_.toLowerCase).forall(grp.name.toLowerCase.contains(_)))
}
val nextId = if (filtered.length > maxItems) Some(filtered(maxItems - 1).id) else None
val groups = filtered.take(maxItems)
@ -227,6 +235,23 @@ class MembershipService(
ListMyGroupsResponse(groups, groupNameFilter, startFrom, nextId, maxItems, ignoreAccess)
}
def getGroupChange(
groupChangeId: String,
authPrincipal: AuthPrincipal
): Result[GroupChangeInfo] =
for {
result <- groupChangeRepo
.getGroupChange(groupChangeId)
.toResult[Option[GroupChange]]
_ <- isGroupChangePresent(result).toResult
_ <- canSeeGroup(result.get.newGroup.id, authPrincipal).toResult
groupChangeMessage <- determineGroupDifference(Seq(result.get))
groupChanges = (groupChangeMessage, Seq(result.get)).zipped.map{ (a, b) => b.copy(groupChangeMessage = Some(a)) }
userIds = Seq(result.get).map(_.userId).toSet
users <- getUsers(userIds).map(_.users)
userMap = users.map(u => (u.id, u.userName)).toMap
} yield groupChanges.map(change => GroupChangeInfo.apply(change.copy(userName = userMap.get(change.userId)))).head
def getGroupActivity(
groupId: String,
startFrom: Option[String],
@ -238,13 +263,65 @@ class MembershipService(
result <- groupChangeRepo
.getGroupChanges(groupId, startFrom, maxItems)
.toResult[ListGroupChangesResults]
groupChangeMessage <- determineGroupDifference(result.changes)
groupChanges = (groupChangeMessage, result.changes).zipped.map{ (a, b) => b.copy(groupChangeMessage = Some(a)) }
userIds = result.changes.map(_.userId).toSet
users <- getUsers(userIds).map(_.users)
userMap = users.map(u => (u.id, u.userName)).toMap
} yield ListGroupChangesResponse(
result.changes.map(GroupChangeInfo.apply),
groupChanges.map(change => GroupChangeInfo.apply(change.copy(userName = userMap.get(change.userId)))),
startFrom,
result.lastEvaluatedTimeStamp,
maxItems
)
def determineGroupDifference(groupChange: Seq[GroupChange]): Result[Seq[String]] = {
var groupChangeMessage: Seq[String] = Seq.empty[String]
for (change <- groupChange) {
val sb = new StringBuilder
if (change.oldGroup.isDefined) {
if (change.oldGroup.get.name != change.newGroup.name) {
sb.append(s"Group name changed to '${change.newGroup.name}'. ")
}
if (change.oldGroup.get.email != change.newGroup.email) {
sb.append(s"Group email changed to '${change.newGroup.email}'. ")
}
if (change.oldGroup.get.description != change.newGroup.description) {
sb.append(s"Group description changed to '${change.newGroup.description.get}'. ")
}
val adminAddDifference = change.newGroup.adminUserIds.diff(change.oldGroup.get.adminUserIds)
if (adminAddDifference.nonEmpty) {
sb.append(s"Group admin/s with userId/s (${adminAddDifference.mkString(",")}) added. ")
}
val adminRemoveDifference = change.oldGroup.get.adminUserIds.diff(change.newGroup.adminUserIds)
if (adminRemoveDifference.nonEmpty) {
sb.append(s"Group admin/s with userId/s (${adminRemoveDifference.mkString(",")}) removed. ")
}
val memberAddDifference = change.newGroup.memberIds.diff(change.oldGroup.get.memberIds)
if (memberAddDifference.nonEmpty) {
sb.append(s"Group member/s with userId/s (${memberAddDifference.mkString(",")}) added. ")
}
val memberRemoveDifference = change.oldGroup.get.memberIds.diff(change.newGroup.memberIds)
if (memberRemoveDifference.nonEmpty) {
sb.append(s"Group member/s with userId/s (${memberRemoveDifference.mkString(",")}) removed. ")
}
groupChangeMessage = groupChangeMessage :+ sb.toString().trim
}
// It'll be in else statement if the group was created or deleted
else {
if (change.changeType == GroupChangeType.Create) {
sb.append("Group Created.")
}
else if (change.changeType == GroupChangeType.Delete){
sb.append("Group Deleted.")
}
groupChangeMessage = groupChangeMessage :+ sb.toString()
}
}
groupChangeMessage
}.toResult
/**
* Retrieves the requested User from the given userIdentifier, which can be a userId or username
* @param userIdentifier The userId or username
@ -277,6 +354,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)

View File

@ -39,6 +39,8 @@ trait MembershipServiceAlgebra {
def getGroup(id: String, authPrincipal: AuthPrincipal): Result[Group]
def getGroupChange(id: String, authPrincipal: AuthPrincipal): Result[GroupChangeInfo]
def listMyGroups(
groupNameFilter: Option[String],
startFrom: Option[String],

View File

@ -19,7 +19,7 @@ package vinyldns.api.domain.membership
import vinyldns.api.Interfaces.ensuring
import vinyldns.core.domain.auth.AuthPrincipal
import vinyldns.api.domain.zone.NotAuthorizedError
import vinyldns.core.domain.membership.Group
import vinyldns.core.domain.membership.{Group, GroupChange}
object MembershipValidations {
@ -44,4 +44,9 @@ object MembershipValidations {
ensuring(NotAuthorizedError("Not authorized")) {
authPrincipal.isGroupMember(groupId) || authPrincipal.isSystemAdmin || canViewGroupDetails
}
def isGroupChangePresent(groupChange: Option[GroupChange]): Either[Throwable, Unit] =
ensuring(InvalidGroupRequestError("Invalid Group Change ID")) {
groupChange.isDefined
}
}

View File

@ -28,7 +28,7 @@ import vinyldns.core.queue.MessageQueue
import cats.data._
import cats.effect.IO
import org.xbill.DNS.ReverseMap
import vinyldns.api.config.HighValueDomainConfig
import vinyldns.api.config.{ZoneAuthConfigs, DottedHostsConfig, HighValueDomainConfig}
import vinyldns.api.domain.DomainValidations.{validateIpv4Address, validateIpv6Address}
import vinyldns.api.domain.access.AccessValidationsAlgebra
import vinyldns.core.domain.record.NameSort.NameSort
@ -46,6 +46,7 @@ object RecordSetService {
backendResolver: BackendResolver,
validateRecordLookupAgainstDnsBackend: Boolean,
highValueDomainConfig: HighValueDomainConfig,
dottedHostsConfig: DottedHostsConfig,
approvedNameServers: List[Regex],
useRecordSetCache: Boolean
): RecordSetService =
@ -61,6 +62,7 @@ object RecordSetService {
backendResolver,
validateRecordLookupAgainstDnsBackend,
highValueDomainConfig,
dottedHostsConfig,
approvedNameServers,
useRecordSetCache
)
@ -78,6 +80,7 @@ class RecordSetService(
backendResolver: BackendResolver,
validateRecordLookupAgainstDnsBackend: Boolean,
highValueDomainConfig: HighValueDomainConfig,
dottedHostsConfig: DottedHostsConfig,
approvedNameServers: List[Regex],
useRecordSetCache: Boolean
) extends RecordSetServiceAlgebra {
@ -88,6 +91,7 @@ class RecordSetService(
def addRecordSet(recordSet: RecordSet, auth: AuthPrincipal): Result[ZoneCommandResult] =
for {
zone <- getZone(recordSet.zoneId)
authZones = dottedHostsConfig.zoneAuthConfigs.map(x => x.zone)
change <- RecordSetChangeGenerator.forAdd(recordSet, zone, Some(auth)).toResult
// because changes happen to the RS in forAdd itself, converting 1st and validating on that
rsForValidations = change.recordSet
@ -107,13 +111,27 @@ class RecordSetService(
ownerGroup <- getGroupIfProvided(rsForValidations.ownerGroupId)
_ <- canUseOwnerGroup(rsForValidations.ownerGroupId, ownerGroup, auth).toResult
_ <- noCnameWithNewName(rsForValidations, existingRecordsWithName, zone).toResult
allowedZoneList <- getAllowedZones(authZones).toResult[Set[String]]
isInAllowedUsers = checkIfInAllowedUsers(zone, dottedHostsConfig, auth)
isUserInAllowedGroups <- checkIfInAllowedGroups(zone, dottedHostsConfig, auth).toResult[Boolean]
isAllowedUser = isInAllowedUsers || isUserInAllowedGroups
isRecordTypeAllowed = checkIfInAllowedRecordType(zone, dottedHostsConfig, rsForValidations)
isRecordTypeAndUserAllowed = isAllowedUser && isRecordTypeAllowed
allowedDotsLimit = getAllowedDotsLimit(zone, dottedHostsConfig)
recordFqdnDoesNotAlreadyExist <- recordFQDNDoesNotExist(rsForValidations, zone).toResult[Boolean]
_ <- typeSpecificValidations(
rsForValidations,
existingRecordsWithName,
zone,
None,
approvedNameServers
approvedNameServers,
recordFqdnDoesNotAlreadyExist,
allowedZoneList,
isRecordTypeAndUserAllowed,
allowedDotsLimit
).toResult
_ <- if(allowedZoneList.contains(zone.name)) checkAllowedDots(allowedDotsLimit, rsForValidations, zone).toResult else ().toResult
_ <- if(allowedZoneList.contains(zone.name)) isNotApexEndsWithDot(rsForValidations, zone).toResult else ().toResult
_ <- messageQueue.send(change).toResult[Unit]
} yield change
@ -143,13 +161,27 @@ class RecordSetService(
validateRecordLookupAgainstDnsBackend
)
_ <- noCnameWithNewName(rsForValidations, existingRecordsWithName, zone).toResult
authZones = dottedHostsConfig.zoneAuthConfigs.map(x => x.zone)
allowedZoneList <- getAllowedZones(authZones).toResult[Set[String]]
isInAllowedUsers = checkIfInAllowedUsers(zone, dottedHostsConfig, auth)
isUserInAllowedGroups <- checkIfInAllowedGroups(zone, dottedHostsConfig, auth).toResult[Boolean]
isAllowedUser = isInAllowedUsers || isUserInAllowedGroups
isRecordTypeAllowed = checkIfInAllowedRecordType(zone, dottedHostsConfig, rsForValidations)
isRecordTypeAndUserAllowed = isAllowedUser && isRecordTypeAllowed
allowedDotsLimit = getAllowedDotsLimit(zone, dottedHostsConfig)
_ <- typeSpecificValidations(
rsForValidations,
existingRecordsWithName,
zone,
Some(existing),
approvedNameServers
approvedNameServers,
true,
allowedZoneList,
isRecordTypeAndUserAllowed,
allowedDotsLimit
).toResult
_ <- if(existing.name == rsForValidations.name) ().toResult else if(allowedZoneList.contains(zone.name)) checkAllowedDots(allowedDotsLimit, rsForValidations, zone).toResult else ().toResult
_ <- if(allowedZoneList.contains(zone.name)) isNotApexEndsWithDot(rsForValidations, zone).toResult else ().toResult
_ <- messageQueue.send(change).toResult[Unit]
} yield change
@ -169,6 +201,178 @@ class RecordSetService(
_ <- messageQueue.send(change).toResult[Unit]
} yield change
// For dotted hosts. Check if a record that may conflict with dotted host exist or not
def recordFQDNDoesNotExist(newRecordSet: RecordSet, zone: Zone): IO[Boolean] = {
// Use fqdn for searching through `recordset` mysql table to see if it already exist
val newRecordFqdn = if(newRecordSet.name != zone.name) newRecordSet.name + "." + zone.name else newRecordSet.name
for {
record <- recordSetRepository.getRecordSetsByFQDNs(Set(newRecordFqdn))
isRecordAlreadyExist = doesRecordWithSameTypeExist(record, newRecordSet)
doesNotExist = if(isRecordAlreadyExist) false else true
} yield doesNotExist
}
// Check if a record with same type already exist in 'recordset' mysql table
def doesRecordWithSameTypeExist(oldRecord: List[RecordSet], newRecord: RecordSet): Boolean = {
if(oldRecord.nonEmpty) {
val typeExists = oldRecord.map(x => x.typ == newRecord.typ)
if (typeExists.contains(true)) true else false
}
else {
false
}
}
// Get zones that are allowed to create dotted hosts using the zones present in dotted hosts config
def getAllowedZones(zones: List[String]): IO[Set[String]] = {
if(zones.isEmpty){
val noZones: IO[Set[String]] = IO(Set.empty)
noZones
}
else {
// Wildcard zones needs to be passed to a separate method
val wildcardZones = zones.filter(_.contains("*")).map(_.replace("*", "%"))
// Zones without wildcard character are passed to a separate function
val namedZones = zones.filter(zone => !zone.contains("*"))
for{
namedZoneResult <- zoneRepository.getZonesByNames(namedZones.toSet)
wildcardZoneResult <- zoneRepository.getZonesByFilters(wildcardZones.toSet)
zoneResult = namedZoneResult ++ wildcardZoneResult // Combine the zones
} yield zoneResult.map(x => x.name)
}
}
// Check if user is allowed to create dotted hosts using the users present in dotted hosts config
def getAllowedDotsLimit(zone: Zone, config: DottedHostsConfig): Int = {
val configZones = config.zoneAuthConfigs.map(x => x.zone)
val zoneName = if(zone.name.takeRight(1) != ".") zone.name + "." else zone.name
val dottedZoneConfig = configZones.filter(_.contains("*")).map(_.replace("*", "[A-Za-z0-9.]*"))
val isContainWildcardZone = dottedZoneConfig.exists(x => zoneName.matches(x))
val isContainNormalZone = configZones.contains(zoneName)
if(isContainNormalZone){
config.zoneAuthConfigs.filter(x => x.zone == zoneName).head.dotsLimit
}
else if(isContainWildcardZone){
config.zoneAuthConfigs.filter(x => zoneName.matches(x.zone.replace("*", "[A-Za-z0-9.]*"))).head.dotsLimit
}
else {
0
}
}
// Check if user is allowed to create dotted hosts using the users present in dotted hosts config
def checkIfInAllowedUsers(zone: Zone, config: DottedHostsConfig, auth: AuthPrincipal): Boolean = {
val configZones = config.zoneAuthConfigs.map(x => x.zone)
val zoneName = if(zone.name.takeRight(1) != ".") zone.name + "." else zone.name
val dottedZoneConfig = configZones.filter(_.contains("*")).map(_.replace("*", "[A-Za-z0-9.]*"))
val isContainWildcardZone = dottedZoneConfig.exists(x => zoneName.matches(x))
val isContainNormalZone = configZones.contains(zoneName)
if(isContainNormalZone){
val users = config.zoneAuthConfigs.flatMap {
x: ZoneAuthConfigs =>
if (x.zone == zoneName) x.userList else List.empty
}
if(users.contains(auth.signedInUser.userName)){
true
}
else {
false
}
}
else if(isContainWildcardZone){
val users = config.zoneAuthConfigs.flatMap {
x: ZoneAuthConfigs =>
if (x.zone.contains("*")) {
val wildcardZone = x.zone.replace("*", "[A-Za-z0-9.]*")
if (zoneName.matches(wildcardZone)) x.userList else List.empty
} else List.empty
}
if(users.contains(auth.signedInUser.userName)){
true
}
else {
false
}
}
else {
false
}
}
// Check if user is allowed to create dotted hosts using the record types present in dotted hosts config
def checkIfInAllowedRecordType(zone: Zone, config: DottedHostsConfig, rs: RecordSet): Boolean = {
val configZones = config.zoneAuthConfigs.map(x => x.zone)
val zoneName = if(zone.name.takeRight(1) != ".") zone.name + "." else zone.name
val dottedZoneConfig = configZones.filter(_.contains("*")).map(_.replace("*", "[A-Za-z0-9.]*"))
val isContainWildcardZone = dottedZoneConfig.exists(x => zoneName.matches(x))
val isContainNormalZone = configZones.contains(zoneName)
if(isContainNormalZone){
val rType = config.zoneAuthConfigs.flatMap {
x: ZoneAuthConfigs =>
if (x.zone == zoneName) x.recordTypes else List.empty
}
if(rType.contains(rs.typ.toString)){
true
}
else {
false
}
}
else if(isContainWildcardZone){
val rType = config.zoneAuthConfigs.flatMap {
x: ZoneAuthConfigs =>
if (x.zone.contains("*")) {
val wildcardZone = x.zone.replace("*", "[A-Za-z0-9.]*")
if (zoneName.matches(wildcardZone)) x.recordTypes else List.empty
} else List.empty
}
if(rType.contains(rs.typ.toString)){
true
}
else {
false
}
}
else {
false
}
}
// Check if user is allowed to create dotted hosts using the groups present in dotted hosts config
def checkIfInAllowedGroups(zone: Zone, config: DottedHostsConfig, auth: AuthPrincipal): IO[Boolean] = {
val configZones = config.zoneAuthConfigs.map(x => x.zone)
val zoneName = if(zone.name.takeRight(1) != ".") zone.name + "." else zone.name
val dottedZoneConfig = configZones.filter(_.contains("*")).map(_.replace("*", "[A-Za-z0-9.]*"))
val isContainWildcardZone = dottedZoneConfig.exists(x => zoneName.matches(x))
val isContainNormalZone = configZones.contains(zoneName)
val groups = if(isContainNormalZone){
config.zoneAuthConfigs.flatMap {
x: ZoneAuthConfigs =>
if (x.zone == zoneName) x.groupList else List.empty
}
}
else if(isContainWildcardZone){
config.zoneAuthConfigs.flatMap {
x: ZoneAuthConfigs =>
if (x.zone.contains("*")) {
val wildcardZone = x.zone.replace("*", "[A-Za-z0-9.]*")
if (zoneName.matches(wildcardZone)) x.groupList else List.empty
} else List.empty
}
}
else {
List.empty
}
for{
groupsInConfig <- groupRepository.getGroupsByName(groups.toSet)
members = groupsInConfig.flatMap(x => x.memberIds)
usersList <- if(members.isEmpty) IO(Seq.empty) else userRepository.getUsers(members, None, None).map(x => x.users)
users = if(usersList.isEmpty) Seq.empty else usersList.map(x => x.userName)
isPresent = users.contains(auth.signedInUser.userName)
} yield isPresent
}
def getRecordSet(
recordSetId: String,
authPrincipal: AuthPrincipal

View File

@ -26,7 +26,7 @@ import vinyldns.core.domain.record.RecordType._
import vinyldns.api.domain.zone._
import vinyldns.core.domain.auth.AuthPrincipal
import vinyldns.core.domain.membership.Group
import vinyldns.core.domain.record.{RecordType, RecordSet}
import vinyldns.core.domain.record.{RecordSet, RecordType}
import vinyldns.core.domain.zone.Zone
import vinyldns.core.Messages._
@ -90,6 +90,69 @@ object RecordSetValidations {
!existingRecordsWithName.exists(rs => rs.id != newRecordSet.id && rs.typ == newRecordSet.typ)
)
// Check whether the record has dot or not
def checkForDot(
newRecordSet: RecordSet,
zone: Zone,
existingRecordSet: Option[RecordSet] = None,
recordFqdnDoesNotExist: Boolean,
dottedHostZoneConfig: Set[String],
isRecordTypeAndUserAllowed: Boolean,
allowedDotsLimit: Int = 0
): Either[Throwable, Unit] = {
val zoneName = if(zone.name.takeRight(1) != ".") zone.name + "." else zone.name
// Check if the zone of the recordset is present in dotted hosts config list
val isDomainAllowed = dottedHostZoneConfig.contains(zoneName)
// Check if record set contains dot and if it is in zone which is allowed to have dotted records from dotted hosts config
if(allowedDotsLimit != 0 && newRecordSet.name.contains(".") && isDomainAllowed && newRecordSet.name != zone.name) {
if(!isRecordTypeAndUserAllowed){
isUserAndRecordTypeAuthorized(newRecordSet, zone, existingRecordSet, recordFqdnDoesNotExist, isRecordTypeAndUserAllowed)
}
else {
isDotted(newRecordSet, zone, existingRecordSet, recordFqdnDoesNotExist, isRecordTypeAndUserAllowed)
}
}
else {
isNotDotted(newRecordSet, zone, existingRecordSet)
}
}
// For dotted host. Check if a record is already present which conflicts with the new dotted record. If so, throw an error
def isDotted(
newRecordSet: RecordSet,
zone: Zone,
existingRecordSet: Option[RecordSet] = None,
recordFqdnDoesNotExist: Boolean,
isRecordTypeAndUserAllowed: Boolean
): Either[Throwable, Unit] =
ensuring(
InvalidRequest(
s"Record with fqdn '${newRecordSet.name}.${zone.name}' cannot be created. " +
s"Please check if a record with the same FQDN and type already exist and make the change there."
)
)(
(newRecordSet.name != zone.name || existingRecordSet.exists(_.name == newRecordSet.name)) && recordFqdnDoesNotExist && isRecordTypeAndUserAllowed
)
// For dotted host. Check if the user is authorized and the record type is allowed. If not, throw an error
def isUserAndRecordTypeAuthorized(
newRecordSet: RecordSet,
zone: Zone,
existingRecordSet: Option[RecordSet] = None,
recordFqdnDoesNotExist: Boolean,
isRecordTypeAndUserAllowed: Boolean
): Either[Throwable, Unit] =
ensuring(
InvalidRequest(
s"Record type is not allowed or the user is not authorized to create a dotted host in the zone '${zone.name}'"
)
)(
(newRecordSet.name != zone.name || existingRecordSet.exists(_.name == newRecordSet.name)) && recordFqdnDoesNotExist && isRecordTypeAndUserAllowed
)
// Check if the recordset contains dot but is not in the allowed zones to create dotted records. If so, throw an error
def isNotDotted(
newRecordSet: RecordSet,
zone: Zone,
@ -110,16 +173,20 @@ object RecordSetValidations {
existingRecordsWithName: List[RecordSet],
zone: Zone,
existingRecordSet: Option[RecordSet],
approvedNameServers: List[Regex]
approvedNameServers: List[Regex],
recordFqdnDoesNotExist: Boolean,
dottedHostZoneConfig: Set[String],
isRecordTypeAndUserAllowed: Boolean,
allowedDotsLimit: Int = 0
): Either[Throwable, Unit] =
newRecordSet.typ match {
case CNAME => cnameValidations(newRecordSet, existingRecordsWithName, zone, existingRecordSet)
case NS => nsValidations(newRecordSet, zone, existingRecordSet, approvedNameServers)
case SOA => soaValidations(newRecordSet, zone)
case CNAME => cnameValidations(newRecordSet, existingRecordsWithName, zone, existingRecordSet, recordFqdnDoesNotExist, dottedHostZoneConfig, isRecordTypeAndUserAllowed, allowedDotsLimit)
case NS => nsValidations(newRecordSet, zone, existingRecordSet, approvedNameServers, recordFqdnDoesNotExist, dottedHostZoneConfig, isRecordTypeAndUserAllowed, allowedDotsLimit)
case SOA => soaValidations(newRecordSet, zone, recordFqdnDoesNotExist, dottedHostZoneConfig, isRecordTypeAndUserAllowed, allowedDotsLimit)
case PTR => ptrValidations(newRecordSet, zone)
case SRV | TXT | NAPTR => ().asRight // SRV, TXT and NAPTR do not go through dotted host check
case DS => dsValidations(newRecordSet, existingRecordsWithName, zone)
case _ => isNotDotted(newRecordSet, zone, existingRecordSet)
case DS => dsValidations(newRecordSet, existingRecordsWithName, zone, recordFqdnDoesNotExist, dottedHostZoneConfig, isRecordTypeAndUserAllowed, allowedDotsLimit)
case _ => checkForDot(newRecordSet, zone, existingRecordSet, recordFqdnDoesNotExist, dottedHostZoneConfig, isRecordTypeAndUserAllowed, allowedDotsLimit)
}
def typeSpecificDeleteValidations(recordSet: RecordSet, zone: Zone): Either[Throwable, Unit] =
@ -140,7 +207,11 @@ object RecordSetValidations {
newRecordSet: RecordSet,
existingRecordsWithName: List[RecordSet],
zone: Zone,
existingRecordSet: Option[RecordSet] = None
existingRecordSet: Option[RecordSet] = None,
recordFqdnDoesNotExist: Boolean,
dottedHostZoneConfig: Set[String],
isRecordTypeAndUserAllowed: Boolean,
allowedDotsLimit: Int = 0
): Either[Throwable, Unit] = {
// cannot create a cname record if a record with the same exists
val noRecordWithName = {
@ -173,7 +244,7 @@ object RecordSetValidations {
)
_ <- noRecordWithName
_ <- RDataWithConsecutiveDots
_ <- isNotDotted(newRecordSet, zone, existingRecordSet)
_ <- checkForDot(newRecordSet, zone, existingRecordSet, recordFqdnDoesNotExist, dottedHostZoneConfig, isRecordTypeAndUserAllowed, allowedDotsLimit)
} yield ()
}
@ -181,7 +252,11 @@ object RecordSetValidations {
def dsValidations(
newRecordSet: RecordSet,
existingRecordsWithName: List[RecordSet],
zone: Zone
zone: Zone,
recordFqdnDoesNotExist: Boolean,
dottedHostZoneConfig: Set[String],
isRecordTypeAndUserAllowed: Boolean,
allowedDotsLimit: Int = 0
): Either[Throwable, Unit] = {
// see https://tools.ietf.org/html/rfc4035#section-2.4
val nsChecks = existingRecordsWithName.find(_.typ == NS) match {
@ -194,7 +269,7 @@ object RecordSetValidations {
}
for {
_ <- isNotDotted(newRecordSet, zone)
_ <- checkForDot(newRecordSet, zone, None, recordFqdnDoesNotExist, dottedHostZoneConfig, isRecordTypeAndUserAllowed, allowedDotsLimit)
_ <- isNotOrigin(
newRecordSet,
zone,
@ -208,10 +283,14 @@ object RecordSetValidations {
newRecordSet: RecordSet,
zone: Zone,
oldRecordSet: Option[RecordSet],
approvedNameServers: List[Regex]
approvedNameServers: List[Regex],
recordFqdnDoesNotExist: Boolean,
dottedHostZoneConfig: Set[String],
isRecordTypeAndUserAllowed: Boolean,
allowedDotsLimit: Int = 0
): Either[Throwable, Unit] = {
// TODO kept consistency with old validation. Not sure why NS could be dotted in reverse specifically
val isNotDottedHost = if (!zone.isReverse) isNotDotted(newRecordSet, zone) else ().asRight
val isNotDottedHost = if (!zone.isReverse) checkForDot(newRecordSet, zone, None, recordFqdnDoesNotExist, dottedHostZoneConfig, isRecordTypeAndUserAllowed, allowedDotsLimit) else ().asRight
for {
_ <- isNotDottedHost
@ -233,9 +312,9 @@ object RecordSetValidations {
} yield ()
}
def soaValidations(newRecordSet: RecordSet, zone: Zone): Either[Throwable, Unit] =
def soaValidations(newRecordSet: RecordSet, zone: Zone, recordFqdnDoesNotExist: Boolean, dottedHostZoneConfig: Set[String], isRecordTypeAndUserAllowed: Boolean, allowedDotsLimit: Int = 0): Either[Throwable, Unit] =
// TODO kept consistency with old validation. in theory if SOA always == zone name, no special case is needed here
if (!zone.isReverse) isNotDotted(newRecordSet, zone) else ().asRight
if (!zone.isReverse) checkForDot(newRecordSet, zone, None, recordFqdnDoesNotExist, dottedHostZoneConfig, isRecordTypeAndUserAllowed, allowedDotsLimit) else ().asRight
def ptrValidations(newRecordSet: RecordSet, zone: Zone): Either[Throwable, Unit] =
// TODO we don't check for PTR as dotted...not sure why
@ -278,6 +357,29 @@ object RecordSetValidations {
.leftMap(errors => InvalidRequest(errors.toList.map(_.message).mkString(", ")))
}
def checkAllowedDots(allowedDotsLimit: Int, recordSet: RecordSet, zone: Zone): Either[Throwable, Unit] = {
ensuring(
InvalidRequest(
s"RecordSet with name ${recordSet.name} has more dots than that is allowed in config for this zone " +
s"which is, 'dots-limit = $allowedDotsLimit'."
)
)(
recordSet.name.count(_ == '.') <= allowedDotsLimit || (recordSet.name.count(_ == '.') == 1 &&
recordSet.name.takeRight(1) == ".") || recordSet.name == zone.name ||
(recordSet.typ.toString == "PTR" || recordSet.typ.toString == "SRV" || recordSet.typ.toString == "TXT" || recordSet.typ.toString == "NAPTR")
)
}
def isNotApexEndsWithDot(recordSet: RecordSet, zone: Zone): Either[Throwable, Unit] = {
ensuring(
InvalidRequest(
"RecordSet name cannot end with a dot, unless it's an apex record."
)
)(
recordSet.name.endsWith(zone.name) || !recordSet.name.endsWith(".")
)
}
def canUseOwnerGroup(
ownerGroupId: Option[String],
group: Option[Group],

View File

@ -16,6 +16,7 @@
package vinyldns.api.domain.zone
import cats.effect.IO
import cats.implicits._
import vinyldns.api.domain.access.AccessValidationsAlgebra
import vinyldns.api.Interfaces
@ -142,33 +143,59 @@ class ZoneService(
accessLevel = getZoneAccess(auth, zone)
} yield ZoneInfo(zone, aclInfo, groupName, accessLevel)
// List zones. Uses zone name as default while using search to list zones or by admin group name if selected.
def listZones(
authPrincipal: AuthPrincipal,
nameFilter: Option[String] = None,
startFrom: Option[String] = None,
maxItems: Int = 100,
searchByAdminGroup: Boolean = false,
ignoreAccess: Boolean = false
): Result[ListZonesResponse] = {
for {
listZonesResult <- zoneRepository.listZones(
authPrincipal,
nameFilter,
startFrom,
maxItems,
ignoreAccess
if(!searchByAdminGroup || nameFilter.isEmpty){
for {
listZonesResult <- zoneRepository.listZones(
authPrincipal,
nameFilter,
startFrom,
maxItems,
ignoreAccess
)
zones = listZonesResult.zones
groupIds = zones.map(_.adminGroupId).toSet
groups <- groupRepository.getGroups(groupIds)
zoneSummaryInfos = zoneSummaryInfoMapping(zones, authPrincipal, groups)
} yield ListZonesResponse(
zoneSummaryInfos,
listZonesResult.zonesFilter,
listZonesResult.startFrom,
listZonesResult.nextId,
listZonesResult.maxItems,
listZonesResult.ignoreAccess
)
zones = listZonesResult.zones
groupIds = zones.map(_.adminGroupId).toSet
groups <- groupRepository.getGroups(groupIds)
zoneSummaryInfos = zoneSummaryInfoMapping(zones, authPrincipal, groups)
} yield ListZonesResponse(
zoneSummaryInfos,
listZonesResult.zonesFilter,
listZonesResult.startFrom,
listZonesResult.nextId,
listZonesResult.maxItems,
listZonesResult.ignoreAccess
)
}
else {
for {
groupIds <- getGroupsIdsByName(nameFilter.get)
listZonesResult <- zoneRepository.listZonesByAdminGroupIds(
authPrincipal,
startFrom,
maxItems,
groupIds,
ignoreAccess
)
zones = listZonesResult.zones
groups <- groupRepository.getGroups(groupIds)
zoneSummaryInfos = zoneSummaryInfoMapping(zones, authPrincipal, groups)
} yield ListZonesResponse(
zoneSummaryInfos,
nameFilter,
listZonesResult.startFrom,
listZonesResult.nextId,
listZonesResult.maxItems,
listZonesResult.ignoreAccess
)
}
}.toResult
def zoneSummaryInfoMapping(
@ -242,6 +269,10 @@ class ZoneService(
} yield zoneChange
}
def getGroupsIdsByName(groupName: String): IO[Set[String]] = {
groupRepository.getGroupsByName(groupName).map(x => x.map(_.id))
}
def getBackendIds(): Result[List[String]] =
backendResolver.ids.toList.toResult

View File

@ -42,6 +42,7 @@ trait ZoneServiceAlgebra {
nameFilter: Option[String],
startFrom: Option[String],
maxItems: Int,
searchByAdminGroup: Boolean,
ignoreAccess: Boolean
): Result[ListZonesResponse]

View File

@ -106,7 +106,7 @@ object RecordSetChangeHandler extends TransactionProvider {
): List[SingleChange] =
recordSetChange.status match {
case RecordSetChangeStatus.Complete =>
singleChanges.map(_.complete(recordSetChange.id, recordSetChange.recordSet.id))
singleChanges.map(_.complete(recordSetChange.systemMessage, recordSetChange.id, recordSetChange.recordSet.id))
case RecordSetChangeStatus.Failed =>
singleChanges.map(_.withProcessingError(recordSetChange.systemMessage, recordSetChange.id))
case _ => singleChanges
@ -157,6 +157,15 @@ object RecordSetChangeHandler extends TransactionProvider {
def isDnsMatch(dnsResult: List[RecordSet], recordSet: RecordSet, zoneName: String): Boolean =
dnsResult.exists(matches(_, recordSet, zoneName))
def isRecordExist(existingRecords: List[RecordSet], change: RecordSetChange): Boolean = {
var isExists : Boolean = false
existingRecords.foreach(recordData=>
for (record<-change.recordSet.records)
if (recordData.records.contains(record)) isExists= true
else isExists= false )
isExists
}
// Determine processing status by comparing request against disposition of DNS backend
def getProcessingStatus(
change: RecordSetChange,
@ -165,13 +174,13 @@ object RecordSetChangeHandler extends TransactionProvider {
change.changeType match {
case RecordSetChangeType.Create =>
if (existingRecords.isEmpty) ReadyToApply(change)
else if (isDnsMatch(existingRecords, change.recordSet, change.zone.name))
AlreadyApplied(change)
else Failure(change, "Incompatible record already exists in DNS.")
else if (isDnsMatch(existingRecords, change.recordSet, change.zone.name) || isRecordExist(existingRecords,change))
AlreadyApplied(change) //Record exists in DNS
else Failure(change, "Incompatible record in DNS.")
case RecordSetChangeType.Update =>
if (isDnsMatch(existingRecords, change.recordSet, change.zone.name))
AlreadyApplied(change)
AlreadyApplied(change)
else {
// record must not exist in the DNS backend, or be synced if it exists
val canApply = existingRecords.isEmpty ||
@ -390,7 +399,7 @@ object RecordSetChangeHandler extends TransactionProvider {
case Failure(_, message) =>
Completed(
change.failed(
s"Failed validating update to DNS for change ${change.id}:${change.recordSet.name}: " + message
s"""Failed validating update to DNS for change "${change.id}": "${change.recordSet.name}": """ + message
)
)
case Retry(_) => Retrying(change)
@ -430,7 +439,7 @@ object RecordSetChangeHandler extends TransactionProvider {
case Failure(_, message) =>
Completed(
change.failed(
s"Failed verifying update to DNS for change ${change.id}:${change.recordSet.name}: $message"
s"""Failed verifying update to DNS for change "${change.id}":"${change.recordSet.name}": $message"""
)
)
case _ => Retrying(change)

View File

@ -133,7 +133,9 @@ trait MembershipJsonProtocol extends JsonValidation {
(js \ "userId").required[String]("Missing userId"),
(js \ "oldGroup").optional[GroupInfo],
(js \ "id").default[String](UUID.randomUUID().toString),
(js \ "created").default[Instant](Instant.now.truncatedTo(ChronoUnit.MILLIS))
(js \ "created").default[Instant](Instant.now.truncatedTo(ChronoUnit.MILLIS)),
(js \ "userName").required[String]("Missing userName"),
(js \ "groupChangeMessage").required[String]("Missing groupChangeMessage"),
).mapN(GroupChangeInfo.apply)
}

View File

@ -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)
@ -79,7 +80,7 @@ class MembershipRoute(
} ~
(get & monitor("Endpoint.listMyGroups")) {
parameters(
"startFrom".?,
"startFrom".as[String].?,
"maxItems".as[Int].?(DEFAULT_MAX_ITEMS),
"groupNameFilter".?,
"ignoreAccess".as[Boolean].?(false),
@ -178,6 +179,13 @@ class MembershipRoute(
}
}
} ~
path("groups" / "change" / Segment) { groupChangeId =>
(get & monitor("Endpoint.groupSingleChange")) {
authenticateAndExecute(membershipService.getGroupChange(groupChangeId, _)) { groupChange =>
complete(StatusCodes.OK, groupChange)
}
}
} ~
path("users" / Segment / "lock") { id =>
(put & monitor("Endpoint.lockUser")) {
authenticateAndExecute(membershipService.updateUserLockStatus(id, LockStatus.Locked, _)) {

View File

@ -78,12 +78,14 @@ class ZoneRoute(
"nameFilter".?,
"startFrom".as[String].?,
"maxItems".as[Int].?(DEFAULT_MAX_ITEMS),
"searchByAdminGroup".as[Boolean].?(false),
"ignoreAccess".as[Boolean].?(false)
) {
(
nameFilter: Option[String],
startFrom: Option[String],
maxItems: Int,
searchByAdminGroup: Boolean,
ignoreAccess: Boolean
) =>
{
@ -94,7 +96,7 @@ class ZoneRoute(
) {
authenticateAndExecute(
zoneService
.listZones(_, nameFilter, startFrom, maxItems, ignoreAccess)
.listZones(_, nameFilter, startFrom, maxItems, searchByAdminGroup, ignoreAccess)
) { result =>
complete(StatusCodes.OK, result)
}

View File

@ -1481,8 +1481,8 @@ def test_a_recordtype_add_checks(shared_zone_test_context):
# context validations: conflicting recordsets, unauthorized error
assert_failed_change_in_error_response(response[7], input_name=existing_a_fqdn, record_data="1.2.3.4",
error_messages=[f"Record \"{existing_a_fqdn}\" Already Exists: "
f"cannot add an existing record; to update it, issue a DeleteRecordSet then an Add."])
error_messages=[f"RecordName \"{existing_a_fqdn}\" already exists. Your request will be manually reviewed. "
f"If you intended to update this record, you can avoid manual review by adding a DeleteRecordSet entry followed by an Add."])
assert_failed_change_in_error_response(response[8], input_name=existing_cname_fqdn,
record_data="1.2.3.4",
error_messages=[f'CNAME Conflict: CNAME record names must be unique. '
@ -1542,6 +1542,7 @@ def test_a_recordtype_update_delete_checks(shared_zone_test_context):
get_change_A_AAAA_json(rs_delete_fqdn, change_type="DeleteRecordSet"),
get_change_A_AAAA_json(rs_update_fqdn, change_type="DeleteRecordSet"),
get_change_A_AAAA_json(rs_update_fqdn, ttl=300),
get_change_A_AAAA_json(f"non-existent.{ok_zone_name}", change_type="DeleteRecordSet"),
# input validations failures
get_change_A_AAAA_json("$invalid.host.name.", change_type="DeleteRecordSet"),
@ -1555,7 +1556,6 @@ def test_a_recordtype_update_delete_checks(shared_zone_test_context):
get_change_A_AAAA_json("zone.discovery.error.", change_type="DeleteRecordSet"),
# context validation failures: record does not exist, not authorized
get_change_A_AAAA_json(f"non-existent.{ok_zone_name}", change_type="DeleteRecordSet"),
get_change_A_AAAA_json(rs_delete_dummy_fqdn, change_type="DeleteRecordSet"),
get_change_A_AAAA_json(rs_update_dummy_fqdn, change_type="DeleteRecordSet"),
get_change_A_AAAA_json(rs_update_dummy_fqdn, ttl=300),
@ -1592,43 +1592,40 @@ def test_a_recordtype_update_delete_checks(shared_zone_test_context):
assert_successful_change_in_error_response(response[0], input_name=rs_delete_fqdn, change_type="DeleteRecordSet")
assert_successful_change_in_error_response(response[1], input_name=rs_update_fqdn, change_type="DeleteRecordSet")
assert_successful_change_in_error_response(response[2], input_name=rs_update_fqdn, ttl=300)
assert_successful_change_in_error_response(response[3], input_name=f"non-existent.{ok_zone_name}", change_type="DeleteRecordSet")
# input validations failures
assert_failed_change_in_error_response(response[3], input_name="$invalid.host.name.",
assert_failed_change_in_error_response(response[4], input_name="$invalid.host.name.",
change_type="DeleteRecordSet",
error_messages=['Invalid domain name: "$invalid.host.name.", valid domain names must be letters, '
'numbers, underscores, and hyphens, joined by dots, and terminated with a dot.'])
assert_failed_change_in_error_response(response[4], input_name="reverse.zone.in-addr.arpa.",
assert_failed_change_in_error_response(response[5], input_name="reverse.zone.in-addr.arpa.",
change_type="DeleteRecordSet",
error_messages=['Invalid Record Type In Reverse Zone: record with name "reverse.zone.in-addr.arpa." and type "A" '
'is not allowed in a reverse zone.'])
assert_failed_change_in_error_response(response[5], input_name="$another.invalid.host.name.", ttl=300,
assert_failed_change_in_error_response(response[6], input_name="$another.invalid.host.name.", ttl=300,
error_messages=['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.'])
assert_failed_change_in_error_response(response[6], input_name="$another.invalid.host.name.",
assert_failed_change_in_error_response(response[7], input_name="$another.invalid.host.name.",
change_type="DeleteRecordSet",
error_messages=['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.'])
assert_failed_change_in_error_response(response[7], input_name="another.reverse.zone.in-addr.arpa.", ttl=10,
assert_failed_change_in_error_response(response[8], input_name="another.reverse.zone.in-addr.arpa.", ttl=10,
error_messages=['Invalid Record Type In Reverse Zone: record with name "another.reverse.zone.in-addr.arpa." '
'and type "A" is not allowed in a reverse zone.',
'Invalid TTL: "10", must be a number between 30 and 2147483647.'])
assert_failed_change_in_error_response(response[8], input_name="another.reverse.zone.in-addr.arpa.",
assert_failed_change_in_error_response(response[9], input_name="another.reverse.zone.in-addr.arpa.",
change_type="DeleteRecordSet",
error_messages=['Invalid Record Type In Reverse Zone: record with name "another.reverse.zone.in-addr.arpa." '
'and type "A" is not allowed in a reverse zone.'])
# zone discovery failure
assert_failed_change_in_error_response(response[9], input_name="zone.discovery.error.",
assert_failed_change_in_error_response(response[10], input_name="zone.discovery.error.",
change_type="DeleteRecordSet",
error_messages=['Zone Discovery Failed: zone for "zone.discovery.error." does not exist in VinylDNS. '
'If zone exists, then it must be connected to in VinylDNS.'])
# context validation failures: record does not exist, not authorized
assert_failed_change_in_error_response(response[10], input_name=f"non-existent.{ok_zone_name}",
change_type="DeleteRecordSet",
error_messages=[
f'Record "non-existent.{ok_zone_name}" Does Not Exist: cannot delete a record that does not exist.'])
assert_failed_change_in_error_response(response[11], input_name=rs_delete_dummy_fqdn,
change_type="DeleteRecordSet",
error_messages=[f'User \"ok\" is not authorized. Contact zone owner group: {dummy_group_name} at test@test.com to make DNS changes.'])
@ -1730,11 +1727,9 @@ def test_aaaa_recordtype_add_checks(shared_zone_test_context):
error_messages=[f"Record Name \"cname-duplicate.{parent_zone_name}\" Not Unique In Batch Change: "
f"cannot have multiple \"CNAME\" records with the same name."])
assert_successful_change_in_error_response(response[6], input_name=f"cname-duplicate.{parent_zone_name}",
record_type="AAAA", record_data="1::1")
assert_failed_change_in_error_response(response[7], input_name=existing_aaaa_fqdn, record_type="AAAA",
record_data="1::1",
error_messages=[f"Record \"{existing_aaaa_fqdn}\" Already Exists: cannot add an existing record; "
f"to update it, issue a DeleteRecordSet then an Add."])
record_type="AAAA", record_data="1::1")
assert_successful_change_in_error_response(response[7], input_name=existing_aaaa_fqdn, record_type="AAAA",
record_data="1::1")
assert_failed_change_in_error_response(response[8], input_name=existing_cname_fqdn, record_type="AAAA",
record_data="1::1",
error_messages=[f"CNAME Conflict: CNAME record names must be unique. Existing record with name \"{existing_cname_fqdn}\" "
@ -1781,6 +1776,8 @@ def test_aaaa_recordtype_update_delete_checks(shared_zone_test_context):
get_change_A_AAAA_json(rs_delete_fqdn, record_type="AAAA", change_type="DeleteRecordSet", address="1:0::4:5:6:7:8"),
get_change_A_AAAA_json(rs_update_fqdn, record_type="AAAA", ttl=300, address="1:2:3:4:5:6:7:8"),
get_change_A_AAAA_json(rs_update_fqdn, record_type="AAAA", change_type="DeleteRecordSet"),
get_change_A_AAAA_json(f"delete-nonexistent.{ok_zone_name}", record_type="AAAA", change_type="DeleteRecordSet"),
get_change_A_AAAA_json(f"update-nonexistent.{ok_zone_name}", record_type="AAAA", change_type="DeleteRecordSet"),
# input validations failures
get_change_A_AAAA_json(f"invalid-name$.{ok_zone_name}", record_type="AAAA", change_type="DeleteRecordSet"),
@ -1792,8 +1789,6 @@ def test_aaaa_recordtype_update_delete_checks(shared_zone_test_context):
get_change_A_AAAA_json("no.zone.at.all.", record_type="AAAA", change_type="DeleteRecordSet"),
# context validation failures
get_change_A_AAAA_json(f"delete-nonexistent.{ok_zone_name}", record_type="AAAA", change_type="DeleteRecordSet"),
get_change_A_AAAA_json(f"update-nonexistent.{ok_zone_name}", record_type="AAAA", change_type="DeleteRecordSet"),
get_change_A_AAAA_json(f"update-nonexistent.{ok_zone_name}", record_type="AAAA", address="1::1"),
get_change_A_AAAA_json(rs_delete_dummy_fqdn, record_type="AAAA", change_type="DeleteRecordSet"),
get_change_A_AAAA_json(rs_update_dummy_fqdn, record_type="AAAA", address="1::1"),
@ -1826,39 +1821,37 @@ def test_aaaa_recordtype_update_delete_checks(shared_zone_test_context):
record_data="1:2:3:4:5:6:7:8")
assert_successful_change_in_error_response(response[2], input_name=rs_update_fqdn, record_type="AAAA",
record_data=None, change_type="DeleteRecordSet")
assert_successful_change_in_error_response(response[3], input_name=f"delete-nonexistent.{ok_zone_name}", record_type="AAAA",
record_data=None, change_type="DeleteRecordSet")
assert_successful_change_in_error_response(response[4], input_name=f"update-nonexistent.{ok_zone_name}", record_type="AAAA",
record_data=None, change_type="DeleteRecordSet")
# input validations failures: invalid input name, reverse zone error, invalid ttl
assert_failed_change_in_error_response(response[3], input_name=f"invalid-name$.{ok_zone_name}", record_type="AAAA",
assert_failed_change_in_error_response(response[5], input_name=f"invalid-name$.{ok_zone_name}", record_type="AAAA",
record_data=None, change_type="DeleteRecordSet",
error_messages=[f'Invalid domain name: "invalid-name$.{ok_zone_name}", '
f'valid domain names must be letters, numbers, underscores, and hyphens, joined by dots, and terminated with a dot.'])
assert_failed_change_in_error_response(response[4], input_name="reverse.zone.in-addr.arpa.", record_type="AAAA",
assert_failed_change_in_error_response(response[6], input_name="reverse.zone.in-addr.arpa.", record_type="AAAA",
record_data=None, change_type="DeleteRecordSet",
error_messages=["Invalid Record Type In Reverse Zone: record with name \"reverse.zone.in-addr.arpa.\" and "
"type \"AAAA\" is not allowed in a reverse zone."])
assert_failed_change_in_error_response(response[5], input_name=f"bad-ttl-and-invalid-name$-update.{ok_zone_name}",
assert_failed_change_in_error_response(response[7], input_name=f"bad-ttl-and-invalid-name$-update.{ok_zone_name}",
record_type="AAAA", record_data=None, change_type="DeleteRecordSet",
error_messages=[f'Invalid domain name: "bad-ttl-and-invalid-name$-update.{ok_zone_name}", '
f'valid domain names must be letters, numbers, underscores, and hyphens, joined by dots, and terminated with a dot.'])
assert_failed_change_in_error_response(response[6], input_name=f"bad-ttl-and-invalid-name$-update.{ok_zone_name}", ttl=29,
assert_failed_change_in_error_response(response[8], input_name=f"bad-ttl-and-invalid-name$-update.{ok_zone_name}", ttl=29,
record_type="AAAA", record_data="1:2:3:4:5:6:7:8",
error_messages=['Invalid TTL: "29", must be a number between 30 and 2147483647.',
f'Invalid domain name: "bad-ttl-and-invalid-name$-update.{ok_zone_name}", '
f'valid domain names 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.at.all.", record_type="AAAA",
assert_failed_change_in_error_response(response[9], input_name="no.zone.at.all.", record_type="AAAA",
record_data=None, change_type="DeleteRecordSet",
error_messages=["Zone Discovery Failed: zone for \"no.zone.at.all.\" does not exist in VinylDNS. "
"If zone exists, then it must be connected to in VinylDNS."])
# context validation failures: record does not exist, not authorized
assert_failed_change_in_error_response(response[8], input_name=f"delete-nonexistent.{ok_zone_name}", record_type="AAAA",
record_data=None, change_type="DeleteRecordSet",
error_messages=[f"Record \"delete-nonexistent.{ok_zone_name}\" Does Not Exist: cannot delete a record that does not exist."])
assert_failed_change_in_error_response(response[9], input_name=f"update-nonexistent.{ok_zone_name}", record_type="AAAA",
record_data=None, change_type="DeleteRecordSet",
error_messages=[f"Record \"update-nonexistent.{ok_zone_name}\" Does Not Exist: cannot delete a record that does not exist."])
assert_successful_change_in_error_response(response[10], input_name=f"update-nonexistent.{ok_zone_name}", record_type="AAAA", record_data="1::1")
assert_failed_change_in_error_response(response[11], input_name=rs_delete_dummy_fqdn,
record_type="AAAA", record_data=None, change_type="DeleteRecordSet",
@ -1974,7 +1967,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",
@ -2010,8 +2003,8 @@ def test_cname_recordtype_add_checks(shared_zone_test_context):
f"Existing record with name \"{existing_forward_fqdn}\" and type \"A\" conflicts with this record."])
assert_failed_change_in_error_response(response[14], input_name=existing_cname_fqdn,
record_type="CNAME", record_data="test.com.",
error_messages=[f"Record \"{existing_cname_fqdn}\" Already Exists: cannot add an existing record; to update it, "
f"issue a DeleteRecordSet then an Add.",
error_messages=[f"RecordName \"{existing_cname_fqdn}\" already exists. Your request will be manually reviewed. "
f"If you intended to update this record, you can avoid manual review by adding a DeleteRecordSet entry followed by an Add.",
f"CNAME Conflict: CNAME record names must be unique. "
f"Existing record with name \"{existing_cname_fqdn}\" and type \"CNAME\" conflicts with this record."])
assert_failed_change_in_error_response(response[15], input_name=existing_reverse_fqdn, record_type="CNAME",
@ -2058,6 +2051,8 @@ def test_cname_recordtype_update_delete_checks(shared_zone_test_context):
get_change_CNAME_json(f"delete3.{ok_zone_name}", change_type="DeleteRecordSet"),
get_change_CNAME_json(f"update3.{ok_zone_name}", change_type="DeleteRecordSet"),
get_change_CNAME_json(f"update3.{ok_zone_name}", ttl=300),
get_change_CNAME_json(f"non-existent-delete.{ok_zone_name}", change_type="DeleteRecordSet"),
get_change_CNAME_json(f"non-existent-update.{ok_zone_name}", change_type="DeleteRecordSet"),
# valid changes - reverse zone
get_change_CNAME_json(f"200.{ip4_zone_name}", change_type="DeleteRecordSet"),
@ -2073,8 +2068,6 @@ def test_cname_recordtype_update_delete_checks(shared_zone_test_context):
get_change_CNAME_json("zone.discovery.error.", change_type="DeleteRecordSet"),
# context validation failures: record does not exist, not authorized, failure on update with multiple adds
get_change_CNAME_json(f"non-existent-delete.{ok_zone_name}", change_type="DeleteRecordSet"),
get_change_CNAME_json(f"non-existent-update.{ok_zone_name}", change_type="DeleteRecordSet"),
get_change_CNAME_json(f"non-existent-update.{ok_zone_name}"),
get_change_CNAME_json(f"delete-unauthorized3.{dummy_zone_name}", change_type="DeleteRecordSet"),
get_change_CNAME_json(f"update-unauthorized3.{dummy_zone_name}", change_type="DeleteRecordSet"),
@ -2111,47 +2104,43 @@ def test_cname_recordtype_update_delete_checks(shared_zone_test_context):
change_type="DeleteRecordSet")
assert_successful_change_in_error_response(response[2], input_name=f"update3.{ok_zone_name}", record_type="CNAME", ttl=300,
record_data="test.com.")
assert_successful_change_in_error_response(response[3], input_name=f"non-existent-delete.{ok_zone_name}", record_type="CNAME",
change_type="DeleteRecordSet")
assert_successful_change_in_error_response(response[4], input_name=f"non-existent-update.{ok_zone_name}", record_type="CNAME",
change_type="DeleteRecordSet")
# valid changes - reverse zone
assert_successful_change_in_error_response(response[3], input_name=f"200.{ip4_zone_name}",
assert_successful_change_in_error_response(response[5], input_name=f"200.{ip4_zone_name}",
record_type="CNAME", change_type="DeleteRecordSet")
assert_successful_change_in_error_response(response[4], input_name=f"201.{ip4_zone_name}",
assert_successful_change_in_error_response(response[6], input_name=f"201.{ip4_zone_name}",
record_type="CNAME", change_type="DeleteRecordSet")
assert_successful_change_in_error_response(response[5], input_name=f"201.{ip4_zone_name}",
assert_successful_change_in_error_response(response[7], input_name=f"201.{ip4_zone_name}",
record_type="CNAME", ttl=300, record_data="test.com.")
# ttl, domain name, data
assert_failed_change_in_error_response(response[6], input_name="$invalid.host.name.", record_type="CNAME",
assert_failed_change_in_error_response(response[8], input_name="$invalid.host.name.", record_type="CNAME",
change_type="DeleteRecordSet",
error_messages=['Invalid domain name: "$invalid.host.name.", valid domain names must be letters, numbers, '
'underscores, and hyphens, joined by dots, and terminated with a dot.'])
assert_failed_change_in_error_response(response[7], input_name="$another.invalid.host.name.",
assert_failed_change_in_error_response(response[9], input_name="$another.invalid.host.name.",
record_type="CNAME", change_type="DeleteRecordSet",
error_messages=['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.'])
assert_failed_change_in_error_response(response[8], input_name="$another.invalid.host.name.", ttl=20,
assert_failed_change_in_error_response(response[10], input_name="$another.invalid.host.name.", ttl=20,
record_type="CNAME", record_data="$another.invalid.cname.",
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
assert_failed_change_in_error_response(response[9], input_name="zone.discovery.error.", record_type="CNAME",
assert_failed_change_in_error_response(response[11], input_name="zone.discovery.error.", record_type="CNAME",
change_type="DeleteRecordSet",
error_messages=[
'Zone Discovery Failed: zone for "zone.discovery.error." does not exist in VinylDNS. If zone exists, then it must be connected to in VinylDNS.'])
# context validation failures: record does not exist, not authorized
assert_failed_change_in_error_response(response[10], input_name=f"non-existent-delete.{ok_zone_name}", record_type="CNAME",
change_type="DeleteRecordSet",
error_messages=[
f'Record "non-existent-delete.{ok_zone_name}" Does Not Exist: cannot delete a record that does not exist.'])
assert_failed_change_in_error_response(response[11], input_name=f"non-existent-update.{ok_zone_name}", record_type="CNAME",
change_type="DeleteRecordSet",
error_messages=[
f'Record "non-existent-update.{ok_zone_name}" Does Not Exist: cannot delete a record that does not exist.'])
assert_successful_change_in_error_response(response[12], input_name=f"non-existent-update.{ok_zone_name}",
record_type="CNAME", record_data="test.com.")
assert_failed_change_in_error_response(response[13], input_name=f"delete-unauthorized3.{dummy_zone_name}",
@ -2317,7 +2306,8 @@ def test_ipv4_ptr_recordtype_add_checks(shared_zone_test_context):
# context validations: existing cname recordset
assert_failed_change_in_error_response(response[11], input_name=f"{ip4_prefix}.193", record_type="PTR", record_data="existing-ptr.",
error_messages=[f'Record "{ip4_prefix}.193" Already Exists: cannot add an existing record; to update it, issue a DeleteRecordSet then an Add.'])
error_messages=[f'RecordName "{ip4_prefix}.193" already exists. Your request will be manually reviewed. '
f'If you intended to update this record, you can avoid manual review by adding a DeleteRecordSet entry followed by an Add.'])
assert_failed_change_in_error_response(response[12], input_name=f"{ip4_prefix}.199", record_type="PTR", record_data="existing-cname.",
error_messages=[
f'CNAME Conflict: CNAME record names must be unique. Existing record with name "{ip4_prefix}.199" and type "CNAME" conflicts with this record.'])
@ -2384,6 +2374,8 @@ def test_ipv4_ptr_recordtype_update_delete_checks(shared_zone_test_context):
get_change_PTR_json(f"{ip4_prefix}.25", change_type="DeleteRecordSet"),
get_change_PTR_json(f"{ip4_prefix}.193", ttl=300, ptrdname="has-updated.ptr."),
get_change_PTR_json(f"{ip4_prefix}.193", change_type="DeleteRecordSet"),
get_change_PTR_json(f"{ip4_prefix}.199", change_type="DeleteRecordSet"),
get_change_PTR_json(f"{ip4_prefix}.200", change_type="DeleteRecordSet"),
# valid changes: delete and add of same record name but different type
get_change_CNAME_json(f"21.{ip4_zone_name}", change_type="DeleteRecordSet"),
@ -2400,9 +2392,7 @@ def test_ipv4_ptr_recordtype_update_delete_checks(shared_zone_test_context):
get_change_PTR_json("192.1.1.25", change_type="DeleteRecordSet"),
# context validation failures
get_change_PTR_json(f"{ip4_prefix}.199", change_type="DeleteRecordSet"),
get_change_PTR_json(f"{ip4_prefix}.200", ttl=300, ptrdname="has-updated.ptr."),
get_change_PTR_json(f"{ip4_prefix}.200", change_type="DeleteRecordSet"),
]
}
@ -2423,25 +2413,29 @@ def test_ipv4_ptr_recordtype_update_delete_checks(shared_zone_test_context):
record_data="has-updated.ptr.")
assert_successful_change_in_error_response(response[2], input_name=f"{ip4_prefix}.193", record_type="PTR",
record_data=None, change_type="DeleteRecordSet")
assert_successful_change_in_error_response(response[3], input_name=f"{ip4_prefix}.199", record_type="PTR",
record_data=None, change_type="DeleteRecordSet")
assert_successful_change_in_error_response(response[4], input_name=f"{ip4_prefix}.200", record_type="PTR",
record_data=None, change_type="DeleteRecordSet")
# successful changes: add and delete of same record name but different type
assert_successful_change_in_error_response(response[3], input_name=f"21.{ip4_zone_name}",
assert_successful_change_in_error_response(response[5], input_name=f"21.{ip4_zone_name}",
record_type="CNAME", record_data=None, change_type="DeleteRecordSet")
assert_successful_change_in_error_response(response[4], input_name=f"{ip4_prefix}.21", record_type="PTR",
assert_successful_change_in_error_response(response[6], input_name=f"{ip4_prefix}.21", record_type="PTR",
record_data="replace-cname.ptr.")
assert_successful_change_in_error_response(response[5], input_name=f"17.{ip4_zone_name}",
assert_successful_change_in_error_response(response[7], input_name=f"17.{ip4_zone_name}",
record_type="CNAME", record_data="replace-ptr.cname.")
assert_successful_change_in_error_response(response[6], input_name=f"{ip4_prefix}.17", record_type="PTR",
assert_successful_change_in_error_response(response[8], input_name=f"{ip4_prefix}.17", record_type="PTR",
record_data=None, change_type="DeleteRecordSet")
# input validations failures: invalid IP, ttl, and record data
assert_failed_change_in_error_response(response[7], input_name="1.1.1", record_type="PTR", record_data=None,
assert_failed_change_in_error_response(response[9], input_name="1.1.1", record_type="PTR", record_data=None,
change_type="DeleteRecordSet",
error_messages=['Invalid IP address: "1.1.1".'])
assert_failed_change_in_error_response(response[8], input_name="192.0.2.", record_type="PTR", record_data=None,
assert_failed_change_in_error_response(response[10], input_name="192.0.2.", record_type="PTR", record_data=None,
change_type="DeleteRecordSet",
error_messages=['Invalid IP address: "192.0.2.".'])
assert_failed_change_in_error_response(response[9], ttl=29, input_name="192.0.2.", record_type="PTR",
assert_failed_change_in_error_response(response[11], ttl=29, input_name="192.0.2.", record_type="PTR",
record_data="failed-update$.ptr.",
error_messages=['Invalid TTL: "29", must be a number between 30 and 2147483647.',
'Invalid IP address: "192.0.2.".',
@ -2449,19 +2443,13 @@ def test_ipv4_ptr_recordtype_update_delete_checks(shared_zone_test_context):
'joined by dots, and terminated with a dot.'])
# zone discovery failure
assert_failed_change_in_error_response(response[10], input_name="192.1.1.25", record_type="PTR",
assert_failed_change_in_error_response(response[12], input_name="192.1.1.25", record_type="PTR",
record_data=None, change_type="DeleteRecordSet",
error_messages=["Zone Discovery Failed: zone for \"192.1.1.25\" does not exist in VinylDNS. If zone exists, "
"then it must be connected to in VinylDNS."])
# context validation failures: record does not exist
assert_failed_change_in_error_response(response[11], input_name=f"{ip4_prefix}.199", record_type="PTR",
record_data=None, change_type="DeleteRecordSet",
error_messages=[f"Record \"{ip4_prefix}.199\" Does Not Exist: cannot delete a record that does not exist."])
assert_successful_change_in_error_response(response[12], ttl=300, input_name=f"{ip4_prefix}.200", record_type="PTR", record_data="has-updated.ptr.")
assert_failed_change_in_error_response(response[13], input_name=f"{ip4_prefix}.200", record_type="PTR",
record_data=None, change_type="DeleteRecordSet",
error_messages=[f"Record \"{ip4_prefix}.200\" Does Not Exist: cannot delete a record that does not exist."])
assert_successful_change_in_error_response(response[13], ttl=300, input_name=f"{ip4_prefix}.200", record_type="PTR", record_data="has-updated.ptr.")
finally:
clear_recordset_list(to_delete, ok_client)
@ -2528,8 +2516,8 @@ def test_ipv6_ptr_recordtype_add_checks(shared_zone_test_context):
# context validations: existing record sets pre-request
assert_failed_change_in_error_response(response[5], input_name=f"{ip6_prefix}:1000::bbbb", record_type="PTR",
record_data="existing.ptr.",
error_messages=[f"Record \"{ip6_prefix}:1000::bbbb\" Already Exists: cannot add an existing record; "
"to update it, issue a DeleteRecordSet then an Add."])
error_messages=[f"RecordName \"{ip6_prefix}:1000::bbbb\" already exists. Your request will be manually reviewed. "
f"If you intended to update this record, you can avoid manual review by adding a DeleteRecordSet entry followed by an Add."])
finally:
clear_recordset_list(to_delete, client)
@ -2556,6 +2544,8 @@ def test_ipv6_ptr_recordtype_update_delete_checks(shared_zone_test_context):
get_change_PTR_json(f"{ip6_prefix}:1000::aaaa", change_type="DeleteRecordSet"),
get_change_PTR_json(f"{ip6_prefix}:1000::62", ttl=300, ptrdname="has-updated.ptr."),
get_change_PTR_json(f"{ip6_prefix}:1000::62", change_type="DeleteRecordSet"),
get_change_PTR_json(f"{ip6_prefix}:1000::60", change_type="DeleteRecordSet"),
get_change_PTR_json(f"{ip6_prefix}:1000::65", change_type="DeleteRecordSet"),
# input validations failures
get_change_PTR_json("fd69:27cc:fe91de::ab", change_type="DeleteRecordSet"),
@ -2566,9 +2556,7 @@ def test_ipv6_ptr_recordtype_update_delete_checks(shared_zone_test_context):
get_change_PTR_json("fedc:ba98:7654::abc", change_type="DeleteRecordSet"),
# context validation failures
get_change_PTR_json(f"{ip6_prefix}:1000::60", change_type="DeleteRecordSet"),
get_change_PTR_json(f"{ip6_prefix}:1000::65", ttl=300, ptrdname="has-updated.ptr."),
get_change_PTR_json(f"{ip6_prefix}:1000::65", change_type="DeleteRecordSet")
]
}
@ -2589,15 +2577,19 @@ def test_ipv6_ptr_recordtype_update_delete_checks(shared_zone_test_context):
record_type="PTR", record_data="has-updated.ptr.")
assert_successful_change_in_error_response(response[2], input_name=f"{ip6_prefix}:1000::62", record_type="PTR",
record_data=None, change_type="DeleteRecordSet")
assert_successful_change_in_error_response(response[3], input_name=f"{ip6_prefix}:1000::60", record_type="PTR",
record_data=None, change_type="DeleteRecordSet")
assert_successful_change_in_error_response(response[4], input_name=f"{ip6_prefix}:1000::65", record_type="PTR",
record_data=None, change_type="DeleteRecordSet")
# input validations failures: invalid IP, ttl, and record data
assert_failed_change_in_error_response(response[3], input_name="fd69:27cc:fe91de::ab", record_type="PTR",
assert_failed_change_in_error_response(response[5], input_name="fd69:27cc:fe91de::ab", record_type="PTR",
record_data=None, change_type="DeleteRecordSet",
error_messages=['Invalid IP address: "fd69:27cc:fe91de::ab".'])
assert_failed_change_in_error_response(response[4], input_name="fd69:27cc:fe91de::ba", record_type="PTR",
assert_failed_change_in_error_response(response[6], input_name="fd69:27cc:fe91de::ba", record_type="PTR",
record_data=None, change_type="DeleteRecordSet",
error_messages=['Invalid IP address: "fd69:27cc:fe91de::ba".'])
assert_failed_change_in_error_response(response[5], ttl=29, input_name="fd69:27cc:fe91de::ba",
assert_failed_change_in_error_response(response[7], ttl=29, input_name="fd69:27cc:fe91de::ba",
record_type="PTR", record_data="failed-update$.ptr.",
error_messages=['Invalid TTL: "29", must be a number between 30 and 2147483647.',
'Invalid IP address: "fd69:27cc:fe91de::ba".',
@ -2605,20 +2597,14 @@ def test_ipv6_ptr_recordtype_update_delete_checks(shared_zone_test_context):
'and hyphens, joined by dots, and terminated with a dot.'])
# zone discovery failure
assert_failed_change_in_error_response(response[6], input_name="fedc:ba98:7654::abc", record_type="PTR",
assert_failed_change_in_error_response(response[8], input_name="fedc:ba98:7654::abc", record_type="PTR",
record_data=None, change_type="DeleteRecordSet",
error_messages=["Zone Discovery Failed: zone for \"fedc:ba98:7654::abc\" does not exist in VinylDNS. "
"If zone exists, then it must be connected to in VinylDNS."])
# context validation failures: record does not exist, failure on update with double add
assert_failed_change_in_error_response(response[7], input_name=f"{ip6_prefix}:1000::60", record_type="PTR",
record_data=None, change_type="DeleteRecordSet",
error_messages=[f"Record \"{ip6_prefix}:1000::60\" Does Not Exist: cannot delete a record that does not exist."])
assert_successful_change_in_error_response(response[8], ttl=300, input_name=f"{ip6_prefix}:1000::65",
assert_successful_change_in_error_response(response[9], ttl=300, input_name=f"{ip6_prefix}:1000::65",
record_type="PTR", record_data="has-updated.ptr.")
assert_failed_change_in_error_response(response[9], input_name=f"{ip6_prefix}:1000::65", record_type="PTR",
record_data=None, change_type="DeleteRecordSet",
error_messages=[f"Record \"{ip6_prefix}:1000::65\" Does Not Exist: cannot delete a record that does not exist."])
finally:
clear_recordset_list(to_delete, ok_client)
@ -2697,10 +2683,8 @@ def test_txt_recordtype_add_checks(shared_zone_test_context):
f"cannot have multiple \"CNAME\" records with the same name."])
# context validations: conflicting recordsets, unauthorized error
assert_failed_change_in_error_response(response[5], input_name=existing_txt_fqdn, record_type="TXT",
record_data="test",
error_messages=[f"Record \"{existing_txt_fqdn}\" Already Exists: "
f"cannot add an existing record; to update it, issue a DeleteRecordSet then an Add."])
assert_successful_change_in_error_response(response[5], input_name=existing_txt_fqdn, record_type="TXT",
record_data="test")
assert_failed_change_in_error_response(response[6], input_name=existing_cname_fqdn, record_type="TXT",
record_data="test",
error_messages=[f"CNAME Conflict: CNAME record names must be unique. "
@ -2747,6 +2731,8 @@ def test_txt_recordtype_update_delete_checks(shared_zone_test_context):
get_change_TXT_json(rs_delete_fqdn, change_type="DeleteRecordSet"),
get_change_TXT_json(rs_update_fqdn, change_type="DeleteRecordSet"),
get_change_TXT_json(rs_update_fqdn, ttl=300),
get_change_TXT_json(f"delete-nonexistent.{ok_zone_name}", change_type="DeleteRecordSet"),
get_change_TXT_json(f"update-nonexistent.{ok_zone_name}", change_type="DeleteRecordSet"),
# input validations failures
get_change_TXT_json(f"invalid-name$.{ok_zone_name}", change_type="DeleteRecordSet"),
@ -2756,8 +2742,6 @@ def test_txt_recordtype_update_delete_checks(shared_zone_test_context):
get_change_TXT_json("no.zone.at.all.", change_type="DeleteRecordSet"),
# context validation failures
get_change_TXT_json(f"delete-nonexistent.{ok_zone_name}", change_type="DeleteRecordSet"),
get_change_TXT_json(f"update-nonexistent.{ok_zone_name}", change_type="DeleteRecordSet"),
get_change_TXT_json(f"update-nonexistent.{ok_zone_name}", text="test"),
get_change_TXT_json(rs_delete_dummy_fqdn, change_type="DeleteRecordSet"),
get_change_TXT_json(rs_update_dummy_fqdn, text="test"),
@ -2787,25 +2771,23 @@ def test_txt_recordtype_update_delete_checks(shared_zone_test_context):
assert_successful_change_in_error_response(response[0], input_name=rs_delete_fqdn, record_type="TXT", record_data=None, change_type="DeleteRecordSet")
assert_successful_change_in_error_response(response[1], input_name=rs_update_fqdn, record_type="TXT", record_data=None, change_type="DeleteRecordSet")
assert_successful_change_in_error_response(response[2], ttl=300, input_name=rs_update_fqdn, record_type="TXT", record_data="test")
assert_successful_change_in_error_response(response[3], input_name=f"delete-nonexistent.{ok_zone_name}", record_type="TXT", record_data=None, change_type="DeleteRecordSet")
assert_successful_change_in_error_response(response[4], input_name=f"update-nonexistent.{ok_zone_name}", record_type="TXT", record_data=None, change_type="DeleteRecordSet")
# input validations failures: invalid input name, reverse zone error, invalid ttl
assert_failed_change_in_error_response(response[3], input_name=f"invalid-name$.{ok_zone_name}", record_type="TXT", record_data="test", change_type="DeleteRecordSet",
assert_failed_change_in_error_response(response[5], input_name=f"invalid-name$.{ok_zone_name}", record_type="TXT", record_data="test", change_type="DeleteRecordSet",
error_messages=[f'Invalid domain name: "invalid-name$.{ok_zone_name}", valid domain names must be '
f'letters, numbers, underscores, and hyphens, joined by dots, and terminated with a dot.'])
assert_failed_change_in_error_response(response[4], input_name=f"invalid-ttl.{ok_zone_name}", ttl=29, record_type="TXT", record_data="bad-ttl",
assert_failed_change_in_error_response(response[6], input_name=f"invalid-ttl.{ok_zone_name}", ttl=29, record_type="TXT", record_data="bad-ttl",
error_messages=['Invalid TTL: "29", must be a number between 30 and 2147483647.'])
# zone discovery failure
assert_failed_change_in_error_response(response[5], input_name="no.zone.at.all.", record_type="TXT", record_data=None, change_type="DeleteRecordSet",
assert_failed_change_in_error_response(response[7], input_name="no.zone.at.all.", record_type="TXT", record_data=None, change_type="DeleteRecordSet",
error_messages=[
"Zone Discovery Failed: zone for \"no.zone.at.all.\" does not exist in VinylDNS. "
"If zone exists, then it must be connected to in VinylDNS."])
# context validation failures: record does not exist, not authorized
assert_failed_change_in_error_response(response[6], input_name=f"delete-nonexistent.{ok_zone_name}", record_type="TXT", record_data=None, change_type="DeleteRecordSet",
error_messages=[f"Record \"delete-nonexistent.{ok_zone_name}\" Does Not Exist: cannot delete a record that does not exist."])
assert_failed_change_in_error_response(response[7], input_name=f"update-nonexistent.{ok_zone_name}", record_type="TXT", record_data=None, change_type="DeleteRecordSet",
error_messages=[f"Record \"update-nonexistent.{ok_zone_name}\" Does Not Exist: cannot delete a record that does not exist."])
assert_successful_change_in_error_response(response[8], input_name=f"update-nonexistent.{ok_zone_name}", record_type="TXT", record_data="test")
assert_failed_change_in_error_response(response[9], input_name=rs_delete_dummy_fqdn, record_type="TXT", record_data=None, change_type="DeleteRecordSet",
error_messages=[f"User \"ok\" is not authorized. Contact zone owner group: {dummy_group_name} at test@test.com to make DNS changes."])
@ -2906,10 +2888,8 @@ def test_mx_recordtype_add_checks(shared_zone_test_context):
f"cannot have multiple \"CNAME\" records with the same name."])
# context validations: conflicting recordsets, unauthorized error
assert_failed_change_in_error_response(response[8], input_name=existing_mx_fqdn, record_type="MX",
record_data={"preference": 1, "exchange": "foo.bar."},
error_messages=[f"Record \"{existing_mx_fqdn}\" Already Exists: cannot add an existing record; to update it, "
f"issue a DeleteRecordSet then an Add."])
assert_successful_change_in_error_response(response[8], input_name=existing_mx_fqdn, record_type="MX",
record_data={"preference": 1, "exchange": "foo.bar."})
assert_failed_change_in_error_response(response[9], input_name=existing_cname_fqdn, record_type="MX",
record_data={"preference": 1, "exchange": "foo.bar."},
error_messages=["CNAME Conflict: CNAME record names must be unique. "
@ -2958,6 +2938,8 @@ def test_mx_recordtype_update_delete_checks(shared_zone_test_context):
get_change_MX_json(rs_delete_fqdn, change_type="DeleteRecordSet"),
get_change_MX_json(rs_update_fqdn, change_type="DeleteRecordSet"),
get_change_MX_json(rs_update_fqdn, ttl=300),
get_change_MX_json(f"delete-nonexistent.{ok_zone_name}", change_type="DeleteRecordSet"),
get_change_MX_json(f"update-nonexistent.{ok_zone_name}", change_type="DeleteRecordSet"),
# input validations failures
get_change_MX_json(f"invalid-name$.{ok_zone_name}", change_type="DeleteRecordSet"),
@ -2969,8 +2951,6 @@ def test_mx_recordtype_update_delete_checks(shared_zone_test_context):
get_change_MX_json("no.zone.at.all.", change_type="DeleteRecordSet"),
# context validation failures
get_change_MX_json(f"delete-nonexistent.{ok_zone_name}", change_type="DeleteRecordSet"),
get_change_MX_json(f"update-nonexistent.{ok_zone_name}", change_type="DeleteRecordSet"),
get_change_MX_json(f"update-nonexistent.{ok_zone_name}", preference=1000, exchange="foo.bar."),
get_change_MX_json(rs_delete_dummy_fqdn, change_type="DeleteRecordSet"),
get_change_MX_json(rs_update_dummy_fqdn, preference=1000, exchange="foo.bar."),
@ -3000,37 +2980,35 @@ def test_mx_recordtype_update_delete_checks(shared_zone_test_context):
assert_successful_change_in_error_response(response[0], input_name=rs_delete_fqdn, record_type="MX", record_data=None, change_type="DeleteRecordSet")
assert_successful_change_in_error_response(response[1], input_name=rs_update_fqdn, record_type="MX", record_data=None, change_type="DeleteRecordSet")
assert_successful_change_in_error_response(response[2], ttl=300, input_name=rs_update_fqdn, record_type="MX", record_data={"preference": 1, "exchange": "foo.bar."})
assert_successful_change_in_error_response(response[3], input_name=f"delete-nonexistent.{ok_zone_name}", record_type="MX",
record_data=None, change_type="DeleteRecordSet")
assert_successful_change_in_error_response(response[4], input_name=f"update-nonexistent.{ok_zone_name}", record_type="MX",
record_data=None, change_type="DeleteRecordSet")
# input validations failures: invalid input name, reverse zone error, invalid ttl
assert_failed_change_in_error_response(response[3], input_name=f"invalid-name$.{ok_zone_name}", record_type="MX", record_data={"preference": 1, "exchange": "foo.bar."},
assert_failed_change_in_error_response(response[5], input_name=f"invalid-name$.{ok_zone_name}", record_type="MX", record_data={"preference": 1, "exchange": "foo.bar."},
change_type="DeleteRecordSet",
error_messages=[f'Invalid domain name: "invalid-name$.{ok_zone_name}", valid domain names must be letters, '
f'numbers, underscores, and hyphens, joined by dots, and terminated with a dot.'])
assert_failed_change_in_error_response(response[4], input_name=f"delete.{ok_zone_name}", ttl=29, record_type="MX",
assert_failed_change_in_error_response(response[6], input_name=f"delete.{ok_zone_name}", ttl=29, record_type="MX",
record_data={"preference": 1, "exchange": "foo.bar."},
error_messages=['Invalid TTL: "29", must be a number between 30 and 2147483647.'])
assert_failed_change_in_error_response(response[5], input_name=f"bad-exchange.{ok_zone_name}", record_type="MX",
assert_failed_change_in_error_response(response[7], input_name=f"bad-exchange.{ok_zone_name}", record_type="MX",
record_data={"preference": 1, "exchange": "foo$.bar."},
error_messages=['Invalid domain name: "foo$.bar.", valid domain names must be letters, numbers, '
'underscores, and hyphens, joined by dots, and terminated with a dot.'])
assert_failed_change_in_error_response(response[6], input_name=f"mx.{ip4_zone_name}", record_type="MX",
assert_failed_change_in_error_response(response[8], input_name=f"mx.{ip4_zone_name}", record_type="MX",
record_data={"preference": 1, "exchange": "foo.bar."},
error_messages=[f'Invalid Record Type In Reverse Zone: record with name "mx.{ip4_zone_name}" '
f'and type "MX" is not allowed in a reverse zone.'])
# zone discovery failure
assert_failed_change_in_error_response(response[7], input_name="no.zone.at.all.", record_type="MX",
assert_failed_change_in_error_response(response[9], input_name="no.zone.at.all.", record_type="MX",
record_data=None, change_type="DeleteRecordSet",
error_messages=["Zone Discovery Failed: zone for \"no.zone.at.all.\" does not exist in VinylDNS. "
"If zone exists, then it must be connected to in VinylDNS."])
# context validation failures: record does not exist, not authorized
assert_failed_change_in_error_response(response[8], input_name=f"delete-nonexistent.{ok_zone_name}", record_type="MX",
record_data=None, change_type="DeleteRecordSet",
error_messages=[f"Record \"delete-nonexistent.{ok_zone_name}\" Does Not Exist: cannot delete a record that does not exist."])
assert_failed_change_in_error_response(response[9], input_name=f"update-nonexistent.{ok_zone_name}", record_type="MX",
record_data=None, change_type="DeleteRecordSet",
error_messages=[f"Record \"update-nonexistent.{ok_zone_name}\" Does Not Exist: cannot delete a record that does not exist."])
assert_successful_change_in_error_response(response[10], input_name=f"update-nonexistent.{ok_zone_name}", record_type="MX",
record_data={"preference": 1000, "exchange": "foo.bar."})
assert_failed_change_in_error_response(response[11], input_name=rs_delete_dummy_fqdn, record_type="MX",
@ -3739,39 +3717,27 @@ def test_create_batch_with_zone_name_requiring_manual_review(shared_zone_test_co
rejecter.reject_batch_change(response["id"], status=200)
def test_create_batch_delete_record_for_invalid_record_data_fails(shared_zone_test_context):
def test_create_batch_delete_record_that_does_not_exists_completes(shared_zone_test_context):
"""
Test delete record set fails for non-existent record and non-existent record data
Test delete record set completes for non-existent record
"""
client = shared_zone_test_context.ok_vinyldns_client
ok_zone_name = shared_zone_test_context.ok_zone["name"]
a_delete_name = generate_record_name()
a_delete_fqdn = a_delete_name + f".{ok_zone_name}"
a_delete = create_recordset(shared_zone_test_context.ok_zone, a_delete_fqdn, "A", [{"address": "1.1.1.1"}])
batch_change_input = {
"comments": "test delete record failures",
"changes": [
get_change_A_AAAA_json(f"delete-non-existent-record.{ok_zone_name}", change_type="DeleteRecordSet"),
get_change_A_AAAA_json(a_delete_fqdn, address="4.5.6.7", change_type="DeleteRecordSet")
get_change_A_AAAA_json(f"delete-non-existent-record.{ok_zone_name}", change_type="DeleteRecordSet")
]
}
to_delete = []
response = client.create_batch_change(batch_change_input, status=202)
get_batch = client.get_batch_change(response["id"])
try:
create_rs = client.create_recordset(a_delete, status=202)
to_delete.append(client.wait_until_recordset_change_status(create_rs, "Complete"))
assert_that(get_batch["changes"][0]["systemMessage"], is_("This record does not exist." +
"No further action is required."))
errors = client.create_batch_change(batch_change_input, status=400)
assert_failed_change_in_error_response(errors[0], input_name=f"delete-non-existent-record.{ok_zone_name}", record_data="1.1.1.1", change_type="DeleteRecordSet",
error_messages=[f'Record "delete-non-existent-record.{ok_zone_name}" Does Not Exist: cannot delete a record that does not exist.'])
assert_failed_change_in_error_response(errors[1], input_name=a_delete_fqdn, record_data="4.5.6.7", change_type="DeleteRecordSet",
error_messages=["Record data 4.5.6.7 does not exist for \"" + a_delete_fqdn + "\"."])
finally:
clear_recordset_list(to_delete, client)
assert_successful_change_in_error_response(response["changes"][0], input_name=f"delete-non-existent-record.{ok_zone_name}", record_data="1.1.1.1", change_type="DeleteRecordSet")
@pytest.mark.serial
@ -4169,23 +4135,23 @@ def test_create_batch_change_with_multi_record_adds_with_multi_record_support(sh
get_change_MX_json(f"multi-mx.{ok_zone_name}", preference=0),
get_change_MX_json(f"multi-mx.{ok_zone_name}", preference=1000, exchange="bar.foo."),
get_change_A_AAAA_json(rs_fqdn, address="1.1.1.1")
]
],
"ownerGroupId": shared_zone_test_context.ok_group["id"]
}
try:
create_rs = client.create_recordset(rs_to_create, status=202)
to_delete.append(client.wait_until_recordset_change_status(create_rs, "Complete"))
response = client.create_batch_change(batch_change_input, status=400)
response = client.create_batch_change(batch_change_input, status=202)
assert_successful_change_in_error_response(response[0], input_name=f"multi.{ok_zone_name}", record_data="1.2.3.4")
assert_successful_change_in_error_response(response[1], input_name=f"multi.{ok_zone_name}", record_data="4.5.6.7")
assert_successful_change_in_error_response(response[2], input_name=f"{ip4_prefix}.44", record_type="PTR", record_data="multi.test.")
assert_successful_change_in_error_response(response[3], input_name=f"{ip4_prefix}.44", record_type="PTR", record_data="multi2.test.")
assert_successful_change_in_error_response(response[4], input_name=f"multi-txt.{ok_zone_name}", record_type="TXT", record_data="some-multi-text")
assert_successful_change_in_error_response(response[5], input_name=f"multi-txt.{ok_zone_name}", record_type="TXT", record_data="more-multi-text")
assert_successful_change_in_error_response(response[6], input_name=f"multi-mx.{ok_zone_name}", record_type="MX", record_data={"preference": 0, "exchange": "foo.bar."})
assert_successful_change_in_error_response(response[7], input_name=f"multi-mx.{ok_zone_name}", record_type="MX", record_data={"preference": 1000, "exchange": "bar.foo."})
assert_failed_change_in_error_response(response[8], input_name=rs_fqdn, record_data="1.1.1.1",
error_messages=["Record \"" + rs_fqdn + "\" Already Exists: cannot add an existing record; to update it, issue a DeleteRecordSet then an Add."])
assert_successful_change_in_error_response(response["changes"][0], input_name=f"multi.{ok_zone_name}", record_data="1.2.3.4")
assert_successful_change_in_error_response(response["changes"][1], input_name=f"multi.{ok_zone_name}", record_data="4.5.6.7")
assert_successful_change_in_error_response(response["changes"][2], input_name=f"{ip4_prefix}.44", record_type="PTR", record_data="multi.test.")
assert_successful_change_in_error_response(response["changes"][3], input_name=f"{ip4_prefix}.44", record_type="PTR", record_data="multi2.test.")
assert_successful_change_in_error_response(response["changes"][4], input_name=f"multi-txt.{ok_zone_name}", record_type="TXT", record_data="some-multi-text")
assert_successful_change_in_error_response(response["changes"][5], input_name=f"multi-txt.{ok_zone_name}", record_type="TXT", record_data="more-multi-text")
assert_successful_change_in_error_response(response["changes"][6], input_name=f"multi-mx.{ok_zone_name}", record_type="MX", record_data={"preference": 0, "exchange": "foo.bar."})
assert_successful_change_in_error_response(response["changes"][7], input_name=f"multi-mx.{ok_zone_name}", record_type="MX", record_data={"preference": 1000, "exchange": "bar.foo."})
assert_successful_change_in_error_response(response["changes"][8], input_name=rs_fqdn, record_data="1.1.1.1")
finally:
clear_recordset_list(to_delete, client)

View File

@ -33,11 +33,11 @@ def test_list_group_activity_start_from_success(group_activity_context, shared_z
# start from a known good timestamp
start_from = str(int((datetime.strptime(page_one["changes"][start_from_index]["created"], "%Y-%m-%dT%H:%M:%S.%fZ") - epoch).total_seconds() * 1000))
# now, we say give me all changes since the start_from, which should yield 8-7-6-5-4
result = client.get_group_changes(created_group["id"], start_from=start_from, max_items=5, status=200)
result = client.get_group_changes(created_group["id"], start_from=page_one["nextId"], max_items=5, status=200)
assert_that(result["changes"], has_length(5))
assert_that(result["maxItems"], is_(5))
assert_that(result["startFrom"], is_(start_from))
assert_that(result["startFrom"], is_(page_one["nextId"]))
assert_that(result["nextId"], is_not(none()))
# we should have, in order, changes 8 7 6 5 4

View File

@ -13,20 +13,11 @@ def test_list_my_groups_no_parameters(list_my_groups_context):
Test that we can get all the groups where a user is a member
"""
results = list_my_groups_context.client.list_my_groups(status=200)
assert_that(results, has_length(3)) # 3 fields
# Only count the groups with the group prefix
groups = [x for x in results["groups"] if x["name"].startswith(list_my_groups_context.group_prefix)]
assert_that(groups, has_length(50))
assert_that(results, has_length(4)) # 4 fields
assert_that(results, is_not(has_key("groupNameFilter")))
assert_that(results, is_not(has_key("startFrom")))
assert_that(results, is_not(has_key("nextId")))
assert_that(results["maxItems"], is_(200))
results["groups"] = sorted(groups, key=lambda x: x["name"])
for i in range(0, 50):
assert_that(results["groups"][i]["name"], is_("{0}-{1:0>3}".format(list_my_groups_context.group_prefix, i)))
assert_that(results, is_(has_key("nextId")))
assert_that(results["maxItems"], is_(100))
def test_get_my_groups_using_old_account_auth(list_my_groups_context):
@ -34,11 +25,11 @@ def test_get_my_groups_using_old_account_auth(list_my_groups_context):
Test passing in an account will return an empty set
"""
results = list_my_groups_context.client.list_my_groups(status=200)
assert_that(results, has_length(3))
assert_that(results, has_length(4))
assert_that(results, is_not(has_key("groupNameFilter")))
assert_that(results, is_not(has_key("startFrom")))
assert_that(results, is_not(has_key("nextId")))
assert_that(results["maxItems"], is_(200))
assert_that(results, is_(has_key("nextId")))
assert_that(results["maxItems"], is_(100))
def test_list_my_groups_max_items(list_my_groups_context):
@ -102,7 +93,7 @@ def test_list_my_groups_filter_matches(list_my_groups_context):
assert_that(results["groupNameFilter"], is_(f"{list_my_groups_context.group_prefix}-01"))
assert_that(results, is_not(has_key("startFrom")))
assert_that(results, is_not(has_key("nextId")))
assert_that(results["maxItems"], is_(200))
assert_that(results["maxItems"], is_(100))
results["groups"] = sorted(results["groups"], key=lambda x: x["name"])
@ -133,28 +124,20 @@ def test_list_my_groups_with_ignore_access_true(list_my_groups_context):
Test that we can get all the groups whether a user is a member or not
"""
results = list_my_groups_context.client.list_my_groups(ignore_access=True, status=200)
# Only count the groups with the group prefix
assert_that(results, has_length(4)) # 4 fields
assert_that(len(results["groups"]), greater_than(50))
assert_that(results["maxItems"], is_(200))
assert_that(results["maxItems"], is_(100))
assert_that(results["ignoreAccess"], is_(True))
my_results = list_my_groups_context.client.list_my_groups(status=200)
my_groups = [x for x in my_results["groups"] if x["name"].startswith(list_my_groups_context.group_prefix)]
sorted_groups = sorted(my_groups, key=lambda x: x["name"])
for i in range(0, 50):
assert_that(sorted_groups[i]["name"], is_("{0}-{1:0>3}".format(list_my_groups_context.group_prefix, i)))
def test_list_my_groups_as_support_user(list_my_groups_context):
"""
Test that we can get all the groups as a support user, even without ignore_access
"""
results = list_my_groups_context.support_user_client.list_my_groups(status=200)
assert_that(results, has_length(4)) # 4 fields
assert_that(len(results["groups"]), greater_than(50))
assert_that(results["maxItems"], is_(200))
assert_that(results["maxItems"], is_(100))
assert_that(results["ignoreAccess"], is_(False))
@ -163,7 +146,7 @@ def test_list_my_groups_as_support_user_with_ignore_access_true(list_my_groups_c
Test that we can get all the groups as a support user
"""
results = list_my_groups_context.support_user_client.list_my_groups(ignore_access=True, status=200)
assert_that(results, has_length(4)) # 4 fields
assert_that(len(results["groups"]), greater_than(50))
assert_that(results["maxItems"], is_(200))
assert_that(results["maxItems"], is_(100))
assert_that(results["ignoreAccess"], is_(True))

View File

@ -508,9 +508,11 @@ def test_create_invalid_record_data(shared_zone_test_context):
))
def test_create_dotted_a_record_not_apex_fails(shared_zone_test_context):
def test_create_dotted_a_record_not_apex_fails_when_dotted_hosts_config_not_satisfied(shared_zone_test_context):
"""
Test that creating a dotted host name A record set fails.
Test that creating a dotted host name A record set fails
Here the zone and user (individual) is allowed but record type is not allowed. Hence the test fails
Config present in reference.conf
"""
client = shared_zone_test_context.ok_vinyldns_client
@ -524,8 +526,57 @@ def test_create_dotted_a_record_not_apex_fails(shared_zone_test_context):
zone_name = shared_zone_test_context.parent_zone["name"]
error = client.create_recordset(dotted_host_a_record, status=422)
assert_that(error, is_("Record with name " + dotted_host_a_record["name"] + " and type A is a dotted host which "
"is not allowed in zone " + zone_name))
assert_that(error, is_("Record type is not allowed or the user is not authorized to create a dotted host in the "
"zone '" + zone_name + "'"))
def test_create_dotted_a_record_succeeds_if_all_dotted_hosts_config_satisfied(shared_zone_test_context):
"""
Test that creating a A record set with dotted host record name succeeds
Here the zone, user (in group) and record type is allowed. Hence the test succeeds
Config present in reference.conf
"""
client = shared_zone_test_context.history_client
zone = shared_zone_test_context.dummy_zone
dotted_host_a_record = {
"zoneId": zone["id"],
"name": "dot.ted",
"type": "A",
"ttl": 500,
"records": [{"address": "127.0.0.1"}]
}
dotted_a_record = None
try:
dotted_cname_response = client.create_recordset(dotted_host_a_record, status=202)
dotted_a_record = client.wait_until_recordset_change_status(dotted_cname_response, "Complete")["recordSet"]
assert_that(dotted_a_record["name"], is_(dotted_host_a_record["name"]))
finally:
if dotted_a_record:
delete_result = client.delete_recordset(dotted_a_record["zoneId"], dotted_a_record["id"], status=202)
client.wait_until_recordset_change_status(delete_result, "Complete")
def test_create_dotted_a_record_fails_if_all_dotted_hosts_config_not_satisfied(shared_zone_test_context):
"""
Test that creating a A record set with dotted host record name fails
Here the zone, user (in group) and record type is allowed.
But the record name has more dots than the number of dots allowed for this zone. Hence the test fails
The 'dots-limit' config from dotted-hosts config is not satisfied. Config present in reference.conf
"""
client = shared_zone_test_context.history_client
zone = shared_zone_test_context.dummy_zone
dotted_host_a_record = {
"zoneId": zone["id"],
"name": "dot.ted.trial.test.host",
"type": "A",
"ttl": 500,
"records": [{"address": "127.0.0.1"}]
}
error = client.create_recordset(dotted_host_a_record, status=422)
assert_that(error, is_("RecordSet with name " + dotted_host_a_record["name"] + " has more dots than that is "
"allowed in config for this zone which is, 'dots-limit = 3'."))
def test_create_dotted_a_record_apex_succeeds(shared_zone_test_context):
@ -581,13 +632,15 @@ def test_create_dotted_a_record_apex_with_trailing_dot_succeeds(shared_zone_test
client.wait_until_recordset_change_status(delete_result, "Complete")
def test_create_dotted_cname_record_fails(shared_zone_test_context):
def test_create_dotted_cname_record_fails_when_dotted_hosts_config_not_satisfied(shared_zone_test_context):
"""
Test that creating a CNAME record set with dotted host record name returns an error.
Test that creating a CNAME record set with dotted host record name returns an error
Here the zone is allowed but user (individual or in group) and record type is not allowed. Hence the test fails
Config present in reference.conf
"""
client = shared_zone_test_context.ok_vinyldns_client
zone = shared_zone_test_context.parent_zone
apex_cname_rs = {
client = shared_zone_test_context.dummy_vinyldns_client
zone = shared_zone_test_context.dummy_zone
dotted_host_cname_record = {
"zoneId": zone["id"],
"name": "dot.ted",
"type": "CNAME",
@ -595,8 +648,37 @@ def test_create_dotted_cname_record_fails(shared_zone_test_context):
"records": [{"cname": "foo.bar."}]
}
error = client.create_recordset(apex_cname_rs, status=422)
assert_that(error, is_(f'Record with name dot.ted and type CNAME is a dotted host which is not allowed in zone {zone["name"]}'))
error = client.create_recordset(dotted_host_cname_record, status=422)
assert_that(error, is_("Record type is not allowed or the user is not authorized to create a dotted host in the "
"zone '" + zone["name"] + "'"))
def test_create_dotted_cname_record_succeeds_if_all_dotted_hosts_config_satisfied(shared_zone_test_context):
"""
Test that creating a CNAME record set with dotted host record name succeeds.
Here the zone, user (individual) and record type is allowed. Hence the test succeeds
Config present in reference.conf
"""
client = shared_zone_test_context.ok_vinyldns_client
zone = shared_zone_test_context.parent_zone
dotted_host_cname_record = {
"zoneId": zone["id"],
"name": "dot.ted",
"type": "CNAME",
"ttl": 500,
"records": [{"cname": "foo.bar."}]
}
dotted_cname_record = None
try:
dotted_cname_response = client.create_recordset(dotted_host_cname_record, status=202)
dotted_cname_record = client.wait_until_recordset_change_status(dotted_cname_response, "Complete")["recordSet"]
assert_that(dotted_cname_record["name"], is_(dotted_host_cname_record["name"]))
finally:
if dotted_cname_record:
delete_result = client.delete_recordset(dotted_cname_record["zoneId"], dotted_cname_record["id"],
status=202)
client.wait_until_recordset_change_status(delete_result, "Complete")
def test_create_cname_with_multiple_records(shared_zone_test_context):
@ -701,7 +783,8 @@ def test_create_cname_with_existing_record_with_name_fails(shared_zone_test_cont
a_record = client.wait_until_recordset_change_status(a_create, "Complete")["recordSet"]
error = client.create_recordset(cname_rs, status=409)
assert_that(error, is_(f'RecordSet with name duplicate-test-name already exists in zone {zone["name"]}, CNAME record cannot use duplicate name'))
assert_that(error,
is_(f'RecordSet with name duplicate-test-name already exists in zone {zone["name"]}, CNAME record cannot use duplicate name'))
finally:
if a_record:
delete_result = client.delete_recordset(a_record["zoneId"], a_record["id"], status=202)
@ -744,7 +827,8 @@ def test_create_record_with_existing_cname_fails(shared_zone_test_context):
cname_record = client.wait_until_recordset_change_status(cname_create, "Complete")["recordSet"]
error = client.create_recordset(a_rs, status=409)
assert_that(error, is_(f'RecordSet with name duplicate-test-name and type CNAME already exists in zone {zone["name"]}'))
assert_that(error,
is_(f'RecordSet with name duplicate-test-name and type CNAME already exists in zone {zone["name"]}'))
finally:
if cname_record:
delete_result = client.delete_recordset(cname_record["zoneId"], cname_record["id"], status=202)
@ -1368,7 +1452,6 @@ def test_at_create_recordset(shared_zone_test_context):
}
result = client.create_recordset(new_rs, status=202)
assert_that(result["changeType"], is_("Create"))
assert_that(result["status"], is_("Pending"))
assert_that(result["created"], is_not(none()))
@ -1418,7 +1501,6 @@ def test_create_record_with_escape_characters_in_record_data_succeeds(shared_zon
}
result = client.create_recordset(new_rs, status=202)
assert_that(result["changeType"], is_("Create"))
assert_that(result["status"], is_("Pending"))
assert_that(result["created"], is_not(none()))
@ -1554,6 +1636,29 @@ def test_create_long_txt_record_succeeds(shared_zone_test_context):
pass
def test_create_long_spf_record_succeeds(shared_zone_test_context):
client = shared_zone_test_context.ok_vinyldns_client
zone = shared_zone_test_context.system_test_zone
# Anything larger than 255 will test the limits of SPF, 4000 is the value used by R53
# (https://aws.amazon.com/premiumsupport/knowledge-center/route-53-configure-long-spf-txt-records/)
record_data = "a" * 4000
long_spf_rs = create_recordset(zone, "long-spf-record", "SPF", [{"text": record_data}])
try:
rs_create = client.create_recordset(long_spf_rs, status=202)
rs = client.wait_until_recordset_change_status(rs_create, "Complete")["recordSet"]
assert_that(rs["records"][0]["text"], is_(record_data))
finally:
try:
delete_result = client.delete_recordset(rs["zoneId"], rs["id"], status=202)
client.wait_until_recordset_change_status(delete_result, "Complete")
except Exception:
traceback.print_exc()
pass
def test_txt_dotted_host_create_succeeds(shared_zone_test_context):
"""
Tests that a TXT dotted host recordset create succeeds
@ -1743,7 +1848,8 @@ def test_create_high_value_domain_fails(shared_zone_test_context):
}
error = client.create_recordset(new_rs, status=422)
assert_that(error, is_(f'Record name "high-value-domain.{zone["name"]}" is configured as a High Value Domain, so it cannot be modified.'))
assert_that(error,
is_(f'Record name "high-value-domain.{zone["name"]}" is configured as a High Value Domain, so it cannot be modified.'))
def test_create_high_value_domain_fails_case_insensitive(shared_zone_test_context):
@ -1765,7 +1871,8 @@ def test_create_high_value_domain_fails_case_insensitive(shared_zone_test_contex
}
error = client.create_recordset(new_rs, status=422)
assert_that(error, is_(f'Record name "hIgH-vAlUe-dOmAiN.{zone["name"]}" is configured as a High Value Domain, so it cannot be modified.'))
assert_that(error,
is_(f'Record name "hIgH-vAlUe-dOmAiN.{zone["name"]}" is configured as a High Value Domain, so it cannot be modified.'))
def test_create_high_value_domain_fails_for_ip4_ptr(shared_zone_test_context):
@ -1786,7 +1893,8 @@ def test_create_high_value_domain_fails_for_ip4_ptr(shared_zone_test_context):
}
error_ptr = client.create_recordset(ptr, status=422)
assert_that(error_ptr, is_(f'Record name "{shared_zone_test_context.ip4_classless_prefix}.252" is configured as a High Value Domain, so it cannot be modified.'))
assert_that(error_ptr,
is_(f'Record name "{shared_zone_test_context.ip4_classless_prefix}.252" is configured as a High Value Domain, so it cannot be modified.'))
def test_create_high_value_domain_fails_for_ip6_ptr(shared_zone_test_context):
@ -1807,7 +1915,8 @@ def test_create_high_value_domain_fails_for_ip6_ptr(shared_zone_test_context):
}
error_ptr = client.create_recordset(ptr, status=422)
assert_that(error_ptr, is_(f'Record name "{shared_zone_test_context.ip6_prefix}:0000:0000:0000:0000:ffff" is configured as a High Value Domain, so it cannot be modified.'))
assert_that(error_ptr,
is_(f'Record name "{shared_zone_test_context.ip6_prefix}:0000:0000:0000:0000:ffff" is configured as a High Value Domain, so it cannot be modified.'))
def test_create_with_owner_group_in_private_zone_by_admin_passes(shared_zone_test_context):
@ -1874,7 +1983,8 @@ def test_create_with_owner_group_in_private_zone_by_acl_passes(shared_zone_test_
finally:
clear_ok_acl_rules(shared_zone_test_context)
if create_rs:
delete_result = shared_zone_test_context.ok_vinyldns_client.delete_recordset(zone["id"], create_rs["id"], status=202)
delete_result = shared_zone_test_context.ok_vinyldns_client.delete_recordset(zone["id"], create_rs["id"],
status=202)
shared_zone_test_context.ok_vinyldns_client.wait_until_recordset_change_status(delete_result, "Complete")
@ -1900,8 +2010,11 @@ def test_create_with_owner_group_in_shared_zone_by_acl_passes(shared_zone_test_c
finally:
clear_shared_zone_acl_rules(shared_zone_test_context)
if create_rs:
delete_result = shared_zone_test_context.shared_zone_vinyldns_client.delete_recordset(zone["id"], create_rs["id"], status=202)
shared_zone_test_context.shared_zone_vinyldns_client.wait_until_recordset_change_status(delete_result, "Complete")
delete_result = shared_zone_test_context.shared_zone_vinyldns_client.delete_recordset(zone["id"],
create_rs["id"],
status=202)
shared_zone_test_context.shared_zone_vinyldns_client.wait_until_recordset_change_status(delete_result,
"Complete")
def test_create_in_shared_zone_without_owner_group_id_succeeds(shared_zone_test_context):
@ -1955,10 +2068,12 @@ def test_create_in_shared_zone_by_unassociated_user_fails_if_record_type_is_not_
zone = shared_zone_test_context.shared_zone
group = shared_zone_test_context.dummy_group
record_json = create_recordset(zone, "test_shared_not_approved_record_type", "MX", [{"preference": 3, "exchange": "mx"}])
record_json = create_recordset(zone, "test_shared_not_approved_record_type", "MX",
[{"preference": 3, "exchange": "mx"}])
record_json["ownerGroupId"] = group["id"]
error = client.create_recordset(record_json, status=403)
assert_that(error, is_(f'User dummy does not have access to create test-shared-not-approved-record-type.{zone["name"]}'))
assert_that(error,
is_(f'User dummy does not have access to create test-shared-not-approved-record-type.{zone["name"]}'))
def test_create_with_not_found_owner_group_fails(shared_zone_test_context):
@ -1997,7 +2112,8 @@ def test_create_ds_success(shared_zone_test_context):
zone = shared_zone_test_context.ds_zone
record_data = [
{"keytag": 60485, "algorithm": 5, "digesttype": 1, "digest": "2BB183AF5F22588179A53B0A98631FAD1A292118"},
{"keytag": 60485, "algorithm": 5, "digesttype": 2, "digest": "D4B7D520E7BB5F0F67674A0CCEB1E3E0614B93C4F9E99B8383F6A1E4469DA50A"}
{"keytag": 60485, "algorithm": 5, "digesttype": 2,
"digest": "D4B7D520E7BB5F0F67674A0CCEB1E3E0614B93C4F9E99B8383F6A1E4469DA50A"}
]
record_json = create_recordset(zone, "dskey", "DS", record_data, ttl=3600)
result_rs = None
@ -2039,7 +2155,8 @@ def test_create_ds_unknown_algorithm(shared_zone_test_context):
"""
client = shared_zone_test_context.ok_vinyldns_client
zone = shared_zone_test_context.ds_zone
record_data = [{"keytag": 60485, "algorithm": 0, "digesttype": 1, "digest": "2BB183AF5F22588179A53B0A98631FAD1A292118"}]
record_data = [
{"keytag": 60485, "algorithm": 0, "digesttype": 1, "digest": "2BB183AF5F22588179A53B0A98631FAD1A292118"}]
record_json = create_recordset(zone, "dskey", "DS", record_data)
errors = client.create_recordset(record_json, status=400)["errors"]
assert_that(errors, contains_inanyorder("Algorithm 0 is not a supported DNSSEC algorithm"))
@ -2051,7 +2168,8 @@ def test_create_ds_unknown_digest_type(shared_zone_test_context):
"""
client = shared_zone_test_context.ok_vinyldns_client
zone = shared_zone_test_context.ds_zone
record_data = [{"keytag": 60485, "algorithm": 5, "digesttype": 0, "digest": "2BB183AF5F22588179A53B0A98631FAD1A292118"}]
record_data = [
{"keytag": 60485, "algorithm": 5, "digesttype": 0, "digest": "2BB183AF5F22588179A53B0A98631FAD1A292118"}]
record_json = create_recordset(zone, "dskey", "DS", record_data)
errors = client.create_recordset(record_json, status=400)["errors"]
assert_that(errors, contains_inanyorder("Digest Type 0 is not a supported DS record digest type"))
@ -2063,10 +2181,12 @@ def test_create_ds_no_ns_fails(shared_zone_test_context):
"""
client = shared_zone_test_context.ok_vinyldns_client
zone = shared_zone_test_context.ds_zone
record_data = [{"keytag": 60485, "algorithm": 5, "digesttype": 1, "digest": "2BB183AF5F22588179A53B0A98631FAD1A292118"}]
record_data = [
{"keytag": 60485, "algorithm": 5, "digesttype": 1, "digest": "2BB183AF5F22588179A53B0A98631FAD1A292118"}]
record_json = create_recordset(zone, "no-ns-exists", "DS", record_data, ttl=3600)
error = client.create_recordset(record_json, status=422)
assert_that(error, is_(f'DS record [no-ns-exists] is invalid because there is no NS record with that name in the zone [{zone["name"]}]'))
assert_that(error,
is_(f'DS record [no-ns-exists] is invalid because there is no NS record with that name in the zone [{zone["name"]}]'))
def test_create_apex_ds_fails(shared_zone_test_context):
@ -2075,7 +2195,8 @@ def test_create_apex_ds_fails(shared_zone_test_context):
"""
client = shared_zone_test_context.ok_vinyldns_client
zone = shared_zone_test_context.ds_zone
record_data = [{"keytag": 60485, "algorithm": 5, "digesttype": 1, "digest": "2BB183AF5F22588179A53B0A98631FAD1A292118"}]
record_data = [
{"keytag": 60485, "algorithm": 5, "digesttype": 1, "digest": "2BB183AF5F22588179A53B0A98631FAD1A292118"}]
record_json = create_recordset(zone, "@", "DS", record_data, ttl=100)
error = client.create_recordset(record_json, status=422)
assert_that(error, is_(f'Record with name [{zone["name"]}] is an DS record at apex and cannot be added'))
@ -2087,7 +2208,9 @@ def test_create_dotted_ds_fails(shared_zone_test_context):
"""
client = shared_zone_test_context.ok_vinyldns_client
zone = shared_zone_test_context.ds_zone
record_data = [{"keytag": 60485, "algorithm": 5, "digesttype": 1, "digest": "2BB183AF5F22588179A53B0A98631FAD1A292118"}]
record_data = [
{"keytag": 60485, "algorithm": 5, "digesttype": 1, "digest": "2BB183AF5F22588179A53B0A98631FAD1A292118"}]
record_json = create_recordset(zone, "dotted.ds", "DS", record_data, ttl=100)
error = client.create_recordset(record_json, status=422)
assert_that(error, is_(f'Record with name dotted.ds and type DS is a dotted host which is not allowed in zone {zone["name"]}'))
assert_that(error,
is_(f'Record with name dotted.ds and type DS is a dotted host which is not allowed in zone {zone["name"]}'))

View File

@ -1593,7 +1593,7 @@ def test_update_fails_for_unapplied_unsynced_record_change(shared_zone_test_cont
]
update_response = client.update_recordset(update_rs, status=202)
response = client.wait_until_recordset_change_status(update_response, "Failed")
assert_that(response["systemMessage"], is_(f"Failed validating update to DNS for change {response['id']}:{a_rs['name']}: "
assert_that(response["systemMessage"], is_(f"Failed validating update to DNS for change \"{response['id']}\": \"{a_rs['name']}\": "
f"This record set is out of sync with the DNS backend; sync this zone before attempting to update this record set."))
finally:
try:

View File

@ -174,6 +174,15 @@ class SharedZoneTestContext(object):
"shared": False,
"adminGroupId": self.dummy_group["id"],
"isTest": True,
"acl": {
"rules": [
{
"accessLevel": "Delete",
"description": "some_test_rule",
"userId": "history-id"
}
]
},
"connection": {
"name": "dummy.",
"keyName": VinylDNSTestContext.dns_key_name,

View File

@ -23,6 +23,44 @@ def test_list_zones_success(list_zone_context, shared_zone_test_context):
assert_that(result["nameFilter"], is_(f"*{shared_zone_test_context.partition_id}"))
def test_list_zones_by_admin_group_name(list_zone_context, shared_zone_test_context):
"""
Test that we can retrieve list of zones by searching with admin group name
"""
result = shared_zone_test_context.list_zones_client.list_zones(name_filter=f"list-zones-group{shared_zone_test_context.partition_id}", search_by_admin_group=True, status=200)
retrieved = result["zones"]
assert_that(retrieved, has_length(5))
assert_that(retrieved, has_item(has_entry("name", list_zone_context.search_zone1["name"])))
assert_that(retrieved, has_item(has_entry("name", list_zone_context.search_zone2["name"])))
assert_that(retrieved, has_item(has_entry("name", list_zone_context.search_zone3["name"])))
assert_that(retrieved, has_item(has_entry("name", list_zone_context.non_search_zone1["name"])))
assert_that(retrieved, has_item(has_entry("name", list_zone_context.non_search_zone2["name"])))
assert_that(retrieved, has_item(has_entry("adminGroupName", list_zone_context.list_zones_group["name"])))
assert_that(retrieved, has_item(has_entry("backendId", "func-test-backend")))
assert_that(result["nameFilter"], is_(f"list-zones-group{shared_zone_test_context.partition_id}"))
def test_list_zones_by_admin_group_name_with_wildcard(list_zone_context, shared_zone_test_context):
"""
Test that we can retrieve list of zones by searching with admin group name with wildcard character
"""
result = shared_zone_test_context.list_zones_client.list_zones(name_filter=f"*group{shared_zone_test_context.partition_id}", search_by_admin_group=True, status=200)
retrieved = result["zones"]
assert_that(retrieved, has_length(5))
assert_that(retrieved, has_item(has_entry("name", list_zone_context.search_zone1["name"])))
assert_that(retrieved, has_item(has_entry("name", list_zone_context.search_zone2["name"])))
assert_that(retrieved, has_item(has_entry("name", list_zone_context.search_zone3["name"])))
assert_that(retrieved, has_item(has_entry("name", list_zone_context.non_search_zone1["name"])))
assert_that(retrieved, has_item(has_entry("name", list_zone_context.non_search_zone2["name"])))
assert_that(retrieved, has_item(has_entry("adminGroupName", list_zone_context.list_zones_group["name"])))
assert_that(retrieved, has_item(has_entry("backendId", "func-test-backend")))
assert_that(result["nameFilter"], is_(f"*group{shared_zone_test_context.partition_id}"))
def test_list_zones_max_items_100(shared_zone_test_context):
"""
Test that the default max items for a list zones request is 100

View File

@ -220,7 +220,7 @@ class VinylDNSClient(object):
return data
def list_my_groups(self, group_name_filter=None, start_from=None, max_items=200, ignore_access=False, **kwargs):
def list_my_groups(self, group_name_filter=None, start_from=None, max_items=100, ignore_access=False, **kwargs):
"""
Retrieves my groups
:param start_from: the start key of the page
@ -445,7 +445,7 @@ class VinylDNSClient(object):
response, data = self.make_request(url, "GET", self.headers, not_found_ok=True, **kwargs)
return data
def list_zones(self, name_filter=None, start_from=None, max_items=None, ignore_access=False, **kwargs):
def list_zones(self, name_filter=None, start_from=None, max_items=None, search_by_admin_group=False, ignore_access=False, **kwargs):
"""
Gets a list of zones that currently exist
:return: a list of zones
@ -462,6 +462,9 @@ class VinylDNSClient(object):
if max_items:
query.append("maxItems=" + str(max_items))
if search_by_admin_group:
query.append("searchByAdminGroup=" + str(search_by_admin_group))
if ignore_access:
query.append("ignoreAccess=" + str(ignore_access))

View File

@ -19,6 +19,7 @@ package vinyldns.api
import com.comcast.ip4s.IpAddress
import java.time.{Instant, LocalDateTime, Month, ZoneOffset}
import vinyldns.api.config.{BatchChangeConfig, HighValueDomainConfig, LimitsConfig, ManualReviewConfig, ScheduledChangesConfig}
import vinyldns.api.config.{ZoneAuthConfigs, DottedHostsConfig, BatchChangeConfig, HighValueDomainConfig, LimitsConfig, ManualReviewConfig, ScheduledChangesConfig}
import vinyldns.api.domain.batch.V6DiscoveryNibbleBoundaries
import vinyldns.core.domain.record._
import vinyldns.core.domain.zone._
@ -40,6 +41,10 @@ trait VinylDNSTestHelpers {
val approvedNameServers: List[Regex] = List(new Regex("some.test.ns."))
val dottedHostsConfig: DottedHostsConfig = DottedHostsConfig(List(ZoneAuthConfigs("dotted.xyz.",List("xyz"),List("dummy"),List("CNAME"), 3), ZoneAuthConfigs("abc.zone.recordsets.",List("locked"),List("dummy"),List("CNAME"), 3), ZoneAuthConfigs("xyz.",List("super"),List("xyz"),List("CNAME"), 3), ZoneAuthConfigs("dot.xyz.",List("super"),List("xyz"),List("CNAME"), 0)))
val emptyDottedHostsConfig: DottedHostsConfig = DottedHostsConfig(List.empty)
val defaultTtl: Long = 7200
val manualReviewDomainList: List[Regex] = List(new Regex("needs-review.*"))

View File

@ -398,9 +398,9 @@ class DnsConversionsSpec
verifyMatch(result, testLongTXT)
}
"fail to convert a bad SPF record set" in {
val result = toDnsRRset(testLongSPF, testZoneName).left.value
result shouldBe a[java.lang.IllegalArgumentException]
"convert long SPF record set" in {
val result = toDnsRRset(testLongSPF, testZoneName).right.value
verifyMatch(result, testLongSPF)
}
}
@ -514,6 +514,9 @@ class DnsConversionsSpec
"convert to/from RecordType TXT long TXT record data" in {
verifyMatch(testLongTXT, roundTrip(testLongTXT))
}
"convert to/from RecordType SPF long SPF record data" in {
verifyMatch(testLongSPF, roundTrip(testLongSPF))
}
}
"Converting to DNS RecordType" should {

View File

@ -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.{InvalidDomainName, InvalidLength}
import vinyldns.core.domain.{InvalidDomainName, InvalidCname, InvalidLength}
class DomainValidationsSpec
extends AnyPropSpec
@ -111,4 +110,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]
}
}

View File

@ -38,13 +38,15 @@ import vinyldns.core.domain.record._
import vinyldns.core.domain.zone.Zone
class BatchChangeConverterSpec extends AnyWordSpec with Matchers with CatsHelpers {
private val notExistCompletedMessage: String = "This record does not exist." +
"No further action is required."
private def makeSingleAddChange(
name: String,
recordData: RecordData,
typ: RecordType = A,
zone: Zone = okZone
) = {
name: String,
recordData: RecordData,
typ: RecordType = A,
zone: Zone = okZone
) = {
val fqdn = s"$name.${zone.name}"
SingleAddChange(
Some(zone.id),
@ -78,10 +80,10 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers with CatsHelper
}
private def makeAddChangeForValidation(
recordName: String,
recordData: RecordData,
typ: RecordType = RecordType.A
): AddChangeForValidation =
recordName: String,
recordData: RecordData,
typ: RecordType = RecordType.A
): AddChangeForValidation =
AddChangeForValidation(
okZone,
s"$recordName",
@ -90,9 +92,9 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers with CatsHelper
)
private def makeDeleteRRSetChangeForValidation(
recordName: String,
typ: RecordType = RecordType.A
): DeleteRRSetChangeForValidation =
recordName: String,
typ: RecordType = RecordType.A
): DeleteRRSetChangeForValidation =
DeleteRRSetChangeForValidation(
okZone,
s"$recordName",
@ -161,6 +163,14 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers with CatsHelper
makeAddChangeForValidation("mxToUpdate", MXData(1, Fqdn("update.com.")), MX)
)
private val singleChangesOneDelete = List(
makeSingleDeleteRRSetChange("DoesNotExistToDelete", A)
)
private val changeForValidationOneDelete = List(
makeDeleteRRSetChangeForValidation("DoesNotExistToDelete", A)
)
private val singleChangesOneBad = List(
makeSingleAddChange("one", AData("1.1.1.1")),
makeSingleAddChange("two", AData("1.1.1.2")),
@ -536,6 +546,42 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers with CatsHelper
savedBatch shouldBe Some(returnedBatch)
}
"set status to complete when deleting a record that does not exist" in {
val batchWithBadChange =
BatchChange(
okUser.id,
okUser.userName,
None,
DateTime.now,
singleChangesOneDelete,
approvalStatus = BatchChangeApprovalStatus.AutoApproved
)
val result = rightResultOf(
underTest
.sendBatchForProcessing(
batchWithBadChange,
existingZones,
ChangeForValidationMap(changeForValidationOneDelete.map(_.validNel), existingRecordSets),
None
)
.value
)
val returnedBatch = result.batchChange
// validate completed status returned
val receivedChange = returnedBatch.changes(0)
receivedChange.status shouldBe SingleChangeStatus.Complete
receivedChange.recordChangeId shouldBe None
receivedChange.systemMessage shouldBe Some(notExistCompletedMessage)
returnedBatch.changes(0) shouldBe singleChangesOneDelete(0).copy(systemMessage = Some(notExistCompletedMessage), status = SingleChangeStatus.Complete)
// check the update has been made in the DB
val savedBatch: Option[BatchChange] =
await(batchChangeRepo.getBatchChange(batchWithBadChange.id))
savedBatch shouldBe Some(returnedBatch)
}
"return error if an unsupported record is received" in {
val batchChangeUnsupported =
BatchChange(
@ -678,11 +724,11 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers with CatsHelper
}
private def validateRecordSetChange(
name: String,
recordSetChanges: List[RecordSetChange],
batchChange: BatchChange,
typ: RecordSetChangeType
) = {
name: String,
recordSetChanges: List[RecordSetChange],
batchChange: BatchChange,
typ: RecordSetChangeType
) = {
val singleChangesOut = batchChange.changes.filter { change =>
change.recordName match {
case Some(rn) if rn == name => true
@ -709,10 +755,10 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers with CatsHelper
}
private def validateRecordDataCombination(
name: String,
recordSetChanges: List[RecordSetChange],
batchChange: BatchChange
) = {
name: String,
recordSetChanges: List[RecordSetChange],
batchChange: BatchChange
) = {
val singleChangesOut = batchChange.changes.filter { change =>
change.recordName match {
case Some(rn) if rn == name => true
@ -729,4 +775,4 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers with CatsHelper
recordChangeOut.recordSet.records should contain theSameElementsAs expectedRecords
}
}
}

View File

@ -56,7 +56,7 @@ import vinyldns.api.domain.access.AccessValidations
import scala.concurrent.ExecutionContext
class BatchChangeServiceSpec
extends AnyWordSpec
extends AnyWordSpec
with Matchers
with MockitoSugar
with CatsHelpers
@ -67,8 +67,8 @@ class BatchChangeServiceSpec
private implicit val contextShift: ContextShift[IO] = IO.contextShift(ExecutionContext.global)
private val nonFatalError = ZoneDiscoveryError("test")
private val fatalError = RecordAlreadyExists("test")
private val nonFatalErrorZoneDiscoveryError = ZoneDiscoveryError("test")
private val nonFatalErrorRecordAlreadyExists = RecordAlreadyExists("test", AData("1.1.1.1"), true)
private val validations = new BatchChangeValidations(
new AccessValidations(
@ -748,7 +748,7 @@ class BatchChangeServiceSpec
"succeed if the batchChange is PendingReview and reviewer is authorized" in {
batchChangeRepo.save(batchChangeNeedsApproval)
val result =
val result = {
rightResultOf(
underTestManualEnabled
.approveBatchChange(
@ -758,6 +758,7 @@ class BatchChangeServiceSpec
)
.value
)
}
result.userId shouldBe batchChangeNeedsApproval.userId
result.userName shouldBe batchChangeNeedsApproval.userName
@ -1709,8 +1710,8 @@ class BatchChangeServiceSpec
BatchChangeInput(None, List(apexAddA, onlyBaseAddAAAA, delete), Some("owner-group-ID")),
List(
AddChangeForValidation(apexZone, "apex.test.com.", apexAddA, 7200L).validNel,
nonFatalError.invalidNel,
nonFatalError.invalidNel
nonFatalErrorZoneDiscoveryError.invalidNel,
nonFatalErrorZoneDiscoveryError.invalidNel
),
okAuth,
true
@ -1746,7 +1747,7 @@ class BatchChangeServiceSpec
None,
None,
None,
List(SingleChangeError(nonFatalError)),
List(SingleChangeError(nonFatalErrorZoneDiscoveryError)),
result.changes(1).id
)
result.changes(2) shouldBe SingleDeleteRRSetChange(
@ -1760,7 +1761,7 @@ class BatchChangeServiceSpec
None,
None,
None,
List(SingleChangeError(nonFatalError)),
List(SingleChangeError(nonFatalErrorZoneDiscoveryError)),
result.changes(2).id
)
}
@ -1776,8 +1777,8 @@ class BatchChangeServiceSpec
),
List(
AddChangeForValidation(apexZone, "apex.test.com.", apexAddA, 7200L).validNel,
nonFatalError.invalidNel,
nonFatalError.invalidNel
nonFatalErrorZoneDiscoveryError.invalidNel,
nonFatalErrorZoneDiscoveryError.invalidNel
),
okAuth,
allowManualReview = true
@ -1810,7 +1811,7 @@ class BatchChangeServiceSpec
List(
ZoneDiscoveryError("no.zone.match.").invalidNel,
AddChangeForValidation(baseZone, "non-apex", nonApexAddA, 7200L).validNel,
nonFatalError.invalidNel
nonFatalErrorZoneDiscoveryError.invalidNel
),
okAuth,
true
@ -1826,7 +1827,7 @@ class BatchChangeServiceSpec
ibcr.changeRequestResponses(1) shouldBe Valid(
AddChangeForValidation(baseZone, "non-apex", nonApexAddA, 7200L)
)
ibcr.changeRequestResponses(2) should haveInvalid[DomainValidationError](nonFatalError)
ibcr.changeRequestResponses(2) should haveInvalid[DomainValidationError](nonFatalErrorZoneDiscoveryError)
}
"return a BatchChangeErrorList if all data inputs are valid/soft failures and manual review is disabled" in {
@ -1836,8 +1837,8 @@ class BatchChangeServiceSpec
BatchChangeInput(None, List(apexAddA, onlyBaseAddAAAA, delete)),
List(
AddChangeForValidation(apexZone, "apex.test.com.", apexAddA, 7200L).validNel,
nonFatalError.invalidNel,
nonFatalError.invalidNel
nonFatalErrorZoneDiscoveryError.invalidNel,
nonFatalErrorZoneDiscoveryError.invalidNel
),
okAuth,
true
@ -1861,8 +1862,8 @@ class BatchChangeServiceSpec
),
List(
AddChangeForValidation(apexZone, "apex.test.com.", apexAddA, 7200L).validNel,
nonFatalError.invalidNel,
nonFatalError.invalidNel
nonFatalErrorZoneDiscoveryError.invalidNel,
nonFatalErrorZoneDiscoveryError.invalidNel
),
okAuth,
true
@ -1903,8 +1904,8 @@ class BatchChangeServiceSpec
BatchChangeInput(None, List(apexAddA, onlyBaseAddAAAA, delete)),
List(
AddChangeForValidation(apexZone, "apex.test.com.", apexAddA, 7200L).validNel,
nonFatalError.invalidNel,
nonFatalError.invalidNel
nonFatalErrorZoneDiscoveryError.invalidNel,
nonFatalErrorZoneDiscoveryError.invalidNel
),
okAuth,
false
@ -1927,8 +1928,8 @@ class BatchChangeServiceSpec
),
List(
AddChangeForValidation(apexZone, "apex.test.com.", apexAddA, 7200L).validNel,
nonFatalError.invalidNel,
nonFatalError.invalidNel
nonFatalErrorZoneDiscoveryError.invalidNel,
nonFatalErrorZoneDiscoveryError.invalidNel
),
okAuth,
allowManualReview = false
@ -1946,7 +1947,7 @@ class BatchChangeServiceSpec
BatchChangeInput(None, List(apexAddA, onlyBaseAddAAAA), None),
List(
AddChangeForValidation(apexZone, "apex.test.com.", apexAddA, 7200L).validNel,
nonFatalError.invalidNel
nonFatalErrorZoneDiscoveryError.invalidNel
),
okAuth,
true
@ -2008,7 +2009,7 @@ class BatchChangeServiceSpec
asAdds.head,
7200L
).validNel,
fatalError.invalidNel
nonFatalErrorRecordAlreadyExists.invalidNel
),
reviewInfo
)
@ -2559,4 +2560,4 @@ class BatchChangeServiceSpec
)
}
}
}
}

View File

@ -43,7 +43,7 @@ import java.time.temporal.ChronoUnit
import scala.util.Random
class BatchChangeValidationsSpec
extends AnyPropSpec
extends AnyPropSpec
with Matchers
with ScalaCheckDrivenPropertyChecks
with EitherMatchers
@ -194,9 +194,9 @@ class BatchChangeValidationsSpec
)
private def makeAddUpdateRecord(
recordName: String,
aData: AData = AData("1.2.3.4")
): AddChangeForValidation =
recordName: String,
aData: AData = AData("1.2.3.4")
): AddChangeForValidation =
AddChangeForValidation(
okZone,
s"$recordName",
@ -205,9 +205,9 @@ class BatchChangeValidationsSpec
)
private def makeDeleteUpdateDeleteRRSet(
recordName: String,
recordData: Option[RecordData] = None
): DeleteRRSetChangeForValidation =
recordName: String,
recordData: Option[RecordData] = None
): DeleteRRSetChangeForValidation =
DeleteRRSetChangeForValidation(
okZone,
s"$recordName",
@ -668,7 +668,7 @@ class BatchChangeValidationsSpec
}
property("""validateAddChangeInput: should fail with InvalidIpv4Address
|if validateRecordData fails for an invalid ipv4 address""".stripMargin) {
|if validateRecordData fails for an invalid ipv4 address""".stripMargin) {
val invalidIpv4 = "invalidIpv4:123"
val change = AddChangeInput("test.comcast.com.", RecordType.A, ttl, AData(invalidIpv4))
val result = validateAddChangeInput(change, false)
@ -714,7 +714,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 InvalidLength
@ -826,10 +826,10 @@ class BatchChangeValidationsSpec
result(0) shouldBe valid
result(1) should haveInvalid[DomainValidationError](
RecordAlreadyExists(existingA.inputChange.inputName)
RecordAlreadyExists(existingA.inputChange.inputName, existingA.inputChange.record, false)
)
result(2) should haveInvalid[DomainValidationError](
RecordAlreadyExists(existingCname.inputChange.inputName)
RecordAlreadyExists(existingCname.inputChange.inputName, existingCname.inputChange.record, false)
).and(
haveInvalid[DomainValidationError](
CnameIsNotUniqueError(existingCname.inputChange.inputName, existingCname.inputChange.typ)
@ -1006,7 +1006,7 @@ class BatchChangeValidationsSpec
)
}
property("validateChangesWithContext: should fail for update if record does not exist") {
property("validateChangesWithContext: should complete for update if record does not exist") {
val deleteRRSet = makeDeleteUpdateDeleteRRSet("deleteRRSet")
val deleteRecord = makeDeleteUpdateDeleteRRSet("deleteRecord", Some(AData("1.1.1.1")))
val deleteNonExistentEntry = makeDeleteUpdateDeleteRRSet("ok", Some(AData("1.1.1.1")))
@ -1028,15 +1028,8 @@ class BatchChangeValidationsSpec
)
result(0) shouldBe valid
result(1) should haveInvalid[DomainValidationError](
RecordDoesNotExist(deleteRRSet.inputChange.inputName)
)
result(3) should haveInvalid[DomainValidationError](
RecordDoesNotExist(deleteRecord.inputChange.inputName)
)
result(3) should haveInvalid[DomainValidationError](
RecordDoesNotExist(deleteRecord.inputChange.inputName)
)
result(1) shouldBe valid
result(3) shouldBe valid
result(4) shouldBe valid
deleteNonExistentEntry.inputChange.record.foreach { record =>
result(5) should haveInvalid[DomainValidationError](
@ -1047,7 +1040,7 @@ class BatchChangeValidationsSpec
property(
"""validateChangesWithContext: should succeed for update in shared zone if user belongs to record
| owner group""".stripMargin
| owner group""".stripMargin
) {
val existingRecord =
sharedZoneRecord.copy(
@ -1082,7 +1075,7 @@ class BatchChangeValidationsSpec
}
property("""validateChangesWithContext: should succeed adding a record
|if an existing CNAME with the same name exists but is being deleted""".stripMargin) {
|if an existing CNAME with the same name exists but is being deleted""".stripMargin) {
val existingCname = rsOk.copy(name = "deleteRRSet", typ = RecordType.CNAME)
val existingCname2 =
existingCname.copy(name = "deleteRecord", records = List(CNAMEData(Fqdn("cname.data."))))
@ -1191,7 +1184,7 @@ class BatchChangeValidationsSpec
)
result(0) should haveInvalid[DomainValidationError](
RecordAlreadyExists(input.inputChange.inputName)
RecordAlreadyExists(input.inputChange.inputName, input.inputChange.record, false)
)
}
}
@ -1227,7 +1220,7 @@ class BatchChangeValidationsSpec
}
property("""validateChangesWithContext: should fail with CnameIsNotUniqueError
|if CNAME record name already exists""".stripMargin) {
|if CNAME record name already exists""".stripMargin) {
val addCname = AddChangeForValidation(
validZone,
"existingCname",
@ -1249,7 +1242,7 @@ class BatchChangeValidationsSpec
}
property("""validateChangesWithContext: should succeed for CNAME record
|if there's a duplicate PTR ipv4 record that is being deleted""".stripMargin) {
|if there's a duplicate PTR ipv4 record that is being deleted""".stripMargin) {
val addCname = AddChangeForValidation(
validIp4ReverseZone,
"30",
@ -1277,7 +1270,7 @@ class BatchChangeValidationsSpec
}
property("""validateChangesWithContext: should fail with CnameIsNotUniqueError for CNAME record
|if there's a duplicate PTR ipv6 record""".stripMargin) {
|if there's a duplicate PTR ipv6 record""".stripMargin) {
val addCname = AddChangeForValidation(
validZone,
"0.6.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0",
@ -1303,7 +1296,7 @@ class BatchChangeValidationsSpec
}
property("""validateChangesWithContext: CNAME record should pass
|if no other changes in batch change have same record name""".stripMargin) {
|if no other changes in batch change have same record name""".stripMargin) {
val addA = AddChangeForValidation(
okZone,
"test",
@ -1338,7 +1331,7 @@ class BatchChangeValidationsSpec
}
property("""validateChangesWithContext: CNAME record should fail
|if another add change in batch change has the same record name""".stripMargin) {
|if another add change in batch change has the same record name""".stripMargin) {
val addA = AddChangeForValidation(
okZone,
"test",
@ -1378,7 +1371,7 @@ class BatchChangeValidationsSpec
}
property("""validateChangesWithContext: both CNAME records should fail
|if there are duplicate CNAME add change inputs""".stripMargin) {
|if there are duplicate CNAME add change inputs""".stripMargin) {
val addA = AddChangeForValidation(
okZone,
"test",
@ -1420,7 +1413,7 @@ class BatchChangeValidationsSpec
}
property("""validateChangesWithContext: both PTR records should succeed
|if there are duplicate PTR add change inputs""".stripMargin) {
|if there are duplicate PTR add change inputs""".stripMargin) {
val addA = AddChangeForValidation(
okZone,
"test",
@ -1453,7 +1446,7 @@ class BatchChangeValidationsSpec
}
property("""validateChangesWithContext: should succeed for AddChangeForValidation
|if user has group admin access""".stripMargin) {
|if user has group admin access""".stripMargin) {
val addA = AddChangeForValidation(
validZone,
"valid",
@ -1542,7 +1535,7 @@ class BatchChangeValidationsSpec
}
property("""validateChangesWithContext: should fail with RecordNameNotUniqueInBatch for PTR record
|if valid CNAME with same name exists in batch""".stripMargin) {
|if valid CNAME with same name exists in batch""".stripMargin) {
val addCname = AddChangeForValidation(
validZone,
"existing",
@ -1591,7 +1584,7 @@ class BatchChangeValidationsSpec
}
property(
"""validateChangesWithContext: should fail DeleteChangeForValidation with RecordDoesNotExist
"""validateChangesWithContext: should complete DeleteChangeForValidation
|if record does not exist""".stripMargin
) {
val deleteRRSet = makeDeleteUpdateDeleteRRSet("record-does-not-exist")
@ -1608,16 +1601,12 @@ class BatchChangeValidationsSpec
None
)
result(0) should haveInvalid[DomainValidationError](
RecordDoesNotExist(deleteRRSet.inputChange.inputName)
)
result(1) should haveInvalid[DomainValidationError](
RecordDoesNotExist(deleteRecord.inputChange.inputName)
)
result(0) shouldBe valid
result(1) shouldBe valid
}
property("""validateChangesWithContext: should succeed for DeleteChangeForValidation
|if record set status is Active""".stripMargin) {
|if record set status is Active""".stripMargin) {
val deleteA = DeleteRRSetChangeForValidation(
validZone,
"Active-record-status",
@ -1642,7 +1631,7 @@ class BatchChangeValidationsSpec
}
property("""validateChangesWithContext: should succeed for DeleteChangeForValidation
|if user has group admin access"""".stripMargin) {
|if user has group admin access"""".stripMargin) {
val deleteA =
DeleteRRSetChangeForValidation(
validZone,
@ -1664,7 +1653,7 @@ class BatchChangeValidationsSpec
}
property(""" validateChangesWithContext: should fail for DeleteChangeForValidation
| if user is superUser with no other access""".stripMargin) {
| if user is superUser with no other access""".stripMargin) {
val deleteA =
DeleteRRSetChangeForValidation(
validZone,
@ -1747,7 +1736,7 @@ class BatchChangeValidationsSpec
}
property("""validateChangesWithContext: should properly process batch that contains
|a CNAME and different type record with the same name""".stripMargin) {
|a CNAME and different type record with the same name""".stripMargin) {
val addDuplicateA = AddChangeForValidation(
okZone,
"test",
@ -2166,7 +2155,7 @@ class BatchChangeValidationsSpec
result should haveInvalid[DomainValidationError](InvalidIpv4Address(invalidIp))
}
property("validateChangesWithContext: should fail if MX record in batch already exists") {
property("validateChangesWithContext: should Success if MX record in batch already exists") {
val existingMX = rsOk.copy(
zoneId = okZone.id,
name = "name-conflict",
@ -2187,7 +2176,7 @@ class BatchChangeValidationsSpec
false,
None
)
result(0) should haveInvalid[DomainValidationError](RecordAlreadyExists("name-conflict."))
result(0) shouldBe valid
}
property("validateChangesWithContext: should succeed if duplicate MX records in batch") {
@ -2458,7 +2447,7 @@ class BatchChangeValidationsSpec
property(
"""validateChangesWithContext: should fail validateAddWithContext with
|ZoneDiscoveryError if new record is dotted host but not a TXT record type""".stripMargin
|ZoneDiscoveryError if new record is dotted host but not a TXT record type""".stripMargin
) {
val addA = AddChangeForValidation(
okZone,
@ -2668,4 +2657,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
}
}

View File

@ -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)
@ -714,6 +783,59 @@ class MembershipServiceSpec
}
}
"getGroupChange" should {
"return the single group change" in {
val groupChangeRepoResponse = listOfDummyGroupChanges.take(1).head
doReturn(IO.pure(Option(groupChangeRepoResponse)))
.when(mockGroupChangeRepo)
.getGroupChange(anyString)
doReturn(IO.pure(ListUsersResults(Seq(dummyUser), Some("1"))))
.when(mockUserRepo)
.getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
val userMap = Seq(dummyUser).map(u => (u.id, u.userName)).toMap
val expected: GroupChangeInfo =
listOfDummyGroupChanges.map(change => GroupChangeInfo.apply(change.copy(userName = userMap.get(change.userId)))).take(1).head
val result: GroupChangeInfo =
rightResultOf(underTest.getGroupChange(dummyGroup.id, dummyAuth).value)
result shouldBe expected
}
"return the single group change even if the user is not authorized" in {
val groupChangeRepoResponse = listOfDummyGroupChanges.take(1).head
doReturn(IO.pure(Some(groupChangeRepoResponse)))
.when(mockGroupChangeRepo)
.getGroupChange(anyString)
doReturn(IO.pure(ListUsersResults(Seq(dummyUser), Some("1"))))
.when(mockUserRepo)
.getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
val userMap = Seq(dummyUser).map(u => (u.id, u.userName)).toMap
val expected: GroupChangeInfo =
listOfDummyGroupChanges.map(change => GroupChangeInfo.apply(change.copy(userName = userMap.get(change.userId)))).take(1).head
val result: GroupChangeInfo =
rightResultOf(underTest.getGroupChange(dummyGroup.id, okAuth).value)
result shouldBe expected
}
"return a InvalidGroupRequestError if the group change id is not valid" in {
doReturn(IO.pure(None))
.when(mockGroupChangeRepo)
.getGroupChange(anyString)
doReturn(IO.pure(ListUsersResults(Seq(dummyUser), Some("1"))))
.when(mockUserRepo)
.getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
val result = leftResultOf(underTest.getGroupChange(dummyGroup.id, okAuth).value)
result shouldBe a[InvalidGroupRequestError]
}
}
"getGroupActivity" should {
"return the group activity" in {
val groupChangeRepoResponse = ListGroupChangesResults(
@ -724,8 +846,13 @@ class MembershipServiceSpec
.when(mockGroupChangeRepo)
.getGroupChanges(anyString, any[Option[String]], anyInt)
doReturn(IO.pure(ListUsersResults(Seq(dummyUser), Some("1"))))
.when(mockUserRepo)
.getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
val userMap = Seq(dummyUser).map(u => (u.id, u.userName)).toMap
val expected: List[GroupChangeInfo] =
listOfDummyGroupChanges.map(GroupChangeInfo.apply).take(100)
listOfDummyGroupChanges.map(change => GroupChangeInfo.apply(change.copy(userName = userMap.get(change.userId)))).take(100)
val result: ListGroupChangesResponse =
rightResultOf(underTest.getGroupActivity(dummyGroup.id, None, 100, dummyAuth).value)
@ -744,8 +871,13 @@ class MembershipServiceSpec
.when(mockGroupChangeRepo)
.getGroupChanges(anyString, any[Option[String]], anyInt)
doReturn(IO.pure(ListUsersResults(Seq(dummyUser), Some("1"))))
.when(mockUserRepo)
.getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
val userMap = Seq(dummyUser).map(u => (u.id, u.userName)).toMap
val expected: List[GroupChangeInfo] =
listOfDummyGroupChanges.map(GroupChangeInfo.apply).take(100)
listOfDummyGroupChanges.map(change => GroupChangeInfo.apply(change.copy(userName = userMap.get(change.userId)))).take(100)
val result: ListGroupChangesResponse =
rightResultOf(underTest.getGroupActivity(dummyGroup.id, None, 100, okAuth).value)
@ -756,6 +888,19 @@ class MembershipServiceSpec
}
}
"determine group difference" should {
"return difference between two groups" in {
val groupChange = Seq(okGroupChange, dummyGroupChangeUpdate, okGroupChange.copy(changeType = GroupChangeType.Delete))
val result: Seq[String] = rightResultOf(underTest.determineGroupDifference(groupChange).value)
// Newly created group's change message
result(0) shouldBe "Group Created."
// Updated group's change message
result(1) shouldBe "Group name changed to 'dummy-group'. Group email changed to 'dummy@test.com'. Group description changed to 'dummy group'. Group admin/s with userId/s (12345-abcde-6789,56789-edcba-1234) added. Group admin/s with userId/s (ok) removed. Group member/s with userId/s (12345-abcde-6789,56789-edcba-1234) added. Group member/s with userId/s (ok) removed."
// Deleted group's change message
result(2) shouldBe "Group Deleted."
}
}
"listAdmins" should {
"return a list of admins" in {
val testGroup =

View File

@ -96,5 +96,16 @@ class MembershipValidationsSpec
}
}
"isGroupChangePresent" should {
"return true when there is a group change present for the requested group change id" in {
isGroupChangePresent(Some(okGroupChange)) should be(right)
}
"return an error when there is a group change present for the requested group change id" in {
val error = leftValue(isGroupChangePresent(None))
error shouldBe an[InvalidGroupRequestError]
}
}
}
}

View File

@ -103,4 +103,15 @@ class RecordSetChangeSpec extends AnyWordSpec with Matchers {
result.systemMessage shouldBe None
}
}
"for Already exists" should {
"set the system message when provided" in {
val result = pendingCreateAAAA.successful
result.systemMessage shouldBe None
}
"set the system message to none when not provided" in {
val result = pendingCreateAAAA.successful
result.systemMessage shouldBe None
}
}
}

View File

@ -24,6 +24,7 @@ import org.scalatestplus.mockito.MockitoSugar
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import org.scalatest.BeforeAndAfterEach
import vinyldns.api.config.{ZoneAuthConfigs, DottedHostsConfig}
import vinyldns.api.{ResultHelpers, VinylDNSTestHelpers}
import vinyldns.api.domain.access.AccessValidations
import vinyldns.api.domain.record.RecordSetHelpers._
@ -83,6 +84,7 @@ class RecordSetServiceSpec
mockBackendResolver,
false,
VinylDNSTestHelpers.highValueDomainConfig,
VinylDNSTestHelpers.dottedHostsConfig,
VinylDNSTestHelpers.approvedNameServers,
true
)
@ -101,10 +103,57 @@ class RecordSetServiceSpec
mockBackendResolver,
true,
VinylDNSTestHelpers.highValueDomainConfig,
VinylDNSTestHelpers.dottedHostsConfig,
VinylDNSTestHelpers.approvedNameServers,
true
)
val underTestWithEmptyDottedHostsConfig = new RecordSetService(
mockZoneRepo,
mockGroupRepo,
mockRecordRepo,
mockRecordDataRepo,
mockRecordChangeRepo,
mockUserRepo,
mockMessageQueue,
new AccessValidations(
sharedApprovedTypes = VinylDNSTestHelpers.sharedApprovedTypes
),
mockBackendResolver,
true,
VinylDNSTestHelpers.highValueDomainConfig,
VinylDNSTestHelpers.emptyDottedHostsConfig,
VinylDNSTestHelpers.approvedNameServers,
true
)
def getDottedHostsConfigGroupsAllowed(zone: Zone, config: DottedHostsConfig): List[String] = {
val configZones = config.zoneAuthConfigs.map(x => x.zone)
val zoneName = if(zone.name.takeRight(1) != ".") zone.name + "." else zone.name
val dottedZoneConfig = configZones.filter(_.contains("*")).map(_.replace("*", "[A-Za-z.]*"))
val isContainWildcardZone = dottedZoneConfig.exists(x => zoneName.substring(0, zoneName.length - 1).matches(x))
val isContainNormalZone = configZones.contains(zoneName)
val groups = if (isContainWildcardZone || isContainNormalZone) {
config.zoneAuthConfigs.flatMap {
x: ZoneAuthConfigs =>
if (x.zone.contains("*")) {
val wildcardZone = x.zone.replace("*", "[A-Za-z.]*")
if (zoneName.substring(0, zoneName.length - 1).matches(wildcardZone)) x.groupList else List.empty
} else {
if (x.zone == zoneName) x.groupList else List.empty
}
}
}
else {
List.empty
}
groups
}
val dottedHostsConfigZonesAllowed: List[String] = VinylDNSTestHelpers.dottedHostsConfig.zoneAuthConfigs.map(x => x.zone)
val dottedHostsConfigGroupsAllowed: List[String] = getDottedHostsConfigGroupsAllowed(okZone, VinylDNSTestHelpers.dottedHostsConfig)
"addRecordSet" should {
"return the recordSet change as the result" in {
val record = aaaa.copy(zoneId = okZone.id)
@ -115,6 +164,27 @@ class RecordSetServiceSpec
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByName(okZone.id, record.name)
doReturn(IO.pure(Set(dottedZone, abcZone, xyzZone, dotZone)))
.when(mockZoneRepo)
.getZonesByNames(dottedHostsConfigZonesAllowed.toSet)
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(Set.empty)
doReturn(IO.pure(None))
.when(mockZoneRepo)
.getZoneByName(record.name + "." + okZone.name)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByFQDNs(Set(record.name + "." + okZone.name))
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(Set.empty)
doReturn(IO.pure(Set()))
.when(mockGroupRepo)
.getGroupsByName(dottedHostsConfigGroupsAllowed.toSet)
doReturn(IO.pure(ListUsersResults(Seq(), None)))
.when(mockUserRepo)
.getUsers(Set.empty, None, None)
val result: RecordSetChange =
rightResultOf(
@ -132,7 +202,6 @@ class RecordSetServiceSpec
val result = leftResultOf(underTest.getRecordSetByZone(aaaa.id, mockZone.id, okAuth).value)
result shouldBe a[ZoneNotFoundError]
}
"fail when the account is not authorized" in {
doReturn(IO.pure(Some(aaaa)))
.when(mockRecordRepo)
@ -155,7 +224,7 @@ class RecordSetServiceSpec
val result = leftResultOf(underTest.addRecordSet(aaaa, okAuth).value)
result shouldBe a[RecordSetAlreadyExists]
}
"fail if the record is dotted" in {
"fail if the record is dotted and does not satisfy properties in dotted hosts config" in {
val record =
aaaa.copy(name = "new.name", zoneId = okZone.id, status = RecordSetStatus.Active)
@ -165,10 +234,66 @@ class RecordSetServiceSpec
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByName(okZone.id, record.name)
doReturn(IO.pure(Set(dottedZone, abcZone, xyzZone, dotZone)))
.when(mockZoneRepo)
.getZonesByNames(dottedHostsConfigZonesAllowed.toSet)
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(Set.empty)
doReturn(IO.pure(None))
.when(mockZoneRepo)
.getZoneByName(record.name + "." + okZone.name)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByFQDNs(Set(record.name + "." + okZone.name))
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(record.name.split('.').map(x => x + "." + okZone.name).toSet)
doReturn(IO.pure(Set()))
.when(mockGroupRepo)
.getGroupsByName(dottedHostsConfigGroupsAllowed.toSet)
doReturn(IO.pure(ListUsersResults(Seq(), None)))
.when(mockUserRepo)
.getUsers(Set.empty, None, None)
val result = leftResultOf(underTest.addRecordSet(record, okAuth).value)
result shouldBe an[InvalidRequest]
}
"fail if the record is dotted and dotted hosts config is empty" in {
val record =
aaaa.copy(name = "new.name", zoneId = okZone.id, status = RecordSetStatus.Active)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSets(okZone.id, record.name, record.typ)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByName(okZone.id, record.name)
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByNames(Set.empty)
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(Set.empty)
doReturn(IO.pure(None))
.when(mockZoneRepo)
.getZoneByName(record.name + "." + okZone.name)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByFQDNs(Set(record.name + "." + okZone.name))
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(record.name.split('.').map(x => x + "." + okZone.name).toSet)
doReturn(IO.pure(Set()))
.when(mockGroupRepo)
.getGroupsByName(dottedHostsConfigGroupsAllowed.toSet)
doReturn(IO.pure(ListUsersResults(Seq(), None)))
.when(mockUserRepo)
.getUsers(Set.empty, None, None)
val result = leftResultOf(underTestWithEmptyDottedHostsConfig.addRecordSet(record, okAuth).value)
result shouldBe an[InvalidRequest]
}
"fail if the record is relative with trailing dot" in {
val record =
aaaa.copy(name = "new.", zoneId = okZone.id, status = RecordSetStatus.Active)
@ -179,6 +304,27 @@ class RecordSetServiceSpec
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByName(okZone.id, record.name)
doReturn(IO.pure(Set(dottedZone, abcZone, xyzZone, dotZone)))
.when(mockZoneRepo)
.getZonesByNames(dottedHostsConfigZonesAllowed.toSet)
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(Set.empty)
doReturn(IO.pure(None))
.when(mockZoneRepo)
.getZoneByName(record.name + "." + okZone.name)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByFQDNs(Set(record.name + "." + okZone.name))
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(record.name.split('.').map(x => x + "." + okZone.name).toSet)
doReturn(IO.pure(Set()))
.when(mockGroupRepo)
.getGroupsByName(dottedHostsConfigGroupsAllowed.toSet)
doReturn(IO.pure(ListUsersResults(Seq(), None)))
.when(mockUserRepo)
.getUsers(Set.empty, None, None)
val result =
leftResultOf(underTestWithDnsBackendValidations.addRecordSet(record, okAuth).value)
@ -204,6 +350,27 @@ class RecordSetServiceSpec
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByName(okZone.id, record.name)
doReturn(IO.pure(Set(dottedZone, abcZone, xyzZone, dotZone)))
.when(mockZoneRepo)
.getZonesByNames(dottedHostsConfigZonesAllowed.toSet)
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(Set.empty)
doReturn(IO.pure(None))
.when(mockZoneRepo)
.getZoneByName(record.name)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByFQDNs(Set(record.name))
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(record.name.split('.').map(x => x + "." + okZone.name).toSet)
doReturn(IO.pure(Set()))
.when(mockGroupRepo)
.getGroupsByName(dottedHostsConfigGroupsAllowed.toSet)
doReturn(IO.pure(ListUsersResults(Seq(), None)))
.when(mockUserRepo)
.getUsers(Set.empty, None, None)
val result: RecordSetChange = rightResultOf(
underTest.addRecordSet(record, okAuth).map(_.asInstanceOf[RecordSetChange]).value
@ -222,6 +389,27 @@ class RecordSetServiceSpec
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByName(okZone.id, record.name)
doReturn(IO.pure(Set(dottedZone, abcZone, xyzZone, dotZone)))
.when(mockZoneRepo)
.getZonesByNames(dottedHostsConfigZonesAllowed.toSet)
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(Set.empty)
doReturn(IO.pure(None))
.when(mockZoneRepo)
.getZoneByName(record.name)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByFQDNs(Set(record.name))
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(record.name.split('.').map(x => x + "." + okZone.name).toSet)
doReturn(IO.pure(Set()))
.when(mockGroupRepo)
.getGroupsByName(dottedHostsConfigGroupsAllowed.toSet)
doReturn(IO.pure(ListUsersResults(Seq(), None)))
.when(mockUserRepo)
.getUsers(Set.empty, None, None)
val result: RecordSetChange = rightResultOf(
underTest.addRecordSet(record, okAuth).map(_.asInstanceOf[RecordSetChange]).value
@ -259,6 +447,27 @@ class RecordSetServiceSpec
doReturn(IO.pure(Some(okGroup)))
.when(mockGroupRepo)
.getGroup(okGroup.id)
doReturn(IO.pure(Set(dottedZone, abcZone, xyzZone, dotZone)))
.when(mockZoneRepo)
.getZonesByNames(dottedHostsConfigZonesAllowed.toSet)
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(Set.empty)
doReturn(IO.pure(None))
.when(mockZoneRepo)
.getZoneByName(record.name + "." + okZone.name)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByFQDNs(Set(record.name + "." + okZone.name))
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(Set.empty)
doReturn(IO.pure(Set()))
.when(mockGroupRepo)
.getGroupsByName(dottedHostsConfigGroupsAllowed.toSet)
doReturn(IO.pure(ListUsersResults(Seq(), None)))
.when(mockUserRepo)
.getUsers(Set.empty, None, None)
val result: RecordSetChange = rightResultOf(
underTest.addRecordSet(record, okAuth).map(_.asInstanceOf[RecordSetChange]).value
@ -312,6 +521,27 @@ class RecordSetServiceSpec
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByName(okZone.id, record.name)
doReturn(IO.pure(Set(dottedZone, abcZone, xyzZone, dotZone)))
.when(mockZoneRepo)
.getZonesByNames(dottedHostsConfigZonesAllowed.toSet)
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(Set.empty)
doReturn(IO.pure(None))
.when(mockZoneRepo)
.getZoneByName(record.name + "." + okZone.name)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByFQDNs(Set(record.name + "." + okZone.name))
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(Set.empty)
doReturn(IO.pure(Set()))
.when(mockGroupRepo)
.getGroupsByName(dottedHostsConfigGroupsAllowed.toSet)
doReturn(IO.pure(ListUsersResults(Seq(), None)))
.when(mockUserRepo)
.getUsers(Set.empty, None, None)
val result: RecordSetChange =
rightResultOf(
@ -326,6 +556,296 @@ class RecordSetServiceSpec
result.status shouldBe RecordSetChangeStatus.Pending
}
}
"succeed if the record is dotted and zone, user, record type is in allowed dotted hosts config" in {
val record =
cname.copy(name = "new.name", zoneId = dottedZone.id, status = RecordSetStatus.Active)
val dottedHostsConfigZonesAllowed: List[String] = VinylDNSTestHelpers.dottedHostsConfig.zoneAuthConfigs.map(x => x.zone)
val dottedHostsConfigGroupsAllowed: List[String] = getDottedHostsConfigGroupsAllowed(dottedZone, VinylDNSTestHelpers.dottedHostsConfig)
doReturn(IO.pure(Some(dottedZone))).when(mockZoneRepo).getZone(dottedZone.id)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSets(dottedZone.id, record.name, record.typ)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByName(dottedZone.id, record.name)
doReturn(IO.pure(Set(dottedZone, abcZone, xyzZone, dotZone)))
.when(mockZoneRepo)
.getZonesByNames(dottedHostsConfigZonesAllowed.toSet)
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(Set.empty)
doReturn(IO.pure(None))
.when(mockZoneRepo)
.getZoneByName(record.name + "." + dottedZone.name)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByFQDNs(Set(record.name + "." + dottedZone.name))
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(record.name.split('.').map(x => x + "." + dottedZone.name).toSet)
doReturn(IO.pure(Set(dummyGroup)))
.when(mockGroupRepo)
.getGroupsByName(dottedHostsConfigGroupsAllowed.toSet)
doReturn(IO.pure(ListUsersResults(listOfDummyUsers.toSeq, None)))
.when(mockUserRepo)
.getUsers(dummyGroup.memberIds, None, None)
// passes as all three properties within dotted hosts config (allowed zones, users and record types) are satisfied
val result: RecordSetChange = rightResultOf(
underTest.addRecordSet(record, xyzAuth).map(_.asInstanceOf[RecordSetChange]).value
)
result.recordSet.name shouldBe record.name
}
"succeed if the record is dotted and zone, user in group, record type is in allowed dotted hosts config" in {
val record =
cname.copy(name = "new.name", zoneId = xyzZone.id, status = RecordSetStatus.Active)
val dottedHostsConfigZonesAllowed: List[String] = VinylDNSTestHelpers.dottedHostsConfig.zoneAuthConfigs.map(x => x.zone)
val dottedHostsConfigGroupsAllowed: List[String] = getDottedHostsConfigGroupsAllowed(xyzZone, VinylDNSTestHelpers.dottedHostsConfig)
doReturn(IO.pure(Some(xyzZone))).when(mockZoneRepo).getZone(xyzZone.id)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSets(xyzZone.id, record.name, record.typ)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByName(xyzZone.id, record.name)
doReturn(IO.pure(Set(xyzZone, abcZone, xyzZone)))
.when(mockZoneRepo)
.getZonesByNames(dottedHostsConfigZonesAllowed.toSet)
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(Set.empty)
doReturn(IO.pure(None))
.when(mockZoneRepo)
.getZoneByName(record.name + "." + xyzZone.name)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByFQDNs(Set(record.name + "." + xyzZone.name))
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(record.name.split('.').map(x => x + "." + xyzZone.name).toSet)
doReturn(IO.pure(Set(xyzGroup)))
.when(mockGroupRepo)
.getGroupsByName(dottedHostsConfigGroupsAllowed.toSet)
doReturn(IO.pure(ListUsersResults(Seq(xyzUser), None)))
.when(mockUserRepo)
.getUsers(xyzGroup.memberIds, None, None)
// passes as all three properties within dotted hosts config (allowed zones, users and record types) are satisfied
val result: RecordSetChange = rightResultOf(
underTest.addRecordSet(record, xyzAuth).map(_.asInstanceOf[RecordSetChange]).value
)
result.recordSet.name shouldBe record.name
}
"fail if the record is dotted and zone, user in group, record type is allowed but record name has dot in the end and is not an apex record" in {
val record =
cname.copy(name = "new.name.", zoneId = xyzZone.id, status = RecordSetStatus.Active)
val dottedHostsConfigZonesAllowed: List[String] = VinylDNSTestHelpers.dottedHostsConfig.zoneAuthConfigs.map(x => x.zone)
val dottedHostsConfigGroupsAllowed: List[String] = getDottedHostsConfigGroupsAllowed(xyzZone, VinylDNSTestHelpers.dottedHostsConfig)
doReturn(IO.pure(Some(xyzZone))).when(mockZoneRepo).getZone(xyzZone.id)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSets(xyzZone.id, record.name, record.typ)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByName(xyzZone.id, record.name)
doReturn(IO.pure(Set(xyzZone, abcZone, xyzZone)))
.when(mockZoneRepo)
.getZonesByNames(dottedHostsConfigZonesAllowed.toSet)
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(Set.empty)
doReturn(IO.pure(None))
.when(mockZoneRepo)
.getZoneByName(record.name + "." + xyzZone.name)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByFQDNs(Set(record.name + "." + xyzZone.name))
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(record.name.split('.').map(x => x + "." + xyzZone.name).toSet)
doReturn(IO.pure(Set(xyzGroup)))
.when(mockGroupRepo)
.getGroupsByName(dottedHostsConfigGroupsAllowed.toSet)
doReturn(IO.pure(ListUsersResults(Seq(xyzUser), None)))
.when(mockUserRepo)
.getUsers(xyzGroup.memberIds, None, None)
// fails as dotted host record name has dot at the end and is not an apex record
val result = leftResultOf(underTest.addRecordSet(record, xyzAuth).value)
result shouldBe an[InvalidRequest]
}
"fail if the record is dotted and zone, user, record type is allowed but number of dots allowed in config is 0" in {
val record =
cname.copy(name = "new.name", zoneId = dotZone.id, status = RecordSetStatus.Active)
val dottedHostsConfigZonesAllowed: List[String] = VinylDNSTestHelpers.dottedHostsConfig.zoneAuthConfigs.map(x => x.zone)
val dottedHostsConfigGroupsAllowed: List[String] = getDottedHostsConfigGroupsAllowed(dottedZone, VinylDNSTestHelpers.dottedHostsConfig)
doReturn(IO.pure(Some(dotZone))).when(mockZoneRepo).getZone(dotZone.id)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSets(dotZone.id, record.name, record.typ)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByName(dotZone.id, record.name)
doReturn(IO.pure(Set(dottedZone, abcZone, xyzZone, dotZone)))
.when(mockZoneRepo)
.getZonesByNames(dottedHostsConfigZonesAllowed.toSet)
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(Set.empty)
doReturn(IO.pure(None))
.when(mockZoneRepo)
.getZoneByName(record.name + "." + dotZone.name)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByFQDNs(Set(record.name + "." + dotZone.name))
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(record.name.split('.').map(x => x + "." + dotZone.name).toSet)
doReturn(IO.pure(Set(dummyGroup)))
.when(mockGroupRepo)
.getGroupsByName(dottedHostsConfigGroupsAllowed.toSet)
doReturn(IO.pure(ListUsersResults(listOfDummyUsers.toSeq, None)))
.when(mockUserRepo)
.getUsers(dummyGroup.memberIds, None, None)
// fails as no.of.dots allowed for the zone in the config is 0
val result = leftResultOf(underTest.addRecordSet(record, xyzAuth).value)
result shouldBe an[InvalidRequest]
}
"fail if the record is dotted and user, record type is in allowed dotted hosts config except zone" in {
val record =
cname.copy(name = "new.name", zoneId = okZone.id, status = RecordSetStatus.Active)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSets(okZone.id, record.name, record.typ)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByName(okZone.id, record.name)
doReturn(IO.pure(Set(dottedZone, abcZone, xyzZone, dotZone)))
.when(mockZoneRepo)
.getZonesByNames(dottedHostsConfigZonesAllowed.toSet)
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(Set.empty)
doReturn(IO.pure(None))
.when(mockZoneRepo)
.getZoneByName(record.name + "." + okZone.name)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByFQDNs(Set(record.name + "." + okZone.name))
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(record.name.split('.').map(x => x + "." + okZone.name).toSet)
doReturn(IO.pure(Set()))
.when(mockGroupRepo)
.getGroupsByName(dottedHostsConfigGroupsAllowed.toSet)
doReturn(IO.pure(ListUsersResults(Seq(), None)))
.when(mockUserRepo)
.getUsers(Set.empty, None, None)
// fails as only two properties within dotted hosts config (users and record types) are satisfied while zone is not allowed
val result = leftResultOf(underTest.addRecordSet(record, okAuth).value)
result shouldBe an[InvalidRequest]
}
"fail if the record is dotted and zone, record type is in allowed dotted hosts config except user" in {
val record =
cname.copy(name = "new.name", zoneId = abcZone.id, status = RecordSetStatus.Active)
val dottedHostsConfigZonesAllowed: List[String] = VinylDNSTestHelpers.dottedHostsConfig.zoneAuthConfigs.map(x => x.zone)
val dottedHostsConfigGroupsAllowed: List[String] = getDottedHostsConfigGroupsAllowed(abcZone, VinylDNSTestHelpers.dottedHostsConfig)
doReturn(IO.pure(Some(abcZone))).when(mockZoneRepo).getZone(abcZone.id)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSets(abcZone.id, record.name, record.typ)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByName(abcZone.id, record.name)
doReturn(IO.pure(Set(abcZone, dottedZone, xyzZone)))
.when(mockZoneRepo)
.getZonesByNames(dottedHostsConfigZonesAllowed.toSet)
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(Set.empty)
doReturn(IO.pure(None))
.when(mockZoneRepo)
.getZoneByName(record.name + "." + abcZone.name)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByFQDNs(Set(record.name + "." + abcZone.name))
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(record.name.split('.').map(x => x + "." + abcZone.name).toSet)
doReturn(IO.pure(Set(dummyGroup)))
.when(mockGroupRepo)
.getGroupsByName(dottedHostsConfigGroupsAllowed.toSet)
doReturn(IO.pure(ListUsersResults(listOfDummyUsers.toSeq, None)))
.when(mockUserRepo)
.getUsers(dummyGroup.memberIds, None, None)
// fails as only two properties within dotted hosts config (zones and record types) are satisfied while user is not allowed
val result = leftResultOf(underTest.addRecordSet(record, abcAuth).value)
result shouldBe an[InvalidRequest]
}
"fail if the record is dotted and zone, user is in allowed dotted hosts config except record type" in {
val record =
aaaa.copy(name = "new.name", zoneId = dottedZone.id, status = RecordSetStatus.Active)
val dottedHostsConfigZonesAllowed: List[String] = VinylDNSTestHelpers.dottedHostsConfig.zoneAuthConfigs.map {
case y:ZoneAuthConfigs => y.zone
}
val dottedHostsConfigGroupsAllowed: List[String] = getDottedHostsConfigGroupsAllowed(dottedZone, VinylDNSTestHelpers.dottedHostsConfig)
doReturn(IO.pure(Some(dottedZone))).when(mockZoneRepo).getZone(dottedZone.id)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSets(dottedZone.id, record.name, record.typ)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByName(dottedZone.id, record.name)
doReturn(IO.pure(Set(dottedZone, abcZone, xyzZone, dotZone)))
.when(mockZoneRepo)
.getZonesByNames(dottedHostsConfigZonesAllowed.toSet)
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(Set.empty)
doReturn(IO.pure(None))
.when(mockZoneRepo)
.getZoneByName(record.name + "." + dottedZone.name)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByFQDNs(Set(record.name + "." + dottedZone.name))
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(record.name.split('.').map(x => x + "." + dottedZone.name).toSet)
doReturn(IO.pure(Set(dummyGroup)))
.when(mockGroupRepo)
.getGroupsByName(dottedHostsConfigGroupsAllowed.toSet)
doReturn(IO.pure(ListUsersResults(listOfDummyUsers.toSeq, None)))
.when(mockUserRepo)
.getUsers(dummyGroup.memberIds, None, None)
// fails as only two properties within dotted hosts config (zone and user) are satisfied while record type is not allowed
val result = leftResultOf(underTest.addRecordSet(record, xyzAuth).value)
result shouldBe an[InvalidRequest]
}
"updateRecordSet" should {
"return the recordSet change as the result" in {
@ -341,6 +861,27 @@ class RecordSetServiceSpec
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByName(okZone.id, newRecord.name)
doReturn(IO.pure(Set(dottedZone, abcZone, xyzZone, dotZone)))
.when(mockZoneRepo)
.getZonesByNames(dottedHostsConfigZonesAllowed.toSet)
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(Set.empty)
doReturn(IO.pure(None))
.when(mockZoneRepo)
.getZoneByName(newRecord.name + "." + okZone.name)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByFQDNs(Set(newRecord.name + "." + okZone.name))
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(Set.empty)
doReturn(IO.pure(Set()))
.when(mockGroupRepo)
.getGroupsByName(dottedHostsConfigGroupsAllowed.toSet)
doReturn(IO.pure(ListUsersResults(Seq(), None)))
.when(mockUserRepo)
.getUsers(Set.empty, None, None)
val result: RecordSetChange = rightResultOf(
underTest.updateRecordSet(newRecord, okAuth).map(_.asInstanceOf[RecordSetChange]).value
@ -377,6 +918,27 @@ class RecordSetServiceSpec
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByName(okZone.id, newRecord.name)
doReturn(IO.pure(Set(dottedZone, abcZone, xyzZone, dotZone)))
.when(mockZoneRepo)
.getZonesByNames(dottedHostsConfigZonesAllowed.toSet)
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(Set.empty)
doReturn(IO.pure(None))
.when(mockZoneRepo)
.getZoneByName(newRecord.name + "." + okZone.name)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByFQDNs(Set(newRecord.name + "." + okZone.name))
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(newRecord.name.split('.').map(x => x + "." + okZone.name).toSet)
doReturn(IO.pure(Set()))
.when(mockGroupRepo)
.getGroupsByName(dottedHostsConfigGroupsAllowed.toSet)
doReturn(IO.pure(ListUsersResults(Seq(), None)))
.when(mockUserRepo)
.getUsers(Set.empty, None, None)
val result: RecordSetChange = rightResultOf(
underTest.updateRecordSet(newRecord, okAuth).map(_.asInstanceOf[RecordSetChange]).value
@ -416,6 +978,27 @@ class RecordSetServiceSpec
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByName(okZone.id, newRecord.name)
doReturn(IO.pure(Set(dottedZone, abcZone, xyzZone, dotZone)))
.when(mockZoneRepo)
.getZonesByNames(dottedHostsConfigZonesAllowed.toSet)
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(Set.empty)
doReturn(IO.pure(None))
.when(mockZoneRepo)
.getZoneByName(newRecord.name)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByFQDNs(Set(newRecord.name))
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(newRecord.name.split('.').map(x => x + "." + okZone.name).toSet)
doReturn(IO.pure(Set()))
.when(mockGroupRepo)
.getGroupsByName(dottedHostsConfigGroupsAllowed.toSet)
doReturn(IO.pure(ListUsersResults(Seq(), None)))
.when(mockUserRepo)
.getUsers(Set.empty, None, None)
val result: RecordSetChange = rightResultOf(
underTest.updateRecordSet(newRecord, okAuth).map(_.asInstanceOf[RecordSetChange]).value
@ -438,6 +1021,27 @@ class RecordSetServiceSpec
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByName(okZone.id, newRecord.name)
doReturn(IO.pure(Set(dottedZone, abcZone, xyzZone, dotZone)))
.when(mockZoneRepo)
.getZonesByNames(dottedHostsConfigZonesAllowed.toSet)
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(Set.empty)
doReturn(IO.pure(None))
.when(mockZoneRepo)
.getZoneByName(newRecord.name)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByFQDNs(Set(newRecord.name))
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(newRecord.name.split('.').map(x => x + "." + okZone.name).toSet)
doReturn(IO.pure(Set()))
.when(mockGroupRepo)
.getGroupsByName(dottedHostsConfigGroupsAllowed.toSet)
doReturn(IO.pure(ListUsersResults(Seq(), None)))
.when(mockUserRepo)
.getUsers(Set.empty, None, None)
val result: RecordSetChange = rightResultOf(
underTest.updateRecordSet(newRecord, okAuth).map(_.asInstanceOf[RecordSetChange]).value
@ -460,6 +1064,27 @@ class RecordSetServiceSpec
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByName(okZone.id, newRecord.name)
doReturn(IO.pure(Set(dottedZone, abcZone, xyzZone, dotZone)))
.when(mockZoneRepo)
.getZonesByNames(dottedHostsConfigZonesAllowed.toSet)
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(Set.empty)
doReturn(IO.pure(None))
.when(mockZoneRepo)
.getZoneByName(newRecord.name)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByFQDNs(Set(newRecord.name))
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(newRecord.name.split('.').map(x => x + "." + okZone.name).toSet)
doReturn(IO.pure(Set()))
.when(mockGroupRepo)
.getGroupsByName(dottedHostsConfigGroupsAllowed.toSet)
doReturn(IO.pure(ListUsersResults(Seq(), None)))
.when(mockUserRepo)
.getUsers(Set.empty, None, None)
val result: RecordSetChange = rightResultOf(
underTest.updateRecordSet(newRecord, okAuth).map(_.asInstanceOf[RecordSetChange]).value
@ -595,6 +1220,27 @@ class RecordSetServiceSpec
doReturn(IO.pure(Some(oneUserDummyGroup)))
.when(mockGroupRepo)
.getGroup(oneUserDummyGroup.id)
doReturn(IO.pure(Set(dottedZone, abcZone, xyzZone, dotZone)))
.when(mockZoneRepo)
.getZonesByNames(dottedHostsConfigZonesAllowed.toSet)
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(Set.empty)
doReturn(IO.pure(None))
.when(mockZoneRepo)
.getZoneByName(newRecord.name + "." + okZone.name)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByFQDNs(Set(newRecord.name + "." + okZone.name))
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(Set.empty)
doReturn(IO.pure(Set()))
.when(mockGroupRepo)
.getGroupsByName(dottedHostsConfigGroupsAllowed.toSet)
doReturn(IO.pure(ListUsersResults(Seq(), None)))
.when(mockUserRepo)
.getUsers(Set.empty, None, None)
val result = rightResultOf(
underTest.updateRecordSet(newRecord, auth).map(_.asInstanceOf[RecordSetChange]).value
@ -624,6 +1270,27 @@ class RecordSetServiceSpec
doReturn(IO.pure(List(oldRecord)))
.when(mockRecordRepo)
.getRecordSetsByName(zone.id, newRecord.name)
doReturn(IO.pure(Set(dottedZone, abcZone, xyzZone)))
.when(mockZoneRepo)
.getZonesByNames(dottedHostsConfigZonesAllowed.toSet)
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(Set.empty)
doReturn(IO.pure(None))
.when(mockZoneRepo)
.getZoneByName(newRecord.name + "." + okZone.name)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByFQDNs(Set(newRecord.name + "." + okZone.name))
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(Set.empty)
doReturn(IO.pure(Set()))
.when(mockGroupRepo)
.getGroupsByName(dottedHostsConfigGroupsAllowed.toSet)
doReturn(IO.pure(ListUsersResults(Seq(), None)))
.when(mockUserRepo)
.getUsers(Set.empty, None, None)
val result = rightResultOf(
underTest.updateRecordSet(newRecord, auth).map(_.asInstanceOf[RecordSetChange]).value

View File

@ -45,6 +45,8 @@ class RecordSetValidationsSpec
import RecordSetValidations._
val dottedHostsConfigZonesAllowed: List[String] = VinylDNSTestHelpers.dottedHostsConfig.zoneAuthConfigs.map(x => x.zone)
"RecordSetValidations" should {
"validRecordTypes" should {
"return invalid request when adding a PTR record to a forward zone" in {
@ -185,24 +187,75 @@ class RecordSetValidationsSpec
}
}
"isDotted" should {
"return a failure for any record with dotted hosts if it is already present" in {
val test = aaaa.copy(name = "this.is.a.failure.")
leftValue(isDotted(test, okZone, None, false, true)) shouldBe an[InvalidRequest]
}
"return a failure for any record that is a dotted host if user or record type is not allowed" in {
val test = aaaa.copy(name = "this.is.a.failure." + okZone.name)
leftValue(isDotted(test, okZone, None, true, false)) shouldBe an[InvalidRequest]
}
"return success for a dotted record if it does not already have a record or zone with same name and user is allowed" in {
val test = aaaa.copy(name = "this.passes")
isDotted(test, okZone, None, true, true) should be(right)
}
"return success for a new record that has the same name as the existing record" in {
val newRecord = aaaa.copy(name = "dot.ted")
val existingRecord = newRecord.copy(ttl = 330)
isDotted(newRecord, okZone, Some(existingRecord), true, true) should be(right)
}
}
"typeSpecificValidations" should {
"Run dotted hosts checks" should {
val dottedARecord = rsOk.copy(name = "this.is.a.failure.")
"return a failure for any new record with dotted hosts in forward zones" in {
leftValue(
typeSpecificValidations(dottedARecord, List(), okZone, None, Nil)
typeSpecificValidations(dottedARecord, List(), okZone, None, Nil, true, dottedHostsConfigZonesAllowed.toSet, false)
) shouldBe an[InvalidRequest]
}
"return a failure for any new record with dotted hosts in forward zones (CNAME)" in {
leftValue(
typeSpecificValidations(dottedARecord.copy(typ = CNAME), List(), okZone, None, Nil)
typeSpecificValidations(dottedARecord.copy(typ = CNAME), List(), okZone, None, Nil, true, dottedHostsConfigZonesAllowed.toSet, false)
) shouldBe an[InvalidRequest]
}
"return a failure for any new record with dotted hosts in forward zones (NS)" in {
leftValue(
typeSpecificValidations(dottedARecord.copy(typ = NS), List(), okZone, None, Nil)
typeSpecificValidations(dottedARecord.copy(typ = NS), List(), okZone, None, Nil, true, dottedHostsConfigZonesAllowed.toSet, false)
) shouldBe an[InvalidRequest]
}
"return a success for any new record with dotted hosts in forward zones if it satisfies dotted hosts configs" in {
// Zone, User, Record Type and Number of dots are all satisfied
val record = typeSpecificValidations(dottedARecord.copy(typ = CNAME, zoneId = dottedZone.id), List(), dottedZone, None, Nil, true, dottedHostsConfigZonesAllowed.toSet, true, 5)
record should be(right)
}
"return a failure for any new record with dotted hosts if no.of.dots allowed is 0" in {
// Zone, User, Record Type and Number of dots are all satisfied
leftValue(
typeSpecificValidations(dottedARecord.copy(typ = CNAME, zoneId = dottedZone.id), List(), dottedZone, None, Nil, true, dottedHostsConfigZonesAllowed.toSet, true, 0)
) shouldBe an[InvalidRequest]
}
"return a failure for any new record with dotted hosts in forward zones (A record) if it doesn't satisfy dotted hosts configs" in {
// 'A' record is not allowed in the config
leftValue(
typeSpecificValidations(dottedARecord.copy(zoneId = dottedZone.id), List(), dottedZone, None, Nil, true, dottedHostsConfigZonesAllowed.toSet, false, 5)
) shouldBe an[InvalidRequest]
}
"return a failure for any new record with dotted hosts in forward zones (NS record) if it doesn't satisfy dotted hosts configs" in {
// 'NS' record is not allowed in the config
leftValue(
typeSpecificValidations(dottedARecord.copy(typ = NS, zoneId = dottedZone.id), List(), dottedZone, None, Nil, true, dottedHostsConfigZonesAllowed.toSet, false, 5)
) shouldBe an[InvalidRequest]
}
@ -212,7 +265,10 @@ class RecordSetValidationsSpec
List(),
okZone,
Some(dottedARecord.copy(ttl = 300)),
Nil
Nil,
true,
dottedHostsConfigZonesAllowed.toSet,
false
) should be(right)
}
@ -223,7 +279,10 @@ class RecordSetValidationsSpec
List(),
okZone,
Some(dottedCNAMERecord.copy(ttl = 300)),
Nil
Nil,
true,
dottedHostsConfigZonesAllowed.toSet,
false
) should be(right)
}
@ -235,7 +294,10 @@ class RecordSetValidationsSpec
List(),
okZone,
Some(dottedNSRecord.copy(ttl = 300)),
Nil
Nil,
true,
dottedHostsConfigZonesAllowed.toSet,
false
)
) shouldBe an[InvalidRequest]
}
@ -246,35 +308,35 @@ class RecordSetValidationsSpec
val test = srv.copy(name = "_sip._tcp.example.com.")
val zone = okZone.copy(name = "example.com.")
typeSpecificValidations(test, List(), zone, None, Nil) should be(right)
typeSpecificValidations(test, List(), zone, None, Nil, true, dottedHostsConfigZonesAllowed.toSet, false) should be(right)
}
"return success for an SRV record following convention without FQDN" in {
val test = srv.copy(name = "_sip._tcp")
val zone = okZone.copy(name = "example.com.")
typeSpecificValidations(test, List(), zone, None, Nil) should be(right)
typeSpecificValidations(test, List(), zone, None, Nil, true, dottedHostsConfigZonesAllowed.toSet, false) should be(right)
}
"return success for an SRV record following convention with a record name" in {
val test = srv.copy(name = "_sip._tcp.foo.")
val zone = okZone.copy(name = "example.com.")
typeSpecificValidations(test, List(), zone, None, Nil) should be(right)
typeSpecificValidations(test, List(), zone, None, Nil, true, dottedHostsConfigZonesAllowed.toSet, false) should be(right)
}
"return success on a wildcard SRV that follows convention" in {
val test = srv.copy(name = "*._tcp.example.com.")
val zone = okZone.copy(name = "example.com.")
typeSpecificValidations(test, List(), zone, None, Nil) should be(right)
typeSpecificValidations(test, List(), zone, None, Nil, true, dottedHostsConfigZonesAllowed.toSet, false) should be(right)
}
"return success on a wildcard in second position SRV that follows convention" in {
val test = srv.copy(name = "_sip._*.example.com.")
val zone = okZone.copy(name = "example.com.")
typeSpecificValidations(test, List(), zone, None, Nil) should be(right)
typeSpecificValidations(test, List(), zone, None, Nil, true, dottedHostsConfigZonesAllowed.toSet, false) should be(right)
}
}
"Skip dotted checks on NAPTR" should {
@ -282,21 +344,21 @@ class RecordSetValidationsSpec
val test = naptr.copy(name = "sub.naptr.example.com.")
val zone = okZone.copy(name = "example.com.")
typeSpecificValidations(test, List(), zone, None, Nil) should be(right)
typeSpecificValidations(test, List(), zone, None, Nil, true, dottedHostsConfigZonesAllowed.toSet, false) should be(right)
}
"return success for an NAPTR record without FQDN" in {
val test = naptr.copy(name = "sub.naptr")
val zone = okZone.copy(name = "example.com.")
typeSpecificValidations(test, List(), zone, None, Nil) should be(right)
typeSpecificValidations(test, List(), zone, None, Nil, true, dottedHostsConfigZonesAllowed.toSet, false) should be(right)
}
"return success on a wildcard NAPTR" in {
val test = naptr.copy(name = "*.sub.naptr.example.com.")
val zone = okZone.copy(name = "example.com.")
typeSpecificValidations(test, List(), zone, None, Nil) should be(right)
typeSpecificValidations(test, List(), zone, None, Nil, true, dottedHostsConfigZonesAllowed.toSet, false) should be(right)
}
}
@ -305,7 +367,7 @@ class RecordSetValidationsSpec
val test = ptrIp4.copy(name = "10.1.2.")
val zone = zoneIp4.copy(name = "198.in-addr.arpa.")
typeSpecificValidations(test, List(), zone, None, Nil) should be(right)
typeSpecificValidations(test, List(), zone, None, Nil, true, dottedHostsConfigZonesAllowed.toSet, false) should be(right)
}
}
"Skip dotted checks on TXT" should {
@ -313,7 +375,7 @@ class RecordSetValidationsSpec
val test = txt.copy(name = "sub.txt.example.com.")
val zone = okZone.copy(name = "example.com.")
typeSpecificValidations(test, List(), zone, None, Nil) should be(right)
typeSpecificValidations(test, List(), zone, None, Nil, true, dottedHostsConfigZonesAllowed.toSet, false) should be(right)
}
}
@ -330,7 +392,7 @@ class RecordSetValidationsSpec
List(SOAData(Fqdn("something"), "other", 1, 2, 3, 5, 6))
)
typeSpecificValidations(test, List(), zoneIp4, None, Nil) should be(right)
typeSpecificValidations(test, List(), zoneIp4, None, Nil, true, dottedHostsConfigZonesAllowed.toSet, false) should be(right)
}
}
}
@ -343,29 +405,29 @@ class RecordSetValidationsSpec
records = List(NSData(Fqdn("some.test.ns.")))
)
nsValidations(valid, okZone, None, List(new Regex(".*"))) should be(right)
nsValidations(valid, okZone, None, List(new Regex(".*")), true, dottedHostsConfigZonesAllowed.toSet, false) should be(right)
}
"return an InvalidRequest if an NS record is '@'" in {
val error = leftValue(nsValidations(invalidNsApexRs, okZone, None, Nil))
val error = leftValue(nsValidations(invalidNsApexRs, okZone, None, Nil, true, dottedHostsConfigZonesAllowed.toSet, false))
error shouldBe an[InvalidRequest]
}
"return an InvalidRequest if an NS record is the same as the zone" in {
val invalid = invalidNsApexRs.copy(name = okZone.name)
val error = leftValue(nsValidations(invalid, okZone, None, Nil))
val error = leftValue(nsValidations(invalid, okZone, None, Nil, true, dottedHostsConfigZonesAllowed.toSet, false))
error shouldBe an[InvalidRequest]
}
"return an InvalidRequest if the NS record being updated is '@'" in {
val valid = invalidNsApexRs.copy(name = "this-is-not-origin-mate")
val error = leftValue(nsValidations(valid, okZone, Some(invalidNsApexRs), Nil))
val error = leftValue(nsValidations(valid, okZone, Some(invalidNsApexRs), Nil, true, dottedHostsConfigZonesAllowed.toSet, false))
error shouldBe an[InvalidRequest]
}
"return an InvalidRequest if an NS record data is not in the approved server list" in {
val ns = invalidNsApexRs.copy(records = List(NSData(Fqdn("not.approved."))))
val error = leftValue(nsValidations(ns, okZone, None, List(new Regex("not.*"))))
val error = leftValue(nsValidations(ns, okZone, None, List(new Regex("not.*")), true, dottedHostsConfigZonesAllowed.toSet, false))
error shouldBe an[InvalidRequest]
}
}
@ -373,25 +435,35 @@ class RecordSetValidationsSpec
"DSValidations" should {
val matchingNs = ns.copy(zoneId = ds.zoneId, name = ds.name, ttl = ds.ttl)
"return ok if the record is non-origin DS with matching NS" in {
dsValidations(ds, List(matchingNs), okZone) should be(right)
dsValidations(ds, List(matchingNs), okZone, true, dottedHostsConfigZonesAllowed.toSet, false) should be(right)
}
"return an InvalidRequest if a DS record is '@'" in {
val apex = ds.copy(name = "@")
val error = leftValue(dsValidations(apex, List(matchingNs), okZone))
val error = leftValue(dsValidations(apex, List(matchingNs), okZone, true, dottedHostsConfigZonesAllowed.toSet, false))
error shouldBe an[InvalidRequest]
}
"return an InvalidRequest if a DS record is the same as the zone" in {
val apex = ds.copy(name = okZone.name)
val error = leftValue(dsValidations(apex, List(matchingNs), okZone))
val error = leftValue(dsValidations(apex, List(matchingNs), okZone, true, dottedHostsConfigZonesAllowed.toSet, false))
error shouldBe an[InvalidRequest]
}
"return an InvalidRequest if there is no NS matching the record" in {
val error = leftValue(dsValidations(ds, List(), okZone))
val error = leftValue(dsValidations(ds, List(), okZone, true, dottedHostsConfigZonesAllowed.toSet, false))
error shouldBe an[InvalidRequest]
}
"return an InvalidRequest if the DS is dotted" in {
val error =
leftValue(dsValidations(ds.copy(name = "test.dotted"), List(matchingNs), okZone))
leftValue(dsValidations(ds.copy(name = "test.dotted"), List(matchingNs), okZone, true, dottedHostsConfigZonesAllowed.toSet, false))
error shouldBe an[InvalidRequest]
}
"return ok if the DS is dotted and zone, user, record type is allowed in dotted hosts config" in {
val record =
dsValidations(ds.copy(name = "dotted.trial", zoneId = dottedZone.id), List(matchingNs), dottedZone, true, dottedHostsConfigZonesAllowed.toSet, true, 5)
record should be(right)
}
"return an InvalidRequest if the DS is dotted and zone, user, record type is allowed in dotted hosts config but has a conflict with existing record or zone" in {
val error =
leftValue(dsValidations(ds.copy(name = "dotted.trial", zoneId = dottedZone.id), List(matchingNs), dottedZone, false, dottedHostsConfigZonesAllowed.toSet, true))
error shouldBe an[InvalidRequest]
}
}
@ -399,54 +471,64 @@ class RecordSetValidationsSpec
"CnameValidations" should {
val invalidCnameApexRs: RecordSet = cname.copy(name = "@")
"return a RecordSetAlreadyExistsError if a record with the same name exists and creating a cname" in {
val error = leftValue(cnameValidations(cname, List(aaaa), okZone))
val error = leftValue(cnameValidations(cname, List(aaaa), okZone, None, true, dottedHostsConfigZonesAllowed.toSet, false))
error shouldBe a[RecordSetAlreadyExists]
}
"return ok if name is not '@'" in {
cnameValidations(cname, List(), okZone) should be(right)
cnameValidations(cname, List(), okZone, None, true, dottedHostsConfigZonesAllowed.toSet, false) should be(right)
}
"return an InvalidRequest if a cname record set name is '@'" in {
val error = leftValue(cnameValidations(invalidCnameApexRs, List(), okZone))
val error = leftValue(cnameValidations(invalidCnameApexRs, List(), okZone, None, true, dottedHostsConfigZonesAllowed.toSet, false))
error shouldBe an[InvalidRequest]
}
"return an InvalidRequest if a cname record set name is same as zone" in {
val invalid = invalidCnameApexRs.copy(name = okZone.name)
val error = leftValue(cnameValidations(invalid, List(), okZone))
val error = leftValue(cnameValidations(invalid, List(), okZone, None, true, dottedHostsConfigZonesAllowed.toSet, false))
error shouldBe an[InvalidRequest]
}
"return an InvalidRequest if a cname record set name is dotted" in {
val error = leftValue(cnameValidations(cname.copy(name = "dot.ted"), List(), okZone))
val error = leftValue(cnameValidations(cname.copy(name = "dot.ted"), List(), okZone, None, true, dottedHostsConfigZonesAllowed.toSet, false))
error shouldBe an[InvalidRequest]
}
"return ok if new recordset name does not contain dot" in {
cnameValidations(cname, List(), okZone, Some(cname.copy(name = "not-dotted"))) should be(
cnameValidations(cname, List(), okZone, Some(cname.copy(name = "not-dotted")), true, dottedHostsConfigZonesAllowed.toSet, false) should be(
right
)
}
"return ok if dotted host name doesn't change" in {
val newRecord = cname.copy(name = "dot.ted", ttl = 500)
cnameValidations(newRecord, List(), okZone, Some(newRecord.copy(ttl = 300))) should be(
cnameValidations(newRecord, List(), okZone, Some(newRecord.copy(ttl = 300)), true, dottedHostsConfigZonesAllowed.toSet, false) should be(
right
)
}
"return an InvalidRequest if a cname record set name is updated to '@'" in {
val error = leftValue(cnameValidations(cname.copy(name = "@"), List(), okZone, Some(cname)))
val error = leftValue(cnameValidations(cname.copy(name = "@"), List(), okZone, Some(cname), true, dottedHostsConfigZonesAllowed.toSet, false))
error shouldBe an[InvalidRequest]
}
"return an InvalidRequest if updated cname record set name is same as zone" in {
val error =
leftValue(cnameValidations(cname.copy(name = okZone.name), List(), okZone, Some(cname)))
leftValue(cnameValidations(cname.copy(name = okZone.name), List(), okZone, Some(cname), true, dottedHostsConfigZonesAllowed.toSet, false))
error shouldBe an[InvalidRequest]
}
"return an RecordSetValidation error if recordset data contain more than one sequential '.'" in {
val error = leftValue(cnameValidations(cname.copy(records = List(CNAMEData(Fqdn("record..zone")))), List(), okZone))
val error = leftValue(cnameValidations(cname.copy(records = List(CNAMEData(Fqdn("record..zone")))), List(), okZone, None, true, dottedHostsConfigZonesAllowed.toSet, false))
error shouldBe an[RecordSetValidation]
}
"return ok if recordset data does not contain sequential '.'" in {
cnameValidations(cname.copy(records = List(CNAMEData(Fqdn("record.zone")))), List(), okZone) should be(
cnameValidations(cname.copy(records = List(CNAMEData(Fqdn("record.zone")))), List(), okZone, None, true, dottedHostsConfigZonesAllowed.toSet, false) should be(
right
)
}
"return ok if the CNAME is dotted and zone, user, record type is allowed in dotted hosts config" in {
val record =
cnameValidations(cname.copy(name = "dot.ted", zoneId = dottedZone.id), List(), dottedZone, None, true, dottedHostsConfigZonesAllowed.toSet, true, 5)
record should be(right)
}
"return an InvalidRequest if the CNAME is dotted and zone, user, record type is allowed in dotted hosts config but has a conflict with existing record or zone" in {
val error =
leftValue(cnameValidations(cname.copy(name = "dot.ted", zoneId = dottedZone.id), List(), dottedZone, None, false, dottedHostsConfigZonesAllowed.toSet, true))
error shouldBe an[InvalidRequest]
}
}
"isNotHighValueDomain" should {

View File

@ -242,5 +242,15 @@ class ZoneConnectionValidatorSpec
underTest.isValidBackendId(Some("bad")) shouldBe left
}
}
"Zone Connection toString" should {
"not display key and algorithm" in {
zc.toString shouldBe "ZoneConnection: [name=\"zc.\"; keyName=\"zc.\"; primaryServer=\"10.1.1.1\"; ]"
}
"not display key and algorithm while displaying connection and transferConnection of a Zone" in {
val zoneString = s"""Zone: [id="${testZone.id}"; name="vinyldns."; account="system"; adminGroupId="system"; status="Active"; shared="false"; connection="Some(ZoneConnection: [name="vinyldns."; keyName="vinyldns."; primaryServer="10.1.1.1"; ])"; transferConnection="Some(ZoneConnection: [name="vinyldns."; keyName="vinyldns."; primaryServer="10.1.1.1"; ])"; reverse="false"; isTest="false"; created="${testZone.created}"; ]"""
testZone.toString shouldBe zoneString
}
}
}
}

View File

@ -562,6 +562,45 @@ class ZoneServiceSpec
result.ignoreAccess shouldBe true
}
"name filter must be used to return zones by admin group name, when search by admin group option is true" in {
doReturn(IO.pure(Set(abcGroup)))
.when(mockGroupRepo)
.getGroupsByName(any[String])
doReturn(IO.pure(ListZonesResults(List(abcZone), ignoreAccess = true, zonesFilter = Some("abcGroup"))))
.when(mockZoneRepo)
.listZonesByAdminGroupIds(abcAuth, None, 100, Set(abcGroup.id), ignoreAccess = true)
doReturn(IO.pure(Set(abcGroup))).when(mockGroupRepo).getGroups(any[Set[String]])
// When searchByAdminGroup is true, zones are filtered by admin group name given in nameFilter
val result: ListZonesResponse =
rightResultOf(underTest.listZones(abcAuth, Some("abcGroup"), None, 100, searchByAdminGroup = true, ignoreAccess = true).value)
result.zones shouldBe List(abcZoneSummary)
result.maxItems shouldBe 100
result.startFrom shouldBe None
result.nameFilter shouldBe Some("abcGroup")
result.nextId shouldBe None
result.ignoreAccess shouldBe true
}
"name filter must be used to return zone by zone name, when search by admin group option is false" in {
doReturn(IO.pure(Set(abcGroup)))
.when(mockGroupRepo)
.getGroups(any[Set[String]])
doReturn(IO.pure(ListZonesResults(List(abcZone), ignoreAccess = true, zonesFilter = Some("abcZone"))))
.when(mockZoneRepo)
.listZones(abcAuth, Some("abcZone"), None, 100, true)
// When searchByAdminGroup is false, zone name given in nameFilter is returned
val result: ListZonesResponse =
rightResultOf(underTest.listZones(abcAuth, Some("abcZone"), None, 100, searchByAdminGroup = false, ignoreAccess = true).value)
result.zones shouldBe List(abcZoneSummary)
result.maxItems shouldBe 100
result.startFrom shouldBe None
result.nameFilter shouldBe Some("abcZone")
result.nextId shouldBe None
result.ignoreAccess shouldBe true
}
"return Unknown group name if zone admin group cannot be found" in {
doReturn(IO.pure(ListZonesResults(List(abcZone, xyzZone))))
.when(mockZoneRepo)

View File

@ -154,7 +154,8 @@ class RecordSetChangeHandlerSpec
val batchChangeUpdates = await(batchRepo.getBatchChange(batchChange.id))
val updatedSingleChanges = completeCreateAAAASingleChanges.map { ch =>
ch.copy(
status = SingleChangeStatus.Complete,
systemMessage= None,
status = SingleChangeStatus.Complete,
recordChangeId = Some(rsChange.id),
recordSetId = Some(rsChange.recordSet.id)
)
@ -199,6 +200,7 @@ class RecordSetChangeHandlerSpec
val batchChangeUpdates = await(batchRepo.getBatchChange(batchChange.id))
val updatedSingleChanges = completeCreateAAAASingleChanges.map { ch =>
ch.copy(
systemMessage= None,
status = SingleChangeStatus.Complete,
recordChangeId = Some(rsChange.id),
recordSetId = Some(rsChange.recordSet.id)
@ -603,6 +605,7 @@ class RecordSetChangeHandlerSpec
val batchChangeUpdates = await(batchRepo.getBatchChange(batchChange.id))
val updatedSingleChanges = completeCreateAAAASingleChanges.map { ch =>
ch.copy(
systemMessage= None,
status = SingleChangeStatus.Complete,
recordChangeId = Some(rsChange.id),
recordSetId = Some(rsChange.recordSet.id)
@ -644,7 +647,7 @@ class RecordSetChangeHandlerSpec
changeSet.status shouldBe RecordSetChangeStatus.Failed
changeSet.recordSet.status shouldBe RecordSetStatus.Inactive
changeSet.systemMessage shouldBe Some(
s"Failed validating update to DNS for change ${changeSet.id}:${changeSet.recordSet.name}: " +
s"""Failed validating update to DNS for change "${changeSet.id}": "${changeSet.recordSet.name}": """ +
s"This record set is out of sync with the DNS backend; sync this zone before attempting to " +
"update this record set."
)

View File

@ -84,6 +84,14 @@ trait EmptyZoneRepo extends ZoneRepository {
def getZoneByName(zoneName: String): IO[Option[Zone]] = IO.pure(None)
def listZonesByAdminGroupIds(
authPrincipal: AuthPrincipal,
startFrom: Option[String] = None,
maxItems: Int = 100,
adminGroupIds: Set[String],
ignoreAccess: Boolean = false
): IO[ListZonesResults] = IO.pure(ListZonesResults())
def listZones(
authPrincipal: AuthPrincipal,
zoneNameFilter: Option[String] = None,
@ -111,8 +119,12 @@ trait EmptyGroupRepo extends GroupRepository {
def getGroups(groupIds: Set[String]): IO[Set[Group]] = IO.pure(Set())
def getGroupsByName(groupNames: Set[String]): IO[Set[Group]] = IO.pure(Set())
def getGroupByName(groupName: String): IO[Option[Group]] = IO.pure(None)
def getGroupsByName(groupName: String): IO[Set[Group]] = IO.pure(Set())
def getAllGroups(): IO[Set[Group]] = IO.pure(Set())
}

View File

@ -730,6 +730,37 @@ class MembershipRoutingSpec
}
}
"GET group change" should {
"return a 200 response with the group change when found" in {
val grpChange = GroupChangeInfo(okGroupChange)
doReturn(result(grpChange)).when(membershipService).getGroupChange("ok", okAuth)
Get("/groups/change/ok") ~> Route.seal(membershipRoute) ~> check {
status shouldBe StatusCodes.OK
val result = responseAs[GroupChangeInfo]
result shouldBe grpChange
}
}
"return a 400 Bad Request when the group change id is not valid" in {
doReturn(result(InvalidGroupRequestError("Invalid Group Change ID")))
.when(membershipService)
.getGroupChange("notValid", okAuth)
Get("/groups/change/notValid") ~> Route.seal(membershipRoute) ~> check {
status shouldBe StatusCodes.BadRequest
}
}
"return a 500 response on failure" in {
doReturn(result(new RuntimeException("fail")))
.when(membershipService)
.getGroupChange("bad", okAuth)
Get(s"/groups/change/bad") ~> Route.seal(membershipRoute) ~> check {
status shouldBe StatusCodes.InternalServerError
}
}
}
"PUT update user lock status" should {
"return a 200 response with the user locked" in {
membershipRoute = superUserRoute

View File

@ -124,7 +124,7 @@ class ZoneRoutingSpec
("status" -> "invalidStatus") ~~
("adminGroupId" -> "admin-group-id")
private val zoneCreate = ZoneChange(ok, "ok", ZoneChangeType.Create, ZoneChangeStatus.Complete)
private val zoneCreate = ZoneChange(ok, "ok", ZoneChangeType.Create, ZoneChangeStatus.Synced)
private val listZoneChangeResponse = ListZoneChangesResponse(
ok.id,
List(zoneCreate, zoneUpdate),
@ -157,7 +157,7 @@ class ZoneRoutingSpec
case ok.email | connectionOk.email | trailingDot.email | "invalid-zone-status@test.com" =>
Right(
zoneCreate.copy(
status = ZoneChangeStatus.Complete,
status = ZoneChangeStatus.Synced,
zone = Zone(createZoneInput, false).copy(status = ZoneStatus.Active)
)
)
@ -183,7 +183,7 @@ class ZoneRoutingSpec
case ok.email | connectionOk.email =>
Right(
zoneUpdate.copy(
status = ZoneChangeStatus.Complete,
status = ZoneChangeStatus.Synced,
zone = Zone(updateZoneInput, zoneUpdate.zone).copy(status = ZoneStatus.Active)
)
)
@ -208,7 +208,7 @@ class ZoneRoutingSpec
case notFound.id => Left(ZoneNotFoundError(s"$zoneId"))
case notAuthorized.id => Left(NotAuthorizedError(s"$zoneId"))
case ok.id | connectionOk.id =>
Right(ZoneChange(ok, "ok", ZoneChangeType.Delete, ZoneChangeStatus.Complete))
Right(ZoneChange(ok, "ok", ZoneChangeType.Delete, ZoneChangeStatus.Synced))
case error.id => Left(new RuntimeException("fail"))
case zone1.id => Left(ZoneUnavailableError(zoneId))
}
@ -252,6 +252,7 @@ class ZoneRoutingSpec
nameFilter: Option[String],
startFrom: Option[String],
maxItems: Int,
searchByAdminGroup: Boolean = false,
ignoreAccess: Boolean = false
): Result[ListZonesResponse] = {
@ -372,7 +373,7 @@ class ZoneRoutingSpec
authPrincipal,
NoOpCrypto.instance
)
.copy(status = ZoneChangeStatus.Complete)
.copy(status = ZoneChangeStatus.Synced)
)
case error.id => Left(new RuntimeException("fail"))
}
@ -397,7 +398,7 @@ class ZoneRoutingSpec
authPrincipal,
NoOpCrypto.instance
)
.copy(status = ZoneChangeStatus.Complete)
.copy(status = ZoneChangeStatus.Synced)
)
case error.id => Left(new RuntimeException("fail"))
}
@ -920,6 +921,20 @@ class ZoneRoutingSpec
}
}
"return zones by admin group name when searchByAdminGroup is true" in {
Get(s"/zones?nameFilter=ok&startFrom=zone4.&maxItems=4&searchByAdminGroup=true") ~> zoneRoute ~> check {
val resp = responseAs[ListZonesResponse]
val zones = resp.zones
(zones.map(_.id) should contain)
.only(zone1.id, zone2.id, zone3.id)
resp.nextId shouldBe None
resp.maxItems shouldBe 4
resp.startFrom shouldBe Some("zone4.")
resp.nameFilter shouldBe Some("ok")
resp.ignoreAccess shouldBe false
}
}
"return all zones when list all is true" in {
Get(s"/zones?maxItems=5&ignoreAccess=true") ~> zoneRoute ~> check {
val resp = responseAs[ListZonesResponse]

View File

@ -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."
}

View File

@ -52,6 +52,18 @@ final case class InvalidDomainName(param: String) extends DomainValidationError
"joined by dots, and terminated with a dot."
}
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 =
@ -109,10 +121,15 @@ final case class ZoneDiscoveryError(name: String, fatal: Boolean = false)
"If zone exists, then it must be connected to in VinylDNS."
}
final case class RecordAlreadyExists(name: String) extends DomainValidationError {
def message: String =
s"""Record "$name" Already Exists: cannot add an existing record; to update it, """ +
"issue a DeleteRecordSet then an Add."
final case class RecordAlreadyExists(name: String, recordData: RecordData, isApproved:Boolean,
fatal: Boolean = false) extends DomainValidationError(fatal) {
def message: String = {
if (isApproved == false)
s"""RecordName "$name" already exists. Your request will be manually reviewed. """ +
"If you intended to update this record, you can avoid manual review by adding " +
" a DeleteRecordSet entry followed by an Add."
else s""" Record data "$recordData" is does not exists.
Complete the request in DNS and give approve. """ }
}
final case class RecordDoesNotExist(name: String) extends DomainValidationError {

View File

@ -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

View File

@ -17,7 +17,6 @@
package vinyldns.core.domain.batch
import java.util.UUID
import vinyldns.core.domain.SingleChangeError
import vinyldns.core.domain.batch.SingleChangeStatus.SingleChangeStatus
import vinyldns.core.domain.record.RecordData
@ -47,6 +46,13 @@ sealed trait SingleChange {
delete.copy(status = SingleChangeStatus.Failed, systemMessage = Some(error))
}
def withDoesNotExistMessage(error: String): SingleChange = this match {
case add: SingleAddChange =>
add.copy(status = SingleChangeStatus.Failed, systemMessage = Some(error))
case delete: SingleDeleteRRSetChange =>
delete.copy(status = SingleChangeStatus.Complete, systemMessage = Some(error))
}
def withProcessingError(message: Option[String], failedRecordChangeId: String): SingleChange =
this match {
case add: SingleAddChange =>
@ -63,16 +69,18 @@ sealed trait SingleChange {
)
}
def complete(completeRecordChangeId: String, recordSetId: String): SingleChange = this match {
def complete(message: Option[String], completeRecordChangeId: String, recordSetId: String): SingleChange = this match {
case add: SingleAddChange =>
add.copy(
status = SingleChangeStatus.Complete,
systemMessage = message,
recordChangeId = Some(completeRecordChangeId),
recordSetId = Some(recordSetId)
)
case delete: SingleDeleteRRSetChange =>
delete.copy(
status = SingleChangeStatus.Complete,
systemMessage = message,
recordChangeId = Some(completeRecordChangeId),
recordSetId = Some(recordSetId)
)
@ -140,12 +148,18 @@ object SingleChangeStatus extends Enumeration {
}
case class RecordKey(zoneId: String, recordName: String, recordType: RecordType)
case class RecordKeyData(zoneId: String, recordName: String, recordType: RecordType, recordData: RecordData)
object RecordKey {
def apply(zoneId: String, recordName: String, recordType: RecordType): RecordKey =
new RecordKey(zoneId, recordName.toLowerCase, recordType)
}
object RecordKeyData {
def apply(zoneId: String, recordName: String, recordType: RecordType, recordData: RecordData): RecordKeyData =
new RecordKeyData(zoneId, recordName.toLowerCase, recordType, recordData)
}
object OwnerType extends Enumeration {
type OwnerType = Value
val Record, Zone = Value

View File

@ -35,7 +35,9 @@ case class GroupChange(
userId: String,
oldGroup: Option[Group] = None,
id: String = UUID.randomUUID().toString,
created: Instant = Instant.now.truncatedTo(ChronoUnit.MILLIS)
created: Instant = Instant.now.truncatedTo(ChronoUnit.MILLIS),
userName: Option[String] = None,
groupChangeMessage: Option[String] = None
)
object GroupChange {

View File

@ -24,7 +24,8 @@ import vinyldns.core.repository.Repository
trait GroupChangeRepository extends Repository {
def save(db: DB, groupChange: GroupChange): IO[GroupChange]
def getGroupChange(groupChangeId: String): IO[Option[GroupChange]] // For testing
def getGroupChange(groupChangeId: String): IO[Option[GroupChange]]
def getGroupChanges(
groupId: String,
startFrom: Option[String],

View File

@ -31,8 +31,12 @@ trait GroupRepository extends Repository {
def getGroups(groupIds: Set[String]): IO[Set[Group]]
def getGroupsByName(groupNames: Set[String]): IO[Set[Group]]
def getGroupByName(groupName: String): IO[Option[Group]]
def getGroupsByName(groupName: String): IO[Set[Group]]
def getAllGroups(): IO[Set[Group]]
}

View File

@ -188,6 +188,16 @@ case class ZoneConnection(
def decrypted(crypto: CryptoAlgebra): ZoneConnection =
copy(key = crypto.decrypt(key))
override def toString: String = {
val sb = new StringBuilder
sb.append("ZoneConnection: [")
sb.append("name=\"").append(name).append("\"; ")
sb.append("keyName=\"").append(keyName).append("\"; ")
sb.append("primaryServer=\"").append(primaryServer).append("\"; ")
sb.append("]")
sb.toString
}
}
final case class LegacyDnsBackend(

View File

@ -23,7 +23,7 @@ import java.time.temporal.ChronoUnit
object ZoneChangeStatus extends Enumeration {
type ZoneChangeStatus = Value
val Pending, Complete, Failed, Synced = Value
val Pending, Failed, Synced = Value
}
object ZoneChangeType extends Enumeration {

View File

@ -35,6 +35,14 @@ trait ZoneRepository extends Repository {
def getZonesByFilters(zoneNames: Set[String]): IO[Set[Zone]]
def listZonesByAdminGroupIds(
authPrincipal: AuthPrincipal,
startFrom: Option[String] = None,
maxItems: Int = 100,
adminGroupIds: Set[String],
ignoreAccess: Boolean = false
): IO[ListZonesResults]
def listZones(
authPrincipal: AuthPrincipal,
zoneNameFilter: Option[String] = None,

View File

@ -37,6 +37,7 @@ object TestMembershipData {
val dummyUser = User("dummyName", "dummyAccess", "dummySecret")
val superUser = User("super", "superAccess", "superSecret", isSuper = true)
val xyzUser = User("xyz", "xyzAccess", "xyzSecret")
val supportUser = User("support", "supportAccess", "supportSecret", isSupport = true)
val lockedUser = User("locked", "lockedAccess", "lockedSecret", lockStatus = LockStatus.Locked)
val sharedZoneUser = User("sharedZoneAdmin", "sharedAccess", "sharedSecret")
@ -158,4 +159,13 @@ object TestMembershipData {
id = s"$i"
)
}
val dummyGroupChangeUpdate: GroupChange = GroupChange(
okGroup.copy(name = "dummy-group", email = "dummy@test.com", description = Some("dummy group"),
memberIds = Set(dummyUser.copy(id="12345-abcde-6789").id, superUser.copy(id="56789-edcba-1234").id),
adminUserIds = Set(dummyUser.copy(id="12345-abcde-6789").id, superUser.copy(id="56789-edcba-1234").id)),
GroupChangeType.Update,
okUser.id,
Some(okGroup),
created = DateTime.now.secondOfDay().roundFloorCopy()
)
}

View File

@ -35,6 +35,8 @@ object TestZoneData {
adminGroupId = okGroup.id,
connection = testConnection
)
val dottedZone: Zone = Zone("dotted.xyz.", "dotted@xyz.com", adminGroupId = xyzGroup.id)
val dotZone: Zone = Zone("dot.xyz.", "dotted@xyz.com", adminGroupId = xyzGroup.id)
val abcZone: Zone = Zone("abc.zone.recordsets.", "test@test.com", adminGroupId = abcGroup.id)
val xyzZone: Zone = Zone("xyz.", "abc@xyz.com", adminGroupId = xyzGroup.id)
val zoneIp4: Zone = Zone("0.162.198.in-addr.arpa.", "test@test.com", adminGroupId = abcGroup.id)
@ -80,11 +82,11 @@ object TestZoneData {
okZone,
"ok",
ZoneChangeType.Create,
ZoneChangeStatus.Complete,
ZoneChangeStatus.Synced,
created = Instant.now.truncatedTo(ChronoUnit.MILLIS).minusMillis(1000)
)
val zoneUpdate: ZoneChange = zoneChangePending.copy(status = ZoneChangeStatus.Complete)
val zoneUpdate: ZoneChange = zoneChangePending.copy(status = ZoneChangeStatus.Synced)
def makeTestPendingZoneChange(zone: Zone): ZoneChange =
ZoneChange(zone, "userId", ZoneChangeType.Update, ZoneChangeStatus.Pending)

View File

@ -27,7 +27,7 @@ class ZoneChangeSpec extends AnyWordSpec with Matchers {
Zone("test", "test"),
"ok",
ZoneChangeType.Create,
ZoneChangeStatus.Complete,
ZoneChangeStatus.Synced,
created = Instant.now.truncatedTo(ChronoUnit.MILLIS).minusMillis(1000)
)

View File

@ -75,7 +75,7 @@ class ProtobufConversionsSpec
zone,
"system",
ZoneChangeType.Update,
ZoneChangeStatus.Complete,
ZoneChangeStatus.Synced,
Instant.now.truncatedTo(ChronoUnit.MILLIS),
Some("hello")
)

View File

@ -536,7 +536,66 @@ v6-discovery-nibble-boundaries {
min = 5
max = 20
}
```
### Dotted Hosts
Configuration setting that determines the zones, users (either individual or based on group) and record types that are
allowed to create dotted hosts. If only all the above are satisfied, one can create a dotted host in VinylDNS.
Note the following:
1. Zones defined in the `zone` must always end with a dot. Eg: `comcast.com.`
2. Wildcard character `*` can be used in `zone` to allow dotted hosts for all zones matching it.
3. Individual users who are allowed to create dotted hosts are added to the `user-list` using their username.
4. A set of users in a group who are allowed to create dotted hosts are added to the `group-list` using group name.
5. If the user is either in `user-list` or `group-list`, they are allowed to create a dotted host. It is
not necessary for the user to be in both `user-list` and `group-list`.
6. The record types which are allowed while creating a dotted host is added to the `record-types`.
7. The number of dots allowed in a record name for a zone is given in `dots-limit`.
8. If `user-list` is left empty (`user-list = []`), no user will be allowed to create dotted hosts unless
they're present in `group-list` and vice-versa. If both `user-list` and `group-list` is left empty
no users will be allowed to create dotted hosts in that zone.
9. If `record-types` is left empty (`record-types = []`), user cannot create dotted hosts of any record type
in that zone.
10. If `dots-limit` is set to 0 (`dots-limit = 0`), we cannot create dotted hosts record in that zone.
```yaml
# approved zones, individual users, users in groups, record types and no.of.dots that are allowed for dotted hosts
dotted-hosts = {
allowed-settings = [
{
zone = "dummy."
user-list = ["testuser"]
group-list = ["dummy-group"]
record-types = ["AAAA"]
dots-limit = 3
},
{
# for wildcard zones. Settings will be applied to all matching zones
zone = "*ent.com."
user-list = ["professor", "testuser"]
group-list = ["testing-group"]
record-types = ["A", "CNAME"]
dots-limit = 3
}
]
}
```
In the above, the dotted hosts can be created only in the zone `dummy.` and zones matching `*ent.com.` (parent.com., child.parent.com.)
Also, it must satisfy the allowed users or group users and record type of the respective zone to create a dotted host.
For eg, we can't create a dotted host with `CNAME` record type in the zone `dummy.` as it's not in `record-types`.
And the user `professor` can't create a dotted host in the zone `dummy.` as the user is not in `user-list` or
`group-list` (not part of `dummy-group`).
The config can be left empty as follows if we don't want to use it:
```yaml
dotted-hosts = {
allowed-settings = []
}
```
### Full Example Config
@ -713,6 +772,27 @@ v6-discovery-nibble-boundaries {
}
}
# approved zones, individual users, users in groups, record types and no.of.dots that are allowed for dotted hosts
dotted-hosts = {
allowed-settings = [
{
zone = "dummy."
user-list = ["testuser"]
group-list = ["dummy-group"]
record-types = ["AAAA"]
dots-limit = 3
},
{
# for wildcard zones. Settings will be applied to all matching zones
zone = "*ent.com."
user-list = ["professor", "testuser"]
group-list = ["testing-group"]
record-types = ["A", "CNAME"]
dots-limit = 3
}
]
}
# true if you want to enable manual review for non-fatal errors
manual-batch-review-enabled = true

View File

@ -116,7 +116,7 @@ class MySqlBatchChangeRepositoryIntegrationSpec
val pendingBatchChange: BatchChange = randomBatchChange().copy(createdTimestamp = Instant.now.truncatedTo(ChronoUnit.MILLIS))
val completeBatchChange: BatchChange = randomBatchChangeWithList(
randomBatchChange().changes.map(_.complete("recordChangeId", "recordSetId"))
randomBatchChange().changes.map(_.complete(Some("Complete"),"recordChangeId", "recordSetId"))
).copy(createdTimestamp = Instant.now.truncatedTo(ChronoUnit.MILLIS).plusMillis(1000))
val failedBatchChange: BatchChange =
@ -124,7 +124,7 @@ class MySqlBatchChangeRepositoryIntegrationSpec
.copy(createdTimestamp = Instant.now.truncatedTo(ChronoUnit.MILLIS).plusMillis(100000))
val partialFailureBatchChange: BatchChange = randomBatchChangeWithList(
randomBatchChange().changes.take(2).map(_.complete("recordChangeId", "recordSetId"))
randomBatchChange().changes.take(2).map(_.complete(Some("Complete"),"recordChangeId", "recordSetId"))
++ randomBatchChange().changes.drop(2).map(_.withFailureMessage("failed"))
).copy(createdTimestamp = Instant.now.truncatedTo(ChronoUnit.MILLIS).plusMillis(1000000))
@ -411,7 +411,7 @@ class MySqlBatchChangeRepositoryIntegrationSpec
"update single changes" in {
val batchChange = randomBatchChange()
val completed = batchChange.changes.map(_.complete("aaa", "bbb"))
val completed = batchChange.changes.map(_.complete(Some("Complete"),"aaa", "bbb"))
val f =
for {
_ <- repo.save(batchChange)
@ -430,7 +430,7 @@ class MySqlBatchChangeRepositoryIntegrationSpec
"update some changes in a batch" in {
val batchChange = randomBatchChange()
val completed = batchChange.changes.take(2).map(_.complete("recordChangeId", "recordSetId"))
val completed = batchChange.changes.take(2).map(_.complete(Some("Complete"),"recordChangeId", "recordSetId"))
val incomplete = batchChange.changes.drop(2)
val f =
for {
@ -444,7 +444,7 @@ class MySqlBatchChangeRepositoryIntegrationSpec
"return the batch when updating single changes" in {
val batchChange = randomBatchChange()
val completed = batchChange.changes.take(2).map(_.complete("recordChangeId", "recordSetId"))
val completed = batchChange.changes.take(2).map(_.complete(Some("Complete"),"recordChangeId", "recordSetId"))
val f =
for {
_ <- repo.save(batchChange)

View File

@ -93,6 +93,24 @@ class MySqlGroupRepositoryIntegrationSpec
}
}
"MySqlGroupRepository.getGroupsByName" should {
"omits all non existing groups" in {
val result = repo.getGroupsByName(Set("no-existo", groups.head.name)).unsafeRunSync()
result should contain theSameElementsAs Set(groups.head)
}
"returns correct list of groups" in {
val names = Set(groups(0).name, groups(1).name, groups(2).name)
val result = repo.getGroupsByName(names).unsafeRunSync()
result should contain theSameElementsAs groups.take(3).toSet
}
"returns empty list when given no names" in {
val result = repo.getGroupsByName(Set[String]()).unsafeRunSync()
result should contain theSameElementsAs Set()
}
}
"MySqlGroupRepository.getGroupByName" should {
"retrieve a group" in {
repo.getGroupByName(groups.head.name).unsafeRunSync() shouldBe Some(groups.head)
@ -103,6 +121,20 @@ class MySqlGroupRepositoryIntegrationSpec
}
}
"MySqlGroupRepository.getGroupsByName" should {
"retrieve a group" in {
repo.getGroupsByName(groups.head.name).unsafeRunSync() shouldBe Set(groups.head)
}
"retrieve groups with wildcard character" in {
repo.getGroupsByName("*-group-*").unsafeRunSync() shouldBe groups.toSet
}
"returns empty set when group does not exist" in {
repo.getGroupsByName("no-existo").unsafeRunSync() shouldBe Set()
}
}
"MySqlGroupRepository.getAllGroups" should {
"retrieve all groups" in {
repo.getAllGroups().unsafeRunSync() should contain theSameElementsAs groups.toSet

View File

@ -29,14 +29,16 @@ import vinyldns.core.domain.zone._
import vinyldns.core.TestZoneData.okZone
import vinyldns.core.TestMembershipData._
import vinyldns.core.domain.zone.ZoneRepository.DuplicateZoneError
import vinyldns.mysql.TestMySqlInstance
import vinyldns.mysql.{TestMySqlInstance, TransactionProvider}
import vinyldns.mysql.TestMySqlInstance.groupRepository
class MySqlZoneRepositoryIntegrationSpec
extends AnyWordSpec
with BeforeAndAfterAll
with BeforeAndAfterEach
with Matchers
with Inspectors {
with Inspectors
with TransactionProvider {
private var repo: ZoneRepository = _
@ -221,6 +223,32 @@ class MySqlZoneRepositoryIntegrationSpec
(repo.listZones(dummyAuth).unsafeRunSync().zones should contain).only(testZones.head)
}
"get authorized zone by admin group name" in {
executeWithinTransaction { db: DB =>
groupRepository.save(db, okGroup.copy(id = testZoneAdminGroupId))
}.unsafeRunSync()
// store all of the zones
val f = saveZones(testZones)
// query for all zones for the ok user, he should have access to all of the zones
val okUserAuth = AuthPrincipal(
signedInUser = okUser,
memberGroupIds = groups.map(_.id)
)
f.unsafeRunSync()
repo.listZonesByAdminGroupIds(okUserAuth, None, 100, Set(testZoneAdminGroupId)).unsafeRunSync().zones should contain theSameElementsAs testZones
// dummy user only has access to one zone
(repo.listZonesByAdminGroupIds(dummyAuth, None, 100, Set(testZoneAdminGroupId)).unsafeRunSync().zones should contain).only(testZones.head)
// delete the group created to test
groupRepository.delete(okGroup).unsafeRunSync()
}
"get all zones" in {
// store all of the zones
val privateZone = okZone.copy(
@ -259,6 +287,82 @@ class MySqlZoneRepositoryIntegrationSpec
.zones should contain theSameElementsAs testZones
}
"get all zones by admin group name" in {
executeWithinTransaction { db: DB =>
groupRepository.save(db, okGroup)
}.unsafeRunSync()
val group = groupRepository.getGroupsByName(okGroup.name).unsafeRunSync()
val groupId = group.head.id
// store all of the zones
val privateZone = okZone.copy(
name = "private-zone.",
id = UUID.randomUUID().toString,
acl = ZoneACL(),
adminGroupId = groupId
)
val sharedZone = okZone.copy(
name = "shared-zone.",
id = UUID.randomUUID().toString,
acl = ZoneACL(),
shared = true,
adminGroupId = groupId
)
val testZones = Seq(privateZone, sharedZone)
val f = saveZones(testZones)
// query for all zones for the ok user, should have all of the zones returned
val okUserAuth = AuthPrincipal(
signedInUser = okUser,
memberGroupIds = groups.map(_.id)
)
f.unsafeRunSync()
repo
.listZonesByAdminGroupIds(okUserAuth, None, 100, Set(groupId), ignoreAccess = true)
.unsafeRunSync()
.zones should contain theSameElementsAs testZones
// dummy user only have all of the zones returned
repo
.listZonesByAdminGroupIds(dummyAuth, None, 100, Set(groupId), ignoreAccess = true)
.unsafeRunSync()
.zones should contain theSameElementsAs testZones
// delete the group created to test
groupRepository.delete(okGroup).unsafeRunSync()
}
"get empty list when no matching admin group name is found while filtering zones by group name" in {
executeWithinTransaction { db: DB =>
groupRepository.save(db, okGroup.copy(id = testZoneAdminGroupId))
}.unsafeRunSync()
// store all of the zones
val f = saveZones(testZones)
// query for all zones for the ok user, he should have access to all of the zones
val okUserAuth = AuthPrincipal(
signedInUser = okUser,
memberGroupIds = groups.map(_.id)
)
f.unsafeRunSync()
repo.listZonesByAdminGroupIds(okUserAuth, None, 100, Set()).unsafeRunSync().zones shouldBe empty
// delete the group created to test
groupRepository.delete(okGroup).unsafeRunSync()
}
"get zones that are accessible by everyone" in {
//user and group id being set to None implies EVERYONE access
@ -468,6 +572,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(

View File

@ -69,6 +69,13 @@ class MySqlGroupRepository extends GroupRepository with GroupProtobufConversions
| WHERE id
""".stripMargin
private val BASE_GET_GROUPS_BY_NAMES =
"""
|SELECT data
| FROM groups
| WHERE name
""".stripMargin
def save(db: DB, group: Group): IO[Group] =
monitor("repo.Group.save") {
IO {
@ -141,6 +148,27 @@ class MySqlGroupRepository extends GroupRepository with GroupProtobufConversions
}
}
def getGroupsByName(groupNames: Set[String]): IO[Set[Group]] =
monitor("repo.Group.getGroups") {
IO {
logger.debug(s"Getting group with names: $groupNames")
if (groupNames.isEmpty)
Set[Group]()
else {
DB.readOnly { implicit s =>
val groupNameList = groupNames.toList
val inClause = " IN (" + groupNameList.as("?").mkString(",") + ")"
val query = BASE_GET_GROUPS_BY_NAMES + inClause
SQL(query)
.bind(groupNameList: _*)
.map(toGroup(1))
.list()
.apply()
}.toSet
}
}
}
def getGroupByName(groupName: String): IO[Option[Group]] =
monitor("repo.Group.getGroupByName") {
IO {
@ -155,6 +183,30 @@ class MySqlGroupRepository extends GroupRepository with GroupProtobufConversions
}
}
def getGroupsByName(nameFilter: String): IO[Set[Group]] =
monitor("repo.Group.getGroupByName") {
IO {
logger.debug(s"Getting groups with name: $nameFilter")
val initialQuery = "SELECT data FROM groups WHERE name"
val sb = new StringBuilder
sb.append(initialQuery)
val groupsLike = if (nameFilter.contains('*')) {
s" LIKE '${nameFilter.replace('*', '%')}'"
} else {
s" LIKE '$nameFilter%'"
}
sb.append(groupsLike)
val query = sb.toString()
DB.readOnly { implicit s =>
SQL(query)
.map(toGroup(1))
.list()
.apply()
}.toSet
}
}
def getAllGroups(): IO[Set[Group]] =
monitor("repo.Group.getAllGroups") {
IO {

View File

@ -229,6 +229,65 @@ class MySqlZoneRepository extends ZoneRepository with ProtobufConversions with M
}
}
/**
* This is somewhat complicated due to how we need to build the SQL.
*
* - Dynamically build the accessor list combining the user id and group ids
* - Dynamically build the LIMIT clause. We cannot specify an offset if this is the first page (offset == 0)
*
* @return a ListZonesResults
*/
def listZonesByAdminGroupIds(
authPrincipal: AuthPrincipal,
startFrom: Option[String] = None,
maxItems: Int = 100,
adminGroupIds: Set[String],
ignoreAccess: Boolean = false
): IO[ListZonesResults] =
monitor("repo.ZoneJDBC.listZonesByAdminGroupIds") {
IO {
DB.readOnly { implicit s =>
val (withAccessorCheck, accessors) =
withAccessors(authPrincipal.signedInUser, authPrincipal.memberGroupIds, ignoreAccess)
val sb = new StringBuilder
sb.append(withAccessorCheck)
if(adminGroupIds.nonEmpty) {
val groupIds = adminGroupIds.map(x => "'" + x + "'").mkString(",")
sb.append(s" WHERE admin_group_id IN ($groupIds) ")
} else {
sb.append(s" WHERE admin_group_id IN ('') ")
}
sb.append(s" GROUP BY z.name ")
sb.append(s" LIMIT ${maxItems + 1}")
val query = sb.toString
val results: List[Zone] = SQL(query)
.bind(accessors: _*)
.map(extractZone(1))
.list()
.apply()
val (newResults, nextId) =
if (results.size > maxItems)
(results.dropRight(1), results.dropRight(1).lastOption.map(_.name))
else (results, None)
ListZonesResults(
zones = newResults,
nextId = nextId,
startFrom = startFrom,
maxItems = maxItems,
zonesFilter = None,
ignoreAccess = ignoreAccess,
)
}
}
}
/**
* This is somewhat complicated due to how we need to build the SQL.
*

View File

@ -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'},

View File

@ -214,6 +214,32 @@ class VinylDNS @Inject() (
})
}
def getGroupChange(gcid: String): Action[AnyContent] = userAction.async { implicit request =>
val vinyldnsRequest = VinylDNSRequest("GET", s"$vinyldnsServiceBackend", s"groups/change/$gcid")
executeRequest(vinyldnsRequest, request.user).map(response => {
logger.info(s"group change [$gcid] retrieved with status [${response.status}]")
Status(response.status)(response.body)
.withHeaders(cacheHeaders: _*)
})
}
def listGroupChanges(id: String): Action[AnyContent] = userAction.async { implicit request =>
val queryParameters = new HashMap[String, java.util.List[String]]()
for {
(name, values) <- request.queryString
} queryParameters.put(name, values.asJava)
val vinyldnsRequest = new VinylDNSRequest(
"GET",
s"$vinyldnsServiceBackend",
s"groups/$id/activity",
parameters = queryParameters
)
executeRequest(vinyldnsRequest, request.user).map(response => {
Status(response.status)(response.body)
.withHeaders(cacheHeaders: _*)
})
}
def getUser(id: String): Action[AnyContent] = userAction.async { implicit request =>
val vinyldnsRequest = VinylDNSRequest("GET", s"$vinyldnsServiceBackend", s"users/$id")
executeRequest(vinyldnsRequest, request.user).map(response => {
@ -431,6 +457,23 @@ class VinylDNS @Inject() (
})
}
def getZoneChange(id: String): Action[AnyContent] = userAction.async { implicit request =>
val queryParameters = new HashMap[String, java.util.List[String]]()
for {
(name, values) <- request.queryString
} queryParameters.put(name, values.asJava)
val vinyldnsRequest =
new VinylDNSRequest(
"GET",
s"$vinyldnsServiceBackend",
s"zones/$id/changes",
parameters = queryParameters)
executeRequest(vinyldnsRequest, request.user).map(response => {
Status(response.status)(response.body)
.withHeaders(cacheHeaders: _*)
})
}
def syncZone(id: String): Action[AnyContent] = userAction.async { implicit request =>
// $COVERAGE-OFF$
val vinyldnsRequest =

View File

@ -158,10 +158,16 @@
<span ng-if="change.status == 'Cancelled'" class="label label-default">{{batch.status}}</span>
</td>
<td class="wrap-long-text">
<p ng-repeat="error in change.validationErrors">
{{error.message ? error.message : error}}
</p>
{{change.systemMessage}}
<div ng-if="batch.approvalStatus != 'AutoApproved'
&& change.status != 'Rejected'
&& change.status != 'Cancelled'">
<p ng-repeat="error in change.validationErrors">
{{error.message ? error.message : error}} </p>{{change.systemMessage}}</div>
<div ng-if="batch.approvalStatus =='ManuallyRejected' || batch.approvalStatus =='Cancelled'">
</div>
<div ng-if="batch.approvalStatus == 'AutoApproved'
&& change.status =='Complete'">
No further action is required.</div>
</td>
</tr>
</tbody>

View File

@ -2,110 +2,281 @@
@content = {
<!-- PAGE CONTENT -->
<div class="right_col" role="main" >
<!-- PAGE CONTENT -->
<div class="right_col" role="main" >
<div>
<!-- START BREADCRUMB -->
<ul class="breadcrumb">
<li><a href="/">Home</a></li>
<li><a href="/groups">Groups</a></li>
<li class="active">{{membership.group.name}}</li>
</ul>
<!-- END BREADCRUMB -->
<div>
<!-- START BREADCRUMB -->
<ul class="breadcrumb">
<li><a href="/">Home</a></li>
<li><a href="/groups">Groups</a></li>
<li class="active">{{membership.group.name}}</li>
</ul>
<!-- END BREADCRUMB -->
<!-- PAGE TITLE -->
<div class="page-title">
<h3><span class="fa fa-group"></span> Group {{membership.group.name}}</h3>
</div>
<!-- END PAGE TITLE -->
<!-- PAGE CONTENT WRAPPER -->
<div class="page-content-wrap">
<div class="alert-wrapper">
<div ng-repeat="alert in alerts">
<notification ng-model="alert"></notification>
</div>
<!-- PAGE TITLE -->
<div class="page-title">
<h3><span class="fa fa-group"></span> Group {{membership.group.name}}</h3>
</div>
<!-- END PAGE TITLE -->
<div class="row">
<div class="col-md-12">
<p ng-if="membership.group.description"><strong>Description:</strong> {{membership.group.description}}</p>
<p><strong>Group Email:</strong> {{membership.group.email}}</p>
<!-- START SIMPLE DATATABLE -->
<div class="panel panel-default">
<div class="panel-heading">
<div ng-if="isGroupAdmin">
<form class="form-inline" role="form" name="addMemberForm" ng-submit="addMemberForm.$valid && addMember();">
<div class="col-md-8">
<div class="form-group">
<div class="input-group">
<input type="text" ng-model="newMemberData.login" name="newMemberLogin" class="form-control" placeholder="User Name" required/>
<!-- PAGE CONTENT WRAPPER -->
<div class="page-content-wrap">
<div class="alert-wrapper">
<div ng-repeat="alert in alerts">
<notification ng-model="alert"></notification>
</div>
</div>
<!-- START VERTICAL TABS -->
<div class="panel panel-default panel-tabs">
<ul class="nav nav-tabs bar_tabs">
<li class="active"><a href="#tab1" data-toggle="tab">Manage Groups</a></li>
<li><a href="#tab2" data-toggle="tab">Change History</a></li>
</ul>
<div class="panel-body tab-content">
<div class="tab-pane active" id="tab1">
<div class="row">
<div class="col-md-12">
<p ng-if="membership.group.description"><strong>Description:</strong> {{membership.group.description}}</p>
<p><strong>Group Email:</strong> {{membership.group.email}}</p>
<!-- START SIMPLE DATATABLE -->
<div class="panel panel-default">
<div class="panel-heading">
<div ng-if="isGroupAdmin">
<form class="form-inline" role="form" name="addMemberForm" ng-submit="addMemberForm.$valid && addMember();">
<div class="col-md-8">
<div class="form-group">
<div class="input-group">
<input type="text" ng-model="newMemberData.login" name="newMemberLogin" class="form-control" placeholder="User Name" required/>
</div>
</div>
<div class="form-group" style="padding-left: 10px;">
<label class="check">
<input type="checkbox"
ng-model="newMemberData.isAdmin" name="newMemberAdmin"
class="icheckbox_minimal-grey"/>
Is Group Manager?</label>
</div>
<div class="form-group" style="padding-left: 10px;">
<div class="input-group">
<button type="submit" class="btn btn-sm vinyldns-btn-dark">Add Group Member</button>
</div>
</div>
</div>
</form>
</div>
</div>
<div class="form-group" style="padding-left: 10px;">
<label class="check">
<input type="checkbox"
ng-model="newMemberData.isAdmin" name="newMemberAdmin"
class="icheckbox_minimal-grey"/>
Is Group Manager?</label>
</div>
<div class="form-group" style="padding-left: 10px;">
<div class="input-group">
<button type="submit" class="btn btn-sm vinyldns-btn-dark">Add Group Member</button>
</div>
<div class="panel-body">
<p id="no-group-list" ng-if="!membershipLoaded">Loading members...</p>
<p id="no-group-list" ng-if="!membership.members.length && membershipLoaded">You don't have any members yet.</p>
<table class="table datatable_simple group-members" ng-if="membership.members.length">
<thead>
<tr>
<th>User Name</th>
<th>Name</th>
<th>Email</th>
<th>Group Manager</th>
<th>Status</th>
<th ng-if="isGroupAdmin">Actions</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="member in membership.members | orderBy:'+userName'">
<td>{{member.userName}}</td>
<td>{{([member.lastName, member.firstName] | filter: "" ).join(", ")}}</td>
<td>{{member.email}}</td>
<td>
<label class="switch col-md-1">
<input class="switch-checkbox" type="checkbox" ng-model="member.isAdmin" ng-disabled="!isGroupAdmin" ng-change="toggleAdmin(member);"/>
<span class="slider"></span>
</label>
</td>
<td>{{member.lockStatus}}</td>
<td ng-if="isGroupAdmin">
<button class="btn btn-danger btn-rounded" ng-click="removeMember(member.id);">
Delete
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</form>
<!-- END SIMPLE DATATABLE -->
</div>
</div>
</div>
<div class="panel-body">
<p id="no-group-list" ng-if="!membershipLoaded">Loading members...</p>
<p id="no-group-list" ng-if="!membership.members.length && membershipLoaded">You don't have any members yet.</p>
<table class="table datatable_simple group-members" ng-if="membership.members.length">
<thead>
<tr>
<th>User Name</th>
<th>Name</th>
<th>Email</th>
<th>Group Manager</th>
<th>Status</th>
<th ng-if="isGroupAdmin">Actions</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="member in membership.members | orderBy:'+userName'">
<td>{{member.userName}}</td>
<td>{{([member.lastName, member.firstName] | filter: "" ).join(", ")}}</td>
<td>{{member.email}}</td>
<td>
<label class="switch col-md-1">
<input class="switch-checkbox" type="checkbox" ng-model="member.isAdmin" ng-disabled="!isGroupAdmin" ng-change="toggleAdmin(member);"/>
<span class="slider"></span>
</label>
</td>
<td>{{member.lockStatus}}</td>
<td ng-if="isGroupAdmin">
<button class="btn btn-danger btn-rounded" ng-click="removeMember(member.id);">
Delete
</button>
</td>
</tr>
</tbody>
</table>
<div class="tab-pane" id="tab2">
<!-- START SIMPLE DATATABLE -->
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">All Group Changes {{ getChangePageTitle() }}</h3>
</div>
<div class="panel-body">
<div class="btn-group">
<button class="btn btn-default" ng-click="refreshGroupChanges()"><span class="fa fa-refresh"></span> Refresh</button>
</div>
<!-- PAGINATION -->
<div class="dataTables_paginate">
<ul class="pagination">
<li class="paginate_button previous">
<a ng-if="changePrevPageEnabled()" ng-click="changePrevPage()" class="paginate_button">Previous</a>
</li>
<li class="paginate_button next">
<a ng-if="changeNextPageEnabled()" ng-click="changeNextPage()" class="paginate_button">Next</a>
</li>
</ul>
</div>
<!-- END PAGINATION -->
<table id="changeDataTable" class="table table-hover table-striped">
<thead>
<tr>
<th>Time</th>
<th>Group Change ID</th>
<th>Change Type</th>
<th>Change Message</th>
<th>Change Info</th>
<th>User</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="change in groupChanges track by $index">
<td>{{change.created}}</td>
<td>{{change.id}}</td>
<td>{{change.changeType}}</td>
<td class="col-md-3 wrap-long-text" ng-bind-html="changeMessage(change.groupChangeMessage)"></td>
<td class="col-md-3 wrap-long-text">
<a ng-if="change.changeType =='Create'" ng-click="viewGroupInfo(change.newGroup)" class="force-cursor">View created group</a>
<div><a ng-if="change.changeType =='Update'" ng-click="viewGroupInfo(change.newGroup)" class="force-cursor">View new group</a></div>
<div><a ng-if="change.changeType =='Update'" ng-click="viewGroupInfo(change.oldGroup)" class="force-cursor">View old group</a></div>
</td>
<td>{{change.userName}}</td>
</tr>
</tbody>
</table>
<!-- PAGINATION -->
<div class="dataTables_paginate">
<ul class="pagination">
<li class="paginate_button previous">
<a ng-if="changePrevPageEnabled()" ng-click="changePrevPage()" class="paginate_button">Previous</a>
</li>
<li class="paginate_button next">
<a ng-if="changeNextPageEnabled()" ng-click="changeNextPage()" class="paginate_button">Next</a>
</li>
</ul>
</div>
<!-- END PAGINATION -->
</div>
<div class="panel-footer"></div>
</div>
<!-- END SIMPLE DATATABLE -->
</div>
</div>
<!-- END SIMPLE DATATABLE -->
</div>
</div>
<!-- END VERTICAL TABS -->
</div>
<!-- PAGE CONTENT WRAPPER -->
</div>
</div>
<!-- PAGE CONTENT WRAPPER -->
</div>
<!-- END PAGE CONTENT -->
<form name="viewGroupForm" role="form" class="form-horizontal" novalidate>
<modal modal-id="group_modal" modal-title="{{ groupModal.title }}">
<modal-body>
<modal-element label="Group ID">
<input id="create-group-id-text" type="text" name="groupID" class="form-control"
ng-model="currentGroup.id"
ng-class="groupModal.details.class"
ng-readonly="groupModal.details.readOnly"/>
</modal-element>
<modal-element label="Group Name">
<input id="create-group-name-text" type="text" name="groupName" class="form-control"
ng-model="currentGroup.name"
ng-class="groupModal.details.class"
ng-readonly="groupModal.details.readOnly"/>
</modal-element>
<modal-element label="Group Description">
<input id="create-group-description-text" type="text" name="groupDescription" class="form-control"
ng-model="currentGroup.description"
ng-class="groupModal.details.class"
ng-readonly="groupModal.details.readOnly"/>
</modal-element>
<modal-element label="Email">
<input id="create-group-email-text" type="text"
name="groupEmail"
class="form-control"
ng-model="currentGroup.email"
ng-class="groupModal.details.class"
ng-readonly="groupModal.details.readOnly"/>
</modal-element>
<modal-element label="Group Created">
<input id="create-group-created-text" type="text"
name="groupCreated"
class="form-control"
ng-model="currentGroup.created"
ng-class="groupModal.details.class"
ng-readonly="groupModal.details.readOnly"/>
</modal-element>
<modal-element label="Group Status">
<input id="create-group-status-text" type="text"
name="groupStatus"
class="form-control"
ng-model="currentGroup.status"
ng-class="groupModal.details.class"
ng-readonly="groupModal.details.readOnly"/>
</modal-element>
<modal-element label="Group Members IDs (one per line)">
<textarea id="create-group-members-ids-text"
name="groupMembers"
ng-model="currentGroup.memberIds"
rows="5"
class="form-control"
ng-list="&#10;"
ng-trim="false"
ng-class="groupModal.details.class"
ng-readonly="groupModal.details.readOnly">
</textarea>
</modal-element>
<modal-element label="Group Admins IDs (one per line)">
<textarea id="create-group-admins-ids-text"
name="groupAdmins"
ng-model="currentGroup.adminIds"
rows="5"
class="form-control"
ng-list="&#10;"
ng-trim="false"
ng-class="groupModal.details.class"
ng-readonly="groupModal.details.readOnly">
</textarea>
</modal-element>
</modal-body>
<modal-footer>
<span ng-if="groupModal.action == groupModalState.VIEW_DETAILS">
<button type="button" class="btn btn-default" data-dismiss="modal" ng-click="closeGroupModal()">Close</button>
</span>
</modal-footer>
</modal>
</form>
</div>
<!-- END PAGE CONTENT -->
}
@plugins = {

View File

@ -26,12 +26,12 @@
<!-- START VERTICAL TABS -->
<div class="panel panel-default panel-tabs">
<ul class="nav nav-tabs bar_tabs">
<li class="active"><a data-toggle="tab" ng-click="myGroups()">My Groups</a></li>
<li><a data-toggle="tab" ng-click="allGroups()">All Groups</a></li>
<li class="active"><a href="#myGroups" data-toggle="tab">My Groups</a></li>
<li><a id="tab2-button" href="#allGroups" data-toggle="tab">All Groups</a></li>
</ul>
<div class="panel-body tab-content">
<div class="tab-pane active" id="groups">
<div class="tab-pane active" id="myGroups">
<div class="row">
<div class="col-md-12">
@ -66,7 +66,22 @@
<div id="group-list" class="panel-body">
<p ng-if="!groupsLoaded">Loading groups...</p>
<p ng-if="haveNoGroups(groups.items.length)">You don't have any groups yet.</p>
<p ng-if="searchCriteria(groups.items.length)">No groups match the search criteria.</p>
<p ng-if="$scope.groupsLoaded && searchCriteria(groups.items.length)">No groups match the search criteria.</p>
<!-- PAGINATION -->
<div class="dataTables_paginate vinyldns_paginate">
<span class="vinyldns_page_number">{{ getGroupsPageNumber("myGroups") }}</span>
<ul class="pagination">
<li class="paginate_button previous">
<a ng-if="prevPageEnabled('myGroups')" ng-click="prevPageMyGroups()">Previous</a>
</li>
<li class="paginate_button next">
<a ng-if="nextPageEnabled('myGroups')" ng-click="nextPageMyGroups()">Next</a>
</li>
</ul>
</div>
<!-- END PAGINATION -->
<table class="table datatable_simple" ng-if="groups.items.length">
<thead>
<tr>
@ -96,6 +111,118 @@
</tr>
</tbody>
</table>
<!-- PAGINATION -->
<div class="dataTables_paginate vinyldns_paginate">
<span class="vinyldns_page_number">{{ getGroupsPageNumber("myGroups") }}</span>
<ul class="pagination">
<li class="paginate_button previous">
<a ng-if="prevPageEnabled('myGroups')" ng-click="prevPageMyGroups()">Previous</a>
</li>
<li class="paginate_button next">
<a ng-if="nextPageEnabled('myGroups')" ng-click="nextPageMyGroups()">Next</a>
</li>
</ul>
</div>
<!-- END PAGINATION -->
</div>
</div>
<!-- END SIMPLE DATATABLE -->
</div>
</div>
</div>
<div class="tab-pane" id="allGroups">
<div class="row">
<div class="col-md-12">
<!-- SIMPLE DATATABLE -->
<div class="panel panel-default">
<div class="panel-heading">
<div class="btn-group">
<button id="open-group-modal-button" class="btn btn-default" ng-click="openModal($event);">
<span class="fa fa-plus"></span> New Group
</button>
<button id="refresh-group-button" class="btn btn-default" ng-click="refresh();">
<span class="fa fa-refresh"></span> Refresh
</button>
</div>
<!-- SEARCH BOX -->
<div class="pull-right">
<form class="input-group" ng-submit="refresh()">
<div class="input-group">
<span class="input-group-btn">
<button id="group-search-button" type="submit" class="btn btn-primary btn-left-round">
<span class="fa fa-search"></span>
</button>
</span>
<input id="group-search-text" ng-model="query" type="text" class="form-control" placeholder="Group Name"/>
</div>
</form>
</div>
<!-- END SEARCH BOX -->
</div>
<div id="group-list" class="panel-body">
<p ng-if="!allGroupsLoaded">Loading groups...</p>
<p ng-if="$scope.allGroupsLoaded && searchCriteria(allGroups.items.length)">No groups match the search criteria.</p>
<!-- PAGINATION -->
<div class="dataTables_paginate vinyldns_paginate">
<span class="vinyldns_page_number">{{ getGroupsPageNumber("allGroups") }}</span>
<ul class="pagination">
<li class="paginate_button previous">
<a ng-if="prevPageEnabled('allGroups')" ng-click="prevPageAllGroups()">Previous</a>
</li>
<li class="paginate_button next">
<a ng-if="nextPageEnabled('allGroups')" ng-click="nextPageAllGroups()">Next</a>
</li>
</ul>
</div>
<!-- END PAGINATION -->
<table class="table datatable_simple" ng-if="allGroup.items.length">
<thead>
<tr>
<th>Group Name</th>
<th>Email</th>
<th>Description</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="group in allGroup.items | orderBy:'+name'">
<td class="wrap-long-text">
<a ng-href="/groups/{{group.id}}">{{group.name}}</a>
</td>
<td class="wrap-long-text">{{group.email}}</td>
<td class="wrap-long-text">{{group.description}}</td>
<td>
<div class="table-form-group">
<a class="btn btn-info btn-rounded" ng-href="/groups/{{group.id}}">
View</a>
<a ng-if="groupAdmin(group)" class="btn btn-warning btn-rounded" ng-click="editGroup(group);">
Edit</a>
<button ng-if="groupAdmin(group)" id="delete-group-{{group.name}}" class="btn btn-danger btn-rounded" ng-click="confirmDeleteGroup(group);">
Delete</button>
</div>
</td>
</tr>
</tbody>
</table>
<!-- PAGINATION -->
<div class="dataTables_paginate vinyldns_paginate">
<span class="vinyldns_page_number">{{ getGroupsPageNumber("allGroups") }}</span>
<ul class="pagination">
<li class="paginate_button previous">
<a ng-if="prevPageEnabled('allGroups')" ng-click="prevPageAllGroups()">Previous</a>
</li>
<li class="paginate_button next">
<a ng-if="nextPageEnabled('allGroups')" ng-click="nextPageAllGroups()">Next</a>
</li>
</ul>
</div>
<!-- END PAGINATION -->
</div>
</div>
<!-- END SIMPLE DATATABLE -->

View File

@ -1,6 +1,6 @@
@(rootAccountName: String)(implicit request: play.api.mvc.Request[Any])
<div class="top_nav">
<div class="top_nav" id="fixed-top-nav">
<div class="nav_menu">
<nav>
<div class="nav toggle">

View File

@ -16,10 +16,12 @@
<!-- CSS INCLUDE -->
<link rel="stylesheet" type="text/css" id="theme" href="/public/css/bootstrap.min.css"/>
<link rel="stylesheet" type="text/css" href="/public/css/jquery-ui.css">
<link rel="stylesheet" type="text/css" href="/public/css/font-awesome.min.css">
<link rel="stylesheet" type="text/css" href="/public/css/ui.css" />
<link rel="stylesheet" type="text/css" id="custom" href="/public/css/theme-overrides.css"/>
<link rel="stylesheet" type="text/css" id="custom" href="/public/css/vinyldns.css"/>
<link rel="stylesheet" type="text/css" href="/public/css/jquery-ui.css">
<!-- EOF CSS INCLUDE -->
</head>
@ -35,7 +37,7 @@
<div class="main_container" ng-cloak ng-controller="@controller">
<!-- START PAGE SIDEBAR -->
<div class="col-md-3 left_col">
<div class="col-md-3 left_col" id="fixed-side-menu">
<div class="left_col scroll-view">
<div class="navbar nav_title vinyldns-nav-title">
<a href="/index"><span><img src="/assets/images/vinyldns-portal.png" class="vinyldns-logo"/></span></a>
@ -149,6 +151,7 @@
<script src="/public/js/moment.min.js"></script>
<script src="/public/js/jquery.min.js"></script>
<script src="/public/js/jquery-ui.js"></script>
<script src="/public/js/bootstrap.min.js"></script>
<script src="/public/js/angular.min.js"></script>
<script src="/public/js/ui.js"></script>

View File

@ -41,7 +41,8 @@
<ul class="nav nav-tabs bar_tabs">
<li class="active"><a href="#tab1" data-toggle="tab">Manage Records</a></li>
<li><a id="tab2-button" href="#tab2" data-toggle="tab">Manage Zone</a></li>
<li><a href="#tab3" data-toggle="tab">Change History</a></li>
<li><a href="#tab3" data-toggle="tab">Record Change History</a></li>
<li><a href="#tab4" data-toggle="tab">Zone Change History</a></li>
</ul>
<div class="panel-body tab-content">
@ -54,6 +55,9 @@
<div class="tab-pane" id="tab3">
@changeHistory(request)
</div>
<div class="tab-pane" id="tab4" ng-controller="ManageZonesController">
@zoneChangeHistory(request)
</div>
</div>
</div>
<!-- END VERTICAL TABS -->

View File

@ -122,7 +122,7 @@
<tr ng-repeat="(recordName, record) in records track by $index">
<td>
<div ng-if="record.isDotted && record.type != 'TXT' && record.type != 'SRV' && record.type != 'NAPTR'" class="text-danger wrap-long-text" data-toggle="tooltip" data-placement="top"
title="Dotted hosts are invalid! Please delete or update without a '.'">
title="This is a dotted host!">
{{record.name}} <span class="fa fa-warning" />
</div>
<div class="wrap-long-text" ng-if="!record.isDotted || (record.type != 'TXT' || record.type != 'SRV' || record.type != 'NAPTR')">

View File

@ -0,0 +1,121 @@
@(implicit request: play.api.mvc.Request[Any])
<!-- START SIMPLE DATATABLE -->
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Zone Change History</h3>
</div>
<div class="panel-body">
<div class="btn-group">
<button class="btn btn-default" ng-click="refreshZoneChange()"><span class="fa fa-refresh"></span> Refresh</button>
</div>
<table id="zoneChangeDataTable" class="table table-hover table-striped">
<thead>
<tr>
<th>User name</th>
<th>Email</th>
<th>Access</th>
<th>Created</th>
<th>Updated</th>
<th>Change type</th>
<th>Admin group</th>
<th>ACL</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="zoneChange in zoneChanges track by $index">
<td>{{ zoneChange.userName }}</td>
<td>{{ zoneChange.zone.email }}</td>
<td>{{ zoneChange.zone.shared ? "Shared" : "Private" }}</td>
<td>{{ zoneChange.zone.created }}</td>
<td>{{ zoneChange.zone.updated }}</td>
<td>{{ zoneChange.changeType }}</td>
<td><a ng-bind="zoneChange.zone.adminGroupName" href="/groups/{{zoneChange.zone.adminGroupId}}"></a>
</td>
<td>
<button class="btn btn-info btn-sm"
ng-if="zoneChange.zone.acl.rules.length != 0"
ng-click="refreshAclRule($index)">ACL Rules
</button>
</td>
</tr>
</tbody>
</table>
<!-- PAGINATION -->
<div class="dataTables_paginate vinyldns_zones_paginate">
<span class="vinyldns_zones_page_number">{{ getZoneHistoryPageNumber() }}</span>
<ul class="pagination">
<li class="paginate_button previous">
<a ng-if="prevPageEnabled()" ng-click="prevPageZoneHistory()" class="paginate_button">Previous</a>
</li>
<li class="paginate_button next">
<a ng-if="nextPageEnabled()" ng-click="nextPageZoneHistory()" class="paginate_button">Next</a>
</li>
</ul>
</div>
<!-- END PAGINATION -->
</div>
</div>
<div class="panel-footer"></div>
<!-- END SIMPLE DATATABLE -->
<!-- THE ACL RULE MODAL FORM STARTS -->
<form name="aclModalViewForm" role="form" class="form-horizontal" novalidate>
<modal modal-id="aclModalView" modal-title="{{ aclRulesModal.title }}">
<modal-body>
<table id="aclRuleTable" class="table table-hover table-striped">
<thead>
<tr>
<th>User/Group</th>
<th>Access Level</th>
<th>Record Types</th>
<th>Record Mask</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="rule in allAclRules track by $index">
<td class="wrap-long-text">
<a ng-if="rule.groupId != undefined" href="/groups/{{rule.groupId}}">
{{rule.groupName}}
</a>
<span ng-if="rule.groupId == undefined">
{{rule.userName}}
</span>
</td>
<td>
{{rule.accessLevel}}
</td>
<td>
<span ng-if="rule.recordTypes.length == 0">All Types</span>
<ul class="table-cell-list">
<li ng-repeat="item in rule.recordTypes">
{{item}}
</li>
</ul>
</td>
<td class="wrap-long-text">
{{rule.recordMask}}
</td>
<td class="wrap-long-text">
{{rule.description}}
</td>
</tr>
</tbody>
</table>
</modal-body>
<modal-footer>
<span>
<button type="button" class="btn btn-default" data-dismiss="modal" ng-click="closeAclModalView()">Close</button>
</span>
</modal-footer>
</modal>
</form>
<!-- THE ACL RULE MODAL FORM ENDS -->

View File

@ -59,7 +59,12 @@
<span class="fa fa-search"></span>
</button>
</span>
<input id="zone-search-text" ng-model="query" type="text" class="form-control" placeholder="Zone Name"/>
<input id="zone-search-text" ng-model="query" type="text" class="form-control" placeholder="{{!searchByAdminGroup ? 'Zone Name' : 'Admin Group Name'}}"/>
</div>
<div class="checkbox pull-right">
<label>
<input type="checkbox" ng-model="searchByAdminGroup" ng-change="refreshZones()"> Search by admin group
</label>
</div>
</form>
</div>
@ -72,8 +77,8 @@
<p ng-if="hasZones && zonesLoaded && !zones.length">No zones match the search criteria.</p>
<!-- PAGINATION -->
<div class="dataTables_paginate vinyldns_zones_paginate">
<span class="vinyldns_zones_page_number">{{ getZonesPageNumber("myZones") }}</span>
<div class="dataTables_paginate vinyldns_paginate">
<span class="vinyldns_page_number">{{ getZonesPageNumber("myZones") }}</span>
<ul class="pagination">
<li class="paginate_button previous">
<a ng-if="prevPageEnabled('myZones')" ng-click="prevPageMyZones()">Previous</a>
@ -123,8 +128,8 @@
</table>
<!-- PAGINATION -->
<div class="dataTables_paginate vinyldns_zones_paginate">
<span class="vinyldns_zones_page_number">{{ getZonesPageNumber("myZones") }}</span>
<div class="dataTables_paginate vinyldns_paginate">
<span class="vinyldns_page_number">{{ getZonesPageNumber("myZones") }}</span>
<ul class="pagination">
<li class="paginate_button previous">
<a ng-if="prevPageEnabled('myZones')" ng-click="prevPageMyZones()">Previous</a>
@ -144,6 +149,9 @@
</div>
</div>
</div>
<div class="modal fade" id="loader" tabindex="-1" role="dialog" >
<div class="spinner" ></div>
</div>
<div class="tab-pane" id="allZones">
<div class="row">
<div class="col-md-12">
@ -165,8 +173,14 @@
<span class="fa fa-search"></span>
</button>
</span>
<input id="all-zones-search-text" ng-model="query" type="text" class="form-control" placeholder="Zone Name"/>
<input id="all-zones-search-text" ng-model="query" type="text" class="form-control" placeholder="{{!searchByAdminGroup ? 'Zone Name' : 'Admin Group Name'}}"/>
</div>
<div class="checkbox pull-right">
<label>
<input type="checkbox" ng-model="searchByAdminGroup" ng-change="refreshZones()"> Search by admin group
</label>
</div>
</form>
</div>
<!-- END SEARCH BOX -->
@ -177,8 +191,8 @@
<p ng-if="allZonesLoaded && !allZones.length">No zones match the search criteria.</p>
<!-- PAGINATION -->
<div class="dataTables_paginate vinyldns_zones_paginate">
<span class="vinyldns_zones_page_number">{{ getZonesPageNumber("allZones") }}</span>
<div class="dataTables_paginate vinyldns_paginate">
<span class="vinyldns_page_number">{{ getZonesPageNumber("allZones") }}</span>
<ul class="pagination">
<li class="paginate_button previous">
<a ng-if="prevPageEnabled('allZones')" ng-click="prevPageAllZones()">Previous</a>
@ -231,8 +245,8 @@
</table>
<!-- PAGINATION -->
<div class="dataTables_paginate vinyldns_zones_paginate">
<span class="vinyldns_zones_page_number">{{ getZonesPageNumber("allZones") }}</span>
<div class="dataTables_paginate vinyldns_paginate">
<span class="vinyldns_page_number">{{ getZonesPageNumber("allZones") }}</span>
<ul class="pagination">
<li class="paginate_button previous">
<a ng-if="prevPageEnabled('allZones')" ng-click="prevPageAllZones()">Previous</a>

View File

@ -30,6 +30,7 @@ GET /api/zones @controllers.VinylDNS.getZones
GET /api/zones/backendids @controllers.VinylDNS.getBackendIds
GET /api/zones/:id @controllers.VinylDNS.getZone(id: String)
GET /api/zones/name/:name @controllers.VinylDNS.getZoneByName(name: String)
GET /api/zones/:id/changes @controllers.VinylDNS.getZoneChange(id: String)
POST /api/zones @controllers.VinylDNS.addZone
PUT /api/zones/:id @controllers.VinylDNS.updateZone(id: String)
DELETE /api/zones/:id @controllers.VinylDNS.deleteZone(id: String)
@ -44,6 +45,8 @@ GET /api/zones/:id/recordsetchanges @controllers.VinylDNS.listRecor
GET /api/groups @controllers.VinylDNS.getGroups
GET /api/groups/:gid @controllers.VinylDNS.getGroup(gid: String)
GET /api/groups/:gid/groupchanges @controllers.VinylDNS.listGroupChanges(gid: String)
GET /api/groups/change/:gcid @controllers.VinylDNS.getGroupChange(gcid: String)
POST /api/groups @controllers.VinylDNS.newGroup
PUT /api/groups/:gid @controllers.VinylDNS.updateGroup(gid: String)
DELETE /api/groups/:gid @controllers.VinylDNS.deleteGroup(gid: String)

View File

@ -15,6 +15,8 @@ module.exports = function(config) {
// list of files / patterns to load in the browser
files: [
'js/jquery.min.js',
'js/jquery-ui-dist.js',
'js/jquery-ui.js',
'js/bootstrap.min.js',
'js/angular.min.js',
'js/moment.min.js',

View File

@ -24,6 +24,7 @@
"jasmine-core": "^2.99.1",
"jasmine-jquery": "2.1.1",
"jquery": "^3.6.0",
"jquery-ui-dist": "^1.13.1",
"karma": "^6.3.17",
"karma-chrome-launcher": "^2.2.0",
"karma-jasmine": "^1.0.2",

View File

@ -104,7 +104,7 @@ a.action-link {
top: 3px;
left: 50%;
width: 40%;
z-index: 100;
z-index: 2000;
}
.dns-connection-form {
@ -434,12 +434,12 @@ input[type="file"] {
vertical-align: top;
}
.vinyldns_zones_paginate {
.vinyldns_paginate {
display: flex;
align-items: center;
}
.vinyldns_zones_page_number {
.vinyldns_page_number {
margin-right: 10px;
}
@ -491,3 +491,34 @@ input[type="file"] {
.modal-backdrop.show {
opacity: 0.75;
}
.ui-menu .ui-menu-item div {
background: white;
width: 100%;
font-size: 14px;
color: black;
border: none;
border-left: 1px;
}
.ui-menu .ui-menu-item div:hover {
background: #f5f5f5;
}
.ui-autocomplete {
max-height: 200px;
width:150px;
overflow-y: auto;
overflow-x: hidden;
}
#fixed-side-menu {
position: fixed;
top: 0;
}
#fixed-top-nav {
position: sticky;
top: 0;
z-index: 1000;
}

View File

@ -14,19 +14,25 @@
* limitations under the License.
*/
angular.module('controller.groups', []).controller('GroupsController', function ($scope, $log, $location, groupsService, profileService, utilityService) {
angular.module('controller.groups', []).controller('GroupsController', function ($scope, $log, $location, groupsService, profileService, utilityService, pagingService, $timeout) {
//registering bootstrap modal close event to refresh data after create group action
angular.element('#modal_new_group').one('hide.bs.modal', function () {
$scope.closeModal();
});
$scope.groups = {items: []};
$scope.allGroup = {items: []};
$scope.groupsLoaded = false;
$scope.allGroupsLoaded = false;
$scope.alerts = [];
$scope.ignoreAccess = false;
$scope.hasGroups = false; // Re-assigned each time groups are fetched without a query
$scope.hasGroups = false;
$scope.query = "";
// Paging status for group sets
var groupsPaging = pagingService.getNewPagingParams(100);
var allGroupsPaging = pagingService.getNewPagingParams(100);
function handleError(error, type) {
var alert = utilityService.failure(error, type);
$scope.alerts.push(alert);
@ -64,6 +70,44 @@ angular.module('controller.groups', []).controller('GroupsController', function
return true;
};
// Autocomplete for group search
$("#group-search-text").autocomplete({
source: function( request, response ) {
$.ajax({
url: "/api/groups?maxItems=100&abridged=true",
dataType: "json",
data: {groupNameFilter: request.term, ignoreAccess: $scope.ignoreAccess},
success: function(data) {
const search = JSON.parse(JSON.stringify(data));
response($.map(search.groups, function(group) {
return {value: group.name, label: group.name}
}))
}
});
},
minLength: 1,
select: function (event, ui) {
$scope.query = ui.item.value;
$("#group-search-text").val(ui.item.value);
return false;
},
open: function() {
$(this).removeClass("ui-corner-all").addClass("ui-corner-top");
},
close: function() {
$(this).removeClass("ui-corner-top").addClass("ui-corner-all");
}
});
// Autocomplete text-highlight
$.ui.autocomplete.prototype._renderItem = function(ul, item) {
let txt = String(item.label).replace(new RegExp(this.term, "gi"),"<b>$&</b>");
return $("<li></li>")
.data("ui-autocomplete-item", item.value)
.append("<div>" + txt + "</div>")
.appendTo(ul);
};
$scope.createGroup = function (name, email, description) {
//prevent user executing service call multiple times
//if true prevent, if false allow for execution of rest of code
@ -104,30 +148,33 @@ angular.module('controller.groups', []).controller('GroupsController', function
});
};
$scope.allGroups = function () {
$scope.ignoreAccess = true;
$scope.refresh();
}
$scope.myGroups = function () {
$scope.ignoreAccess = false;
$scope.refresh();
}
$scope.refresh = function () {
function success(result) {
$log.log('getGroups:refresh-success', result);
//update groups
$scope.groups.items = result.groups;
$scope.groupsLoaded = true;
if (!$scope.query.length) {
$scope.hasGroups = $scope.groups.items.length > 0;
}
return result;
}
groupsPaging = pagingService.resetPaging(groupsPaging);
allGroupsPaging = pagingService.resetPaging(allGroupsPaging);
getGroupsAbridged($scope.ignoreAccess)
.then(success)
groupsService
.getGroupsAbridged(groupsPaging.maxItems, undefined, false, $scope.query)
.then(function (result) {
$log.debug('getGroups:refresh-success', result);
//update groups
groupsPaging.next = result.data.nextId;
updateGroupDisplay(result.data.groups);
if (!$scope.query.length) {
$scope.hasGroups = $scope.groups.items.length > 0;
}
})
.catch(function (error) {
handleError(error, 'getGroups::refresh-failure');
});
groupsService
.getGroupsAbridged(allGroupsPaging.maxItems, undefined, true, $scope.query)
.then(function (result) {
$log.debug('getGroups:refresh-success', result);
//update groups
allGroupsPaging.next = result.data.nextId;
updateAllGroupDisplay(result.data.groups);
})
.catch(function (error) {
handleError(error, 'getGroups::refresh-failure');
});
@ -161,20 +208,6 @@ angular.module('controller.groups', []).controller('GroupsController', function
});
}
function getGroupsAbridged() {
function success(response) {
$log.log('groupsService::getGroups-success');
return response.data;
}
return groupsService
.getGroupsAbridged($scope.ignoreAccess, $scope.query)
.then(success)
.catch(function (error) {
handleError(error, 'groupsService::getGroups-failure');
});
}
// Return true if there are no groups created by the user
$scope.haveNoGroups = function (groupLength) {
if (!$scope.hasGroups && !groupLength && $scope.groupsLoaded && $scope.query.length == "") {
@ -186,7 +219,7 @@ angular.module('controller.groups', []).controller('GroupsController', function
// Return true if no groups are found related to the search query
$scope.searchCriteria = function (groupLength) {
if ($scope.groupsLoaded && !groupLength && $scope.query.length != "") {
if (!groupLength && $scope.query.length != "") {
return true
} else {
return false
@ -298,4 +331,115 @@ angular.module('controller.groups', []).controller('GroupsController', function
.then(profileSuccess, profileFailure)
.catch(profileFailure);
function updateGroupDisplay (groups) {
$scope.groups.items = groups;
$scope.groupsLoaded = true;
$log.debug("Displaying my groups: ", $scope.groups.items);
if($scope.groups.items.length > 0) {
$("td.dataTables_empty").hide();
} else {
$("td.dataTables_empty").show();
}
}
function updateAllGroupDisplay (groups) {
$scope.allGroup.items = groups;
$scope.allGroupsLoaded = true;
$log.debug("Displaying all groups: ", $scope.allGroup.items);
if($scope.allGroup.items.length > 0) {
$("td.dataTables_empty").hide();
} else {
$("td.dataTables_empty").show();
}
}
/*
* Group set paging
*/
$scope.getGroupsPageNumber = function(tab) {
switch(tab) {
case 'myGroups':
return pagingService.getPanelTitle(groupsPaging);
case 'allGroups':
return pagingService.getPanelTitle(allGroupsPaging);
}
};
$scope.prevPageEnabled = function(tab) {
switch(tab) {
case 'myGroups':
return pagingService.prevPageEnabled(groupsPaging);
case 'allGroups':
return pagingService.prevPageEnabled(allGroupsPaging);
}
};
$scope.nextPageEnabled = function(tab) {
switch(tab) {
case 'myGroups':
return pagingService.nextPageEnabled(groupsPaging);
case 'allGroups':
return pagingService.nextPageEnabled(allGroupsPaging);
}
};
$scope.prevPageMyGroups = function() {
var startFrom = pagingService.getPrevStartFrom(groupsPaging);
return groupsService
.getGroupsAbridged(groupsPaging.maxItems, startFrom, false, $scope.query)
.then(function(response) {
groupsPaging = pagingService.prevPageUpdate(response.data.nextId, groupsPaging);
updateGroupDisplay(response.data.groups);
})
.catch(function (error) {
handleError(error,'groupsService::prevPageMyGroups-failure');
});
}
$scope.prevPageAllGroups = function() {
var startFrom = pagingService.getPrevStartFrom(allGroupsPaging);
return groupsService
.getGroupsAbridged(allGroupsPaging.maxItems, startFrom, true, $scope.query)
.then(function(response) {
allGroupsPaging = pagingService.prevPageUpdate(response.data.nextId, allGroupsPaging);
updateAllGroupDisplay(response.data.groups);
})
.catch(function (error) {
handleError(error,'groupsService::prevPageAllGroups-failure');
});
}
$scope.nextPageMyGroups = function () {
return groupsService
.getGroupsAbridged(groupsPaging.maxItems, groupsPaging.next, false, $scope.query)
.then(function(response) {
var groupSets = response.data.groups;
groupsPaging = pagingService.nextPageUpdate(groupSets, response.data.nextId, groupsPaging);
if (groupSets.length > 0) {
updateGroupDisplay(response.data.groups);
}
})
.catch(function (error) {
handleError(error,'groupsService::nextPageMyGroups-failure')
});
};
$scope.nextPageAllGroups = function () {
return groupsService
.getGroupsAbridged(allGroupsPaging.maxItems, allGroupsPaging.next, true, $scope.query)
.then(function(response) {
var groupSets = response.data.groups;
allGroupsPaging = pagingService.nextPageUpdate(groupSets, response.data.nextId, allGroupsPaging);
if (groupSets.length > 0) {
updateAllGroupDisplay(response.data.groups);
}
})
.catch(function (error) {
handleError(error,'groupsService::nextPageAllGroups-failure')
});
};
$timeout($scope.refresh, 0);
});

View File

@ -19,14 +19,16 @@ describe('Controller: GroupsController', function () {
module('ngMock'),
module('service.groups'),
module('service.profile'),
module('service.utility')
module('service.utility'),
module('service.paging'),
module('controller.groups')
});
beforeEach(inject(function ($rootScope, $controller, $q, groupsService, profileService, utilityService) {
beforeEach(inject(function ($rootScope, $controller, $q, groupsService, profileService, utilityService, pagingService) {
this.scope = $rootScope.$new();
this.groupsService = groupsService;
this.utilityService = utilityService;
this.q = $q;
this.pagingService = pagingService;
profileService.getAuthenticatedUserData = function() {
return $q.when('data')
@ -38,6 +40,15 @@ describe('Controller: GroupsController', function () {
}
})
};
groupsService.getGroupsAbridged = function () {
return $q.when({
data: {
groups: ["all my groups"]
}
});
};
this.controller = $controller('GroupsController', {'$scope': this.scope});
this.mockSuccessAlert = 'success';
@ -64,7 +75,7 @@ describe('Controller: GroupsController', function () {
this.scope.refresh();
this.scope.$digest();
expect(getGroups.calls.count()).toBe(1);
expect(getGroups.calls.count()).toBe(2);
expect(this.scope.groups.items).toBe("all my groups");
});
@ -111,4 +122,110 @@ describe('Controller: GroupsController', function () {
expect(this.utilityFailure.calls.count()).toBe(1);
expect(this.scope.alerts).toEqual([this.mockFailureAlert]);
});
it('nextPageMyGroups should call getGroupsAbridged with the correct parameters', function () {
var response = {
data: {
groups: "all my groups"
}
};
var getGroupSets = spyOn(this.groupsService, 'getGroupsAbridged')
.and.stub()
.and.returnValue(this.q.when(response));
var expectedMaxItems = 100;
var expectedStartFrom = undefined;
var expectedQuery = this.scope.query;
var expectedIgnoreAccess = false;
this.scope.nextPageMyGroups();
expect(getGroupSets.calls.count()).toBe(1);
expect(getGroupSets.calls.mostRecent().args).toEqual(
[expectedMaxItems, expectedStartFrom, expectedIgnoreAccess, expectedQuery]);
});
it('prevPageMyGroups should call getGroupsAbridged with the correct parameters', function () {
var response = {
data: {
groups: "all my groups"
}
};
var getGroupSets = spyOn(this.groupsService, 'getGroupsAbridged')
.and.stub()
.and.returnValue(this.q.when(response));
var expectedMaxItems = 100;
var expectedStartFrom = undefined;
var expectedQuery = this.scope.query;
var expectedIgnoreAccess = false;
this.scope.prevPageMyGroups();
expect(getGroupSets.calls.count()).toBe(1);
expect(getGroupSets.calls.mostRecent().args).toEqual(
[expectedMaxItems, expectedStartFrom, expectedIgnoreAccess, expectedQuery]);
this.scope.nextPageMyGroups();
this.scope.prevPageMyGroups();
expect(getGroupSets.calls.count()).toBe(3);
expect(getGroupSets.calls.mostRecent().args).toEqual(
[expectedMaxItems, expectedStartFrom, expectedIgnoreAccess, expectedQuery]);
});
it('nextPageAllGroups should call getGroupsAbridged with the correct parameters', function () {
var response = {
data: {
groups: "all groups"
}
};
var getGroupSets = spyOn(this.groupsService, 'getGroupsAbridged')
.and.stub()
.and.returnValue(this.q.when(response));
var expectedMaxItems = 100;
var expectedStartFrom = undefined;
var expectedQuery = this.scope.query;
var expectedIgnoreAccess = true;
this.scope.nextPageAllGroups();
expect(getGroupSets.calls.count()).toBe(1);
expect(getGroupSets.calls.mostRecent().args).toEqual(
[expectedMaxItems, expectedStartFrom, expectedIgnoreAccess, expectedQuery]);
});
it('prevPageAllGroups should call getGroupsAbridged with the correct parameters', function () {
var response = {
data: {
groups: "all groups"
}
};
var getGroupSets = spyOn(this.groupsService, 'getGroupsAbridged')
.and.stub()
.and.returnValue(this.q.when(response));
var expectedMaxItems = 100;
var expectedStartFrom = undefined;
var expectedQuery = this.scope.query;
var expectedIgnoreAccess = true;
this.scope.prevPageAllGroups();
expect(getGroupSets.calls.count()).toBe(1);
expect(getGroupSets.calls.mostRecent().args).toEqual(
[expectedMaxItems, expectedStartFrom, expectedIgnoreAccess, expectedQuery]);
this.scope.nextPageAllGroups();
this.scope.prevPageAllGroups();
expect(getGroupSets.calls.count()).toBe(3);
expect(getGroupSets.calls.mostRecent().args).toEqual(
[expectedMaxItems, expectedStartFrom, expectedIgnoreAccess, expectedQuery]);
});
});

View File

@ -16,7 +16,7 @@
angular.module('controller.manageZones', [])
.controller('ManageZonesController', function ($scope, $timeout, $log, recordsService, zonesService, groupsService,
profileService, utilityService) {
profileService, utilityService, pagingService) {
groupsService.getGroupsStored()
.then(function (results) {
@ -38,6 +38,7 @@ angular.module('controller.manageZones', [])
$scope.alerts = [];
$scope.zoneInfo = {};
$scope.zoneChanges = {};
$scope.updateZoneInfo = {};
$scope.manageZoneState = {
UPDATE: 0,
@ -60,7 +61,8 @@ angular.module('controller.manageZones', [])
CREATE: 0,
UPDATE: 1,
CONFIRM_UPDATE: 2,
CONFIRM_DELETE: 3
CONFIRM_DELETE: 3,
VIEW_DETAILS: 4
};
$scope.aclModalParams = {
readOnly: {
@ -74,6 +76,8 @@ angular.module('controller.manageZones', [])
};
$scope.aclRecordTypes = ['A', 'AAAA', 'CNAME', 'DS', 'MX', 'NS', 'PTR', 'SRV', 'NAPTR', 'SSHFP', 'TXT'];
var zoneHistoryPaging = pagingService.getNewPagingParams(100);
/**
* Zone modal control functions
*/
@ -276,6 +280,7 @@ angular.module('controller.manageZones', [])
$scope.updateZoneInfo.hiddenTransferKey = '';
$scope.currentManageZoneState = $scope.manageZoneState.UPDATE;
$scope.refreshAclRuleDisplay();
$scope.refreshZoneChange();
}
return recordsService
.getZone($scope.zoneId)
@ -285,6 +290,53 @@ angular.module('controller.manageZones', [])
});
};
$scope.refreshZoneChange = function() {
zoneHistoryPaging = pagingService.resetPaging(zoneHistoryPaging);
function success(response) {
$log.log('zonesService::getZoneChanges-success');
zoneHistoryPaging.next = response.data.nextId;
$scope.zoneChanges = response.data.zoneChanges;
$scope.updateZoneChangeDisplay(response.data.zoneChanges);
}
return zonesService
.getZoneChanges(zoneHistoryPaging.maxItems, undefined, $scope.zoneId)
.then(success)
.catch(function (error) {
handleError(error, 'zonesService::getZoneChanges-failure');
});
};
$scope.refreshAclRule = function (index) {
$scope.allAclRules = [];
$scope.aclRulesModal = {
action: $scope.aclModalState.VIEW_DETAILS,
title: "ACL Rules Info",
basics: $scope.aclModalParams.readOnly,
details: $scope.aclModalParams.readOnly,
};
if ($scope.zoneChanges[index].zone.acl.rules.length!=0){
for (var length = 0; length < $scope.zoneChanges[index].zone.acl.rules.length; length++) {
$scope.allAclRules.push($scope.zoneChanges[index].zone.acl.rules[length]);
if ($scope.allAclRules[length].hasOwnProperty('userId')){
getAclUser($scope.allAclRules[length].userId, length); }
else{ getAclGroup($scope.allAclRules[length].groupId, length);}
}
$scope.aclModalViewForm.$setPristine();
$("#aclModalView").modal("show");}
else{$("#aclModalView").modal("hide");}
};
$scope.closeAclModalView = function() {
$scope.aclModalViewForm.$setPristine();
};
$scope.updateZoneChangeDisplay = function (zoneChange) {
for (var length = 0; length < zoneChange.length; length++) {
getZoneGroup(zoneChange[length].zone.adminGroupId, length);
getZoneUser(zoneChange[length].userId, length);
}
};
$scope.refreshAclRuleDisplay = function() {
$scope.aclRules = [];
angular.forEach($scope.zoneInfo.acl.rules, function (rule) {
@ -292,6 +344,109 @@ angular.module('controller.manageZones', [])
});
};
/**
* Get User name and Group Name with Ids for Zone history
*/
function getZoneGroup(groupId, length) {
function success(response) {
$log.log('groupsService::getZoneGroup-success');
$scope.zoneChanges[length].zone.adminGroupName = response.data.name;
}
return groupsService
.getGroup(groupId)
.then(success)
.catch(function (error) {
handleError(error, 'groupsService::getZoneGroup-failure');
});
}
function getZoneUser(userId, length) {
function success(response) {
$log.log('profileService::getZoneUserDataById-success');
$scope.zoneChanges[length].userName = response.data.userName;
}
return profileService
.getUserDataById(userId)
.then(success)
.catch(function (error) {
handleError(error, 'profileService::getZoneUserDataById-failure');
});
};
function getAclGroup(groupId, length) {
function success(response) {
$log.log('groupsService::getAclGroup-success');
$scope.allAclRules[length].groupName = response.data.name;
}
return groupsService
.getGroup(groupId)
.then(success)
.catch(function (error) {
handleError(error, 'groupsService::getAclGroup-failure');
});
}
function getAclUser(userId, length) {
function success(response) {
$log.log('profileService::getAclUserDataById-success');
$scope.allAclRules[length].userName = response.data.userName;
}
return profileService
.getUserDataById(userId)
.then(success)
.catch(function (error) {
handleError(error, 'profileService::getAclUserDataById-failure');
});
};
/**
* Zone history Pagination
*/
$scope.getZoneHistoryPageNumber = function() {
return pagingService.getPanelTitle(zoneHistoryPaging);
};
$scope.prevPageEnabled = function() {
return pagingService.prevPageEnabled(zoneHistoryPaging);
};
$scope.nextPageEnabled = function(tab) {
return pagingService.nextPageEnabled(zoneHistoryPaging);
};
$scope.nextPageZoneHistory = function () {
return zonesService
.getZoneChanges(zoneHistoryPaging.maxItems, zoneHistoryPaging.next, $scope.zoneId )
.then(function(response) {
var zoneChanges = response.data.zoneChanges;
zoneHistoryPaging = pagingService.nextPageUpdate(zoneChanges, response.data.nextId, zoneHistoryPaging);
if (zoneChanges.length > 0) {
$scope.zoneChanges = response.data.zoneChanges;
$scope.updateZoneChangeDisplay(response.data.zoneChanges)
}
})
.catch(function (error) {
handleError(error,'zonesService::nextPage-failure')
});
};
$scope.prevPageZoneHistory = function() {
var startFrom = pagingService.getPrevStartFrom(zoneHistoryPaging);
return zonesService
.getZoneChanges(zoneHistoryPaging.maxItems, startFrom, $scope.zoneId )
.then(function(response) {
zoneHistoryPaging = pagingService.prevPageUpdate(response.data.nextId, zoneHistoryPaging);
$scope.zoneChanges = response.data.zoneChanges;
$scope.updateZoneChangeDisplay(response.data.zoneChanges);
})
.catch(function (error) {
handleError(error,'zonesService::prevPage-failure');
});
};
/**
* Service interaction functions
*/

View File

@ -22,16 +22,18 @@ describe('Controller: ManageZonesController', function () {
module('service.utility'),
module('service.zones'),
module('service.profile'),
module('service.paging'),
module('controller.manageZones')
});
beforeEach(inject(function ($rootScope, $controller, $q, groupsService, recordsService, zonesService,
profileService) {
profileService, pagingService) {
this.rootScope = $rootScope;
this.scope = $rootScope.$new();
this.groupsService = groupsService;
this.zonesService = zonesService;
this.recordsService = recordsService;
this.profileService = profileService;
this.pagingService = pagingService;
this.q = $q;
this.groupsService.getGroups = function () {
return $q.when({
@ -205,11 +207,14 @@ describe('Controller: ManageZonesController', function () {
}
}
};
var getZone = spyOn(this.recordsService, 'getZone')
.and.stub()
.and.returnValue(this.q.when(mockResponse));
var refreshAclRuleDisplay = spyOn(this.scope, 'refreshAclRuleDisplay')
.and.stub();
var refreshZoneChange = spyOn(this.scope, 'refreshZoneChange')
.and.stub();
this.scope.currentManageZoneState = this.scope.manageZoneState.CONFIRM_UPDATE;
this.scope.updateZoneInfo.hiddenKey = 'some key';
this.scope.updateZoneInfo.hiddenTransferKey = 'some key';
@ -217,6 +222,7 @@ describe('Controller: ManageZonesController', function () {
this.scope.$digest();
expect(getZone.calls.count()).toBe(1);
expect(refreshAclRuleDisplay.calls.count()).toBe(1);
expect(refreshZoneChange.calls.count()).toBe(1);
expect(this.scope.zoneInfo).toEqual(mockResponse.data.zone);
expect(this.scope.updateZoneInfo. adminGroupId).toEqual('id101112');
expect(this.scope.updateZoneInfo.hiddenKey).toEqual('');
@ -554,4 +560,99 @@ describe('Controller: ManageZonesController', function () {
expect(toDisplayAclRule.calls.count()).toBe(3);
expect(this.scope.aclRules).toEqual(this.scope.zoneInfo.acl.rules);
});
it('next page should call listZoneChangesByZoneId with the correct parameters', function () {
var mockZoneChange = {data: {
zoneId: "c5c87405-2ec8-4e03-b2dc-c6758a5d9666",
zoneChanges: [{ zone: {
name: "dummy.",
email: "test@test.com",
status: "Active",
created: "2017-02-15T14:58:39Z",
account: "c8234503-bfda-4b80-897f-d74129051eaa",
acl: {rules: []},
adminGroupId: "c8234503-bfda-4b80-897f-d74129051eaa",
id: "c5c87405-2ec8-4e03-b2dc-c6758a5d9666",
shared: false,
status: "Active",
latestSync: "2017-02-15T14:58:39Z",
isTest: true
}}],maxItems: 100}};
var getZoneChanges = spyOn(this.zonesService, 'getZoneChanges')
.and.stub()
.and.returnValue(this.q.when(mockZoneChange));
var expectedMaxItems = 100;
var expectedStartFrom = undefined;
var expectedZoneId = this.scope.zoneId;
this.scope.nextPageZoneHistory();
expect(getZoneChanges.calls.count()).toBe(1);
expect(getZoneChanges.calls.mostRecent().args).toEqual(
[expectedMaxItems, expectedStartFrom, expectedZoneId]);
});
it('prev page should call getZoneChanges with the correct parameters', function () {
var mockZoneChange = {data: {
zoneId: "c5c87405-2ec8-4e03-b2dc-c6758a5d9666",
zoneChanges: [{ zone: {
name: "dummy.",
email: "test@test.com",
status: "Active",
created: "2017-02-15T14:58:39Z",
account: "c8234503-bfda-4b80-897f-d74129051eaa",
acl: {rules: []},
adminGroupId: "c8234503-bfda-4b80-897f-d74129051eaa",
id: "c5c87405-2ec8-4e03-b2dc-c6758a5d9666",
shared: false,
status: "Active",
latestSync: "2017-02-15T14:58:39Z",
isTest: true
}}],maxItems: 100}};
var getZoneChanges = spyOn(this.zonesService, 'getZoneChanges')
.and.stub()
.and.returnValue(this.q.when(mockZoneChange));
var expectedMaxItems = 100;
var expectedStartFrom = undefined;
var expectedZoneId = this.scope.zoneId;
this.scope.prevPageZoneHistory();
expect(getZoneChanges.calls.count()).toBe(1);
expect(getZoneChanges.calls.mostRecent().args).toEqual(
[expectedMaxItems, expectedStartFrom, expectedZoneId]);
});
it('test that we properly get Zone History data', function(){
this.scope.zoneChanges = {};
var mockZoneChange = {data: {
zoneId: "c5c87405-2ec8-4e03-b2dc-c6758a5d9666",
zoneChanges: [{ zone: {
name: "dummy.",
email: "test@test.com",
status: "Active",
created: "2017-02-15T14:58:39Z",
account: "c8234503-bfda-4b80-897f-d74129051eaa",
acl: {rules: []},
adminGroupId: "c8234503-bfda-4b80-897f-d74129051eaa",
id: "c5c87405-2ec8-4e03-b2dc-c6758a5d9666",
shared: false,
status: "Active",
latestSync: "2017-02-15T14:58:39Z",
isTest: true
}}],maxItems: 100}};
var updateZoneChangeDisplay = spyOn(this.scope, 'updateZoneChangeDisplay')
.and.stub();
var getZoneChanges = spyOn(this.zonesService, 'getZoneChanges')
.and.stub()
.and.returnValue(this.q.when(mockZoneChange));
this.scope.refreshZoneChange();
this.scope.$digest();
expect(getZoneChanges.calls.count()).toBe(1);
expect(this.scope.zoneChanges).toEqual(mockZoneChange.data.zoneChanges);
});
});

View File

@ -14,13 +14,35 @@
* limitations under the License.
*/
angular.module('controller.membership', []).controller('MembershipController', function ($scope, $log, $location, $timeout,
angular.module('controller.membership', []).controller('MembershipController', function ($scope, $log, $location, $sce, $timeout, pagingService,
groupsService, profileService, utilityService) {
$scope.membership = { members: [], group: {} };
$scope.membershipLoaded = false;
$scope.alerts = [];
$scope.isGroupAdmin = false;
$scope.groupChanges = {};
$scope.currentGroup = {};
$scope.groupModalState = {
VIEW_DETAILS: 1
};
// read-only data for setting various classes/attributes in group modal
$scope.groupModalParams = {
readOnly: {
class: "",
readOnly: true
}
};
$scope.changeMessage = function (groupChangeMessage) {
message = groupChangeMessage.replaceAll('. ', '.<br>')
return $sce.trustAsHtml(message);
};
// paging status for group changes
var changePaging = pagingService.getNewPagingParams(100);
function handleError(error, type) {
var alert = utilityService.failure(error, type);
@ -207,8 +229,113 @@ angular.module('controller.membership', []).controller('MembershipController', f
$scope.resetNewMemberData();
$scope.getGroupInfo(id);
$scope.refreshGroupChanges(id);
};
$scope.refreshGroupChanges = function(id) {
if(!id){
var id = $location.absUrl().toString();
id = id.substring(id.lastIndexOf('/') + 1);
id = id.substring(0, id.indexOf('#'))
}
$log.debug('refreshGroupChanges, loading group with id ', id);
changePaging = pagingService.resetPaging(changePaging);
function success(response) {
$log.debug('groupsService::getGroupChanges-success');
changePaging.next = response.data.nextId;
updateChangeDisplay(response.data.changes)
}
return groupsService
.getGroupChanges(id, changePaging.maxItems, undefined)
.then(success)
.catch(function (error){
handleError(error, 'groupsService::getGroupChanges-failure');
});
};
function updateChangeDisplay(changes) {
var newChanges = [];
angular.forEach(changes, function(change) {
newChanges.push(change);
});
$scope.groupChanges = newChanges;
}
/**
* Group change paging
*/
$scope.getChangePageTitle = function() {
return pagingService.getPanelTitle(changePaging);
};
$scope.changePrevPageEnabled = function() {
return pagingService.prevPageEnabled(changePaging);
};
$scope.changeNextPageEnabled = function() {
return pagingService.nextPageEnabled(changePaging);
};
$scope.changePrevPage = function() {
var startFrom = pagingService.getPrevStartFrom(changePaging);
var id = $location.absUrl().toString();
id = id.substring(id.lastIndexOf('/') + 1);
id = id.substring(0, id.indexOf('#'))
$log.debug('changePrevPage, loading group with id ', id);
return groupsService
.getGroupChanges(id, changePaging.maxItems, startFrom)
.then(function(response) {
changePaging = pagingService.prevPageUpdate(response.data.nextId, changePaging);
updateChangeDisplay(response.data.changes);
})
.catch(function (error) {
handleError(error, 'groupsService::changePrevPage-failure');
});
};
$scope.changeNextPage = function() {
var id = $location.absUrl().toString();
id = id.substring(id.lastIndexOf('/') + 1);
id = id.substring(0, id.indexOf('#'))
$log.debug('changeNextPage, loading group with id ', id);
return groupsService
.getGroupChanges(id, changePaging.maxItems, changePaging.next)
.then(function(response) {
var changes = response.data.changes;
changePaging = pagingService.nextPageUpdate(changes, response.data.nextId, changePaging);
if(changes.length > 0 ){
updateChangeDisplay(changes);
}
})
.catch(function (error) {
handleError(error, 'groupsService::changeNextPage-failure');
});
};
$scope.viewGroupInfo = function(group) {
var newGroup = angular.copy(group);
newGroup.adminIds = [];
angular.forEach(group.admins, function(admin) {
newGroup.adminIds.push(admin.id);
});
newGroup.memberIds = [];
angular.forEach(group.members, function(member) {
newGroup.memberIds.push(member.id);
});
$scope.currentGroup = newGroup;
$scope.groupModal = {
action: $scope.groupModalState.VIEW_DETAILS,
title: "Group Info",
basics: $scope.groupModalParams.readOnly,
details: $scope.groupModalParams.readOnly,
};
$("#group_modal").modal("show");
};
$scope.closeGroupModal = function() {
$scope.viewGroupForm.$setPristine();
};
$timeout($scope.refresh, 0);
});

View File

@ -20,14 +20,16 @@ describe('Controller: MembershipController', function () {
module('service.groups'),
module('service.profile'),
module('service.utility'),
module('service.paging'),
module('controller.membership')
});
beforeEach(inject(function ($rootScope, $controller, $q, groupsService, profileService, utilityService) {
beforeEach(inject(function ($rootScope, $controller, $q, groupsService, profileService, utilityService, pagingService) {
this.rootScope = $rootScope;
this.scope = $rootScope.$new();
this.groupsService = groupsService;
this.profileService = profileService;
this.utilityService = utilityService;
this.pagingService = pagingService;
this.q = $q;
var mockGroup = {
data: {
@ -68,6 +70,47 @@ describe('Controller: MembershipController', function () {
this.groupsService.getGroupMemberList = function() {
return $q.when(mockGroupList);
};
this.groupsService.getGroupChanges = function () {
return $q.when({
data: {
changes: [
{
newGroup: {
id: "f9329f39-595d-45c9-8cdf-ac36e96e085d",
name: "test-group",
email: "test@test.com",
created: "2022-07-20T10:14:49Z",
status: "Active",
members: [
{
id: "ea7ec24e-3cc2-4740-b1b8-acde0158271f"
},
{
id: "5bda099e-be26-4aac-a310-ecf221ee2451"
}
],
admins: [
{
id: "ea7ec24e-3cc2-4740-b1b8-acde0158271f"
},
{
id: "5bda099e-be26-4aac-a310-ecf221ee2451"
}
]
},
changeType: "Delete",
userId: "ea7ec24e-3cc2-4740-b1b8-acde0158271f",
id: "13516a79-1c61-4b9d-b442-0a773fc9c99f",
created: "2022-07-20T10:24:28Z",
userName: "professor"
}
],
maxItems: 100
}
});
};
this.controller = $controller('MembershipController', {'$scope': this.scope});
this.mockSuccessAlert = "success";
@ -401,4 +444,169 @@ describe('Controller: MembershipController', function () {
expect(this.scope.membership.members).toEqual(expectedMembership);
expect(this.scope.isGroupAdmin).toBe(true);
});
it('test that we properly get group change data', function(){
this.scope.groupChanges = {};
var response = {
data: {
changes: [
{
newGroup: {
id: "f9329f39-595d-45c9-8cdf-ac36e96e085d",
name: "test-group",
email: "test@test.com",
created: "2022-07-20T10:14:49Z",
status: "Active",
members: [
{
id: "ea7ec24e-3cc2-4740-b1b8-acde0158271f"
},
{
id: "5bda099e-be26-4aac-a310-ecf221ee2451"
}
],
admins: [
{
id: "ea7ec24e-3cc2-4740-b1b8-acde0158271f"
},
{
id: "5bda099e-be26-4aac-a310-ecf221ee2451"
}
]
},
changeType: "Delete",
userId: "ea7ec24e-3cc2-4740-b1b8-acde0158271f",
id: "13516a79-1c61-4b9d-b442-0a773fc9c99f",
created: "2022-07-20T10:24:28Z",
userName: "professor"
}
],
maxItems: 100
}
};
var getGroupChangesSets = spyOn(this.groupsService, 'getGroupChanges')
.and.stub()
.and.returnValue(this.q.when(response));
this.scope.refresh();
this.scope.$digest();
expect(getGroupChangesSets.calls.count()).toBe(1);
expect(this.scope.groupChanges).toEqual(response.data.changes);
});
it('nextPage should call getGroupChanges with the correct parameters', function () {
var response = {
data: {
changes: [
{
newGroup: {
id: "f9329f39-595d-45c9-8cdf-ac36e96e085d",
name: "test-group",
email: "test@test.com",
created: "2022-07-20T10:14:49Z",
status: "Active",
members: [
{
id: "ea7ec24e-3cc2-4740-b1b8-acde0158271f"
},
{
id: "5bda099e-be26-4aac-a310-ecf221ee2451"
}
],
admins: [
{
id: "ea7ec24e-3cc2-4740-b1b8-acde0158271f"
},
{
id: "5bda099e-be26-4aac-a310-ecf221ee2451"
}
]
},
changeType: "Delete",
userId: "ea7ec24e-3cc2-4740-b1b8-acde0158271f",
id: "13516a79-1c61-4b9d-b442-0a773fc9c99f",
created: "2022-07-20T10:24:28Z",
userName: "professor"
}
],
maxItems: 100
}
};
var getGroupChangesSets = spyOn(this.groupsService, 'getGroupChanges')
.and.stub()
.and.returnValue(this.q.when(response));
var expectedId = "";
var expectedMaxItems = 100;
var expectedStartFrom = undefined;
this.scope.changeNextPage();
expect(getGroupChangesSets.calls.count()).toBe(1);
expect(getGroupChangesSets.calls.mostRecent().args).toEqual(
[expectedId, expectedMaxItems, expectedStartFrom]);
});
it('prevPage should call getGroupChanges with the correct parameters', function () {
var response = {
data: {
changes: [
{
newGroup: {
id: "f9329f39-595d-45c9-8cdf-ac36e96e085d",
name: "test-group",
email: "test@test.com",
created: "2022-07-20T10:14:49Z",
status: "Active",
members: [
{
id: "ea7ec24e-3cc2-4740-b1b8-acde0158271f"
},
{
id: "5bda099e-be26-4aac-a310-ecf221ee2451"
}
],
admins: [
{
id: "ea7ec24e-3cc2-4740-b1b8-acde0158271f"
},
{
id: "5bda099e-be26-4aac-a310-ecf221ee2451"
}
]
},
changeType: "Delete",
userId: "ea7ec24e-3cc2-4740-b1b8-acde0158271f",
id: "13516a79-1c61-4b9d-b442-0a773fc9c99f",
created: "2022-07-20T10:24:28Z",
userName: "professor"
}
],
maxItems: 100
}
};
var getGroupChangesSets = spyOn(this.groupsService, 'getGroupChanges')
.and.stub()
.and.returnValue(this.q.when(response));
var expectedId = "";
var expectedMaxItems = 100;
var expectedStartFrom = undefined;
this.scope.changePrevPage();
expect(getGroupChangesSets.calls.count()).toBe(1);
expect(getGroupChangesSets.calls.mostRecent().args).toEqual(
[expectedId, expectedMaxItems, expectedStartFrom]);
this.scope.changeNextPage();
this.scope.changePrevPage();
expect(getGroupChangesSets.calls.count()).toBe(3);
expect(getGroupChangesSets.calls.mostRecent().args).toEqual(
[expectedId, expectedMaxItems, expectedStartFrom]);
});
});

View File

@ -53,7 +53,7 @@ angular.module('controller.zones', [])
$scope.currentZone.transferConnection = {};
};
groupsService.getGroupsAbridged(true, "").then(function (results) {
groupsService.getGroups(true, "").then(function (results) {
if (results.data) {
// Get all groups where the group members include the current user
$scope.myGroups = results.data.groups.filter(grp => grp.members.findIndex(mem => mem.id === $scope.profile.id) >= 0);
@ -87,7 +87,7 @@ angular.module('controller.zones', [])
allZonesPaging = pagingService.resetPaging(allZonesPaging);
zonesService
.getZones(zonesPaging.maxItems, undefined, $scope.query)
.getZones(zonesPaging.maxItems, undefined, $scope.query, $scope.searchByAdminGroup)
.then(function (response) {
$log.log('zonesService::getZones-success (' + response.data.zones.length + ' zones)');
zonesPaging.next = response.data.nextId;
@ -101,7 +101,7 @@ angular.module('controller.zones', [])
});
zonesService
.getZones(zonesPaging.maxItems, undefined, $scope.query, true)
.getZones(zonesPaging.maxItems, undefined, $scope.query, $scope.searchByAdminGroup, true)
.then(function (response) {
$log.log('zonesService::getZones-success (' + response.data.zones.length + ' zones)');
allZonesPaging.next = response.data.nextId;
@ -207,7 +207,7 @@ angular.module('controller.zones', [])
$scope.prevPageMyZones = function() {
var startFrom = pagingService.getPrevStartFrom(zonesPaging);
return zonesService
.getZones(zonesPaging.maxItems, startFrom, $scope.query, false)
.getZones(zonesPaging.maxItems, startFrom, $scope.query, $scope.searchByAdminGroup, false)
.then(function(response) {
zonesPaging = pagingService.prevPageUpdate(response.data.nextId, zonesPaging);
updateZoneDisplay(response.data.zones);
@ -220,7 +220,7 @@ angular.module('controller.zones', [])
$scope.prevPageAllZones = function() {
var startFrom = pagingService.getPrevStartFrom(allZonesPaging);
return zonesService
.getZones(allZonesPaging.maxItems, startFrom, $scope.query, true)
.getZones(allZonesPaging.maxItems, startFrom, $scope.query, $scope.searchByAdminGroup, true)
.then(function(response) {
allZonesPaging = pagingService.prevPageUpdate(response.data.nextId, allZonesPaging);
updateAllZonesDisplay(response.data.zones);
@ -232,7 +232,7 @@ angular.module('controller.zones', [])
$scope.nextPageMyZones = function () {
return zonesService
.getZones(zonesPaging.maxItems, zonesPaging.next, $scope.query, false)
.getZones(zonesPaging.maxItems, zonesPaging.next, $scope.query, $scope.searchByAdminGroup, false)
.then(function(response) {
var zoneSets = response.data.zones;
zonesPaging = pagingService.nextPageUpdate(zoneSets, response.data.nextId, zonesPaging);
@ -248,7 +248,7 @@ angular.module('controller.zones', [])
$scope.nextPageAllZones = function () {
return zonesService
.getZones(allZonesPaging.maxItems, allZonesPaging.next, $scope.query, true)
.getZones(allZonesPaging.maxItems, allZonesPaging.next, $scope.query, $scope.searchByAdminGroup, true)
.then(function(response) {
var zoneSets = response.data.zones;
allZonesPaging = pagingService.nextPageUpdate(zoneSets, response.data.nextId, allZonesPaging);

View File

@ -39,7 +39,7 @@ describe('Controller: ZonesController', function () {
profileService.getAuthenticatedUserData = function() {
return $q.when({data: {id: "userId"}});
};
groupsService.getGroupsAbridged = function () {
groupsService.getGroups = function () {
return $q.when({
data: {
groups: [{id: "all my groups", members: [{id: "userId"}]}]
@ -76,13 +76,14 @@ describe('Controller: ZonesController', function () {
var expectedMaxItems = 100;
var expectedStartFrom = undefined;
var expectedQuery = this.scope.query;
var expectedSearchByAdminGroup = this.scope.searchByAdminGroup;
var expectedignoreAccess = false;
this.scope.nextPageMyZones();
expect(getZoneSets.calls.count()).toBe(1);
expect(getZoneSets.calls.mostRecent().args).toEqual(
[expectedMaxItems, expectedStartFrom, expectedQuery, expectedignoreAccess]);
[expectedMaxItems, expectedStartFrom, expectedQuery, expectedSearchByAdminGroup, expectedignoreAccess]);
});
it('prevPageMyZones should call getZones with the correct parameters', function () {
@ -93,19 +94,20 @@ describe('Controller: ZonesController', function () {
var expectedMaxItems = 100;
var expectedStartFrom = undefined;
var expectedQuery = this.scope.query;
var expectedSearchByAdminGroup = this.scope.searchByAdminGroup;
var expectedignoreAccess = false;
this.scope.prevPageMyZones();
expect(getZoneSets.calls.count()).toBe(1);
expect(getZoneSets.calls.mostRecent().args).toEqual(
[expectedMaxItems, expectedStartFrom, expectedQuery, expectedignoreAccess]);
[expectedMaxItems, expectedStartFrom, expectedQuery, expectedSearchByAdminGroup, expectedignoreAccess]);
this.scope.nextPageMyZones();
this.scope.prevPageMyZones();
expect(getZoneSets.calls.count()).toBe(3);
expect(getZoneSets.calls.mostRecent().args).toEqual(
[expectedMaxItems, expectedStartFrom, expectedQuery, expectedignoreAccess]);
[expectedMaxItems, expectedStartFrom, expectedQuery, expectedSearchByAdminGroup, expectedignoreAccess]);
});
});

View File

@ -31,15 +31,57 @@
// paging status for recordsets
var recordsPaging = pagingService.getNewPagingParams(100);
var recordType = [];
var recordName = [];
$( "#record-search-text" ).autocomplete({
source: function( request, response ) {
$.ajax({
url: "/api/recordsets?maxItems=100",
dataType: "json",
data: "recordNameFilter="+request.term+"%25&nameSort=asc",
success: function( data ) {
const recordSearch = JSON.parse(JSON.stringify(data));
response($.map(recordSearch.recordSets, function(item) {
return {value: item.fqdn +' | '+ item.type , label: 'name: ' + item.fqdn + ' | type: ' + item.type }}))}
});
},
minLength: 2,
select: function (event, ui) {
$scope.query = ui.item.value;
$("#record-search-text").val(ui.item.value);
return false;
},
open: function() {
$( this ).removeClass( "ui-corner-all" ).addClass( "ui-corner-top" );
},
close: function() {
$( this ).removeClass( "ui-corner-top" ).addClass( "ui-corner-all" );
}
});
$.ui.autocomplete.prototype._renderItem = function( ul, item ) {
let recordSet = String(item.label).replace(new RegExp(this.term, "gi"),"<b>$&</b>");
return $("<li></li>")
.data("ui-autocomplete-item", item.value)
.append("<div>" + recordSet + "</div>")
.appendTo(ul); };
$scope.refreshRecords = function() {
recordsPaging = pagingService.resetPaging(recordsPaging);
if($scope.query.includes("|")) {
const queryRecord = $scope.query.split('|');
recordName = queryRecord[0].trim();
recordType = queryRecord[1].trim(); }
else { recordName = $scope.query;
recordType = $scope.selectedRecordTypes.toString(); }
recordsPaging = pagingService.resetPaging(recordsPaging);
function success(response) {
recordsPaging.next = response.data.nextId;
updateRecordDisplay(response.data['recordSets']);
}
return recordsService
.listRecordSetData(recordsPaging.maxItems, undefined, $scope.query, $scope.selectedRecordTypes.toString(), $scope.nameSort, $scope.ownerGroupFilter)
.listRecordSetData(recordsPaging.maxItems, undefined, recordName, recordType, $scope.nameSort, $scope.ownerGroupFilter)
.then(success)
.catch(function (error) {
handleError(error, 'dnsChangesService::getRecordSet-failure');
@ -79,6 +121,7 @@
}
};
function updateRecordDisplay(records) {
var newRecords = [];
angular.forEach(records, function(record) {

View File

@ -75,7 +75,7 @@ angular.module('service.groups', [])
query = null;
}
var params = {
"maxItems": 1500,
"maxItems": 3000,
"groupNameFilter": query,
"ignoreAccess": ignoreAccess
};
@ -84,12 +84,13 @@ angular.module('service.groups', [])
return $http.get(url);
};
this.getGroupsAbridged = function (ignoreAccess, query) {
this.getGroupsAbridged = function (limit, startFrom, ignoreAccess, query) {
if (query == "") {
query = null;
}
var params = {
"maxItems": 1500,
"maxItems": limit,
"startFrom": startFrom,
"groupNameFilter": query,
"ignoreAccess": ignoreAccess,
"abridged": true
@ -105,6 +106,12 @@ angular.module('service.groups', [])
return $http.get(url);
};
this.getGroupChanges = function (groupId, count, startFrom) {
var url = '/api/groups/' + groupId + '/groupchanges';
url = this.urlBuilder(url, { 'startFrom': startFrom, 'maxItems': count });
return $http.get(url);
};
this.getGroupsStored = function () {
if (_refreshMyGroups || _myGroupsPromise == undefined) {
_myGroupsPromise = this.getGroups().then(

View File

@ -26,6 +26,10 @@ angular.module('service.profile', [])
return $http.get('/api/users/lookupuser/' + username);
}
this.getUserDataById = function(userId){
return $http.get('/api/users/' + userId);
}
this.regenerateCredentials = function(){
return $http.post('/regenerate-creds', {}, {headers: utilityService.getCsrfHeader()});
}

View File

@ -43,6 +43,10 @@ describe('Service: profileService', function () {
expect(this.profileService.getUserDataByUsername).toBeDefined();
});
it('should have getUserDataById method', function () {
expect(this.profileService.getUserDataByUsername).toBeDefined();
});
it('should have regenerateCredentials method', function () {
expect(this.profileService.regenerateCredentials()).toBeDefined();
});
@ -119,6 +123,39 @@ describe('Service: profileService', function () {
this.$httpBackend.flush();
});
it('getUserDataByUserId method should return 200 with valid user', function (done) {
this.$httpBackend.expectGET('/api/users/userId').respond('success');
this.profileService.getUserDataById('userId')
.then(function (response) {
expect(response.status).toBe(200);
expect(response.data).toBe('success');
done();
}, function (error) {
fail('lookupUserAccount expected 200, but got ' + error.status.toString());
done();
});
this.$httpBackend.flush();
});
it('getUserDataByUserId method should return 400 with invalid user', function (done) {
var url = '/api/users/:userId';
this.$httpBackend.whenRoute('GET', url)
.respond(function () {
return [400, 'response body', {}, 'TestPhrase'];
});
this.profileService.getUserDataById('badUserId')
.then(function (response) {
fail('lookupUserAccount expected 400, but got ' + response.status.toString());
done();
}, function (error) {
expect(error.status).toBe(400);
done();
});
this.$httpBackend.flush();
});
it('regenerateCredentials method should return 400 with invalid user', function (done) {
var url = '/regenerate-creds';
this.$httpBackend.whenRoute('POST', url).respond(400);

View File

@ -19,7 +19,7 @@
angular.module('service.zones', [])
.service('zonesService', function ($http, groupsService, $log, utilityService) {
this.getZones = function (limit, startFrom, query, ignoreAccess) {
this.getZones = function (limit, startFrom, query, searchByAdminGroup, ignoreAccess) {
if (query == "") {
query = null;
}
@ -27,10 +27,29 @@ angular.module('service.zones', [])
"maxItems": limit,
"startFrom": startFrom,
"nameFilter": query,
"searchByAdminGroup": searchByAdminGroup,
"ignoreAccess": ignoreAccess
};
var url = groupsService.urlBuilder("/api/zones", params);
return $http.get(url);
let loader = $("#loader");
loader.modal({
backdrop: "static", //remove ability to close modal with click
keyboard: false, //remove option to close with keyboard
show: true //Display loader!
})
let promis = $http.get(url);
// Hide loader when api gets response
promis.then(()=>loader.modal("hide"), ()=>loader.modal("hide"))
return promis
};
this.getZoneChanges = function (limit, startFrom, zoneId) {
var params = {
"maxItems": limit,
"startFrom": startFrom
}
var url = utilityService.urlBuilder ( "/api/zones/" + zoneId + "/changes", params);
return $http.get(url);
};
this.getBackendIds = function() {

Some files were not shown because too many files have changed in this diff Show More