mirror of
https://github.com/VinylDNS/vinyldns
synced 2025-08-22 10:10:12 +00:00
Merge branch 'master' into nspadaccino/view-shared-zones
This commit is contained in:
commit
e1df5a8958
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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"
|
||||
|
@ -32,7 +32,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
|
||||
@ -64,14 +63,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",
|
||||
@ -100,6 +109,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",
|
||||
@ -254,8 +273,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)
|
||||
)
|
||||
@ -273,6 +292,7 @@ class RecordSetServiceIntegrationSpec
|
||||
val zoneRecords = List(
|
||||
apexTestRecordA,
|
||||
apexTestRecordAAAA,
|
||||
dottedTestRecord,
|
||||
subTestRecordA,
|
||||
subTestRecordAAAA,
|
||||
subTestRecordNS,
|
||||
@ -300,6 +320,7 @@ class RecordSetServiceIntegrationSpec
|
||||
mockBackendResolver,
|
||||
false,
|
||||
vinyldnsConfig.highValueDomainConfig,
|
||||
vinyldnsConfig.dottedHostsConfig,
|
||||
vinyldnsConfig.serverConfig.approvedNameServers,
|
||||
useRecordSetCache = true
|
||||
)
|
||||
@ -338,6 +359,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
|
||||
@ -549,6 +657,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
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -139,6 +139,7 @@ object Boot extends App {
|
||||
backendResolver,
|
||||
vinyldnsConfig.serverConfig.validateRecordLookupAgainstDnsBackend,
|
||||
vinyldnsConfig.highValueDomainConfig,
|
||||
vinyldnsConfig.dottedHostsConfig,
|
||||
vinyldnsConfig.serverConfig.approvedNameServers,
|
||||
vinyldnsConfig.serverConfig.useRecordSetCache
|
||||
)
|
||||
|
@ -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))
|
||||
}
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -27,12 +27,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(
|
||||
@ -67,8 +70,13 @@ class BatchChangeConverter(batchChangeRepo: BatchChangeRepository, messageQueue:
|
||||
recordSetChanges: List[RecordSetChange]
|
||||
): BatchResult[Unit] = {
|
||||
val convertedIds = recordSetChanges.flatMap(_.singleBatchChangeIds).toSet
|
||||
|
||||
singleChanges.find(ch => !convertedIds.contains(ch.id)) match {
|
||||
// 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
|
||||
@ -104,7 +112,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)
|
||||
@ -113,19 +120,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(
|
||||
|
@ -32,19 +32,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
|
||||
|
@ -17,15 +17,9 @@
|
||||
package vinyldns.api.domain.batch
|
||||
|
||||
import java.net.InetAddress
|
||||
|
||||
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
|
||||
@ -34,7 +28,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 {
|
||||
@ -211,7 +205,7 @@ class BatchChangeValidations(
|
||||
isApproved: Boolean
|
||||
): SingleValidation[Unit] = {
|
||||
val validTTL = addChangeInput.ttl.map(validateTTL(_).asUnit).getOrElse(().valid)
|
||||
val validRecord = validateRecordData(addChangeInput.record)
|
||||
val validRecord = validateRecordData(addChangeInput.record, addChangeInput)
|
||||
val validInput = validateInputName(addChangeInput, isApproved)
|
||||
|
||||
validTTL |+| validRecord |+| validInput
|
||||
@ -222,7 +216,7 @@ class BatchChangeValidations(
|
||||
isApproved: Boolean
|
||||
): SingleValidation[Unit] = {
|
||||
val validRecord = deleteRRSetChangeInput.record match {
|
||||
case Some(recordData) => validateRecordData(recordData)
|
||||
case Some(recordData) => validateRecordData(recordData, deleteRRSetChangeInput)
|
||||
case None => ().validNel
|
||||
}
|
||||
val validInput = validateInputName(deleteRRSetChangeInput, isApproved)
|
||||
@ -230,11 +224,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 =>
|
||||
@ -347,7 +348,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)
|
||||
}
|
||||
@ -401,7 +402,7 @@ class BatchChangeValidations(
|
||||
zoneDoesNotRequireManualReview(change, isApproved) |+|
|
||||
ensureRecordExists(change, groupedChanges)
|
||||
case None =>
|
||||
RecordDoesNotExist(change.inputChange.inputName).invalidNel
|
||||
RecordDoesNotExist(change.inputChange.inputName).validNel
|
||||
}
|
||||
|
||||
validations.map(_ => change)
|
||||
@ -435,11 +436,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)
|
||||
}
|
||||
|
||||
@ -477,11 +479,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(
|
||||
|
@ -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
|
||||
|
||||
|
@ -59,7 +59,9 @@ final case class GroupChangeInfo(
|
||||
userId: String,
|
||||
oldGroup: Option[GroupInfo] = None,
|
||||
id: String = UUID.randomUUID().toString,
|
||||
created: String = DateTime.now.getMillis.toString
|
||||
created: DateTime = DateTime.now,
|
||||
userName: String,
|
||||
groupChangeMessage: String
|
||||
)
|
||||
|
||||
object GroupChangeInfo {
|
||||
@ -69,7 +71,9 @@ object GroupChangeInfo {
|
||||
userId = groupChange.userId,
|
||||
oldGroup = groupChange.oldGroup.map(GroupInfo.apply),
|
||||
id = groupChange.id,
|
||||
created = groupChange.created.getMillis.toString
|
||||
created = groupChange.created,
|
||||
userName = groupChange.userName.getOrElse("unknown user"),
|
||||
groupChangeMessage = groupChange.groupChangeMessage.getOrElse("")
|
||||
)
|
||||
}
|
||||
|
||||
@ -171,6 +175,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)
|
||||
|
@ -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)
|
||||
|
@ -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],
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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],
|
||||
|
@ -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,13 +143,16 @@ 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] = {
|
||||
if(!searchByAdminGroup || nameFilter.isEmpty){
|
||||
for {
|
||||
listZonesResult <- zoneRepository.listZones(
|
||||
authPrincipal,
|
||||
@ -169,6 +173,29 @@ class ZoneService(
|
||||
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
|
||||
|
||||
|
@ -42,6 +42,7 @@ trait ZoneServiceAlgebra {
|
||||
nameFilter: Option[String],
|
||||
startFrom: Option[String],
|
||||
maxItems: Int,
|
||||
searchByAdminGroup: Boolean,
|
||||
ignoreAccess: Boolean
|
||||
): Result[ListZonesResponse]
|
||||
|
||||
|
@ -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,9 +174,9 @@ 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))
|
||||
@ -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)
|
||||
|
@ -120,7 +120,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[String](DateTime.now.getMillis.toString)
|
||||
(js \ "created").default[DateTime](DateTime.now),
|
||||
(js \ "userName").required[String]("Missing userName"),
|
||||
(js \ "groupChangeMessage").required[String]("Missing groupChangeMessage"),
|
||||
).mapN(GroupChangeInfo.apply)
|
||||
}
|
||||
}
|
||||
|
@ -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, _)) {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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.'])
|
||||
@ -1731,10 +1728,8 @@ def test_aaaa_recordtype_add_checks(shared_zone_test_context):
|
||||
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."])
|
||||
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)
|
||||
|
@ -26,16 +26,12 @@ def test_list_group_activity_start_from_success(group_activity_context, shared_z
|
||||
# we grab 3 items, which when sorted by most recent will give the 3 most recent items
|
||||
page_one = client.get_group_changes(created_group["id"], max_items=3, status=200)
|
||||
|
||||
# our start from will align with the created on the 3rd change in the list
|
||||
start_from_index = 2
|
||||
start_from = page_one["changes"][start_from_index]["created"] # start from a known good timestamp
|
||||
|
||||
# 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
|
||||
|
@ -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))
|
||||
|
@ -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()))
|
||||
@ -1743,7 +1825,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 +1848,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 +1870,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 +1892,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 +1960,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 +1987,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 +2045,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 +2089,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 +2132,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 +2145,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 +2158,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 +2172,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 +2185,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"]}'))
|
||||
|
@ -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:
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
|
||||
|
@ -18,7 +18,7 @@ package vinyldns.api
|
||||
|
||||
import com.comcast.ip4s.IpAddress
|
||||
import org.joda.time.DateTime
|
||||
import vinyldns.api.config.{BatchChangeConfig, HighValueDomainConfig, LimitsConfig, ManualReviewConfig, ScheduledChangesConfig}
|
||||
import vinyldns.api.config.{ZoneAuthConfigs, BatchChangeConfig, DottedHostsConfig, HighValueDomainConfig, LimitsConfig, ManualReviewConfig, ScheduledChangesConfig}
|
||||
import vinyldns.api.domain.batch.V6DiscoveryNibbleBoundaries
|
||||
import vinyldns.core.domain.record._
|
||||
import vinyldns.core.domain.zone._
|
||||
@ -40,6 +40,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.*"))
|
||||
|
@ -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]
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,8 @@ 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,
|
||||
@ -160,6 +162,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")),
|
||||
@ -535,6 +545,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(
|
||||
|
@ -66,8 +66,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(
|
||||
@ -747,7 +747,7 @@ class BatchChangeServiceSpec
|
||||
"succeed if the batchChange is PendingReview and reviewer is authorized" in {
|
||||
batchChangeRepo.save(batchChangeNeedsApproval)
|
||||
|
||||
val result =
|
||||
val result = {
|
||||
rightResultOf(
|
||||
underTestManualEnabled
|
||||
.approveBatchChange(
|
||||
@ -757,6 +757,7 @@ class BatchChangeServiceSpec
|
||||
)
|
||||
.value
|
||||
)
|
||||
}
|
||||
|
||||
result.userId shouldBe batchChangeNeedsApproval.userId
|
||||
result.userName shouldBe batchChangeNeedsApproval.userName
|
||||
@ -1708,8 +1709,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
|
||||
@ -1745,7 +1746,7 @@ class BatchChangeServiceSpec
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
List(SingleChangeError(nonFatalError)),
|
||||
List(SingleChangeError(nonFatalErrorZoneDiscoveryError)),
|
||||
result.changes(1).id
|
||||
)
|
||||
result.changes(2) shouldBe SingleDeleteRRSetChange(
|
||||
@ -1759,7 +1760,7 @@ class BatchChangeServiceSpec
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
List(SingleChangeError(nonFatalError)),
|
||||
List(SingleChangeError(nonFatalErrorZoneDiscoveryError)),
|
||||
result.changes(2).id
|
||||
)
|
||||
}
|
||||
@ -1775,8 +1776,8 @@ class BatchChangeServiceSpec
|
||||
),
|
||||
List(
|
||||
AddChangeForValidation(apexZone, "apex.test.com.", apexAddA, 7200L).validNel,
|
||||
nonFatalError.invalidNel,
|
||||
nonFatalError.invalidNel
|
||||
nonFatalErrorZoneDiscoveryError.invalidNel,
|
||||
nonFatalErrorZoneDiscoveryError.invalidNel
|
||||
),
|
||||
okAuth,
|
||||
allowManualReview = true
|
||||
@ -1809,7 +1810,7 @@ class BatchChangeServiceSpec
|
||||
List(
|
||||
ZoneDiscoveryError("no.zone.match.").invalidNel,
|
||||
AddChangeForValidation(baseZone, "non-apex", nonApexAddA, 7200L).validNel,
|
||||
nonFatalError.invalidNel
|
||||
nonFatalErrorZoneDiscoveryError.invalidNel
|
||||
),
|
||||
okAuth,
|
||||
true
|
||||
@ -1825,7 +1826,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 {
|
||||
@ -1835,8 +1836,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
|
||||
@ -1860,8 +1861,8 @@ class BatchChangeServiceSpec
|
||||
),
|
||||
List(
|
||||
AddChangeForValidation(apexZone, "apex.test.com.", apexAddA, 7200L).validNel,
|
||||
nonFatalError.invalidNel,
|
||||
nonFatalError.invalidNel
|
||||
nonFatalErrorZoneDiscoveryError.invalidNel,
|
||||
nonFatalErrorZoneDiscoveryError.invalidNel
|
||||
),
|
||||
okAuth,
|
||||
true
|
||||
@ -1902,8 +1903,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
|
||||
@ -1926,8 +1927,8 @@ class BatchChangeServiceSpec
|
||||
),
|
||||
List(
|
||||
AddChangeForValidation(apexZone, "apex.test.com.", apexAddA, 7200L).validNel,
|
||||
nonFatalError.invalidNel,
|
||||
nonFatalError.invalidNel
|
||||
nonFatalErrorZoneDiscoveryError.invalidNel,
|
||||
nonFatalErrorZoneDiscoveryError.invalidNel
|
||||
),
|
||||
okAuth,
|
||||
allowManualReview = false
|
||||
@ -1945,7 +1946,7 @@ class BatchChangeServiceSpec
|
||||
BatchChangeInput(None, List(apexAddA, onlyBaseAddAAAA), None),
|
||||
List(
|
||||
AddChangeForValidation(apexZone, "apex.test.com.", apexAddA, 7200L).validNel,
|
||||
nonFatalError.invalidNel
|
||||
nonFatalErrorZoneDiscoveryError.invalidNel
|
||||
),
|
||||
okAuth,
|
||||
true
|
||||
@ -2007,7 +2008,7 @@ class BatchChangeServiceSpec
|
||||
asAdds.head,
|
||||
7200L
|
||||
).validNel,
|
||||
fatalError.invalidNel
|
||||
nonFatalErrorRecordAlreadyExists.invalidNel
|
||||
),
|
||||
reviewInfo
|
||||
)
|
||||
|
@ -712,7 +712,7 @@ class BatchChangeValidationsSpec
|
||||
)
|
||||
val result = validateAddChangeInput(change, false)
|
||||
|
||||
result should haveInvalid[DomainValidationError](InvalidDomainName(s"$invalidCNAMERecordData."))
|
||||
result should haveInvalid[DomainValidationError](InvalidCname(s"$invalidCNAMERecordData.",false))
|
||||
}
|
||||
|
||||
property("""validateAddChangeInput: should fail with InvalidLength
|
||||
@ -824,10 +824,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)
|
||||
@ -1004,7 +1004,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")))
|
||||
@ -1026,15 +1026,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](
|
||||
@ -1189,7 +1182,7 @@ class BatchChangeValidationsSpec
|
||||
)
|
||||
|
||||
result(0) should haveInvalid[DomainValidationError](
|
||||
RecordAlreadyExists(input.inputChange.inputName)
|
||||
RecordAlreadyExists(input.inputChange.inputName, input.inputChange.record, false)
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1589,7 +1582,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")
|
||||
@ -1606,12 +1599,8 @@ 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
|
||||
@ -2164,7 +2153,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",
|
||||
@ -2185,7 +2174,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") {
|
||||
@ -2666,4 +2655,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
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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 =
|
||||
|
@ -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]
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -44,6 +44,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 {
|
||||
@ -184,24 +186,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]
|
||||
}
|
||||
|
||||
@ -211,7 +264,10 @@ class RecordSetValidationsSpec
|
||||
List(),
|
||||
okZone,
|
||||
Some(dottedARecord.copy(ttl = 300)),
|
||||
Nil
|
||||
Nil,
|
||||
true,
|
||||
dottedHostsConfigZonesAllowed.toSet,
|
||||
false
|
||||
) should be(right)
|
||||
}
|
||||
|
||||
@ -222,7 +278,10 @@ class RecordSetValidationsSpec
|
||||
List(),
|
||||
okZone,
|
||||
Some(dottedCNAMERecord.copy(ttl = 300)),
|
||||
Nil
|
||||
Nil,
|
||||
true,
|
||||
dottedHostsConfigZonesAllowed.toSet,
|
||||
false
|
||||
) should be(right)
|
||||
}
|
||||
|
||||
@ -234,7 +293,10 @@ class RecordSetValidationsSpec
|
||||
List(),
|
||||
okZone,
|
||||
Some(dottedNSRecord.copy(ttl = 300)),
|
||||
Nil
|
||||
Nil,
|
||||
true,
|
||||
dottedHostsConfigZonesAllowed.toSet,
|
||||
false
|
||||
)
|
||||
) shouldBe an[InvalidRequest]
|
||||
}
|
||||
@ -245,35 +307,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 {
|
||||
@ -281,21 +343,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)
|
||||
}
|
||||
|
||||
}
|
||||
@ -304,7 +366,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 {
|
||||
@ -312,7 +374,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)
|
||||
}
|
||||
|
||||
}
|
||||
@ -329,7 +391,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -342,29 +404,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]
|
||||
}
|
||||
}
|
||||
@ -372,25 +434,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]
|
||||
}
|
||||
}
|
||||
@ -398,54 +470,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 {
|
||||
|
@ -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)
|
||||
|
@ -153,6 +153,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)
|
||||
@ -198,6 +199,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)
|
||||
@ -602,6 +604,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)
|
||||
@ -643,7 +646,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."
|
||||
)
|
||||
|
@ -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())
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -252,6 +252,7 @@ class ZoneRoutingSpec
|
||||
nameFilter: Option[String],
|
||||
startFrom: Option[String],
|
||||
maxItems: Int,
|
||||
searchByAdminGroup: Boolean = false,
|
||||
ignoreAccess: Boolean = false
|
||||
): Result[ListZonesResponse] = {
|
||||
|
||||
@ -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]
|
||||
|
@ -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."
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -34,7 +34,9 @@ case class GroupChange(
|
||||
userId: String,
|
||||
oldGroup: Option[Group] = None,
|
||||
id: String = UUID.randomUUID().toString,
|
||||
created: DateTime = DateTime.now
|
||||
created: DateTime = DateTime.now,
|
||||
userName: Option[String] = None,
|
||||
groupChangeMessage: Option[String] = None
|
||||
)
|
||||
|
||||
object GroupChange {
|
||||
|
@ -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],
|
||||
|
@ -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]]
|
||||
|
||||
}
|
||||
|
@ -17,7 +17,6 @@
|
||||
package vinyldns.core.domain.record
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
import org.joda.time.DateTime
|
||||
import vinyldns.core.domain.zone.{Zone, ZoneCommand, ZoneCommandResult}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -36,6 +36,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")
|
||||
@ -157,4 +158,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()
|
||||
)
|
||||
}
|
||||
|
@ -34,6 +34,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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -115,7 +115,7 @@ class MySqlBatchChangeRepositoryIntegrationSpec
|
||||
val pendingBatchChange: BatchChange = randomBatchChange().copy(createdTimestamp = DateTime.now)
|
||||
|
||||
val completeBatchChange: BatchChange = randomBatchChangeWithList(
|
||||
randomBatchChange().changes.map(_.complete("recordChangeId", "recordSetId"))
|
||||
randomBatchChange().changes.map(_.complete(Some("Complete"),"recordChangeId", "recordSetId"))
|
||||
).copy(createdTimestamp = DateTime.now.plusMillis(1000))
|
||||
|
||||
val failedBatchChange: BatchChange =
|
||||
@ -123,7 +123,7 @@ class MySqlBatchChangeRepositoryIntegrationSpec
|
||||
.copy(createdTimestamp = DateTime.now.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 = DateTime.now.plusMillis(1000000))
|
||||
|
||||
@ -410,7 +410,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)
|
||||
@ -429,7 +429,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 {
|
||||
@ -443,7 +443,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)
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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 {
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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'},
|
||||
|
@ -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 =
|
||||
|
@ -158,10 +158,16 @@
|
||||
<span ng-if="change.status == 'Cancelled'" class="label label-default">{{batch.status}}</span>
|
||||
</td>
|
||||
<td class="wrap-long-text">
|
||||
<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}}
|
||||
{{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>
|
||||
|
@ -29,6 +29,15 @@
|
||||
</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>
|
||||
@ -100,10 +109,172 @@
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<!-- END VERTICAL TABS -->
|
||||
|
||||
</div>
|
||||
<!-- PAGE CONTENT WRAPPER -->
|
||||
</div>
|
||||
|
||||
<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=" "
|
||||
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=" "
|
||||
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 -->
|
||||
}
|
||||
|
@ -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 -->
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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 -->
|
||||
|
@ -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')">
|
||||
|
@ -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 -->
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -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,7 +173,13 @@
|
||||
<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>
|
||||
@ -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>
|
||||
|
@ -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)
|
||||
|
@ -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',
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
groupsPaging = pagingService.resetPaging(groupsPaging);
|
||||
allGroupsPaging = pagingService.resetPaging(allGroupsPaging);
|
||||
|
||||
groupsService
|
||||
.getGroupsAbridged(groupsPaging.maxItems, undefined, false, $scope.query)
|
||||
.then(function (result) {
|
||||
$log.debug('getGroups:refresh-success', result);
|
||||
//update groups
|
||||
$scope.groups.items = result.groups;
|
||||
$scope.groupsLoaded = true;
|
||||
groupsPaging.next = result.data.nextId;
|
||||
updateGroupDisplay(result.data.groups);
|
||||
if (!$scope.query.length) {
|
||||
$scope.hasGroups = $scope.groups.items.length > 0;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
handleError(error, 'getGroups::refresh-failure');
|
||||
});
|
||||
|
||||
getGroupsAbridged($scope.ignoreAccess)
|
||||
.then(success)
|
||||
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);
|
||||
});
|
||||
|
@ -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]);
|
||||
});
|
||||
});
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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]);
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
|
@ -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]);
|
||||
});
|
||||
});
|
||||
|
@ -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() {
|
||||
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) {
|
||||
|
@ -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(
|
||||
|
@ -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()});
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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,9 +27,28 @@ angular.module('service.zones', [])
|
||||
"maxItems": limit,
|
||||
"startFrom": startFrom,
|
||||
"nameFilter": query,
|
||||
"searchByAdminGroup": searchByAdminGroup,
|
||||
"ignoreAccess": ignoreAccess
|
||||
};
|
||||
var url = groupsService.urlBuilder("/api/zones", params);
|
||||
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);
|
||||
};
|
||||
|
||||
|
@ -27,14 +27,23 @@ describe('Service: zoneService', function () {
|
||||
}));
|
||||
|
||||
it('http backend gets called properly when getting zones', function () {
|
||||
this.$httpBackend.expectGET('/api/zones?maxItems=100&startFrom=start&nameFilter=someQuery&ignoreAccess=false').respond('zone returned');
|
||||
this.zonesService.getZones('100', 'start', 'someQuery', false)
|
||||
this.$httpBackend.expectGET('/api/zones?maxItems=100&startFrom=start&nameFilter=someQuery&searchByAdminGroup=false&ignoreAccess=false').respond('zone returned');
|
||||
this.zonesService.getZones('100', 'start', 'someQuery', false, false)
|
||||
.then(function(response) {
|
||||
expect(response.data).toBe('zone returned');
|
||||
});
|
||||
this.$httpBackend.flush();
|
||||
});
|
||||
|
||||
it('http backend gets called properly when getting zoneChanges', function () {
|
||||
this.$httpBackend.expectGET('/api/zones/zoneid/changes?maxItems=100&startFrom=start').respond('zoneChanges returned');
|
||||
this.zonesService.getZoneChanges('100', 'start', 'zoneid', false)
|
||||
.then(function(response) {
|
||||
expect(response.data).toBe('zoneChanges returned');
|
||||
});
|
||||
this.$httpBackend.flush();
|
||||
});
|
||||
|
||||
it('http backend gets called properly when sending zone', function (done) {
|
||||
this.$httpBackend.expectPOST('/api/zones').respond('zone sent');
|
||||
this.zonesService.sendZone('zone payload')
|
||||
|
@ -145,6 +145,48 @@ trait TestApplicationData { this: Mockito =>
|
||||
| }
|
||||
""".stripMargin)
|
||||
|
||||
val hobbitGroupChangeId = "b6018a9b-c893-40e9-aa25-4ccfee460c18"
|
||||
val hobbitGroupChange: JsValue = Json.parse(s"""{
|
||||
| "newGroup": {
|
||||
| "id": "$hobbitGroupId",
|
||||
| "name": "hobbits",
|
||||
| "email": "hobbitAdmin@shire.me",
|
||||
| "description": "Hobbits of the shire",
|
||||
| "members": [ { "id": "${frodoUser.id}" }, { "id": "samwise-userId" } ],
|
||||
| "admins": [ { "id": "${frodoUser.id}" } ]
|
||||
| },
|
||||
| "changeType": "Create",
|
||||
| "userId": "${frodoUser.id}",
|
||||
| "oldGroup": {},
|
||||
| "id": "b6018a9b-c893-40e9-aa25-4ccfee460c18",
|
||||
| "created": "2022-07-22T08:19:22Z",
|
||||
| "userName": "$frodoUser",
|
||||
| "groupChangeMessage": ""
|
||||
| }
|
||||
""".stripMargin)
|
||||
|
||||
val hobbitGroupChanges: JsValue = Json.parse(s"""{
|
||||
| "changes": [{
|
||||
| "newGroup": {
|
||||
| "id": "$hobbitGroupId",
|
||||
| "name": "hobbits",
|
||||
| "email": "hobbitAdmin@shire.me",
|
||||
| "description": "Hobbits of the shire",
|
||||
| "members": [ { "id": "${frodoUser.id}" }, { "id": "samwise-userId" } ],
|
||||
| "admins": [ { "id": "${frodoUser.id}" } ]
|
||||
| },
|
||||
| "changeType": "Create",
|
||||
| "userId": "${frodoUser.id}",
|
||||
| "oldGroup": {},
|
||||
| "id": "b6018a9b-c893-40e9-aa25-4ccfee460c18",
|
||||
| "created": "2022-07-22T08:19:22Z",
|
||||
| "userName": "$frodoUser",
|
||||
| "groupChangeMessage": ""
|
||||
| }],
|
||||
| "maxItems": 100
|
||||
| }
|
||||
""".stripMargin)
|
||||
|
||||
val ringbearerGroup: JsValue = Json.parse(
|
||||
s"""{
|
||||
| "id": "ringbearer-group-uuid",
|
||||
@ -196,6 +238,25 @@ trait TestApplicationData { this: Mockito =>
|
||||
| }
|
||||
""".stripMargin)
|
||||
|
||||
val hobbitZoneChange: JsValue = Json.parse(s"""{
|
||||
| "zoneId": "$hobbitZoneId",
|
||||
| "zoneChanges":
|
||||
| [{ "zone": {
|
||||
| "name": "$hobbitZoneName",
|
||||
| "email": "hobbitAdmin@shire.me",
|
||||
| "status": "Active",
|
||||
| "account": "system",
|
||||
| "acl": "rules",
|
||||
| "adminGroupId": "$hobbitGroupId",
|
||||
| "id": "$hobbitZoneId",
|
||||
| "shared": false,
|
||||
| "status": "Active",
|
||||
| "isTest": true
|
||||
| }}],
|
||||
| "maxItems": 100
|
||||
| }
|
||||
""".stripMargin)
|
||||
|
||||
val hobbitZoneRequest: JsValue = Json.parse(s"""{
|
||||
| "name": "hobbits",
|
||||
| "email": "hobbitAdmin@shire.me",
|
||||
|
@ -800,6 +800,154 @@ class VinylDNSSpec extends Specification with Mockito with TestApplicationData w
|
||||
}
|
||||
}
|
||||
|
||||
".getGroupChange" should {
|
||||
tag("slow")
|
||||
"return the group change if it is found - status ok (200)" in new WithApplication(app) {
|
||||
val client = MockWS {
|
||||
case (GET, u) if u == s"http://localhost:9001/groups/change/${hobbitGroupChangeId}" =>
|
||||
defaultActionBuilder { Results.Ok(hobbitGroupChange) }
|
||||
}
|
||||
|
||||
val mockUserAccessor = mock[UserAccountAccessor]
|
||||
mockUserAccessor.get(anyString).returns(IO.pure(Some(frodoUser)))
|
||||
mockUserAccessor.getUserByKey(anyString).returns(IO.pure(Some(frodoUser)))
|
||||
val underTest = withClient(client)
|
||||
val result =
|
||||
underTest.getGroupChange(hobbitGroupChangeId)(
|
||||
FakeRequest(GET, s"/groups/change/$hobbitGroupChangeId")
|
||||
.withSession("username" -> frodoUser.userName, "accessKey" -> frodoUser.accessKey)
|
||||
)
|
||||
|
||||
status(result) must beEqualTo(OK)
|
||||
hasCacheHeaders(result)
|
||||
contentAsJson(result) must beEqualTo(hobbitGroupChange)
|
||||
}
|
||||
"return authentication failed (401) when auth fails in the backend" in new WithApplication(
|
||||
app
|
||||
) {
|
||||
val client = MockWS {
|
||||
case (GET, u) if u == s"http://localhost:9001/groups/change/${hobbitGroupChangeId}" =>
|
||||
defaultActionBuilder { Results.Unauthorized("Invalid credentials") }
|
||||
}
|
||||
val mockUserAccessor = mock[UserAccountAccessor]
|
||||
mockUserAccessor.get(anyString).returns(IO.pure(Some(frodoUser)))
|
||||
mockUserAccessor.getUserByKey(anyString).returns(IO.pure(Some(frodoUser)))
|
||||
val underTest = withClient(client)
|
||||
val result =
|
||||
underTest.getGroupChange(hobbitGroupChangeId)(
|
||||
FakeRequest(GET, s"/groups/change/$hobbitGroupChangeId")
|
||||
.withSession("username" -> frodoUser.userName, "accessKey" -> frodoUser.accessKey)
|
||||
)
|
||||
|
||||
status(result) must beEqualTo(UNAUTHORIZED)
|
||||
hasCacheHeaders(result)
|
||||
}
|
||||
"return a not found (404) if the group change does not exist" in new WithApplication(app) {
|
||||
val client = MockWS {
|
||||
case (GET, u) if u == "http://localhost:9001/groups/change/not-hobbits" =>
|
||||
defaultActionBuilder { Results.NotFound }
|
||||
}
|
||||
val mockUserAccessor = mock[UserAccountAccessor]
|
||||
mockUserAccessor.get(anyString).returns(IO.pure(Some(frodoUser)))
|
||||
mockUserAccessor.getUserByKey(anyString).returns(IO.pure(Some(frodoUser)))
|
||||
val underTest = withClient(client)
|
||||
val result = underTest.getGroupChange("not-hobbits")(
|
||||
FakeRequest(GET, "/groups/change/not-hobbits")
|
||||
.withSession("username" -> frodoUser.userName, "accessKey" -> frodoUser.accessKey)
|
||||
)
|
||||
|
||||
status(result) must beEqualTo(NOT_FOUND)
|
||||
hasCacheHeaders(result)
|
||||
}
|
||||
"return status forbidden (403) if the user account is locked" in new WithApplication(app) {
|
||||
val client = mock[WSClient]
|
||||
val underTest =
|
||||
TestVinylDNS(
|
||||
testConfigLdap,
|
||||
mockLdapAuthenticator,
|
||||
mockLockedUserAccessor,
|
||||
client,
|
||||
components,
|
||||
crypto,
|
||||
mockOidcAuth
|
||||
)
|
||||
val result =
|
||||
underTest.getGroupChange(hobbitGroupChangeId)(
|
||||
FakeRequest(GET, s"/groups/change/$hobbitGroupChangeId")
|
||||
.withSession(
|
||||
"username" -> lockedFrodoUser.userName,
|
||||
"accessKey" -> lockedFrodoUser.accessKey
|
||||
)
|
||||
)
|
||||
|
||||
status(result) mustEqual 403
|
||||
hasCacheHeaders(result)
|
||||
contentAsString(result) must beEqualTo(
|
||||
s"User account for `${lockedFrodoUser.userName}` is locked."
|
||||
)
|
||||
}
|
||||
"return unauthorized (401) if user is not logged in" in new WithApplication(app) {
|
||||
val client = mock[WSClient]
|
||||
val underTest = withClient(client)
|
||||
val result =
|
||||
underTest.getGroupChange(hobbitGroupChangeId)(FakeRequest(GET, s"/groups/change/$hobbitGroupChangeId"))
|
||||
|
||||
status(result) must beEqualTo(401)
|
||||
contentAsString(result) must beEqualTo("You are not logged in. Please login to continue.")
|
||||
hasCacheHeaders(result)
|
||||
}
|
||||
}
|
||||
|
||||
".listGroupChanges" should {
|
||||
"return group changes - status ok (200)" in new WithApplication(app) {
|
||||
val client = MockWS {
|
||||
case (GET, u) if u == s"http://localhost:9001/groups/${hobbitGroupId}/activity" =>
|
||||
defaultActionBuilder { Results.Ok(hobbitGroupChanges) }
|
||||
}
|
||||
val mockUserAccessor = mock[UserAccountAccessor]
|
||||
mockUserAccessor.get(anyString).returns(IO.pure(Some(frodoUser)))
|
||||
mockUserAccessor.getUserByKey(anyString).returns(IO.pure(Some(frodoUser)))
|
||||
val underTest = withClient(client)
|
||||
val result =
|
||||
underTest.listGroupChanges(hobbitGroupId)(
|
||||
FakeRequest(GET, s"/groups/$hobbitGroupId/activity")
|
||||
.withSession("username" -> frodoUser.userName, "accessKey" -> frodoUser.accessKey)
|
||||
)
|
||||
|
||||
status(result) must beEqualTo(OK)
|
||||
hasCacheHeaders(result)
|
||||
contentAsJson(result) must beEqualTo(hobbitGroupChanges)
|
||||
}
|
||||
"return unauthorized (401) if requesting user is not logged in" in new WithApplication(app) {
|
||||
val client = mock[WSClient]
|
||||
val underTest = withClient(client)
|
||||
val result =
|
||||
underTest.listGroupChanges(hobbitGroupId)(
|
||||
FakeRequest(GET, s"/api/groups/$hobbitGroupId/activity")
|
||||
)
|
||||
|
||||
status(result) mustEqual 401
|
||||
hasCacheHeaders(result)
|
||||
contentAsString(result) must beEqualTo("You are not logged in. Please login to continue.")
|
||||
}
|
||||
"return forbidden (403) if user account is locked" in new WithApplication(app) {
|
||||
val client = mock[WSClient]
|
||||
val underTest = withLockedClient(client)
|
||||
val result = underTest.listGroupChanges(hobbitGroupId)(
|
||||
FakeRequest(GET, s"/api/groups/$hobbitGroupId/activity").withSession(
|
||||
"username" -> lockedFrodoUser.userName,
|
||||
"accessKey" -> lockedFrodoUser.accessKey
|
||||
)
|
||||
)
|
||||
|
||||
status(result) mustEqual 403
|
||||
hasCacheHeaders(result)
|
||||
contentAsString(result) must beEqualTo(
|
||||
s"User account for `${lockedFrodoUser.userName}` is locked."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
".deleteGroup" should {
|
||||
"return ok with no content (204) when delete is successful" in new WithApplication(app) {
|
||||
val client = MockWS {
|
||||
@ -1502,6 +1650,78 @@ class VinylDNSSpec extends Specification with Mockito with TestApplicationData w
|
||||
}
|
||||
}
|
||||
|
||||
".getZoneChange" should {
|
||||
|
||||
"return ok (200) if the zoneChanges is found" in new WithApplication(app) {
|
||||
val client = MockWS {
|
||||
case (GET, u) if u == s"http://localhost:9001/zones/$hobbitZoneId/changes" =>
|
||||
defaultActionBuilder { Results.Ok(hobbitZoneChange) }
|
||||
}
|
||||
val mockUserAccessor = mock[UserAccountAccessor]
|
||||
mockUserAccessor.get(anyString).returns(IO.pure(Some(frodoUser)))
|
||||
mockUserAccessor.getUserByKey(anyString).returns(IO.pure(Some(frodoUser)))
|
||||
val underTest = withClient(client)
|
||||
val result =
|
||||
underTest.getZoneChange(hobbitZoneId)(
|
||||
FakeRequest(GET, s"/zones/$hobbitZoneId/changes")
|
||||
.withSession("username" -> frodoUser.userName, "accessKey" -> frodoUser.accessKey)
|
||||
)
|
||||
|
||||
status(result) must beEqualTo(OK)
|
||||
hasCacheHeaders(result)
|
||||
contentAsJson(result) must beEqualTo(hobbitZoneChange)
|
||||
}
|
||||
|
||||
"return a not found (404) if the zoneChanges does not exist" in new WithApplication(app) {
|
||||
val client = MockWS {
|
||||
case (GET, u) if u == s"http://localhost:9001/zones/not-hobbits/changes" =>
|
||||
defaultActionBuilder { Results.NotFound }
|
||||
}
|
||||
val mockUserAccessor = mock[UserAccountAccessor]
|
||||
mockUserAccessor.get(anyString).returns(IO.pure(Some(frodoUser)))
|
||||
mockUserAccessor.getUserByKey(anyString).returns(IO.pure(Some(frodoUser)))
|
||||
val underTest = withClient(client)
|
||||
val result =
|
||||
underTest.getZoneChange("not-hobbits")(
|
||||
FakeRequest(GET, "/zones/not-hobbits/changes")
|
||||
.withSession("username" -> frodoUser.userName, "accessKey" -> frodoUser.accessKey)
|
||||
)
|
||||
|
||||
status(result) must beEqualTo(NOT_FOUND)
|
||||
hasCacheHeaders(result)
|
||||
}
|
||||
|
||||
"return unauthorized (401) if requesting user is not logged in" in new WithApplication(app) {
|
||||
val client = mock[WSClient]
|
||||
val underTest = withClient(client)
|
||||
val result =
|
||||
underTest.getZoneChange(hobbitZoneId)(
|
||||
FakeRequest(GET, s"/api/zones/$hobbitZoneId/changes")
|
||||
)
|
||||
|
||||
status(result) mustEqual 401
|
||||
hasCacheHeaders(result)
|
||||
contentAsString(result) must beEqualTo("You are not logged in. Please login to continue.")
|
||||
}
|
||||
|
||||
"return forbidden (403) if user account is locked" in new WithApplication(app) {
|
||||
val client = mock[WSClient]
|
||||
val underTest = withLockedClient(client)
|
||||
val result = underTest.getZoneChange(hobbitZoneId)(
|
||||
FakeRequest(GET, s"/api/zones/$hobbitZoneId/changes").withSession(
|
||||
"username" -> lockedFrodoUser.userName,
|
||||
"accessKey" -> lockedFrodoUser.accessKey
|
||||
)
|
||||
)
|
||||
|
||||
status(result) mustEqual 403
|
||||
hasCacheHeaders(result)
|
||||
contentAsString(result) must beEqualTo(
|
||||
s"User account for `${lockedFrodoUser.userName}` is locked."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
".getZoneByName" should {
|
||||
"return ok (200) if the zone is found" in new WithApplication(app) {
|
||||
val client = MockWS {
|
||||
|
@ -1 +1 @@
|
||||
version in ThisBuild := "0.14.0"
|
||||
version in ThisBuild := "0.16.2"
|
||||
|
Loading…
x
Reference in New Issue
Block a user