mirror of
https://github.com/VinylDNS/vinyldns
synced 2025-08-21 17:37:15 +00:00
Merge branch 'master' into dependency_upgrade
This commit is contained in:
commit
7eed595bd4
149
.github/workflows/release-beta.yml
vendored
Normal file
149
.github/workflows/release-beta.yml
vendored
Normal file
@ -0,0 +1,149 @@
|
||||
name: VinylDNS Beta Release
|
||||
concurrency:
|
||||
cancel-in-progress: true
|
||||
group: "release"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
verify-first:
|
||||
description: 'Verify First?'
|
||||
required: true
|
||||
default: 'true'
|
||||
create-gh-release:
|
||||
description: 'Create a GitHub Release?'
|
||||
required: true
|
||||
default: 'true'
|
||||
publish-images:
|
||||
description: 'Publish Docker Images?'
|
||||
required: true
|
||||
default: 'true'
|
||||
pre-release:
|
||||
description: 'Is this a pre-release?'
|
||||
required: true
|
||||
default: 'true'
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
name: Verify Release
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout current branch
|
||||
if: github.event.inputs.verify-first == 'true'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Run Tests
|
||||
id: verify
|
||||
if: github.event.inputs.verify-first == 'true'
|
||||
run: cd build/ && ./assemble_api.sh && ./run_all_tests.sh
|
||||
|
||||
create-gh-release:
|
||||
name: Create GitHub Release
|
||||
needs: verify
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.inputs.create-gh-release == 'true'
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout current branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Build Artifacts
|
||||
id: build
|
||||
run: cd build/ && ./assemble_api.sh && ./assemble_portal.sh
|
||||
|
||||
- name: Get Version
|
||||
id: get-version
|
||||
run: echo "::set-output name=vinyldns_version::$(awk -F'"' '{print $2}' ./version.sbt)"
|
||||
|
||||
- name: Create GitHub Release
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: v${{ steps.get-version.outputs.vinyldns_version }}
|
||||
generate_release_notes: true
|
||||
files: artifacts/*
|
||||
prerelease: ${{ github.event.inputs['pre-release'] == 'true' }}
|
||||
|
||||
docker-release-api:
|
||||
name: Release API Docker Image
|
||||
needs: [ verify, create-gh-release ]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.inputs.publish-images == 'true'
|
||||
|
||||
steps:
|
||||
- name: Get Version
|
||||
id: get-version
|
||||
run: echo "::set-output name=vinyldns_version::$(curl -s https://api.github.com/repos/vinyldns/vinyldns/releases | jq -rc '.[0].tag_name')"
|
||||
|
||||
- name: Checkout current branch (full)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ steps.get-version.outputs.vinyldns_version }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
- name: Import Content Trust Key
|
||||
run: docker trust key load <(echo "${SIGNING_KEY}") --name vinyldns_svc
|
||||
env:
|
||||
SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
|
||||
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ secrets.DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE }}
|
||||
|
||||
# This will publish the latest release
|
||||
- name: Publish API Docker Image
|
||||
run: make -C build/docker/api publish
|
||||
env:
|
||||
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ secrets.DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE }}
|
||||
|
||||
docker-release-portal:
|
||||
name: Release Portal Docker Image
|
||||
needs: [ verify, create-gh-release ]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.inputs.publish-images == 'true'
|
||||
|
||||
steps:
|
||||
- name: Get Version
|
||||
id: get-version
|
||||
run: echo "::set-output name=vinyldns_version::$(curl -s https://api.github.com/repos/vinyldns/vinyldns/releases | jq -rc '.[0].tag_name')"
|
||||
|
||||
- name: Checkout current branch (full)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ steps.get-version.outputs.vinyldns_version }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
- name: Import Content Trust Key
|
||||
run: docker trust key load <(echo "${SIGNING_KEY}") --name vinyldns_svc
|
||||
env:
|
||||
SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
|
||||
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ secrets.DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE }}
|
||||
|
||||
# This will publish the latest release
|
||||
- name: Publish Portal Docker Image
|
||||
run: make -C build/docker/portal publish
|
||||
env:
|
||||
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ secrets.DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE }}
|
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
@ -34,7 +34,7 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout current branch
|
||||
if: github.event.inputs.verify-first == 'true'
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@ -53,7 +53,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout current branch
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@ -67,7 +67,7 @@ jobs:
|
||||
|
||||
- name: Create GitHub Release
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@1e07f4398721186383de40550babbdf2b84acfc5 # v0.1.14
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: v${{ steps.get-version.outputs.vinyldns_version }}
|
||||
generate_release_notes: true
|
||||
@ -85,13 +85,13 @@ jobs:
|
||||
run: echo "::set-output name=vinyldns_version::$(curl -s https://api.github.com/repos/vinyldns/vinyldns/releases | jq -rc '.[0].tag_name')"
|
||||
|
||||
- name: Checkout current branch (full)
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ steps.get-version.outputs.vinyldns_version }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
@ -120,13 +120,13 @@ jobs:
|
||||
run: echo "::set-output name=vinyldns_version::$(curl -s https://api.github.com/repos/vinyldns/vinyldns/releases | jq -rc '.[0].tag_name')"
|
||||
|
||||
- name: Checkout current branch (full)
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ steps.get-version.outputs.vinyldns_version }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
8
.github/workflows/verify.yml
vendored
8
.github/workflows/verify.yml
vendored
@ -31,28 +31,28 @@ jobs:
|
||||
|
||||
- name: Codecov
|
||||
id: codecov0
|
||||
uses: codecov/codecov-action@v2
|
||||
uses: codecov/codecov-action@v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
- name: Codecov Retry
|
||||
id: codecov1
|
||||
if: steps.codecov0.outcome=='failure'
|
||||
uses: codecov/codecov-action@v2
|
||||
uses: codecov/codecov-action@v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
- name: Codecov Retry 2
|
||||
id: codecov2
|
||||
if: steps.codecov1.outcome=='failure'
|
||||
uses: codecov/codecov-action@v2
|
||||
uses: codecov/codecov-action@v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
- name: Codecov Retry 3
|
||||
id: codecov3
|
||||
if: steps.codecov2.outcome=='failure'
|
||||
uses: codecov/codecov-action@v2
|
||||
uses: codecov/codecov-action@v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
@ -21,12 +21,14 @@ in any way, but do not see your name here, please open a PR to add yourself (in
|
||||
- Joe Crowe
|
||||
- Jearvon Dharrie
|
||||
- Andrew Dunn
|
||||
- Josh Edwards
|
||||
- Ryan Emerle
|
||||
- David Grizzanti
|
||||
- Alejandro Guirao
|
||||
- Daniel Jin
|
||||
- Harry Kauffman
|
||||
- Krista Khare
|
||||
- Sokitha Krishnan
|
||||
- Patrick Lee
|
||||
- Sheree Liu
|
||||
- Michael Ly
|
||||
@ -41,8 +43,9 @@ in any way, but do not see your name here, please open a PR to add yourself (in
|
||||
- Khalid Reid
|
||||
- Timo Schmid
|
||||
- Trent Schmidt
|
||||
- Nick Spadaccino
|
||||
- Arpit Shah
|
||||
- Ghafar Shah
|
||||
- Nick Spadaccino
|
||||
- Rebecca Star
|
||||
- Jess Stodola
|
||||
- Juan Valencia
|
||||
|
@ -145,9 +145,9 @@ See the [Contributing Guide](CONTRIBUTING.md).
|
||||
|
||||
The current maintainers (people who can merge pull requests) are:
|
||||
|
||||
- Ryan Emerle ([@remerle](https://github.com/remerle))
|
||||
- Sriram Ramakrishnan ([@sramakr](https://github.com/sramakr))
|
||||
- Jim Wakemen ([@jwakemen](https://github.com/jwakemen))
|
||||
- Arpit Shah ([@arpit4ever](https://github.com/arpit4ever))
|
||||
- Nick Spadaccino ([@nspadaccino](https://github.com/nspadaccino))
|
||||
- Jay Velkumar ([@Jay07GIT](https://github.com/Jay07GIT))
|
||||
|
||||
See [AUTHORS.md](AUTHORS.md) for the full list of contributors to VinylDNS.
|
||||
|
||||
|
@ -72,6 +72,9 @@ lazy val apiAssemblySettings = Seq(
|
||||
MergeStrategy.discard
|
||||
case PathList("scala", "tools", "nsc", "doc", "html", "resource", "lib", "template.js") =>
|
||||
MergeStrategy.discard
|
||||
case "simulacrum/op.class" | "simulacrum/op$.class" | "simulacrum/typeclass$.class"
|
||||
| "simulacrum/typeclass.class" | "simulacrum/noop.class" =>
|
||||
MergeStrategy.discard
|
||||
case x if x.endsWith("module-info.class") => MergeStrategy.discard
|
||||
case x =>
|
||||
val oldStrategy = (assemblyMergeStrategy in assembly).value
|
||||
|
@ -27,10 +27,16 @@ vinyldns {
|
||||
scheduled-changes-enabled = ${?SCHEDULED_CHANGES_ENABLED}
|
||||
multi-record-batch-change-enabled = true
|
||||
multi-record-batch-change-enabled = ${?MULTI_RECORD_BATCH_CHANGE_ENABLED}
|
||||
|
||||
# Server settings
|
||||
use-recordset-cache = true
|
||||
use-recordset-cache = ${?USE_RECORDSET_CACHE}
|
||||
load-test-data = false
|
||||
load-test-data = ${?LOAD_TEST_DATA}
|
||||
# should be true while running locally or when we have only one api server/instance, for zone sync scheduler to work
|
||||
is-zone-sync-schedule-allowed = true
|
||||
# should be set to true only on a single server/instance else automated sync will be performed at every server/instance
|
||||
is-zone-sync-schedule-allowed = ${?IS_ZONE_SYNC_SCHEDULE_ALLOWED}
|
||||
|
||||
# configured backend providers
|
||||
backend {
|
||||
@ -155,6 +161,17 @@ vinyldns {
|
||||
port=${?API_SERVICE_PORT}
|
||||
}
|
||||
|
||||
api {
|
||||
limits {
|
||||
batchchange-routing-max-items-limit = 100
|
||||
membership-routing-default-max-items = 100
|
||||
membership-routing-max-items-limit = 1000
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
approved-name-servers = [
|
||||
"172.17.42.1.",
|
||||
@ -182,9 +199,9 @@ vinyldns {
|
||||
name = ${?DATABASE_NAME}
|
||||
driver = "org.mariadb.jdbc.Driver"
|
||||
driver = ${?JDBC_DRIVER}
|
||||
migration-url = "jdbc:mariadb://localhost:19002/?user=root&password=pass"
|
||||
migration-url = "jdbc:mariadb://localhost:19002/?user=root&password=pass&socketTimeout=20000"
|
||||
migration-url = ${?JDBC_MIGRATION_URL}
|
||||
url = "jdbc:mariadb://localhost:19002/vinyldns?user=root&password=pass"
|
||||
url = "jdbc:mariadb://localhost:19002/vinyldns?user=root&password=pass&socketTimeout=20000"
|
||||
url = ${?JDBC_URL}
|
||||
user = "root"
|
||||
user = ${?JDBC_USER}
|
||||
@ -192,6 +209,12 @@ vinyldns {
|
||||
password = ${?JDBC_PASSWORD}
|
||||
flyway-out-of-order = false
|
||||
flyway-out-of-order = ${?FLYWAY_OUT_OF_ORDER}
|
||||
|
||||
max-lifetime = 300000
|
||||
connection-timeout-millis = 30000
|
||||
idle-timeout = 150000
|
||||
maximum-pool-size = 20
|
||||
minimum-idle = 5
|
||||
}
|
||||
|
||||
# TODO: Remove the need for these useless configuration blocks
|
||||
@ -328,6 +351,10 @@ akka.http {
|
||||
# Set to `infinite` to disable.
|
||||
bind-timeout = 5s
|
||||
|
||||
# A default request timeout is applied globally to all routes and can be configured using the
|
||||
# akka.http.server.request-timeout setting (which defaults to 20 seconds).
|
||||
# request-timeout = 60s
|
||||
|
||||
# Show verbose error messages back to the client
|
||||
verbose-error-messages = on
|
||||
}
|
||||
|
@ -9,10 +9,15 @@
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
|
||||
<logger name="vinyldns.core.route.Monitor" level="OFF"/>
|
||||
|
||||
<logger name="scalikejdbc.StatementExecutor$$anon$1" level="OFF"/>
|
||||
|
||||
<logger name="com.zaxxer.hikari" level="ERROR">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</logger>
|
||||
|
||||
<root level="${VINYLDNS_LOG_LEVEL}">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</root>
|
||||
|
@ -50,9 +50,9 @@ mysql {
|
||||
name = ${?DATABASE_NAME}
|
||||
driver = "org.mariadb.jdbc.Driver"
|
||||
driver = ${?JDBC_DRIVER}
|
||||
migration-url = "jdbc:mariadb://"${mysql.endpoint}"/?user=root&password=pass"
|
||||
migration-url = "jdbc:mariadb://"${mysql.endpoint}"/?user=root&password=pass&socketTimeout=20000"
|
||||
migration-url = ${?JDBC_MIGRATION_URL}
|
||||
url = "jdbc:mariadb://"${mysql.endpoint}"/vinyldns?user=root&password=pass"
|
||||
url = "jdbc:mariadb://"${mysql.endpoint}"/vinyldns?user=root&password=pass&socketTimeout=20000"
|
||||
url = ${?JDBC_URL}
|
||||
user = "root"
|
||||
user = ${?JDBC_USER}
|
||||
@ -60,6 +60,12 @@ mysql {
|
||||
password = ${?JDBC_PASSWORD}
|
||||
flyway-out-of-order = false
|
||||
flyway-out-of-order = ${?FLYWAY_OUT_OF_ORDER}
|
||||
|
||||
max-lifetime = 300000
|
||||
connection-timeout-millis = 30000
|
||||
idle-timeout = 150000
|
||||
maximum-pool-size = 20
|
||||
minimum-idle = 5
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,6 +76,18 @@ crypto {
|
||||
secret = ${?CRYPTO_SECRET}
|
||||
}
|
||||
|
||||
api {
|
||||
limits {
|
||||
batchchange-routing-max-items-limit = 100
|
||||
membership-routing-default-max-items = 100
|
||||
membership-routing-max-items-limit = 1000
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
http.port = 9001
|
||||
http.port = ${?PORTAL_PORT}
|
||||
|
||||
@ -83,6 +101,12 @@ shared-display-enabled = ${?SHARED_ZONES_ENABLED}
|
||||
play.http.secret.key = "changeme"
|
||||
play.http.secret.key = ${?PLAY_HTTP_SECRET_KEY}
|
||||
|
||||
# See https://www.playframework.com/documentation/2.8.x/AllowedHostsFilter for more details.
|
||||
# Note: allowed = ["."] matches all hosts hence would not be recommended in a production environment.
|
||||
play.filters.hosts {
|
||||
allowed = ["."]
|
||||
}
|
||||
|
||||
# You can provide configuration overrides via local.conf if you don't want to replace everything in
|
||||
# this configuration file
|
||||
include "local.conf"
|
||||
|
@ -15,6 +15,10 @@
|
||||
<logger name="play" level="INFO" />
|
||||
<logger name="application" level="DEBUG" />
|
||||
|
||||
<logger name="com.zaxxer.hikari" level="ERROR">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</logger>
|
||||
|
||||
<root level="${VINYLDNS_LOG_LEVEL}">
|
||||
<appender-ref ref="CONSOLE" />
|
||||
</root>
|
||||
|
@ -348,6 +348,10 @@ akka.http {
|
||||
# Set to `infinite` to disable.
|
||||
bind-timeout = 5s
|
||||
|
||||
# A default request timeout is applied globally to all routes and can be configured using the
|
||||
# akka.http.server.request-timeout setting (which defaults to 20 seconds).
|
||||
# request-timeout = 60s
|
||||
|
||||
# Show verbose error messages back to the client
|
||||
verbose-error-messages = on
|
||||
}
|
||||
|
@ -34,6 +34,6 @@ trait MySqlApiIntegrationSpec extends MySqlIntegrationSpec {
|
||||
|
||||
def clearGroupRepo(): Unit =
|
||||
DB.localTx { s =>
|
||||
s.executeUpdate("DELETE FROM groups")
|
||||
s.executeUpdate("DELETE FROM `groups`")
|
||||
}
|
||||
}
|
||||
|
@ -22,14 +22,8 @@ import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpec
|
||||
import vinyldns.api.backend.dns.DnsProtocol.NoError
|
||||
import vinyldns.core.crypto.NoOpCrypto
|
||||
import vinyldns.core.domain.record.{
|
||||
AData,
|
||||
RecordSet,
|
||||
RecordSetChange,
|
||||
RecordSetChangeType,
|
||||
RecordSetStatus,
|
||||
RecordType
|
||||
}
|
||||
import vinyldns.core.domain.Encrypted
|
||||
import vinyldns.core.domain.record.{AData, RecordSet, RecordSetChange, RecordSetChangeType, RecordSetStatus, RecordType}
|
||||
import vinyldns.core.domain.zone.{Algorithm, Zone, ZoneConnection}
|
||||
|
||||
class DnsBackendIntegrationSpec extends AnyWordSpec with Matchers {
|
||||
@ -37,7 +31,7 @@ class DnsBackendIntegrationSpec extends AnyWordSpec with Matchers {
|
||||
private val testConnection = ZoneConnection(
|
||||
"vinyldns.",
|
||||
"vinyldns.",
|
||||
"nzisn+4G2ldMn0q1CV3vsg==",
|
||||
Encrypted("nzisn+4G2ldMn0q1CV3vsg=="),
|
||||
sys.env.getOrElse("DEFAULT_DNS_ADDRESS", "127.0.0.1:19001"),
|
||||
Algorithm.HMAC_MD5
|
||||
)
|
||||
|
@ -19,6 +19,7 @@ package vinyldns.api.domain.record
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
import cats.scalatest.EitherMatchers
|
||||
import org.mockito.Matchers.any
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
import org.mockito.Mockito._
|
||||
@ -34,13 +35,16 @@ import vinyldns.api.domain.zone._
|
||||
import vinyldns.api.engine.TestMessageQueue
|
||||
import vinyldns.mysql.TransactionProvider
|
||||
import vinyldns.core.TestZoneData.testConnection
|
||||
import vinyldns.core.domain.{Fqdn, HighValueDomainError}
|
||||
import vinyldns.core.domain.{Encrypted, Fqdn, HighValueDomainError}
|
||||
import vinyldns.core.domain.auth.AuthPrincipal
|
||||
import vinyldns.core.domain.backend.{Backend, BackendResolver}
|
||||
import vinyldns.core.domain.membership.{Group, GroupRepository, User, UserRepository}
|
||||
import vinyldns.core.domain.record.RecordType._
|
||||
import vinyldns.core.domain.record._
|
||||
import vinyldns.core.domain.zone._
|
||||
import vinyldns.core.notifier.{AllNotifiers, Notification, Notifier}
|
||||
|
||||
import scala.concurrent.ExecutionContext
|
||||
|
||||
class RecordSetServiceIntegrationSpec
|
||||
extends AnyWordSpec
|
||||
@ -53,19 +57,25 @@ class RecordSetServiceIntegrationSpec
|
||||
with BeforeAndAfterAll
|
||||
with TransactionProvider {
|
||||
|
||||
private implicit val contextShift: ContextShift[IO] = IO.contextShift(ExecutionContext.global)
|
||||
|
||||
private val vinyldnsConfig = VinylDNSConfig.load().unsafeRunSync()
|
||||
|
||||
private val recordSetRepo = recordSetRepository
|
||||
private val recordSetCacheRepo = recordSetCacheRepository
|
||||
|
||||
private val mockNotifier = mock[Notifier]
|
||||
private val mockNotifiers = AllNotifiers(List(mockNotifier))
|
||||
|
||||
private val zoneRepo: ZoneRepository = zoneRepository
|
||||
private val groupRepo: GroupRepository = groupRepository
|
||||
|
||||
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 user = User("live-test-user", "key", Encrypted("secret"))
|
||||
private val testUser = User("testuser", "key", Encrypted("secret"))
|
||||
private val user2 = User("shared-record-test-user", "key-shared", Encrypted("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))
|
||||
@ -118,7 +128,10 @@ class RecordSetServiceIntegrationSpec
|
||||
RecordSetStatus.Active,
|
||||
Instant.now.truncatedTo(ChronoUnit.MILLIS),
|
||||
None,
|
||||
List(AAAAData("fd69:27cc:fe91::60"))
|
||||
List(AAAAData("fd69:27cc:fe91::60")),
|
||||
recordSetGroupChange =
|
||||
Some(OwnerShipTransfer(ownerShipTransferStatus = OwnerShipTransferStatus.None,
|
||||
requestedOwnerGroupId = None))
|
||||
)
|
||||
private val subTestRecordA = RecordSet(
|
||||
zone.id,
|
||||
@ -231,6 +244,36 @@ class RecordSetServiceIntegrationSpec
|
||||
ownerGroupId = Some("non-existent")
|
||||
)
|
||||
|
||||
private val sharedTestRecordPendingReviewOwnerShip = RecordSet(
|
||||
sharedZone.id,
|
||||
"shared-record-ownerShip-pendingReview",
|
||||
A,
|
||||
200,
|
||||
RecordSetStatus.Active,
|
||||
Instant.now.truncatedTo(ChronoUnit.MILLIS),
|
||||
None,
|
||||
List(AData("1.1.1.1")),
|
||||
ownerGroupId = Some(sharedGroup.id),
|
||||
recordSetGroupChange = Some(OwnerShipTransfer(
|
||||
ownerShipTransferStatus = OwnerShipTransferStatus.PendingReview,
|
||||
requestedOwnerGroupId = Some(group.id)))
|
||||
)
|
||||
|
||||
private val sharedTestRecordCancelledOwnerShip = RecordSet(
|
||||
sharedZone.id,
|
||||
"shared-record-ownerShip-cancelled",
|
||||
A,
|
||||
200,
|
||||
RecordSetStatus.Active,
|
||||
Instant.now.truncatedTo(ChronoUnit.MILLIS),
|
||||
None,
|
||||
List(AData("1.1.1.1")),
|
||||
ownerGroupId = Some(sharedGroup.id),
|
||||
recordSetGroupChange = Some(OwnerShipTransfer(
|
||||
ownerShipTransferStatus = OwnerShipTransferStatus.Cancelled,
|
||||
requestedOwnerGroupId = Some(group.id)))
|
||||
)
|
||||
|
||||
private val testOwnerGroupRecordInNormalZone = RecordSet(
|
||||
zone.id,
|
||||
"user-in-owner-group-but-zone-not-shared",
|
||||
@ -284,7 +327,10 @@ class RecordSetServiceIntegrationSpec
|
||||
// Seeding records in DB
|
||||
val sharedRecords = List(
|
||||
sharedTestRecord,
|
||||
sharedTestRecordBadOwnerGroup
|
||||
sharedTestRecordBadOwnerGroup,
|
||||
sharedTestRecordPendingReviewOwnerShip,
|
||||
sharedTestRecordCancelledOwnerShip
|
||||
|
||||
)
|
||||
val conflictRecords = List(
|
||||
subTestRecordNameConflict,
|
||||
@ -323,7 +369,8 @@ class RecordSetServiceIntegrationSpec
|
||||
vinyldnsConfig.highValueDomainConfig,
|
||||
vinyldnsConfig.dottedHostsConfig,
|
||||
vinyldnsConfig.serverConfig.approvedNameServers,
|
||||
useRecordSetCache = true
|
||||
useRecordSetCache = true,
|
||||
mockNotifiers
|
||||
)
|
||||
}
|
||||
|
||||
@ -424,6 +471,227 @@ class RecordSetServiceIntegrationSpec
|
||||
leftValue(result) shouldBe a[InvalidRequest]
|
||||
}
|
||||
|
||||
"auto-approve ownership transfer request, if user tried to update the ownership" in {
|
||||
val newRecord = sharedTestRecord.copy(recordSetGroupChange =
|
||||
Some(OwnerShipTransfer(ownerShipTransferStatus = OwnerShipTransferStatus.AutoApproved,
|
||||
requestedOwnerGroupId = Some(group.id))))
|
||||
|
||||
val result = testRecordSetService
|
||||
.updateRecordSet(newRecord, auth2)
|
||||
.value
|
||||
.unsafeRunSync()
|
||||
|
||||
val change = rightValue(result).asInstanceOf[RecordSetChange]
|
||||
change.recordSet.name shouldBe "shared-record"
|
||||
change.recordSet.ownerGroupId.get shouldBe group.id
|
||||
change.recordSet.recordSetGroupChange.get.ownerShipTransferStatus shouldBe OwnerShipTransferStatus.AutoApproved
|
||||
change.recordSet.recordSetGroupChange.get.requestedOwnerGroupId.get shouldBe group.id
|
||||
}
|
||||
|
||||
"approve ownership transfer request, if user requested for ownership transfer" in {
|
||||
val newRecord = sharedTestRecordPendingReviewOwnerShip.copy(recordSetGroupChange =
|
||||
Some(OwnerShipTransfer(
|
||||
ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyApproved)))
|
||||
|
||||
doReturn(IO.unit).when(mockNotifier).notify(any[Notification[_]])
|
||||
|
||||
val result = testRecordSetService
|
||||
.updateRecordSet(newRecord, auth2)
|
||||
.value
|
||||
.unsafeRunSync()
|
||||
|
||||
val change = rightValue(result).asInstanceOf[RecordSetChange]
|
||||
change.recordSet.name shouldBe "shared-record-ownerShip-pendingReview"
|
||||
change.recordSet.ownerGroupId.get shouldBe group.id
|
||||
change.recordSet.recordSetGroupChange.get.ownerShipTransferStatus shouldBe OwnerShipTransferStatus.ManuallyApproved
|
||||
change.recordSet.recordSetGroupChange.get.requestedOwnerGroupId.get shouldBe group.id
|
||||
}
|
||||
|
||||
"reject ownership transfer request, if user requested for ownership transfer" in {
|
||||
val newRecord = sharedTestRecordPendingReviewOwnerShip.copy(recordSetGroupChange =
|
||||
Some(OwnerShipTransfer(
|
||||
ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyRejected)))
|
||||
|
||||
doReturn(IO.unit).when(mockNotifier).notify(any[Notification[_]])
|
||||
|
||||
val result = testRecordSetService
|
||||
.updateRecordSet(newRecord, auth2)
|
||||
.value
|
||||
.unsafeRunSync()
|
||||
|
||||
val change = rightValue(result).asInstanceOf[RecordSetChange]
|
||||
change.recordSet.name shouldBe "shared-record-ownerShip-pendingReview"
|
||||
change.recordSet.ownerGroupId.get shouldBe sharedGroup.id
|
||||
change.recordSet.recordSetGroupChange.get.ownerShipTransferStatus shouldBe OwnerShipTransferStatus.ManuallyRejected
|
||||
change.recordSet.recordSetGroupChange.get.requestedOwnerGroupId.get shouldBe group.id
|
||||
}
|
||||
|
||||
"request ownership transfer, if user not in the owner group and wants to own the record" in {
|
||||
val newRecord = sharedTestRecord.copy(recordSetGroupChange =
|
||||
Some(OwnerShipTransfer(
|
||||
ownerShipTransferStatus = OwnerShipTransferStatus.Requested,
|
||||
requestedOwnerGroupId = Some(dummyGroup.id))))
|
||||
|
||||
doReturn(IO.unit).when(mockNotifier).notify(any[Notification[_]])
|
||||
|
||||
val result = testRecordSetService
|
||||
.updateRecordSet(newRecord, dummyAuth)
|
||||
.value
|
||||
.unsafeRunSync()
|
||||
|
||||
val change = rightValue(result).asInstanceOf[RecordSetChange]
|
||||
change.recordSet.name shouldBe "shared-record"
|
||||
change.recordSet.ownerGroupId.get shouldBe sharedGroup.id
|
||||
change.recordSet.recordSetGroupChange.get.ownerShipTransferStatus shouldBe OwnerShipTransferStatus.PendingReview
|
||||
change.recordSet.recordSetGroupChange.get.requestedOwnerGroupId.get shouldBe dummyGroup.id
|
||||
}
|
||||
|
||||
"fail requesting ownership transfer if user is not in owner group and tried to update other fields in record set" in {
|
||||
val newRecord = sharedTestRecord.copy(
|
||||
ttl = 3000,
|
||||
recordSetGroupChange =
|
||||
Some(OwnerShipTransfer(
|
||||
ownerShipTransferStatus = OwnerShipTransferStatus.Requested,
|
||||
requestedOwnerGroupId = Some(dummyGroup.id))))
|
||||
|
||||
val result = testRecordSetService
|
||||
.updateRecordSet(newRecord, dummyAuth)
|
||||
.value
|
||||
.unsafeRunSync()
|
||||
|
||||
leftValue(result) shouldBe a[InvalidRequest]
|
||||
}
|
||||
|
||||
"fail updating if user is not in owner group for ownership transfer approval" in {
|
||||
val newRecord = sharedTestRecordPendingReviewOwnerShip.copy(recordSetGroupChange =
|
||||
Some(OwnerShipTransfer(
|
||||
ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyApproved)))
|
||||
|
||||
val result = testRecordSetService
|
||||
.updateRecordSet(newRecord, dummyAuth)
|
||||
.value
|
||||
.unsafeRunSync()
|
||||
|
||||
leftValue(result) shouldBe a[NotAuthorizedError]
|
||||
}
|
||||
|
||||
"fail updating if user is not in owner group for ownership transfer reject" in {
|
||||
val newRecord = sharedTestRecordPendingReviewOwnerShip.copy(recordSetGroupChange =
|
||||
Some(OwnerShipTransfer(
|
||||
ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyRejected)))
|
||||
|
||||
val result = testRecordSetService
|
||||
.updateRecordSet(newRecord, dummyAuth)
|
||||
.value
|
||||
.unsafeRunSync()
|
||||
|
||||
leftValue(result) shouldBe a[NotAuthorizedError]
|
||||
}
|
||||
|
||||
"cancel the ownership transfer request, if user not require ownership transfer further" in {
|
||||
val newRecord = sharedTestRecordPendingReviewOwnerShip.copy(recordSetGroupChange =
|
||||
Some(OwnerShipTransfer(
|
||||
ownerShipTransferStatus = OwnerShipTransferStatus.Cancelled)))
|
||||
|
||||
doReturn(IO.unit).when(mockNotifier).notify(any[Notification[_]])
|
||||
|
||||
val result = testRecordSetService
|
||||
.updateRecordSet(newRecord, auth)
|
||||
.value
|
||||
.unsafeRunSync()
|
||||
|
||||
val change = rightValue(result).asInstanceOf[RecordSetChange]
|
||||
change.recordSet.name shouldBe "shared-record-ownerShip-pendingReview"
|
||||
change.recordSet.ownerGroupId.get shouldBe sharedGroup.id
|
||||
change.recordSet.recordSetGroupChange.get.ownerShipTransferStatus shouldBe OwnerShipTransferStatus.Cancelled
|
||||
change.recordSet.recordSetGroupChange.get.requestedOwnerGroupId.get shouldBe group.id
|
||||
}
|
||||
|
||||
"fail approving ownership transfer request, if user is cancelled" in {
|
||||
val newRecord = sharedTestRecordCancelledOwnerShip.copy(recordSetGroupChange =
|
||||
Some(OwnerShipTransfer(
|
||||
ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyApproved)))
|
||||
val result = testRecordSetService
|
||||
.updateRecordSet(newRecord, auth)
|
||||
.value
|
||||
.unsafeRunSync()
|
||||
|
||||
leftValue(result) shouldBe a[InvalidRequest]
|
||||
}
|
||||
|
||||
"fail rejecting ownership transfer request, if user is cancelled" in {
|
||||
val newRecord = sharedTestRecordCancelledOwnerShip.copy(recordSetGroupChange =
|
||||
Some(OwnerShipTransfer(
|
||||
ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyRejected)))
|
||||
|
||||
val result = testRecordSetService
|
||||
.updateRecordSet(newRecord, auth)
|
||||
.value
|
||||
.unsafeRunSync()
|
||||
|
||||
leftValue(result) shouldBe a[InvalidRequest]
|
||||
}
|
||||
|
||||
"fail auto-approving ownership transfer request, if user is cancelled" in {
|
||||
val newRecord = sharedTestRecordCancelledOwnerShip.copy(recordSetGroupChange =
|
||||
Some(OwnerShipTransfer(
|
||||
ownerShipTransferStatus = OwnerShipTransferStatus.AutoApproved
|
||||
)))
|
||||
|
||||
doReturn(IO.unit).when(mockNotifier).notify(any[Notification[_]])
|
||||
|
||||
val result = testRecordSetService
|
||||
.updateRecordSet(newRecord, auth)
|
||||
.value
|
||||
.unsafeRunSync()
|
||||
|
||||
leftValue(result) shouldBe a[InvalidRequest]
|
||||
}
|
||||
|
||||
"fail auto-approving ownership transfer request, if zone is not shared" in {
|
||||
val newRecord = dottedTestRecord.copy(recordSetGroupChange =
|
||||
Some(OwnerShipTransfer(ownerShipTransferStatus = OwnerShipTransferStatus.AutoApproved,
|
||||
requestedOwnerGroupId = Some(group.id))))
|
||||
|
||||
val result = testRecordSetService
|
||||
.updateRecordSet(newRecord, auth2)
|
||||
.value
|
||||
.unsafeRunSync()
|
||||
|
||||
leftValue(result) shouldBe a[InvalidRequest]
|
||||
}
|
||||
|
||||
"fail approving ownership transfer request, if zone is not shared" in {
|
||||
val newRecord = dottedTestRecord.copy(recordSetGroupChange =
|
||||
Some(OwnerShipTransfer(
|
||||
ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyApproved
|
||||
)))
|
||||
|
||||
val result = testRecordSetService
|
||||
.updateRecordSet(newRecord, auth2)
|
||||
.value
|
||||
.unsafeRunSync()
|
||||
|
||||
leftValue(result) shouldBe a[InvalidRequest]
|
||||
}
|
||||
|
||||
"fail requesting ownership transfer, if zone is not shared" in {
|
||||
val newRecord = dottedTestRecord.copy(recordSetGroupChange =
|
||||
Some(OwnerShipTransfer(
|
||||
ownerShipTransferStatus = OwnerShipTransferStatus.Requested,
|
||||
requestedOwnerGroupId = Some(dummyGroup.id)
|
||||
)))
|
||||
|
||||
doReturn(IO.unit).when(mockNotifier).notify(any[Notification[_]])
|
||||
|
||||
val result = testRecordSetService
|
||||
.updateRecordSet(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)
|
||||
|
||||
@ -668,25 +936,23 @@ class RecordSetServiceIntegrationSpec
|
||||
}
|
||||
|
||||
"fail deleting for user not in record owner group in shared zone" in {
|
||||
val result = leftResultOf(
|
||||
val result =
|
||||
testRecordSetService
|
||||
.deleteRecordSet(sharedTestRecord.id, sharedTestRecord.zoneId, dummyAuth)
|
||||
.value
|
||||
)
|
||||
.value.unsafeRunSync().swap.toOption.get
|
||||
|
||||
result shouldBe a[NotAuthorizedError]
|
||||
}
|
||||
|
||||
"fail deleting for user in record owner group in non-shared zone" in {
|
||||
val result = leftResultOf(
|
||||
val result =
|
||||
testRecordSetService
|
||||
.deleteRecordSet(
|
||||
testOwnerGroupRecordInNormalZone.id,
|
||||
testOwnerGroupRecordInNormalZone.zoneId,
|
||||
auth2
|
||||
)
|
||||
.value
|
||||
)
|
||||
.value.unsafeRunSync().swap.toOption.get
|
||||
|
||||
result shouldBe a[NotAuthorizedError]
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ package vinyldns.api.domain.zone
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
import cats.effect._
|
||||
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
import org.mockito.Mockito.doReturn
|
||||
@ -29,6 +30,7 @@ import org.scalatestplus.mockito.MockitoSugar
|
||||
import org.scalatest.time.{Seconds, Span}
|
||||
import scalikejdbc.DB
|
||||
import vinyldns.api.domain.access.AccessValidations
|
||||
import vinyldns.api.domain.membership.MembershipService
|
||||
import vinyldns.api.domain.record.RecordSetChangeGenerator
|
||||
import vinyldns.api.engine.TestMessageQueue
|
||||
import vinyldns.mysql.TransactionProvider
|
||||
@ -62,7 +64,7 @@ class ZoneServiceIntegrationSpec
|
||||
|
||||
private val recordSetRepo = recordSetRepository
|
||||
private val zoneRepo: ZoneRepository = zoneRepository
|
||||
|
||||
private val mockMembershipService = mock[MembershipService]
|
||||
private var testZoneService: ZoneServiceAlgebra = _
|
||||
|
||||
private val badAuth = AuthPrincipal(okUser, Seq())
|
||||
@ -127,7 +129,8 @@ class ZoneServiceIntegrationSpec
|
||||
new ZoneValidations(1000),
|
||||
new AccessValidations(),
|
||||
mockBackendResolver,
|
||||
NoOpCrypto.instance
|
||||
NoOpCrypto.instance,
|
||||
mockMembershipService
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -19,8 +19,8 @@ package vinyldns.api.domain.zone
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpec
|
||||
import org.xbill.DNS.ZoneTransferException
|
||||
import vinyldns.api.backend.dns.DnsBackend
|
||||
import vinyldns.api.config.VinylDNSConfig
|
||||
import vinyldns.core.domain.Encrypted
|
||||
import vinyldns.core.domain.backend.BackendResolver
|
||||
import vinyldns.core.domain.zone.{Zone, ZoneConnection}
|
||||
|
||||
@ -50,15 +50,13 @@ class ZoneViewLoaderIntegrationSpec extends AnyWordSpec with Matchers {
|
||||
ZoneConnection(
|
||||
"vinyldns.",
|
||||
"vinyldns.",
|
||||
"nzisn+4G2ldMn0q1CV3vsg==",
|
||||
Encrypted("nzisn+4G2ldMn0q1CV3vsg=="),
|
||||
sys.env.getOrElse("DEFAULT_DNS_ADDRESS", "127.0.0.1:19001")
|
||||
)
|
||||
),
|
||||
transferConnection =
|
||||
Some(ZoneConnection("invalid-connection.", "bad-key", "invalid-key", "10.1.1.1"))
|
||||
Some(ZoneConnection("invalid-connection.", "bad-key", Encrypted("invalid-key"), "10.1.1.1"))
|
||||
)
|
||||
val backend = backendResolver.resolve(zone).asInstanceOf[DnsBackend]
|
||||
println(s"${backend.id}, ${backend.xfrInfo}, ${backend.resolver.getAddress}")
|
||||
DnsZoneViewLoader(zone, backendResolver.resolve(zone), 10000)
|
||||
.load()
|
||||
.unsafeRunSync()
|
||||
@ -83,7 +81,7 @@ class ZoneViewLoaderIntegrationSpec extends AnyWordSpec with Matchers {
|
||||
ZoneConnection(
|
||||
"vinyldns.",
|
||||
"vinyldns.",
|
||||
"nzisn+4G2ldMn0q1CV3vsg==",
|
||||
Encrypted("nzisn+4G2ldMn0q1CV3vsg=="),
|
||||
sys.env.getOrElse("DEFAULT_DNS_ADDRESS", "127.0.0.1:19001")
|
||||
)
|
||||
),
|
||||
@ -91,7 +89,7 @@ class ZoneViewLoaderIntegrationSpec extends AnyWordSpec with Matchers {
|
||||
ZoneConnection(
|
||||
"vinyldns.",
|
||||
"vinyldns.",
|
||||
"nzisn+4G2ldMn0q1CV3vsg==",
|
||||
Encrypted("nzisn+4G2ldMn0q1CV3vsg=="),
|
||||
sys.env.getOrElse("DEFAULT_DNS_ADDRESS", "127.0.0.1:19001")
|
||||
)
|
||||
)
|
||||
|
@ -24,7 +24,7 @@ import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpecLike
|
||||
import vinyldns.core.domain.batch._
|
||||
import vinyldns.core.domain.record.RecordType
|
||||
import vinyldns.core.domain.record.AData
|
||||
import vinyldns.core.domain.record.{AData, OwnerShipTransferStatus, RecordSetChange, RecordSetChangeStatus, RecordSetChangeType, RecordType}
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
import vinyldns.core.TestMembershipData._
|
||||
@ -35,6 +35,8 @@ import cats.effect.{IO, Resource}
|
||||
import scala.collection.JavaConverters._
|
||||
import org.scalatest.BeforeAndAfterEach
|
||||
import cats.implicits._
|
||||
import vinyldns.core.TestRecordSetData.{ownerShipTransfer, rsOk}
|
||||
import vinyldns.core.TestZoneData.okZone
|
||||
|
||||
class EmailNotifierIntegrationSpec
|
||||
extends MySqlApiIntegrationSpec
|
||||
@ -57,7 +59,7 @@ class EmailNotifierIntegrationSpec
|
||||
|
||||
"Email Notifier" should {
|
||||
|
||||
"send an email" taggedAs (SkipCI) in {
|
||||
"send an email for batch change" taggedAs (SkipCI) in {
|
||||
val batchChange = BatchChange(
|
||||
okUser.id,
|
||||
okUser.userName,
|
||||
@ -84,7 +86,7 @@ class EmailNotifierIntegrationSpec
|
||||
val program = for {
|
||||
_ <- userRepository.save(okUser)
|
||||
notifier <- new EmailNotifierProvider()
|
||||
.load(NotifierConfig("", emailConfig), userRepository)
|
||||
.load(NotifierConfig("", emailConfig), userRepository, groupRepository)
|
||||
_ <- notifier.notify(Notification(batchChange))
|
||||
emailFiles <- retrieveEmailFiles(targetDirectory)
|
||||
} yield emailFiles
|
||||
@ -94,7 +96,29 @@ class EmailNotifierIntegrationSpec
|
||||
files.length should be(1)
|
||||
|
||||
}
|
||||
"send an email for recordSetChange ownerShip transfer" taggedAs (SkipCI) in {
|
||||
val recordSetChange = RecordSetChange(
|
||||
okZone,
|
||||
rsOk.copy(ownerGroupId= Some(okGroup.id),recordSetGroupChange =
|
||||
Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.PendingReview, requestedOwnerGroupId = Some(dummyGroup.id)))),
|
||||
"system",
|
||||
RecordSetChangeType.Create,
|
||||
RecordSetChangeStatus.Complete
|
||||
)
|
||||
|
||||
val program = for {
|
||||
_ <- userRepository.save(okUser)
|
||||
notifier <- new EmailNotifierProvider()
|
||||
.load(NotifierConfig("", emailConfig), userRepository, groupRepository)
|
||||
_ <- notifier.notify(Notification(recordSetChange))
|
||||
emailFiles <- retrieveEmailFiles(targetDirectory)
|
||||
} yield emailFiles
|
||||
|
||||
val files = program.unsafeRunSync()
|
||||
|
||||
files.length should be(1)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
def deleteEmailFiles(path: Path): IO[Unit] =
|
||||
|
@ -111,7 +111,7 @@ class SnsNotifierIntegrationSpec
|
||||
sns.subscribe(topic, "sqs", queueUrl)
|
||||
}
|
||||
notifier <- new SnsNotifierProvider()
|
||||
.load(NotifierConfig("", snsConfig), userRepository)
|
||||
.load(NotifierConfig("", snsConfig), userRepository, groupRepository)
|
||||
_ <- notifier.notify(Notification(batchChange))
|
||||
_ <- IO.sleep(1.seconds)
|
||||
messages <- IO {
|
||||
|
@ -30,7 +30,7 @@ import vinyldns.api.engine.ZoneSyncHandler
|
||||
import vinyldns.api.{MySqlApiIntegrationSpec, ResultHelpers}
|
||||
import vinyldns.core.TestRecordSetData._
|
||||
import vinyldns.core.domain.backend.{Backend, BackendResolver}
|
||||
import vinyldns.core.domain.record.{NameSort, RecordType}
|
||||
import vinyldns.core.domain.record.{NameSort, RecordType, RecordTypeSort}
|
||||
import vinyldns.core.domain.zone.{Zone, ZoneChange, ZoneChangeType}
|
||||
import vinyldns.core.health.HealthCheck.HealthCheck
|
||||
import vinyldns.route53.backend.{Route53Backend, Route53BackendConfig}
|
||||
@ -57,6 +57,8 @@ class Route53ApiIntegrationSpec
|
||||
"test",
|
||||
Some("access"),
|
||||
Some("secret"),
|
||||
None,
|
||||
None,
|
||||
sys.env.getOrElse("R53_SERVICE_ENDPOINT", "http://localhost:19003"),
|
||||
"us-east-1"
|
||||
)
|
||||
@ -120,7 +122,7 @@ class Route53ApiIntegrationSpec
|
||||
|
||||
// We should have both the record we created above as well as at least one NS record
|
||||
val results = recordSetRepository
|
||||
.listRecordSets(Some(testZone.id), None, None, None, None, None, NameSort.ASC)
|
||||
.listRecordSets(Some(testZone.id), None, None, None, None, None, NameSort.ASC, RecordTypeSort.ASC)
|
||||
.unsafeRunSync()
|
||||
results.recordSets.map(_.typ).distinct should contain theSameElementsAs List(
|
||||
rsOk.typ,
|
||||
|
@ -27,10 +27,16 @@ vinyldns {
|
||||
scheduled-changes-enabled = ${?SCHEDULED_CHANGES_ENABLED}
|
||||
multi-record-batch-change-enabled = true
|
||||
multi-record-batch-change-enabled = ${?MULTI_RECORD_BATCH_CHANGE_ENABLED}
|
||||
|
||||
# Server settings
|
||||
use-recordset-cache = false
|
||||
use-recordset-cache = ${?USE_RECORDSET_CACHE}
|
||||
load-test-data = false
|
||||
load-test-data = ${?LOAD_TEST_DATA}
|
||||
# should be true while running locally or when we have only one api server/instance, for zone sync scheduler to work
|
||||
is-zone-sync-schedule-allowed = true
|
||||
# should be set to true only on a single server/instance else automated sync will be performed at every server/instance
|
||||
is-zone-sync-schedule-allowed = ${?IS_ZONE_SYNC_SCHEDULE_ALLOWED}
|
||||
|
||||
# configured backend providers
|
||||
backend {
|
||||
@ -131,7 +137,10 @@ vinyldns {
|
||||
from = ${?EMAIL_FROM}
|
||||
}
|
||||
}
|
||||
|
||||
valid-email-config{
|
||||
email-domains = ["test.com","*dummy.com"]
|
||||
number-of-dots= 2
|
||||
}
|
||||
sns {
|
||||
class-name = "vinyldns.apadi.notifier.sns.SnsNotifierProvider"
|
||||
class-name = ${?SNS_CLASS_NAME}
|
||||
@ -155,6 +164,17 @@ vinyldns {
|
||||
port=${?API_SERVICE_PORT}
|
||||
}
|
||||
|
||||
api {
|
||||
limits {
|
||||
batchchange-routing-max-items-limit = 100
|
||||
membership-routing-default-max-items = 100
|
||||
membership-routing-max-items-limit = 1000
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
approved-name-servers = [
|
||||
"172.17.42.1.",
|
||||
@ -195,9 +215,9 @@ vinyldns {
|
||||
name = ${?DATABASE_NAME}
|
||||
driver = "org.mariadb.jdbc.Driver"
|
||||
driver = ${?JDBC_DRIVER}
|
||||
migration-url = "jdbc:mariadb://localhost:19002/?user=root&password=pass"
|
||||
migration-url = "jdbc:mariadb://localhost:19002/?user=root&password=pass&socketTimeout=20000"
|
||||
migration-url = ${?JDBC_MIGRATION_URL}
|
||||
url = "jdbc:mariadb://localhost:19002/vinyldns?user=root&password=pass"
|
||||
url = "jdbc:mariadb://localhost:19002/vinyldns?user=root&password=pass&socketTimeout=20000"
|
||||
url = ${?JDBC_URL}
|
||||
user = "root"
|
||||
user = ${?JDBC_USER}
|
||||
@ -205,6 +225,12 @@ vinyldns {
|
||||
password = ${?JDBC_PASSWORD}
|
||||
flyway-out-of-order = false
|
||||
flyway-out-of-order = ${?FLYWAY_OUT_OF_ORDER}
|
||||
|
||||
max-lifetime = 300000
|
||||
connection-timeout-millis = 30000
|
||||
idle-timeout = 150000
|
||||
maximum-pool-size = 20
|
||||
minimum-idle = 5
|
||||
}
|
||||
|
||||
# TODO: Remove the need for these useless configuration blocks
|
||||
@ -341,6 +367,10 @@ akka.http {
|
||||
# Set to `infinite` to disable.
|
||||
bind-timeout = 5s
|
||||
|
||||
# A default request timeout is applied globally to all routes and can be configured using the
|
||||
# akka.http.server.request-timeout setting (which defaults to 20 seconds).
|
||||
# request-timeout = 60s
|
||||
|
||||
# Show verbose error messages back to the client
|
||||
verbose-error-messages = on
|
||||
}
|
||||
|
@ -7,6 +7,10 @@
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<logger name="com.zaxxer.hikari" level="ERROR">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</logger>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</root>
|
||||
|
@ -169,7 +169,10 @@ vinyldns {
|
||||
from = "VinylDNS <do-not-reply@vinyldns.io>"
|
||||
}
|
||||
}
|
||||
|
||||
valid-email-config{
|
||||
email-domains = ["test.com","*dummy.com"]
|
||||
number-of-dots= 2
|
||||
}
|
||||
sns {
|
||||
class-name = "vinyldns.api.notifier.sns.SnsNotifierProvider"
|
||||
settings {
|
||||
@ -232,4 +235,9 @@ vinyldns {
|
||||
|
||||
load-test-data = false
|
||||
load-test-data = ${?LOAD_TEST_DATA}
|
||||
|
||||
# should be true while running locally or when we have only one api server/instance, for zone sync scheduler to work
|
||||
is-zone-sync-schedule-allowed = true
|
||||
# should be set to true only on a single server/instance else automated sync will be performed at every server/instance
|
||||
is-zone-sync-schedule-allowed = ${?IS_ZONE_SYNC_SCHEDULE_ALLOWED}
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.stream.{Materializer, ActorMaterializer}
|
||||
import cats.effect.{Timer, IO, ContextShift}
|
||||
import cats.data.NonEmptyList
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import fs2.concurrent.SignallingRef
|
||||
import io.prometheus.client.CollectorRegistry
|
||||
@ -46,10 +47,15 @@ import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.io.{Codec, Source}
|
||||
import vinyldns.core.notifier.NotifierLoader
|
||||
import vinyldns.core.repository.DataStoreLoader
|
||||
import java.util.concurrent.{Executors, ScheduledExecutorService, TimeUnit}
|
||||
|
||||
object Boot extends App {
|
||||
|
||||
private val logger = LoggerFactory.getLogger("Boot")
|
||||
|
||||
// Create a ScheduledExecutorService with a new single thread
|
||||
private val executor: ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor()
|
||||
|
||||
private implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.global
|
||||
private implicit val cs: ContextShift[IO] = IO.contextShift(ec)
|
||||
private implicit val timer: Timer[IO] = IO.timer(ec)
|
||||
@ -90,9 +96,23 @@ object Boot extends App {
|
||||
msgsPerPoll <- IO.fromEither(MessageCount(vinyldnsConfig.messageQueueConfig.messagesPerPoll))
|
||||
notifiers <- NotifierLoader.loadAll(
|
||||
vinyldnsConfig.notifierConfigs,
|
||||
repositories.userRepository
|
||||
repositories.userRepository,
|
||||
repositories.groupRepository
|
||||
)
|
||||
_ <- APIMetrics.initialize(vinyldnsConfig.apiMetricSettings)
|
||||
// Schedule the zone sync task to be executed every 5 seconds
|
||||
_ <- if (vinyldnsConfig.serverConfig.isZoneSyncScheduleAllowed){ IO(executor.scheduleAtFixedRate(() => {
|
||||
val zoneChanges = for {
|
||||
zoneChanges <- ZoneSyncScheduleHandler.zoneSyncScheduler(repositories.zoneRepository)
|
||||
_ <- if (zoneChanges.nonEmpty) messageQueue.sendBatch(NonEmptyList.fromList(zoneChanges.toList).get) else IO.unit
|
||||
} yield ()
|
||||
zoneChanges.unsafeRunAsync {
|
||||
case Right(_) =>
|
||||
logger.debug("Zone sync scheduler ran successfully!")
|
||||
case Left(error) =>
|
||||
logger.error(s"An error occurred while performing the scheduled zone sync. Error: $error")
|
||||
}
|
||||
}, 0, 1, TimeUnit.SECONDS)) } else IO.unit
|
||||
_ <- CommandHandler.run(
|
||||
messageQueue,
|
||||
msgsPerPoll,
|
||||
@ -121,9 +141,10 @@ object Boot extends App {
|
||||
vinyldnsConfig.highValueDomainConfig,
|
||||
vinyldnsConfig.manualReviewConfig,
|
||||
vinyldnsConfig.batchChangeConfig,
|
||||
vinyldnsConfig.scheduledChangesConfig
|
||||
vinyldnsConfig.scheduledChangesConfig,
|
||||
vinyldnsConfig.serverConfig.approvedNameServers
|
||||
)
|
||||
val membershipService = MembershipService(repositories)
|
||||
val membershipService = MembershipService(repositories,vinyldnsConfig.validEmailConfig)
|
||||
|
||||
val connectionValidator =
|
||||
new ZoneConnectionValidator(
|
||||
@ -141,7 +162,8 @@ object Boot extends App {
|
||||
vinyldnsConfig.highValueDomainConfig,
|
||||
vinyldnsConfig.dottedHostsConfig,
|
||||
vinyldnsConfig.serverConfig.approvedNameServers,
|
||||
vinyldnsConfig.serverConfig.useRecordSetCache
|
||||
vinyldnsConfig.serverConfig.useRecordSetCache,
|
||||
notifiers
|
||||
)
|
||||
val zoneService = ZoneService(
|
||||
repositories,
|
||||
@ -150,7 +172,8 @@ object Boot extends App {
|
||||
zoneValidations,
|
||||
recordAccessValidations,
|
||||
backendResolver,
|
||||
vinyldnsConfig.crypto
|
||||
vinyldnsConfig.crypto,
|
||||
membershipService
|
||||
)
|
||||
//limits configured in reference.conf passing here
|
||||
val limits = LimitsConfig(
|
||||
|
@ -39,6 +39,7 @@ import vinyldns.core.queue.{CommandMessage, MessageCount, MessageQueue}
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import vinyldns.core.notifier.AllNotifiers
|
||||
import java.io.{PrintWriter, StringWriter}
|
||||
|
||||
object CommandHandler {
|
||||
|
||||
@ -94,7 +95,9 @@ object CommandHandler {
|
||||
)
|
||||
.parJoin(maxOpen)
|
||||
.handleErrorWith { error =>
|
||||
logger.error("Encountered unexpected error in main flow", error)
|
||||
val errorMessage = new StringWriter
|
||||
error.printStackTrace(new PrintWriter(errorMessage))
|
||||
logger.error(s"Encountered unexpected error in main flow. Error: ${errorMessage.toString.replaceAll("\n",";").replaceAll("\t"," ")}")
|
||||
|
||||
// just continue, the flow should never stop unless explicitly told to do so
|
||||
flow()
|
||||
@ -123,7 +126,9 @@ object CommandHandler {
|
||||
.handleErrorWith { error =>
|
||||
// on error, we make sure we still continue; should only stop when the app stops
|
||||
// or processing is disabled
|
||||
logger.error("Encountered error polling message queue", error)
|
||||
val errorMessage = new StringWriter
|
||||
error.printStackTrace(new PrintWriter(errorMessage))
|
||||
logger.error(s"Encountered error polling message queue. Error: ${errorMessage.toString.replaceAll("\n",";").replaceAll("\t"," ")}")
|
||||
|
||||
// just keep going on the stream
|
||||
pollingStream()
|
||||
@ -137,7 +142,7 @@ object CommandHandler {
|
||||
_.evalMap[IO, Any] { message =>
|
||||
message.command match {
|
||||
case sync: ZoneChange
|
||||
if sync.changeType == ZoneChangeType.Sync || sync.changeType == ZoneChangeType.Create =>
|
||||
if sync.changeType == ZoneChangeType.Sync || sync.changeType == ZoneChangeType.AutomatedSync || sync.changeType == ZoneChangeType.Create =>
|
||||
logger.info(s"Updating visibility timeout for zone change; changeId=${sync.id}")
|
||||
mq.changeMessageTimeout(message, 1.hour)
|
||||
|
||||
@ -158,7 +163,7 @@ object CommandHandler {
|
||||
_.evalMap[IO, MessageOutcome] { message =>
|
||||
message.command match {
|
||||
case sync: ZoneChange
|
||||
if sync.changeType == ZoneChangeType.Sync || sync.changeType == ZoneChangeType.Create =>
|
||||
if sync.changeType == ZoneChangeType.Sync || sync.changeType == ZoneChangeType.AutomatedSync || sync.changeType == ZoneChangeType.Create =>
|
||||
outcomeOf(message)(zoneSyncProcessor(sync))
|
||||
|
||||
case zoneChange: ZoneChange =>
|
||||
@ -182,7 +187,9 @@ object CommandHandler {
|
||||
.attempt
|
||||
.map {
|
||||
case Left(e) =>
|
||||
logger.warn(s"Failed processing message need to retry; $message", e)
|
||||
val errorMessage = new StringWriter
|
||||
e.printStackTrace(new PrintWriter(errorMessage))
|
||||
logger.warn(s"Failed processing message need to retry; $message. Error: ${errorMessage.toString.replaceAll("\n",";").replaceAll("\t"," ")}")
|
||||
RetryMessage(message)
|
||||
case Right(ok) => ok
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ import vinyldns.core.domain.record.RecordType.RecordType
|
||||
import vinyldns.core.domain.record.{RecordSet, RecordSetChange, RecordSetChangeType, RecordType}
|
||||
import vinyldns.core.domain.zone.{Algorithm, Zone, ZoneConnection}
|
||||
|
||||
import java.io.{PrintWriter, StringWriter}
|
||||
import scala.collection.JavaConverters._
|
||||
|
||||
object DnsProtocol {
|
||||
@ -165,6 +166,7 @@ class DnsBackend(val id: String, val resolver: DNS.SimpleResolver, val xfrInfo:
|
||||
val dnsName = recordDnsName(name, zoneName)
|
||||
logger.info(s"Querying for dns dnsRecordName='${dnsName.toString}'; recordType='$typ'")
|
||||
val lookup = new DNS.Lookup(dnsName, toDnsRecordType(typ))
|
||||
|
||||
lookup.setResolver(resolver)
|
||||
lookup.setSearchPath(List(Name.empty).asJava)
|
||||
lookup.setCache(null)
|
||||
@ -213,8 +215,29 @@ class DnsBackend(val id: String, val resolver: DNS.SimpleResolver, val xfrInfo:
|
||||
resp <- toDnsResponse(resp)
|
||||
} yield resp
|
||||
|
||||
val message =
|
||||
for {
|
||||
str <- Either.catchNonFatal(s"DNS Resolver: ${resolver.toString}, " +
|
||||
s"Resolver Address=${resolver.getAddress.getAddress}, Resolver Host=${resolver.getAddress.getHostName}, " +
|
||||
s"Resolver Port=${resolver.getPort}, Timeout=${resolver.getTimeout.toString}"
|
||||
)
|
||||
} yield str
|
||||
|
||||
val resolver_debug_message = message match {
|
||||
case Right(value) => value
|
||||
case Left(_) => s"DNS Resolver: ${resolver.toString}"
|
||||
}
|
||||
|
||||
val receivedResponse = result match {
|
||||
case Right(value) => value.toString.replaceAll("\n",";").replaceAll("\t"," ")
|
||||
case Left(e) =>
|
||||
val errorMessage = new StringWriter
|
||||
e.printStackTrace(new PrintWriter(errorMessage))
|
||||
errorMessage.toString.replaceAll("\n",";").replaceAll("\t"," ")
|
||||
}
|
||||
|
||||
logger.info(
|
||||
s"DnsConnection.send - Sending DNS Message ${obscuredDnsMessage(msg).toString}\n...received response $result"
|
||||
s"DnsConnection.send - Sending DNS Message ${obscuredDnsMessage(msg).toString.replaceAll("\n",";").replaceAll("\t"," ")}. Received response: $receivedResponse. DNS Resolver Info: $resolver_debug_message"
|
||||
)
|
||||
|
||||
result
|
||||
@ -234,10 +257,10 @@ class DnsBackend(val id: String, val resolver: DNS.SimpleResolver, val xfrInfo:
|
||||
// so if we can parse the error into an rcode, then we need to handle it properly; otherwise, we can try again
|
||||
// The DNS.Rcode.value function will return -1 if the error cannot be parsed into an integer
|
||||
if (DNS.Rcode.value(query.error) >= 0) {
|
||||
logger.info(s"Received TRY_AGAIN from DNS lookup; converting error: ${query.error}")
|
||||
logger.warn(s"Received TRY_AGAIN from DNS lookup; converting error: ${query.error.replaceAll("\n",";")}")
|
||||
fromDnsRcodeToError(DNS.Rcode.value(query.error), query.error)
|
||||
} else {
|
||||
logger.warn(s"Unparseable error code returned from DNS: ${query.error}")
|
||||
logger.warn(s"Unparseable error code returned from DNS: ${query.error.replaceAll("\n",";")}")
|
||||
Left(TryAgain(query.error))
|
||||
}
|
||||
|
||||
@ -293,7 +316,7 @@ object DnsBackend {
|
||||
new DNS.TSIG(
|
||||
parseAlgorithm(conn.algorithm),
|
||||
decryptedConnection.keyName,
|
||||
decryptedConnection.key
|
||||
decryptedConnection.key.value
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -205,7 +205,8 @@ trait DnsConversions {
|
||||
ttl = r.getTTL,
|
||||
status = RecordSetStatus.Active,
|
||||
created = Instant.now.truncatedTo(ChronoUnit.MILLIS),
|
||||
records = f(r)
|
||||
records = f(r),
|
||||
recordSetGroupChange = Some(OwnerShipTransfer(ownerShipTransferStatus = OwnerShipTransferStatus.None))
|
||||
)
|
||||
|
||||
// if we do not know the record type, then we cannot parse the records, but we should be able to get everything else
|
||||
@ -217,7 +218,8 @@ trait DnsConversions {
|
||||
ttl = r.getTTL,
|
||||
status = RecordSetStatus.Active,
|
||||
created = Instant.now.truncatedTo(ChronoUnit.MILLIS),
|
||||
records = Nil
|
||||
records = Nil,
|
||||
recordSetGroupChange = Some(OwnerShipTransfer(ownerShipTransferStatus = OwnerShipTransferStatus.None))
|
||||
)
|
||||
|
||||
def fromARecord(r: DNS.ARecord, zoneName: DNS.Name, zoneId: String): RecordSet =
|
||||
|
@ -31,6 +31,6 @@ object HighValueDomainConfig {
|
||||
"ip-list"
|
||||
) {
|
||||
case (regexList, ipList) =>
|
||||
HighValueDomainConfig(toCaseIgnoredRegexList(regexList), ipList.flatMap(IpAddress(_)))
|
||||
HighValueDomainConfig(toCaseIgnoredRegexList(regexList), ipList.flatMap(IpAddress.fromString(_)))
|
||||
}
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ object ManualReviewConfig {
|
||||
ManualReviewConfig(
|
||||
enabled,
|
||||
toCaseIgnoredRegexList(domainsConfig.getStringList("domain-list").asScala.toList),
|
||||
domainsConfig.getStringList("ip-list").asScala.toList.flatMap(IpAddress(_)),
|
||||
domainsConfig.getStringList("ip-list").asScala.toList.flatMap(IpAddress.fromString(_)),
|
||||
domainsConfig.getStringList("zone-name-list").asScala.toSet
|
||||
)
|
||||
}
|
||||
|
@ -34,13 +34,14 @@ final case class ServerConfig(
|
||||
keyName: String,
|
||||
processingDisabled: Boolean,
|
||||
useRecordSetCache: Boolean,
|
||||
loadTestData: Boolean
|
||||
loadTestData: Boolean,
|
||||
isZoneSyncScheduleAllowed: Boolean,
|
||||
)
|
||||
object ServerConfig {
|
||||
|
||||
import ZoneRecordValidations.toCaseIgnoredRegexList
|
||||
|
||||
implicit val configReader: ConfigReader[ServerConfig] = ConfigReader.forProduct12[
|
||||
implicit val configReader: ConfigReader[ServerConfig] = ConfigReader.forProduct13[
|
||||
ServerConfig,
|
||||
Int,
|
||||
Int,
|
||||
@ -53,6 +54,7 @@ object ServerConfig {
|
||||
Config,
|
||||
Boolean,
|
||||
Boolean,
|
||||
Boolean,
|
||||
Boolean
|
||||
](
|
||||
"health-check-timeout",
|
||||
@ -66,7 +68,8 @@ object ServerConfig {
|
||||
"defaultZoneConnection",
|
||||
"processing-disabled",
|
||||
"use-recordset-cache",
|
||||
"load-test-data"
|
||||
"load-test-data",
|
||||
"is-zone-sync-schedule-allowed"
|
||||
) {
|
||||
case (
|
||||
timeout,
|
||||
@ -80,7 +83,8 @@ object ServerConfig {
|
||||
zoneConnConfig,
|
||||
processingDisabled,
|
||||
useRecordSetCache,
|
||||
loadTestData) =>
|
||||
loadTestData,
|
||||
isZoneSyncScheduleAllowed) =>
|
||||
ServerConfig(
|
||||
timeout,
|
||||
ttl,
|
||||
@ -93,7 +97,8 @@ object ServerConfig {
|
||||
zoneConnConfig.getString("keyName"),
|
||||
processingDisabled,
|
||||
useRecordSetCache,
|
||||
loadTestData
|
||||
loadTestData,
|
||||
isZoneSyncScheduleAllowed
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2018 Comcast Cable Communications Management, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package vinyldns.api.config
|
||||
|
||||
import pureconfig.ConfigReader
|
||||
|
||||
case class ValidEmailConfig(
|
||||
valid_domains : List[String],
|
||||
number_of_dots : Int)
|
||||
object ValidEmailConfig {
|
||||
implicit val configReader: ConfigReader[ValidEmailConfig] =
|
||||
ConfigReader.forProduct2[ValidEmailConfig,List[String],Int](
|
||||
"email-domains",
|
||||
"number-of-dots"
|
||||
)
|
||||
{
|
||||
case (
|
||||
valid_domains,
|
||||
number_of_dots,
|
||||
) =>
|
||||
ValidEmailConfig(
|
||||
valid_domains,
|
||||
number_of_dots,
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
}
|
@ -38,6 +38,7 @@ import scala.reflect.ClassTag
|
||||
final case class VinylDNSConfig(
|
||||
serverConfig: ServerConfig,
|
||||
limitsconfig: LimitsConfig,
|
||||
validEmailConfig: ValidEmailConfig,
|
||||
httpConfig: HttpConfig,
|
||||
highValueDomainConfig: HighValueDomainConfig,
|
||||
manualReviewConfig: ManualReviewConfig,
|
||||
@ -83,6 +84,7 @@ object VinylDNSConfig {
|
||||
for {
|
||||
config <- IO.delay(ConfigFactory.load())
|
||||
limitsconfig <- loadIO[LimitsConfig](config, "vinyldns.api.limits") //Added Limitsconfig to fetch data from the reference.config and pass to LimitsConfig.config
|
||||
validEmailConfig <- loadIO[ValidEmailConfig](config, path="vinyldns.valid-email-config")
|
||||
serverConfig <- loadIO[ServerConfig](config, "vinyldns")
|
||||
batchChangeConfig <- loadIO[BatchChangeConfig](config, "vinyldns")
|
||||
backendConfigs <- loadIO[BackendConfigs](config, "vinyldns.backend")
|
||||
@ -103,6 +105,7 @@ object VinylDNSConfig {
|
||||
} yield VinylDNSConfig(
|
||||
serverConfig,
|
||||
limitsconfig,
|
||||
validEmailConfig,
|
||||
httpConfig,
|
||||
hvdConfig,
|
||||
manualReviewConfig,
|
||||
|
@ -27,6 +27,7 @@ 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 =
|
||||
@ -58,16 +59,23 @@ object DomainValidations {
|
||||
val TTL_MIN_LENGTH: Int = 30
|
||||
val TXT_TEXT_MIN_LENGTH: Int = 1
|
||||
val TXT_TEXT_MAX_LENGTH: Int = 64764
|
||||
val MX_PREFERENCE_MIN_VALUE: Int = 0
|
||||
val MX_PREFERENCE_MAX_VALUE: Int = 65535
|
||||
val INTEGER_MIN_VALUE: Int = 0
|
||||
val INTEGER_MAX_VALUE: Int = 65535
|
||||
|
||||
// Cname check - Cname should not be IP address
|
||||
def validateCname(name: Fqdn, isReverse: Boolean): ValidatedNel[DomainValidationError, Fqdn] =
|
||||
validateIpv4Address(name.fqdn.dropRight(1)).isValid match {
|
||||
case true => InvalidIPv4CName(name.toString).invalidNel
|
||||
case false => validateIsReverseCname(name, isReverse)
|
||||
}
|
||||
|
||||
def validateHostName(name: Fqdn): ValidatedNel[DomainValidationError, Fqdn] =
|
||||
validateHostName(name.fqdn).map(_ => name)
|
||||
|
||||
def validateCname(name: Fqdn, isReverse: Boolean): ValidatedNel[DomainValidationError, Fqdn] =
|
||||
validateCname(name.fqdn, isReverse).map(_ => name)
|
||||
def validateIsReverseCname(name: Fqdn, isReverse: Boolean): ValidatedNel[DomainValidationError, Fqdn] =
|
||||
validateIsReverseCname(name.fqdn, isReverse).map(_ => name)
|
||||
|
||||
def validateCname(name: String, isReverse: Boolean): ValidatedNel[DomainValidationError, String] = {
|
||||
def validateIsReverseCname(name: String, isReverse: Boolean): ValidatedNel[DomainValidationError, String] = {
|
||||
isReverse match {
|
||||
case true =>
|
||||
val checkRegex = validReverseZoneFQDNRegex
|
||||
@ -152,7 +160,15 @@ object DomainValidations {
|
||||
def validateTxtTextLength(value: String): ValidatedNel[DomainValidationError, String] =
|
||||
validateStringLength(value, Some(TXT_TEXT_MIN_LENGTH), TXT_TEXT_MAX_LENGTH)
|
||||
|
||||
def validateMxPreference(pref: Int): ValidatedNel[DomainValidationError, Int] =
|
||||
if (pref >= MX_PREFERENCE_MIN_VALUE && pref <= MX_PREFERENCE_MAX_VALUE) pref.validNel
|
||||
else InvalidMxPreference(pref, MX_PREFERENCE_MIN_VALUE, MX_PREFERENCE_MAX_VALUE).invalidNel[Int]
|
||||
def validateMX_NAPTR_SRVData(number: Int, recordDataType: String, recordType: String): ValidatedNel[DomainValidationError, Int] =
|
||||
if (number >= INTEGER_MIN_VALUE && number <= INTEGER_MAX_VALUE) number.validNel
|
||||
else InvalidMX_NAPTR_SRVData(number, INTEGER_MIN_VALUE, INTEGER_MAX_VALUE, recordDataType, recordType).invalidNel[Int]
|
||||
|
||||
def validateNaptrFlag(value: String): ValidatedNel[DomainValidationError, String] =
|
||||
if (value == "U" || value == "S" || value == "A" || value == "P") value.validNel
|
||||
else InvalidNaptrFlag(value).invalidNel[String]
|
||||
|
||||
def validateNaptrRegexp(value: String): ValidatedNel[DomainValidationError, String] =
|
||||
if ((value.startsWith("!") && value.endsWith("!")) || value == "") value.validNel
|
||||
else InvalidNaptrRegexp(value).invalidNel[String]
|
||||
}
|
||||
|
@ -17,7 +17,7 @@
|
||||
package vinyldns.api.domain
|
||||
|
||||
import cats.implicits._
|
||||
import com.aaronbedra.orchard.CIDR
|
||||
import com.comcast.ip4s.{Cidr, Ipv4Address, Ipv6Address}
|
||||
import vinyldns.api.domain.zone.InvalidRequest
|
||||
import vinyldns.core.domain.zone.Zone
|
||||
import vinyldns.api.backend.dns.DnsConversions._
|
||||
@ -30,8 +30,9 @@ object ReverseZoneHelpers {
|
||||
if (zone.isIPv4) {
|
||||
recordsetIsWithinCidrMaskIpv4(mask: String, zone: Zone, recordName: String)
|
||||
} else {
|
||||
val ipAddr = convertPTRtoIPv6(zone, recordName)
|
||||
Try(CIDR.valueOf(mask).contains(ipAddr)).getOrElse(false)
|
||||
val ipAddr = Ipv6Address.fromString(convertPTRtoIPv6(zone, recordName))
|
||||
Try(Cidr(Cidr.fromString6(mask).get.address,Cidr.fromString6(mask).get.prefixBits).contains(ipAddr.get))
|
||||
.getOrElse(false)
|
||||
}
|
||||
|
||||
// NOTE: this will not work for zones with less than 3 octets
|
||||
@ -86,11 +87,12 @@ object ReverseZoneHelpers {
|
||||
zone: Zone,
|
||||
recordName: String
|
||||
): Boolean = {
|
||||
val recordIpAddr = convertPTRtoIPv4(zone, recordName)
|
||||
|
||||
val recordIpAddr = Ipv4Address.fromString(convertPTRtoIPv4(zone, recordName))
|
||||
|
||||
Try {
|
||||
// make sure mask contains 4 octets, expand if not
|
||||
val ipMaskOctets = CIDR.parseBlock(mask).head.split('.').toList
|
||||
val ipMaskOctets = Cidr.fromString4(mask).get.address.toString.split('.').toList
|
||||
|
||||
val fullIp = ipMaskOctets.length match {
|
||||
case 1 => (ipMaskOctets ++ List("0", "0", "0")).mkString(".")
|
||||
@ -99,9 +101,8 @@ object ReverseZoneHelpers {
|
||||
case 4 => ipMaskOctets.mkString(".")
|
||||
}
|
||||
|
||||
val updatedMask = fullIp + "/" + CIDR.valueOf(mask).getMask
|
||||
|
||||
CIDR.valueOf(updatedMask).contains(recordIpAddr)
|
||||
val updatedMask = Cidr(recordIpAddr.get,Cidr.fromString4(mask).get.prefixBits)
|
||||
updatedMask.contains(Ipv4Address.fromString(fullIp).get)
|
||||
}.getOrElse(false)
|
||||
}
|
||||
|
||||
|
@ -38,6 +38,13 @@ class AccessValidations(
|
||||
.isGroupMember(zone.adminGroupId) || userHasAclRules(auth, zone)
|
||||
)
|
||||
|
||||
def canSeeZoneChange(auth: AuthPrincipal, zone: Zone): Either[Throwable, Unit] =
|
||||
ensuring(
|
||||
NotAuthorizedError(s"User ${auth.signedInUser.userName} cannot access zone '${zone.name}' changes")
|
||||
)(
|
||||
auth.isSystemAdmin || zone.shared || auth.isGroupMember(zone.adminGroupId)
|
||||
)
|
||||
|
||||
def canChangeZone(
|
||||
auth: AuthPrincipal,
|
||||
zoneName: String,
|
||||
@ -73,6 +80,7 @@ class AccessValidations(
|
||||
recordType: RecordType,
|
||||
zone: Zone,
|
||||
recordOwnerGroupId: Option[String],
|
||||
superUserCanUpdateOwnerGroup: Boolean = false,
|
||||
newRecordData: List[RecordData] = List.empty
|
||||
): Either[Throwable, Unit] = {
|
||||
val accessLevel =
|
||||
@ -82,7 +90,7 @@ class AccessValidations(
|
||||
s"User ${auth.signedInUser.userName} does not have access to update " +
|
||||
s"$recordName.${zone.name}"
|
||||
)
|
||||
)(accessLevel == AccessLevel.Delete || accessLevel == AccessLevel.Write)
|
||||
)(accessLevel == AccessLevel.Delete || accessLevel == AccessLevel.Write || superUserCanUpdateOwnerGroup)
|
||||
}
|
||||
|
||||
def canDeleteRecordSet(
|
||||
@ -222,7 +230,9 @@ class AccessValidations(
|
||||
AccessLevel.Delete
|
||||
case support if support.isSystemAdmin =>
|
||||
val aclAccess = getAccessFromAcl(auth, recordName, recordType, zone)
|
||||
if (aclAccess == AccessLevel.NoAccess) AccessLevel.Read else aclAccess
|
||||
if (aclAccess == AccessLevel.NoAccess)
|
||||
AccessLevel.Read
|
||||
else aclAccess
|
||||
case globalAclUser
|
||||
if globalAcls.isAuthorized(globalAclUser, recordName, recordType, zone, recordData) =>
|
||||
AccessLevel.Delete
|
||||
|
@ -27,6 +27,8 @@ trait AccessValidationsAlgebra {
|
||||
|
||||
def canSeeZone(auth: AuthPrincipal, zone: Zone): Either[Throwable, Unit]
|
||||
|
||||
def canSeeZoneChange(auth: AuthPrincipal, zone: Zone): Either[Throwable, Unit]
|
||||
|
||||
def canChangeZone(
|
||||
auth: AuthPrincipal,
|
||||
zoneName: String,
|
||||
@ -47,6 +49,7 @@ trait AccessValidationsAlgebra {
|
||||
recordType: RecordType,
|
||||
zone: Zone,
|
||||
recordOwnerGroupId: Option[String],
|
||||
superUserCanUpdateOwnerGroup: Boolean = false,
|
||||
newRecordData: List[RecordData] = List.empty
|
||||
): Either[Throwable, Unit]
|
||||
|
||||
|
@ -34,8 +34,6 @@ 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])
|
||||
|
||||
@ -125,7 +123,7 @@ class BatchChangeConverter(batchChangeRepo: BatchChangeRepository, messageQueue:
|
||||
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)
|
||||
change.withDoesNotExistMessage
|
||||
case _ =>
|
||||
// Failure here means there was a message queue issue for this change
|
||||
change.withFailureMessage(failedMessage)
|
||||
@ -138,9 +136,10 @@ class BatchChangeConverter(batchChangeRepo: BatchChangeRepository, messageQueue:
|
||||
def storeQueuingFailures(batchChange: BatchChange): BatchResult[Unit] = {
|
||||
// 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
|
||||
case change if change.status == SingleChangeStatus.Failed => change
|
||||
}
|
||||
batchChangeRepo.updateSingleChanges(failedAndNotExistsChanges).as(())
|
||||
val storeChanges = batchChangeRepo.updateSingleChanges(failedAndNotExistsChanges).as(())
|
||||
storeChanges
|
||||
}.toBatchResult
|
||||
|
||||
def createRecordSetChangesForBatch(
|
||||
@ -215,7 +214,7 @@ class BatchChangeConverter(batchChangeRepo: BatchChangeRepository, messageQueue:
|
||||
}
|
||||
}
|
||||
|
||||
// New record set for add/update or single delete
|
||||
// New record set for add/update/full deletes
|
||||
lazy val newRecordSet = {
|
||||
val firstAddChange = singleChangeNel.collect {
|
||||
case sac: SingleAddChange => sac
|
||||
@ -237,6 +236,33 @@ class BatchChangeConverter(batchChangeRepo: BatchChangeRepository, messageQueue:
|
||||
Instant.now.truncatedTo(ChronoUnit.MILLIS),
|
||||
None,
|
||||
proposedRecordData.toList,
|
||||
ownerGroupId = setOwnerGroupId,
|
||||
recordSetGroupChange = Some(OwnerShipTransfer(ownerShipTransferStatus = OwnerShipTransferStatus.None))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// New record set for single delete which exists in dns backend but not in vinyl
|
||||
lazy val newDeleteRecordSet = {
|
||||
val firstDeleteChange = singleChangeNel.collect {
|
||||
case sad: SingleDeleteRRSetChange => sad
|
||||
}.headOption
|
||||
|
||||
val newTtlRecordNameTuple = firstDeleteChange
|
||||
.map(del => del.recordName)
|
||||
.orElse(existingRecordSet.map(rs => Some(rs.name)))
|
||||
|
||||
newTtlRecordNameTuple.collect{
|
||||
case Some(recordName) =>
|
||||
RecordSet(
|
||||
zone.id,
|
||||
recordName,
|
||||
recordType,
|
||||
7200L,
|
||||
RecordSetStatus.Pending,
|
||||
Instant.now.truncatedTo(ChronoUnit.MILLIS),
|
||||
None,
|
||||
proposedRecordData.toList,
|
||||
ownerGroupId = setOwnerGroupId
|
||||
)
|
||||
}
|
||||
@ -257,7 +283,12 @@ class BatchChangeConverter(batchChangeRepo: BatchChangeRepository, messageQueue:
|
||||
existingRs <- existingRecordSet
|
||||
newRs <- newRecordSet
|
||||
} yield RecordSetChangeGenerator.forUpdate(existingRs, newRs, zone, userId, singleChangeIds)
|
||||
case _ => None // This case should never happen
|
||||
case OutOfSync =>
|
||||
newDeleteRecordSet.map { newDelRs =>
|
||||
RecordSetChangeGenerator.forOutOfSync(newDelRs, zone, userId, singleChangeIds)
|
||||
}
|
||||
case _ =>
|
||||
None // This case should never happen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -44,12 +44,14 @@ object BatchChangeInput {
|
||||
sealed trait ChangeInput {
|
||||
val inputName: String
|
||||
val typ: RecordType
|
||||
val systemMessage: Option[String]
|
||||
def asNewStoredChange(errors: NonEmptyList[DomainValidationError], defaultTtl: Long): SingleChange
|
||||
}
|
||||
|
||||
final case class AddChangeInput(
|
||||
inputName: String,
|
||||
typ: RecordType,
|
||||
systemMessage: Option[String],
|
||||
ttl: Option[Long],
|
||||
record: RecordData
|
||||
) extends ChangeInput {
|
||||
@ -68,7 +70,7 @@ final case class AddChangeInput(
|
||||
knownTtl,
|
||||
record,
|
||||
SingleChangeStatus.NeedsReview,
|
||||
None,
|
||||
systemMessage,
|
||||
None,
|
||||
None,
|
||||
errors.toList.map(SingleChangeError(_))
|
||||
@ -79,6 +81,7 @@ final case class AddChangeInput(
|
||||
final case class DeleteRRSetChangeInput(
|
||||
inputName: String,
|
||||
typ: RecordType,
|
||||
systemMessage: Option[String],
|
||||
record: Option[RecordData]
|
||||
) extends ChangeInput {
|
||||
def asNewStoredChange(
|
||||
@ -93,7 +96,7 @@ final case class DeleteRRSetChangeInput(
|
||||
typ,
|
||||
record,
|
||||
SingleChangeStatus.NeedsReview,
|
||||
None,
|
||||
systemMessage,
|
||||
None,
|
||||
None,
|
||||
errors.toList.map(SingleChangeError(_))
|
||||
@ -104,6 +107,7 @@ object AddChangeInput {
|
||||
def apply(
|
||||
inputName: String,
|
||||
typ: RecordType,
|
||||
systemMessage: Option[String],
|
||||
ttl: Option[Long],
|
||||
record: RecordData
|
||||
): AddChangeInput = {
|
||||
@ -111,28 +115,29 @@ object AddChangeInput {
|
||||
case PTR => inputName
|
||||
case _ => ensureTrailingDot(inputName)
|
||||
}
|
||||
new AddChangeInput(transformName, typ, ttl, record)
|
||||
new AddChangeInput(transformName, typ, systemMessage, ttl, record)
|
||||
}
|
||||
|
||||
def apply(sc: SingleAddChange): AddChangeInput =
|
||||
AddChangeInput(sc.inputName, sc.typ, Some(sc.ttl), sc.recordData)
|
||||
AddChangeInput(sc.inputName, sc.typ, sc.systemMessage, Some(sc.ttl), sc.recordData)
|
||||
}
|
||||
|
||||
object DeleteRRSetChangeInput {
|
||||
def apply(
|
||||
inputName: String,
|
||||
typ: RecordType,
|
||||
systemMessage: Option[String],
|
||||
record: Option[RecordData] = None
|
||||
): DeleteRRSetChangeInput = {
|
||||
val transformName = typ match {
|
||||
case PTR => inputName
|
||||
case _ => ensureTrailingDot(inputName)
|
||||
}
|
||||
new DeleteRRSetChangeInput(transformName, typ, record)
|
||||
new DeleteRRSetChangeInput(transformName, typ, systemMessage, record)
|
||||
}
|
||||
|
||||
def apply(sc: SingleDeleteRRSetChange): DeleteRRSetChangeInput =
|
||||
DeleteRRSetChangeInput(sc.inputName, sc.typ, sc.recordData)
|
||||
DeleteRRSetChangeInput(sc.inputName, sc.typ, sc.systemMessage, sc.recordData)
|
||||
}
|
||||
|
||||
object ChangeInputType extends Enumeration {
|
||||
|
@ -20,6 +20,7 @@ import cats.data.Validated.{Invalid, Valid}
|
||||
import cats.data._
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
import org.slf4j.{Logger, LoggerFactory}
|
||||
@ -33,6 +34,7 @@ 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.batch.BatchChangeStatus.BatchChangeStatus
|
||||
import vinyldns.core.domain.{CnameAtZoneApexError, SingleChangeError, UserIsNotAuthorizedError, ZoneDiscoveryError}
|
||||
import vinyldns.core.domain.membership.{Group, GroupRepository, ListUsersResults, User, UserRepository}
|
||||
import vinyldns.core.domain.record.RecordType._
|
||||
@ -340,7 +342,7 @@ class BatchChangeService(
|
||||
): ValidatedBatch[ChangeForValidation] =
|
||||
changes.mapValid { change =>
|
||||
change.typ match {
|
||||
case A | AAAA | CNAME | MX | TXT => forwardZoneDiscovery(change, zoneMap)
|
||||
case A | AAAA | CNAME | MX | TXT | NS | NAPTR | SRV => forwardZoneDiscovery(change, zoneMap)
|
||||
case PTR if validateIpv4Address(change.inputName).isValid =>
|
||||
ptrIpv4ZoneDiscovery(change, zoneMap)
|
||||
case PTR if validateIpv6Address(change.inputName).isValid =>
|
||||
@ -470,14 +472,15 @@ class BatchChangeService(
|
||||
val hardErrorsPresent = allErrors.exists(_.isFatal)
|
||||
val noErrors = allErrors.isEmpty
|
||||
val isScheduled = batchChangeInput.scheduledTime.isDefined && this.scheduledChangesEnabled
|
||||
val isNSRecordsPresent = batchChangeInput.changes.exists(_.typ == NS)
|
||||
|
||||
if (hardErrorsPresent) {
|
||||
// Always error out
|
||||
errorResponse
|
||||
} else if (noErrors && !isScheduled) {
|
||||
} else if (noErrors && !isScheduled && !isNSRecordsPresent) {
|
||||
// There are no errors and this is not scheduled, so process immediately
|
||||
processNowResponse
|
||||
} else if (this.manualReviewEnabled && allowManualReview) {
|
||||
} else if (this.manualReviewEnabled && allowManualReview || isNSRecordsPresent) {
|
||||
if ((noErrors && isScheduled) || batchChangeInput.ownerGroupId.isDefined) {
|
||||
// There are no errors and this is scheduled
|
||||
// or we have soft errors and owner group is defined
|
||||
@ -579,15 +582,22 @@ class BatchChangeService(
|
||||
|
||||
def listBatchChangeSummaries(
|
||||
auth: AuthPrincipal,
|
||||
userName: Option[String] = None,
|
||||
dateTimeStartRange: Option[String] = None,
|
||||
dateTimeEndRange: Option[String] = None,
|
||||
startFrom: Option[Int] = None,
|
||||
maxItems: Int = 100,
|
||||
ignoreAccess: Boolean = false,
|
||||
batchStatus: Option[BatchChangeStatus] = None,
|
||||
approvalStatus: Option[BatchChangeApprovalStatus] = None
|
||||
): BatchResult[BatchChangeSummaryList] = {
|
||||
val userId = if (ignoreAccess && auth.isSystemAdmin) None else Some(auth.userId)
|
||||
val submitterUserName = if(userName.isDefined && userName.get.isEmpty) None else userName
|
||||
val startDateTime = if(dateTimeStartRange.isDefined && dateTimeStartRange.get.isEmpty) None else dateTimeStartRange
|
||||
val endDateTime = if(dateTimeEndRange.isDefined && dateTimeEndRange.get.isEmpty) None else dateTimeEndRange
|
||||
for {
|
||||
listResults <- batchChangeRepo
|
||||
.getBatchChangeSummaries(userId, startFrom, maxItems, approvalStatus)
|
||||
.getBatchChangeSummaries(userId, submitterUserName, startDateTime, endDateTime, startFrom, maxItems, batchStatus, approvalStatus)
|
||||
.toBatchResult
|
||||
rsOwnerGroupIds = listResults.batchChanges.flatMap(_.ownerGroupId).toSet
|
||||
rsOwnerGroups <- groupRepository.getGroups(rsOwnerGroupIds).toBatchResult
|
||||
@ -604,7 +614,10 @@ class BatchChangeService(
|
||||
listWithGroupNames = listResults.copy(
|
||||
batchChanges = summariesWithReviewerUserNames,
|
||||
ignoreAccess = ignoreAccess,
|
||||
approvalStatus = approvalStatus
|
||||
approvalStatus = approvalStatus,
|
||||
userName = userName,
|
||||
dateTimeStartRange = dateTimeStartRange,
|
||||
dateTimeEndRange = dateTimeEndRange
|
||||
)
|
||||
} yield listWithGroupNames
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ package vinyldns.api.domain.batch
|
||||
import vinyldns.api.domain.batch.BatchChangeInterfaces.BatchResult
|
||||
import vinyldns.core.domain.auth.AuthPrincipal
|
||||
import vinyldns.core.domain.batch.BatchChangeApprovalStatus.BatchChangeApprovalStatus
|
||||
import vinyldns.core.domain.batch.BatchChangeStatus.BatchChangeStatus
|
||||
import vinyldns.core.domain.batch.{BatchChange, BatchChangeInfo, BatchChangeSummaryList}
|
||||
|
||||
// $COVERAGE-OFF$
|
||||
@ -33,9 +34,13 @@ trait BatchChangeServiceAlgebra {
|
||||
|
||||
def listBatchChangeSummaries(
|
||||
auth: AuthPrincipal,
|
||||
userName: Option[String] = None,
|
||||
dateTimeStartRange: Option[String] = None,
|
||||
dateTimeEndRange: Option[String] = None,
|
||||
startFrom: Option[Int],
|
||||
maxItems: Int,
|
||||
ignoreAccess: Boolean,
|
||||
batchStatus: Option[BatchChangeStatus],
|
||||
approvalStatus: Option[BatchChangeApprovalStatus]
|
||||
): BatchResult[BatchChangeSummaryList]
|
||||
|
||||
|
@ -16,7 +16,6 @@
|
||||
|
||||
package vinyldns.api.domain.batch
|
||||
|
||||
import java.net.InetAddress
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
import cats.data._
|
||||
@ -27,11 +26,16 @@ import vinyldns.api.domain.access.AccessValidationsAlgebra
|
||||
import vinyldns.core.domain.auth.AuthPrincipal
|
||||
import vinyldns.api.domain.batch.BatchChangeInterfaces._
|
||||
import vinyldns.api.domain.batch.BatchTransformations._
|
||||
import vinyldns.api.domain.zone.ZoneRecordValidations.isStringInRegexList
|
||||
import vinyldns.api.domain.zone.ZoneRecordValidations
|
||||
import vinyldns.core.Messages.{nonExistentRecordDataDeleteMessage, nonExistentRecordDeleteMessage}
|
||||
import vinyldns.core.domain.DomainHelpers.omitTrailingDot
|
||||
import vinyldns.core.domain.record._
|
||||
import vinyldns.core.domain._
|
||||
import vinyldns.core.domain.batch.{BatchChange, BatchChangeApprovalStatus, OwnerType, RecordKey, RecordKeyData}
|
||||
import vinyldns.core.domain.membership.Group
|
||||
import vinyldns.core.domain.zone.Zone
|
||||
import scala.util.matching.Regex
|
||||
|
||||
trait BatchChangeValidationsAlgebra {
|
||||
|
||||
@ -81,7 +85,8 @@ class BatchChangeValidations(
|
||||
highValueDomainConfig: HighValueDomainConfig,
|
||||
manualReviewConfig: ManualReviewConfig,
|
||||
batchChangeConfig: BatchChangeConfig,
|
||||
scheduledChangesConfig: ScheduledChangesConfig
|
||||
scheduledChangesConfig: ScheduledChangesConfig,
|
||||
approvedNameServers: List[Regex]
|
||||
) extends BatchChangeValidationsAlgebra {
|
||||
|
||||
import RecordType._
|
||||
@ -214,8 +219,8 @@ class BatchChangeValidations(
|
||||
}
|
||||
|
||||
def validateDeleteRRSetChangeInput(
|
||||
deleteRRSetChangeInput: DeleteRRSetChangeInput,
|
||||
isApproved: Boolean
|
||||
deleteRRSetChangeInput: DeleteRRSetChangeInput,
|
||||
isApproved: Boolean
|
||||
): SingleValidation[Unit] = {
|
||||
val validRecord = deleteRRSetChangeInput.record match {
|
||||
case Some(recordData) => validateRecordData(recordData, deleteRRSetChangeInput)
|
||||
@ -241,14 +246,17 @@ class BatchChangeValidations(
|
||||
case ptr: PTRData => validateHostName(ptr.ptrdname).asUnit
|
||||
case txt: TXTData => validateTxtTextLength(txt.text).asUnit
|
||||
case mx: MXData =>
|
||||
validateMxPreference(mx.preference).asUnit |+| validateHostName(mx.exchange).asUnit
|
||||
validateMX_NAPTR_SRVData(mx.preference, "preference", "MX").asUnit |+| validateHostName(mx.exchange).asUnit
|
||||
case ns: NSData => validateHostName(ns.nsdname).asUnit
|
||||
case naptr: NAPTRData => validateMX_NAPTR_SRVData(naptr.preference, "preference", "NAPTR").asUnit |+| validateMX_NAPTR_SRVData(naptr.order, "order", "NAPTR").asUnit |+| validateHostName(naptr.replacement).asUnit |+| validateNaptrFlag(naptr.flags).asUnit |+| validateNaptrRegexp(naptr.regexp).asUnit
|
||||
case srv: SRVData => validateMX_NAPTR_SRVData(srv.priority, "priority", "SRV").asUnit |+| validateMX_NAPTR_SRVData(srv.port, "port", "SRV").asUnit |+| validateMX_NAPTR_SRVData(srv.weight, "weight", "SRV").asUnit |+| validateHostName(srv.target).asUnit
|
||||
case other =>
|
||||
InvalidBatchRecordType(other.toString, SupportedBatchChangeRecordTypes.get).invalidNel[Unit]
|
||||
}
|
||||
|
||||
def validateInputName(change: ChangeInput, isApproved: Boolean): SingleValidation[Unit] = {
|
||||
val typedChecks = change.typ match {
|
||||
case A | AAAA | MX =>
|
||||
case A | AAAA | MX | NS | NAPTR | SRV =>
|
||||
validateHostName(change.inputName).asUnit |+| notInReverseZone(change)
|
||||
case CNAME | TXT =>
|
||||
validateHostName(change.inputName).asUnit
|
||||
@ -307,35 +315,34 @@ class BatchChangeValidations(
|
||||
else
|
||||
().validNel
|
||||
|
||||
def matchRecordData(existingRecordSetData: List[RecordData], recordData: RecordData): Boolean =
|
||||
existingRecordSetData.exists { rd =>
|
||||
(rd, recordData) match {
|
||||
case (AAAAData(rdAddress), AAAAData(proposedAddress)) =>
|
||||
InetAddress.getByName(proposedAddress).getHostName == InetAddress
|
||||
.getByName(rdAddress)
|
||||
.getHostName
|
||||
case _ => rd == recordData
|
||||
}
|
||||
def matchRecordData(existingRecordSetData: List[RecordData], recordData: RecordData): Boolean = {
|
||||
existingRecordSetData.par.exists { rd =>
|
||||
rd == recordData
|
||||
}
|
||||
}
|
||||
|
||||
def ensureRecordExists(
|
||||
change: ChangeForValidation,
|
||||
groupedChanges: ChangeForValidationMap
|
||||
): SingleValidation[Unit] =
|
||||
change: ChangeForValidation,
|
||||
groupedChanges: ChangeForValidationMap
|
||||
): Boolean = {
|
||||
change match {
|
||||
// For DeleteRecord inputs, need to verify that the record data actually exists
|
||||
case DeleteRRSetChangeForValidation(
|
||||
_,
|
||||
_,
|
||||
DeleteRRSetChangeInput(inputName, _, Some(recordData))
|
||||
)
|
||||
if !groupedChanges
|
||||
.getExistingRecordSet(change.recordKey)
|
||||
.exists(rs => matchRecordData(rs.records, recordData)) =>
|
||||
DeleteRecordDataDoesNotExist(inputName, recordData).invalidNel
|
||||
case DeleteRRSetChangeForValidation(_, _, DeleteRRSetChangeInput(_, _, _, Some(recordData)))
|
||||
if !groupedChanges
|
||||
.getExistingRecordSet(change.recordKey)
|
||||
.exists(rs => matchRecordData(rs.records, recordData)) =>
|
||||
false
|
||||
case _ =>
|
||||
().validNel
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
def updateSystemMessage(changeInput: ChangeInput, systemMessage: String): ChangeInput = {
|
||||
changeInput match {
|
||||
case dci: DeleteRRSetChangeInput => dci.copy(systemMessage = Some(systemMessage))
|
||||
case _ => changeInput
|
||||
}
|
||||
}
|
||||
|
||||
def validateDeleteWithContext(
|
||||
change: ChangeForValidation,
|
||||
@ -344,15 +351,34 @@ class BatchChangeValidations(
|
||||
isApproved: Boolean
|
||||
): SingleValidation[ChangeForValidation] = {
|
||||
|
||||
val validations =
|
||||
groupedChanges.getExistingRecordSet(change.recordKey) match {
|
||||
case Some(rs) =>
|
||||
userCanDeleteRecordSet(change, auth, rs.ownerGroupId, rs.records) |+|
|
||||
zoneDoesNotRequireManualReview(change, isApproved) |+|
|
||||
ensureRecordExists(change, groupedChanges)
|
||||
case None => RecordDoesNotExist(change.inputChange.inputName).validNel
|
||||
}
|
||||
validations.map(_ => change)
|
||||
val recordData = change match {
|
||||
case AddChangeForValidation(_, _, inputChange, _, _) => inputChange.record.toString
|
||||
case DeleteRRSetChangeForValidation(_, _, inputChange) => inputChange.record.map(_.toString).getOrElse("")
|
||||
}
|
||||
|
||||
val addInBatch = groupedChanges.getProposedAdds(change.recordKey)
|
||||
val isSameRecordUpdateInBatch = recordData.nonEmpty && addInBatch.contains(RecordData.fromString(recordData, change.inputChange.typ).get)
|
||||
|
||||
// Perform the system message update based on the condition
|
||||
val updatedChange = if (groupedChanges.getExistingRecordSet(change.recordKey).isEmpty && !isSameRecordUpdateInBatch) {
|
||||
val updatedChangeInput = updateSystemMessage(change.inputChange, nonExistentRecordDeleteMessage)
|
||||
change.withUpdatedInputChange(updatedChangeInput)
|
||||
} else if (!ensureRecordExists(change, groupedChanges)) {
|
||||
val updatedChangeInput = updateSystemMessage(change.inputChange, nonExistentRecordDataDeleteMessage)
|
||||
change.withUpdatedInputChange(updatedChangeInput)
|
||||
} else {
|
||||
change
|
||||
}
|
||||
|
||||
val validations = groupedChanges.getExistingRecordSet(updatedChange.recordKey) match {
|
||||
case Some(rs) =>
|
||||
userCanDeleteRecordSet(updatedChange, auth, rs.ownerGroupId, rs.records) |+|
|
||||
zoneDoesNotRequireManualReview(updatedChange, isApproved)
|
||||
case None =>
|
||||
if (isSameRecordUpdateInBatch) InvalidUpdateRequest(updatedChange.inputChange.inputName).invalidNel else ().validNel
|
||||
}
|
||||
|
||||
validations.map(_ => updatedChange)
|
||||
}
|
||||
|
||||
def validateAddUpdateWithContext(
|
||||
@ -381,7 +407,7 @@ class BatchChangeValidations(
|
||||
) |+|
|
||||
zoneDoesNotRequireManualReview(change, isApproved)
|
||||
case None =>
|
||||
RecordDoesNotExist(change.inputChange.inputName).invalidNel
|
||||
InvalidUpdateRequest(change.inputChange.inputName).invalidNel
|
||||
}
|
||||
}
|
||||
|
||||
@ -396,18 +422,38 @@ class BatchChangeValidations(
|
||||
auth: AuthPrincipal,
|
||||
isApproved: Boolean
|
||||
): SingleValidation[ChangeForValidation] = {
|
||||
|
||||
// To handle add and delete for the record with same record data is present in the batch
|
||||
val recordData = change match {
|
||||
case AddChangeForValidation(_, _, inputChange, _, _) => inputChange.record.toString
|
||||
case DeleteRRSetChangeForValidation(_, _, inputChange) => inputChange.record.map(_.toString).getOrElse("")
|
||||
}
|
||||
|
||||
val addInBatch = groupedChanges.getProposedAdds(change.recordKey)
|
||||
val isSameRecordUpdateInBatch = recordData.nonEmpty && addInBatch.contains(RecordData.fromString(recordData, change.inputChange.typ).get)
|
||||
|
||||
// Perform the system message update based on the condition
|
||||
val updatedChange = if (groupedChanges.getExistingRecordSet(change.recordKey).isEmpty && !isSameRecordUpdateInBatch) {
|
||||
val updatedChangeInput = updateSystemMessage(change.inputChange, nonExistentRecordDeleteMessage)
|
||||
change.withUpdatedInputChange(updatedChangeInput)
|
||||
} else if (!ensureRecordExists(change, groupedChanges)) {
|
||||
val updatedChangeInput = updateSystemMessage(change.inputChange, nonExistentRecordDataDeleteMessage)
|
||||
change.withUpdatedInputChange(updatedChangeInput)
|
||||
} else {
|
||||
change
|
||||
}
|
||||
|
||||
val validations =
|
||||
groupedChanges.getExistingRecordSet(change.recordKey) match {
|
||||
groupedChanges.getExistingRecordSet(updatedChange.recordKey) match {
|
||||
case Some(rs) =>
|
||||
val adds = groupedChanges.getProposedAdds(change.recordKey).toList
|
||||
userCanUpdateRecordSet(change, auth, rs.ownerGroupId, adds) |+|
|
||||
zoneDoesNotRequireManualReview(change, isApproved) |+|
|
||||
ensureRecordExists(change, groupedChanges)
|
||||
val adds = groupedChanges.getProposedAdds(updatedChange.recordKey).toList
|
||||
userCanUpdateRecordSet(updatedChange, auth, rs.ownerGroupId, adds) |+|
|
||||
zoneDoesNotRequireManualReview(updatedChange, isApproved)
|
||||
case None =>
|
||||
RecordDoesNotExist(change.inputChange.inputName).validNel
|
||||
if(isSameRecordUpdateInBatch) InvalidUpdateRequest(updatedChange.inputChange.inputName).invalidNel else ().validNel
|
||||
}
|
||||
|
||||
validations.map(_ => change)
|
||||
validations.map(_ => updatedChange)
|
||||
}
|
||||
|
||||
def validateAddWithContext(
|
||||
@ -418,8 +464,10 @@ class BatchChangeValidations(
|
||||
ownerGroupId: Option[String]
|
||||
): SingleValidation[ChangeForValidation] = {
|
||||
val typedValidations = change.inputChange.typ match {
|
||||
case A | AAAA | MX =>
|
||||
case A | AAAA | MX | SRV | NAPTR =>
|
||||
newRecordSetIsNotDotted(change)
|
||||
case NS =>
|
||||
newRecordSetIsNotDotted(change) |+| nsValidations(change.inputChange.record, change.recordName, change.zone, approvedNameServers)
|
||||
case CNAME =>
|
||||
cnameHasUniqueNameInBatch(change, groupedChanges) |+|
|
||||
newRecordSetIsNotDotted(change)
|
||||
@ -429,8 +477,29 @@ class BatchChangeValidations(
|
||||
InvalidBatchRecordType(other.toString, SupportedBatchChangeRecordTypes.get).invalidNel
|
||||
}
|
||||
|
||||
// To handle add and delete for the record with same record data is present in the batch
|
||||
val recordData = change match {
|
||||
case AddChangeForValidation(_, _, inputChange, _, _) => inputChange.record.toString
|
||||
}
|
||||
|
||||
val deletes = groupedChanges.getProposedDeletes(change.recordKey)
|
||||
val isDeleteExists = deletes.nonEmpty
|
||||
val isSameRecordUpdateInBatch = if(recordData.nonEmpty){
|
||||
if(deletes.contains(RecordData.fromString(recordData, change.inputChange.typ).get)) true else false
|
||||
} else false
|
||||
|
||||
val commonValidations: SingleValidation[Unit] = {
|
||||
groupedChanges.getExistingRecordSet(change.recordKey) match {
|
||||
case Some(_) =>
|
||||
().validNel
|
||||
case None =>
|
||||
if(isSameRecordUpdateInBatch) InvalidUpdateRequest(change.inputChange.inputName).invalidNel else ().validNel
|
||||
}
|
||||
}
|
||||
|
||||
val validations =
|
||||
typedValidations |+|
|
||||
commonValidations |+|
|
||||
noIncompatibleRecordExists(change, groupedChanges) |+|
|
||||
userCanAddRecordSet(change, auth) |+|
|
||||
recordDoesNotExist(
|
||||
@ -440,7 +509,7 @@ class BatchChangeValidations(
|
||||
change.inputChange.typ,
|
||||
change.inputChange.record,
|
||||
groupedChanges,
|
||||
isApproved
|
||||
isDeleteExists
|
||||
) |+|
|
||||
ownerGroupProvidedIfNeeded(change, None, ownerGroupId) |+|
|
||||
zoneDoesNotRequireManualReview(change, isApproved)
|
||||
@ -483,13 +552,14 @@ class BatchChangeValidations(
|
||||
typ: RecordType,
|
||||
recordData: RecordData,
|
||||
groupedChanges: ChangeForValidationMap,
|
||||
isApproved: Boolean
|
||||
isDeleteExist: 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}
|
||||
case false => if(isDeleteExist) ().validNel else RecordAlreadyExists(inputName).invalidNel
|
||||
}
|
||||
} else ().validNel
|
||||
}
|
||||
|
||||
@ -561,6 +631,7 @@ class BatchChangeValidations(
|
||||
input.inputChange.typ,
|
||||
input.zone,
|
||||
ownerGroupId,
|
||||
false,
|
||||
addRecords
|
||||
)
|
||||
result
|
||||
@ -697,4 +768,46 @@ class BatchChangeValidations(
|
||||
change.inputChange.inputName
|
||||
)
|
||||
}
|
||||
|
||||
def nsValidations(
|
||||
newRecordSetData: RecordData,
|
||||
newRecordSetName: String,
|
||||
zone: Zone,
|
||||
approvedNameServers: List[Regex]
|
||||
): SingleValidation[Unit] = {
|
||||
|
||||
isNotOrigin(
|
||||
newRecordSetName,
|
||||
zone,
|
||||
s"Record with name $newRecordSetName is an NS record at apex and cannot be added"
|
||||
)
|
||||
containsApprovedNameServers(newRecordSetData, approvedNameServers)
|
||||
}
|
||||
|
||||
def isNotOrigin(recordSet: String, zone: Zone, err: String): SingleValidation[Unit] =
|
||||
if(!isOriginRecord(recordSet, omitTrailingDot(zone.name))) ().validNel else InvalidBatchRequest(err).invalidNel
|
||||
|
||||
def isOriginRecord(recordSetName: String, zoneName: String): Boolean =
|
||||
recordSetName == "@" || omitTrailingDot(recordSetName) == omitTrailingDot(zoneName)
|
||||
|
||||
def containsApprovedNameServers(
|
||||
nsRecordSet: RecordData,
|
||||
approvedNameServers: List[Regex]
|
||||
): SingleValidation[Unit] = {
|
||||
val nsData = nsRecordSet match {
|
||||
case ns: NSData => ns
|
||||
case _ => ??? // this would never be the case
|
||||
}
|
||||
isApprovedNameServer(approvedNameServers, nsData)
|
||||
}
|
||||
|
||||
def isApprovedNameServer(
|
||||
approvedServerList: List[Regex],
|
||||
nsData: NSData
|
||||
): SingleValidation[Unit] =
|
||||
if (isStringInRegexList(approvedServerList, nsData.nsdname.fqdn)) {
|
||||
().validNel
|
||||
} else {
|
||||
NotApprovedNSError(nsData.nsdname.fqdn).invalidNel
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,6 @@
|
||||
|
||||
package vinyldns.api.domain.batch
|
||||
|
||||
import java.net.InetAddress
|
||||
import java.util.UUID
|
||||
|
||||
import vinyldns.api.domain.ReverseZoneHelpers
|
||||
@ -24,13 +23,13 @@ import vinyldns.api.domain.batch.BatchChangeInterfaces.ValidatedBatch
|
||||
import vinyldns.api.domain.batch.BatchTransformations.LogicalChangeType.LogicalChangeType
|
||||
import vinyldns.api.backend.dns.DnsConversions.getIPv6FullReverseName
|
||||
import vinyldns.core.domain.batch._
|
||||
import vinyldns.core.domain.record.{AAAAData, RecordData, RecordSet, RecordSetChange}
|
||||
import vinyldns.core.domain.record.{RecordData, RecordSet, RecordSetChange}
|
||||
import vinyldns.core.domain.record.RecordType._
|
||||
import vinyldns.core.domain.zone.Zone
|
||||
import vinyldns.core.domain.record.RecordType.RecordType
|
||||
|
||||
object SupportedBatchChangeRecordTypes {
|
||||
val supportedTypes = Set(A, AAAA, CNAME, PTR, TXT, MX)
|
||||
val supportedTypes = Set(A, AAAA, CNAME, PTR, TXT, MX, NS, SRV, NAPTR)
|
||||
def get: Set[RecordType] = supportedTypes
|
||||
}
|
||||
|
||||
@ -82,6 +81,7 @@ object BatchTransformations {
|
||||
val recordKey = RecordKey(zone.id, recordName, inputChange.typ)
|
||||
def asStoredChange(changeId: Option[String] = None): SingleChange
|
||||
def isAddChangeForValidation: Boolean
|
||||
def withUpdatedInputChange(inputChange: ChangeInput): ChangeForValidation
|
||||
}
|
||||
|
||||
object ChangeForValidation {
|
||||
@ -118,7 +118,7 @@ object BatchTransformations {
|
||||
ttl,
|
||||
inputChange.record,
|
||||
SingleChangeStatus.Pending,
|
||||
None,
|
||||
inputChange.systemMessage,
|
||||
None,
|
||||
None,
|
||||
List.empty,
|
||||
@ -127,6 +127,10 @@ object BatchTransformations {
|
||||
}
|
||||
|
||||
def isAddChangeForValidation: Boolean = true
|
||||
|
||||
def withUpdatedInputChange(inputChange: ChangeInput): ChangeForValidation = {
|
||||
this.copy(inputChange = inputChange.asInstanceOf[AddChangeInput])
|
||||
}
|
||||
}
|
||||
|
||||
final case class DeleteRRSetChangeForValidation(
|
||||
@ -143,7 +147,7 @@ object BatchTransformations {
|
||||
inputChange.typ,
|
||||
inputChange.record,
|
||||
SingleChangeStatus.Pending,
|
||||
None,
|
||||
inputChange.systemMessage,
|
||||
None,
|
||||
None,
|
||||
List.empty,
|
||||
@ -151,6 +155,10 @@ object BatchTransformations {
|
||||
)
|
||||
|
||||
def isAddChangeForValidation: Boolean = false
|
||||
|
||||
def withUpdatedInputChange(inputChange: ChangeInput): ChangeForValidation = {
|
||||
this.copy(inputChange = inputChange.asInstanceOf[DeleteRRSetChangeInput])
|
||||
}
|
||||
}
|
||||
|
||||
final case class BatchConversionOutput(
|
||||
@ -180,6 +188,9 @@ object BatchTransformations {
|
||||
def getProposedAdds(recordKey: RecordKey): Set[RecordData] =
|
||||
innerMap.get(recordKey).map(_.proposedAdds).toSet.flatten
|
||||
|
||||
def getProposedDeletes(recordKey: RecordKey): Set[RecordData] =
|
||||
innerMap.get(recordKey).map(_.proposedDeletes).toSet.flatten
|
||||
|
||||
// The new, net record data factoring in existing records, deletes and adds
|
||||
// If record is not edited in batch, will fallback to look up record in existing
|
||||
// records
|
||||
@ -194,13 +205,6 @@ object BatchTransformations {
|
||||
}
|
||||
|
||||
object ValidationChanges {
|
||||
def matchRecordData(existingRecord: RecordData, recordData: String): Boolean =
|
||||
existingRecord match {
|
||||
case AAAAData(address) =>
|
||||
InetAddress.getByName(address).getHostName ==
|
||||
InetAddress.getByName(recordData).getHostName
|
||||
case _ => false
|
||||
}
|
||||
|
||||
def apply(
|
||||
changes: List[ChangeForValidation],
|
||||
@ -220,16 +224,11 @@ object BatchTransformations {
|
||||
case DeleteRRSetChangeForValidation(
|
||||
_,
|
||||
_,
|
||||
DeleteRRSetChangeInput(_, AAAA, Some(AAAAData(address)))
|
||||
) =>
|
||||
existingRecords.filter(r => matchRecordData(r, address))
|
||||
case DeleteRRSetChangeForValidation(
|
||||
_,
|
||||
_,
|
||||
DeleteRRSetChangeInput(_, _, Some(recordData))
|
||||
DeleteRRSetChangeInput(_, _, _, Some(recordData))
|
||||
) =>
|
||||
Set(recordData)
|
||||
case _: DeleteRRSetChangeForValidation => existingRecords
|
||||
case _: DeleteRRSetChangeForValidation =>
|
||||
existingRecords
|
||||
}
|
||||
.toSet
|
||||
.flatten
|
||||
@ -237,26 +236,45 @@ object BatchTransformations {
|
||||
// New proposed record data (assuming all validations pass)
|
||||
val proposedRecordData = existingRecords -- deleteChangeSet ++ addChangeRecordDataSet
|
||||
|
||||
// Note: "Update" where an Add and DeleteRecordSet is provided for a DNS record that does not exist will be
|
||||
// treated as a logical Add since the delete validation will fail (on record does not exist)
|
||||
val logicalChangeType = (addChangeRecordDataSet.nonEmpty, deleteChangeSet.nonEmpty) match {
|
||||
case (true, true) => LogicalChangeType.Update
|
||||
case (true, false) => LogicalChangeType.Add
|
||||
case (false, true) =>
|
||||
if ((existingRecords -- deleteChangeSet).isEmpty) {
|
||||
LogicalChangeType.FullDelete
|
||||
case (true, true) =>
|
||||
if (existingRecords.isEmpty) {
|
||||
// Note: "Add" where an Add and DeleteRecordSet is provided for a DNS record that does not exist.
|
||||
// Adds the record if it doesn't exist and ignores the delete.
|
||||
LogicalChangeType.Add
|
||||
} else {
|
||||
// Note: "Update" where an Add and DeleteRecordSet is provided for a DNS record that exist, but record data for DeleteRecordSet does not exist.
|
||||
// Updates the record and ignores the delete.
|
||||
LogicalChangeType.Update
|
||||
}
|
||||
case (false, false) => LogicalChangeType.NotEditedInBatch
|
||||
case (true, false) => LogicalChangeType.Add
|
||||
case (false, true) =>
|
||||
if (existingRecords == deleteChangeSet) {
|
||||
LogicalChangeType.FullDelete
|
||||
} else if (existingRecords.nonEmpty) {
|
||||
LogicalChangeType.Update
|
||||
} else {
|
||||
LogicalChangeType.OutOfSync
|
||||
}
|
||||
case (false, false) =>
|
||||
if(changes.exists {
|
||||
case _: DeleteRRSetChangeForValidation => true
|
||||
case _ => false
|
||||
}
|
||||
){
|
||||
LogicalChangeType.OutOfSync
|
||||
} else {
|
||||
LogicalChangeType.NotEditedInBatch
|
||||
}
|
||||
}
|
||||
|
||||
new ValidationChanges(addChangeRecordDataSet, proposedRecordData, logicalChangeType)
|
||||
new ValidationChanges(addChangeRecordDataSet, deleteChangeSet, proposedRecordData, logicalChangeType)
|
||||
}
|
||||
}
|
||||
|
||||
final case class ValidationChanges(
|
||||
proposedAdds: Set[RecordData],
|
||||
proposedDeletes: Set[RecordData],
|
||||
proposedRecordData: Set[RecordData],
|
||||
logicalChangeType: LogicalChangeType
|
||||
)
|
||||
@ -269,6 +287,6 @@ object BatchTransformations {
|
||||
|
||||
object LogicalChangeType extends Enumeration {
|
||||
type LogicalChangeType = Value
|
||||
val Add, FullDelete, Update, NotEditedInBatch = Value
|
||||
val Add, FullDelete, Update, NotEditedInBatch, OutOfSync = Value
|
||||
}
|
||||
}
|
||||
|
@ -104,14 +104,16 @@ object UserInfo {
|
||||
|
||||
case class UserResponseInfo(
|
||||
id: String,
|
||||
userName: Option[String] = None
|
||||
userName: Option[String] = None,
|
||||
groupId: Set[String] = Set.empty
|
||||
)
|
||||
|
||||
object UserResponseInfo {
|
||||
def apply(user: User): UserResponseInfo =
|
||||
def apply(user: User , group: Group): UserResponseInfo =
|
||||
UserResponseInfo(
|
||||
id = user.id,
|
||||
userName = Some(user.userName)
|
||||
userName = Some(user.userName),
|
||||
groupId = Set(group.id)
|
||||
)
|
||||
}
|
||||
|
||||
@ -158,8 +160,8 @@ final case class ListAdminsResponse(admins: Seq[UserInfo])
|
||||
|
||||
final case class ListGroupChangesResponse(
|
||||
changes: Seq[GroupChangeInfo],
|
||||
startFrom: Option[String] = None,
|
||||
nextId: Option[String] = None,
|
||||
startFrom: Option[Int] = None,
|
||||
nextId: Option[Int] = None,
|
||||
maxItems: Int
|
||||
)
|
||||
|
||||
@ -178,6 +180,8 @@ final case class GroupAlreadyExistsError(msg: String) extends Throwable(msg)
|
||||
|
||||
final case class GroupValidationError(msg: String) extends Throwable(msg)
|
||||
|
||||
final case class EmailValidationError(msg: String) extends Throwable(msg)
|
||||
|
||||
final case class UserNotFoundError(msg: String) extends Throwable(msg)
|
||||
|
||||
final case class InvalidGroupError(msg: String) extends Throwable(msg)
|
||||
|
@ -20,6 +20,7 @@ import cats.effect.IO
|
||||
import cats.implicits._
|
||||
import scalikejdbc.DB
|
||||
import vinyldns.api.Interfaces._
|
||||
import vinyldns.api.config.ValidEmailConfig
|
||||
import vinyldns.api.repository.ApiDataAccessor
|
||||
import vinyldns.core.domain.auth.AuthPrincipal
|
||||
import vinyldns.core.domain.membership.LockStatus.LockStatus
|
||||
@ -30,14 +31,15 @@ import vinyldns.core.Messages._
|
||||
import vinyldns.mysql.TransactionProvider
|
||||
|
||||
object MembershipService {
|
||||
def apply(dataAccessor: ApiDataAccessor): MembershipService =
|
||||
def apply(dataAccessor: ApiDataAccessor,emailConfig:ValidEmailConfig): MembershipService =
|
||||
new MembershipService(
|
||||
dataAccessor.groupRepository,
|
||||
dataAccessor.userRepository,
|
||||
dataAccessor.membershipRepository,
|
||||
dataAccessor.zoneRepository,
|
||||
dataAccessor.groupChangeRepository,
|
||||
dataAccessor.recordSetRepository
|
||||
dataAccessor.recordSetRepository,
|
||||
emailConfig
|
||||
)
|
||||
}
|
||||
|
||||
@ -47,7 +49,8 @@ class MembershipService(
|
||||
membershipRepo: MembershipRepository,
|
||||
zoneRepo: ZoneRepository,
|
||||
groupChangeRepo: GroupChangeRepository,
|
||||
recordSetRepo: RecordSetRepository
|
||||
recordSetRepo: RecordSetRepository,
|
||||
validDomains: ValidEmailConfig
|
||||
) extends MembershipServiceAlgebra with TransactionProvider {
|
||||
|
||||
import MembershipValidations._
|
||||
@ -58,6 +61,7 @@ class MembershipService(
|
||||
val nonAdminMembers = inputGroup.memberIds.diff(adminMembers)
|
||||
for {
|
||||
_ <- groupValidation(newGroup)
|
||||
_ <- emailValidation(newGroup.email)
|
||||
_ <- hasMembersAndAdmins(newGroup).toResult
|
||||
_ <- groupWithSameNameDoesNotExist(newGroup.name)
|
||||
_ <- usersExist(newGroup.memberIds)
|
||||
@ -65,6 +69,11 @@ class MembershipService(
|
||||
} yield newGroup
|
||||
}
|
||||
|
||||
def listEmailDomains(authPrincipal: AuthPrincipal): Result[List[String]] = {
|
||||
val validEmailDomains = validDomains.valid_domains
|
||||
IO(validEmailDomains).toResult
|
||||
}
|
||||
|
||||
def updateGroup(
|
||||
groupId: String,
|
||||
name: String,
|
||||
@ -78,6 +87,7 @@ class MembershipService(
|
||||
existingGroup <- getExistingGroup(groupId)
|
||||
newGroup = existingGroup.withUpdates(name, email, description, memberIds, adminUserIds)
|
||||
_ <- groupValidation(newGroup)
|
||||
_ <- emailValidation(newGroup.email)
|
||||
_ <- canEditGroup(existingGroup, authPrincipal).toResult
|
||||
addedAdmins = newGroup.adminUserIds.diff(existingGroup.adminUserIds)
|
||||
// new non-admin members ++ admins converted to non-admins
|
||||
@ -244,8 +254,10 @@ class MembershipService(
|
||||
.getGroupChange(groupChangeId)
|
||||
.toResult[Option[GroupChange]]
|
||||
_ <- isGroupChangePresent(result).toResult
|
||||
_ <- canSeeGroup(result.get.newGroup.id, authPrincipal).toResult
|
||||
groupChangeMessage <- determineGroupDifference(Seq(result.get))
|
||||
_ <- canSeeGroupChange(result.get.newGroup.id, authPrincipal).toResult
|
||||
allUserIds = getGroupUserIds(Seq(result.get))
|
||||
allUserMap <- getUsers(allUserIds).map(_.users.map(x => x.id -> x.userName).toMap.withDefaultValue("unknown user"))
|
||||
groupChangeMessage <- determineGroupDifference(Seq(result.get), allUserMap)
|
||||
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)
|
||||
@ -254,16 +266,18 @@ class MembershipService(
|
||||
|
||||
def getGroupActivity(
|
||||
groupId: String,
|
||||
startFrom: Option[String],
|
||||
startFrom: Option[Int],
|
||||
maxItems: Int,
|
||||
authPrincipal: AuthPrincipal
|
||||
): Result[ListGroupChangesResponse] =
|
||||
for {
|
||||
_ <- canSeeGroup(groupId, authPrincipal).toResult
|
||||
_ <- canSeeGroupChange(groupId, authPrincipal).toResult
|
||||
result <- groupChangeRepo
|
||||
.getGroupChanges(groupId, startFrom, maxItems)
|
||||
.toResult[ListGroupChangesResults]
|
||||
groupChangeMessage <- determineGroupDifference(result.changes)
|
||||
allUserIds = getGroupUserIds(result.changes)
|
||||
allUserMap <- getUsers(allUserIds).map(_.users.map(x => x.id -> x.userName).toMap.withDefaultValue("unknown user"))
|
||||
groupChangeMessage <- determineGroupDifference(result.changes, allUserMap)
|
||||
groupChanges = (groupChangeMessage, result.changes).zipped.map{ (a, b) => b.copy(groupChangeMessage = Some(a)) }
|
||||
userIds = result.changes.map(_.userId).toSet
|
||||
users <- getUsers(userIds).map(_.users)
|
||||
@ -271,11 +285,25 @@ class MembershipService(
|
||||
} yield ListGroupChangesResponse(
|
||||
groupChanges.map(change => GroupChangeInfo.apply(change.copy(userName = userMap.get(change.userId)))),
|
||||
startFrom,
|
||||
result.lastEvaluatedTimeStamp,
|
||||
result.nextId,
|
||||
maxItems
|
||||
)
|
||||
|
||||
def determineGroupDifference(groupChange: Seq[GroupChange]): Result[Seq[String]] = {
|
||||
def getGroupUserIds(groupChange: Seq[GroupChange]): Set[String] = {
|
||||
var userIds: Set[String] = Set.empty[String]
|
||||
for (change <- groupChange) {
|
||||
if (change.oldGroup.isDefined) {
|
||||
val adminAddDifference = change.newGroup.adminUserIds.diff(change.oldGroup.get.adminUserIds)
|
||||
val adminRemoveDifference = change.oldGroup.get.adminUserIds.diff(change.newGroup.adminUserIds)
|
||||
val memberAddDifference = change.newGroup.memberIds.diff(change.oldGroup.get.memberIds)
|
||||
val memberRemoveDifference = change.oldGroup.get.memberIds.diff(change.newGroup.memberIds)
|
||||
userIds = userIds ++ adminAddDifference ++ adminRemoveDifference ++ memberAddDifference ++ memberRemoveDifference
|
||||
}
|
||||
}
|
||||
userIds
|
||||
}
|
||||
|
||||
def determineGroupDifference(groupChange: Seq[GroupChange], allUserMap: Map[String, String]): Result[Seq[String]] = {
|
||||
var groupChangeMessage: Seq[String] = Seq.empty[String]
|
||||
|
||||
for (change <- groupChange) {
|
||||
@ -288,23 +316,24 @@ class MembershipService(
|
||||
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 description = if(change.newGroup.description.isEmpty) "" else change.newGroup.description.get
|
||||
sb.append(s"Group description changed to '$description'. ")
|
||||
}
|
||||
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. ")
|
||||
sb.append(s"Group admin/s with user name/s '${adminAddDifference.map(x => allUserMap(x)).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. ")
|
||||
sb.append(s"Group admin/s with user name/s '${adminRemoveDifference.map(x => allUserMap(x)).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. ")
|
||||
sb.append(s"Group member/s with user name/s '${memberAddDifference.map(x => allUserMap(x)).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. ")
|
||||
sb.append(s"Group member/s with user name/s '${memberRemoveDifference.map(x => allUserMap(x)).mkString("','")}' removed. ")
|
||||
}
|
||||
groupChangeMessage = groupChangeMessage :+ sb.toString().trim
|
||||
}
|
||||
@ -333,6 +362,14 @@ class MembershipService(
|
||||
.orFail(UserNotFoundError(s"User $userIdentifier was not found"))
|
||||
.toResult[User]
|
||||
|
||||
|
||||
def getUserDetails(userIdentifier: String, authPrincipal: AuthPrincipal): Result[UserResponseInfo] =
|
||||
for{
|
||||
user <- getUser(userIdentifier,authPrincipal)
|
||||
group <- membershipRepo.getGroupsForUser(user.id).toResult[Set[String]]
|
||||
} yield UserResponseInfo(user.id, Some(user.userName), group)
|
||||
|
||||
|
||||
def getUsers(
|
||||
userIds: Set[String],
|
||||
startFrom: Option[String] = None,
|
||||
@ -363,6 +400,37 @@ class MembershipService(
|
||||
().asRight
|
||||
}
|
||||
}.toResult
|
||||
// Validate email details.Email domains details are fetched from the config file.
|
||||
def emailValidation(email: String): Result[Unit] = {
|
||||
val emailDomains = validDomains.valid_domains
|
||||
val numberOfDots= validDomains.number_of_dots
|
||||
val splitEmailDomains = emailDomains.mkString(",")
|
||||
val emailRegex ="""^(?!\.)(?!.*\.$)(?!.*\.\.)[a-zA-Z0-9._+!&-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$""".r
|
||||
val index = email.indexOf('@');
|
||||
val emailSplit = if(index != -1){
|
||||
email.substring(index+1,email.length)}
|
||||
val wildcardEmailDomains=if(splitEmailDomains.contains("*")){
|
||||
emailDomains.map(x=>x.replaceAllLiterally("*",""))}
|
||||
else emailDomains
|
||||
|
||||
Option(email) match {
|
||||
case Some(value) if (emailRegex.findFirstIn(value) != None && emailSplit.toString.count(_ == '.')>0)=>
|
||||
|
||||
if ((emailDomains.contains(emailSplit) || emailDomains.isEmpty || wildcardEmailDomains.exists(x => emailSplit.toString.endsWith(x)))&&
|
||||
emailSplit.toString.count(_ == '.')<=numberOfDots)
|
||||
().asRight
|
||||
else {
|
||||
if(emailSplit.toString.count(_ == '.')>numberOfDots){
|
||||
EmailValidationError(DotsValidationErrorMsg + " " + numberOfDots).asLeft
|
||||
}
|
||||
else {
|
||||
EmailValidationError(EmailValidationErrorMsg + " " + wildcardEmailDomains.mkString(",")).asLeft
|
||||
}
|
||||
}
|
||||
case _ =>
|
||||
EmailValidationError(InvalidEmailValidationErrorMsg).asLeft
|
||||
}}.toResult
|
||||
|
||||
|
||||
def groupWithSameNameDoesNotExist(name: String): Result[Unit] =
|
||||
groupRepo
|
||||
|
@ -25,6 +25,8 @@ trait MembershipServiceAlgebra {
|
||||
|
||||
def createGroup(inputGroup: Group, authPrincipal: AuthPrincipal): Result[Group]
|
||||
|
||||
def listEmailDomains(authPrincipal: AuthPrincipal):Result[List[String]]
|
||||
|
||||
def updateGroup(
|
||||
groupId: String,
|
||||
name: String,
|
||||
@ -61,7 +63,7 @@ trait MembershipServiceAlgebra {
|
||||
|
||||
def getGroupActivity(
|
||||
groupId: String,
|
||||
startFrom: Option[String],
|
||||
startFrom: Option[Int],
|
||||
maxItems: Int,
|
||||
authPrincipal: AuthPrincipal
|
||||
): Result[ListGroupChangesResponse]
|
||||
@ -76,4 +78,9 @@ trait MembershipServiceAlgebra {
|
||||
userIdentifier: String,
|
||||
authPrincipal: AuthPrincipal
|
||||
): Result[User]
|
||||
|
||||
def getUserDetails(
|
||||
userIdentifier: String,
|
||||
authPrincipal: AuthPrincipal
|
||||
): Result[UserResponseInfo]
|
||||
}
|
||||
|
@ -45,6 +45,11 @@ object MembershipValidations {
|
||||
authPrincipal.isGroupMember(groupId) || authPrincipal.isSystemAdmin || canViewGroupDetails
|
||||
}
|
||||
|
||||
def canSeeGroupChange(groupId: String, authPrincipal: AuthPrincipal): Either[Throwable, Unit] =
|
||||
ensuring(NotAuthorizedError("Not authorized")) {
|
||||
authPrincipal.isGroupMember(groupId) || authPrincipal.isSystemAdmin
|
||||
}
|
||||
|
||||
def isGroupChangePresent(groupChange: Option[GroupChange]): Either[Throwable, Unit] =
|
||||
ensuring(InvalidGroupRequestError("Invalid Group Change ID")) {
|
||||
groupChange.isDefined
|
||||
|
@ -17,13 +17,13 @@
|
||||
package vinyldns.api.domain.record
|
||||
|
||||
import vinyldns.api.domain.zone.RecordSetChangeInfo
|
||||
import vinyldns.core.domain.record.ListRecordSetChangesResults
|
||||
import vinyldns.core.domain.record.{ListFailedRecordSetChangesResults, ListRecordSetChangesResults, RecordSetChange}
|
||||
|
||||
case class ListRecordSetChangesResponse(
|
||||
zoneId: String,
|
||||
recordSetChanges: List[RecordSetChangeInfo] = Nil,
|
||||
nextId: Option[String],
|
||||
startFrom: Option[String],
|
||||
nextId: Option[Int],
|
||||
startFrom: Option[Int],
|
||||
maxItems: Int
|
||||
)
|
||||
|
||||
@ -41,3 +41,43 @@ object ListRecordSetChangesResponse {
|
||||
listResults.maxItems
|
||||
)
|
||||
}
|
||||
|
||||
case class ListRecordSetHistoryResponse(
|
||||
zoneId: Option[String],
|
||||
recordSetChanges: List[RecordSetChangeInfo] = Nil,
|
||||
nextId: Option[Int],
|
||||
startFrom: Option[Int],
|
||||
maxItems: Int
|
||||
)
|
||||
|
||||
object ListRecordSetHistoryResponse {
|
||||
def apply(
|
||||
zoneId: Option[String],
|
||||
listResults: ListRecordSetChangesResults,
|
||||
info: List[RecordSetChangeInfo]
|
||||
): ListRecordSetHistoryResponse =
|
||||
ListRecordSetHistoryResponse(
|
||||
zoneId,
|
||||
info,
|
||||
listResults.nextId,
|
||||
listResults.startFrom,
|
||||
listResults.maxItems
|
||||
)
|
||||
}
|
||||
|
||||
case class ListFailedRecordSetChangesResponse(
|
||||
failedRecordSetChanges: List[RecordSetChange] = Nil,
|
||||
nextId: Int,
|
||||
startFrom: Int,
|
||||
maxItems: Int
|
||||
)
|
||||
|
||||
object ListFailedRecordSetChangesResponse {
|
||||
def apply(
|
||||
ListFailedRecordSetChanges: ListFailedRecordSetChangesResults
|
||||
): ListFailedRecordSetChangesResponse =
|
||||
ListFailedRecordSetChangesResponse(
|
||||
ListFailedRecordSetChanges.items,
|
||||
ListFailedRecordSetChanges.nextId,
|
||||
ListFailedRecordSetChanges.startFrom,
|
||||
ListFailedRecordSetChanges.maxItems)}
|
||||
|
@ -19,7 +19,7 @@ package vinyldns.api.domain.record
|
||||
import cats.effect.IO
|
||||
import org.slf4j.LoggerFactory
|
||||
import scalikejdbc.DB
|
||||
import vinyldns.core.domain.record.{NameSort, ListRecordSetResults, RecordSetCacheRepository, RecordSetRepository}
|
||||
import vinyldns.core.domain.record.{ListRecordSetResults, NameSort, RecordSetCacheRepository, RecordSetRepository, RecordTypeSort}
|
||||
import vinyldns.mysql.TransactionProvider
|
||||
|
||||
|
||||
@ -30,7 +30,7 @@ class RecordSetCacheService(recordSetRepository: RecordSetRepository,
|
||||
final def populateRecordSetCache(nextId: Option[String] = None): IO[ListRecordSetResults] = {
|
||||
logger.info(s"Populating recordset data. Starting at $nextId")
|
||||
for {
|
||||
result <- recordSetRepository.listRecordSets(None, nextId, Some(1000), None, None, None, NameSort.ASC)
|
||||
result <- recordSetRepository.listRecordSets(None, nextId, Some(1000), None, None, None, NameSort.ASC, RecordTypeSort.ASC)
|
||||
|
||||
_ <- executeWithinTransaction { db: DB =>
|
||||
IO {
|
||||
|
@ -113,6 +113,25 @@ object RecordSetChangeGenerator extends DnsConversions {
|
||||
singleBatchChangeIds = singleBatchChangeIds
|
||||
)
|
||||
|
||||
def forOutOfSync(
|
||||
recordSet: RecordSet,
|
||||
zone: Zone,
|
||||
userId: String,
|
||||
singleBatchChangeIds: List[String]
|
||||
): RecordSetChange =
|
||||
RecordSetChange(
|
||||
zone = zone,
|
||||
recordSet = recordSet.copy(
|
||||
name = relativize(recordSet.name, zone.name),
|
||||
status = RecordSetStatus.PendingDelete,
|
||||
updated = Some(Instant.now.truncatedTo(ChronoUnit.MILLIS))
|
||||
),
|
||||
userId = userId,
|
||||
changeType = RecordSetChangeType.Sync,
|
||||
updates = Some(recordSet),
|
||||
singleBatchChangeIds = singleBatchChangeIds
|
||||
)
|
||||
|
||||
def forDelete(
|
||||
recordSet: RecordSet,
|
||||
zone: Zone,
|
||||
|
@ -27,6 +27,7 @@ import vinyldns.core.domain.zone.{Zone, ZoneCommandResult, ZoneRepository}
|
||||
import vinyldns.core.queue.MessageQueue
|
||||
import cats.data._
|
||||
import cats.effect.IO
|
||||
import org.slf4j.{Logger, LoggerFactory}
|
||||
import org.xbill.DNS.ReverseMap
|
||||
import vinyldns.api.config.{ZoneAuthConfigs, DottedHostsConfig, HighValueDomainConfig}
|
||||
import vinyldns.api.domain.DomainValidations.{validateIpv4Address, validateIpv6Address}
|
||||
@ -35,6 +36,8 @@ import vinyldns.core.domain.record.NameSort.NameSort
|
||||
import vinyldns.core.domain.record.RecordType.RecordType
|
||||
import vinyldns.core.domain.DomainHelpers.ensureTrailingDot
|
||||
import vinyldns.core.domain.backend.{Backend, BackendResolver}
|
||||
import vinyldns.core.domain.record.RecordTypeSort.RecordTypeSort
|
||||
import vinyldns.core.notifier.{AllNotifiers, Notification}
|
||||
|
||||
import scala.util.matching.Regex
|
||||
|
||||
@ -48,7 +51,8 @@ object RecordSetService {
|
||||
highValueDomainConfig: HighValueDomainConfig,
|
||||
dottedHostsConfig: DottedHostsConfig,
|
||||
approvedNameServers: List[Regex],
|
||||
useRecordSetCache: Boolean
|
||||
useRecordSetCache: Boolean,
|
||||
notifiers: AllNotifiers
|
||||
): RecordSetService =
|
||||
new RecordSetService(
|
||||
dataAccessor.zoneRepository,
|
||||
@ -64,7 +68,9 @@ object RecordSetService {
|
||||
highValueDomainConfig,
|
||||
dottedHostsConfig,
|
||||
approvedNameServers,
|
||||
useRecordSetCache
|
||||
useRecordSetCache,
|
||||
notifiers
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
@ -82,12 +88,18 @@ class RecordSetService(
|
||||
highValueDomainConfig: HighValueDomainConfig,
|
||||
dottedHostsConfig: DottedHostsConfig,
|
||||
approvedNameServers: List[Regex],
|
||||
useRecordSetCache: Boolean
|
||||
useRecordSetCache: Boolean,
|
||||
notifiers: AllNotifiers
|
||||
) extends RecordSetServiceAlgebra {
|
||||
|
||||
import RecordSetValidations._
|
||||
import accessValidation._
|
||||
|
||||
val logger: Logger = LoggerFactory.getLogger(classOf[RecordSetService])
|
||||
|
||||
val approverOwnerShipTransferStatus = List(OwnerShipTransferStatus.ManuallyApproved , OwnerShipTransferStatus.AutoApproved, OwnerShipTransferStatus.ManuallyRejected)
|
||||
val requestorOwnerShipTransferStatus = List(OwnerShipTransferStatus.Cancelled , OwnerShipTransferStatus.Requested, OwnerShipTransferStatus.PendingReview)
|
||||
|
||||
def addRecordSet(recordSet: RecordSet, auth: AuthPrincipal): Result[ZoneCommandResult] =
|
||||
for {
|
||||
zone <- getZone(recordSet.zoneId)
|
||||
@ -142,13 +154,31 @@ class RecordSetService(
|
||||
_ <- unchangedRecordName(existing, recordSet, zone).toResult
|
||||
_ <- unchangedRecordType(existing, recordSet).toResult
|
||||
_ <- unchangedZoneId(existing, recordSet).toResult
|
||||
_ <- if(requestorOwnerShipTransferStatus.contains(recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("<none>"))
|
||||
&& !auth.isSuper && !auth.isGroupMember(existing.ownerGroupId.getOrElse("None")))
|
||||
unchangedRecordSet(existing, recordSet).toResult else ().toResult
|
||||
_ <- if(existing.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("<none>") == OwnerShipTransferStatus.Cancelled
|
||||
&& !auth.isSuper) {
|
||||
recordSetOwnerShipApproveStatus(recordSet).toResult
|
||||
} else ().toResult
|
||||
_ = logger.info(s"updated recordsetgroupchange: ${recordSet.recordSetGroupChange}")
|
||||
_ = logger.info(s"existing recordsetgroupchange: ${existing.recordSetGroupChange}")
|
||||
recordSet <- updateRecordSetGroupChangeStatus(recordSet, existing, zone)
|
||||
change <- RecordSetChangeGenerator.forUpdate(existing, recordSet, zone, Some(auth)).toResult
|
||||
// because changes happen to the RS in forUpdate itself, converting 1st and validating on that
|
||||
rsForValidations = change.recordSet
|
||||
superUserCanUpdateOwnerGroup = canSuperUserUpdateOwnerGroup(existing, recordSet, zone, auth)
|
||||
_ <- isNotHighValueDomain(recordSet, zone, highValueDomainConfig).toResult
|
||||
_ <- canUpdateRecordSet(auth, existing.name, existing.typ, zone, existing.ownerGroupId).toResult
|
||||
_ <- if(requestorOwnerShipTransferStatus.contains(recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("<none>"))
|
||||
&& !auth.isSuper && !auth.isGroupMember(existing.ownerGroupId.getOrElse("None"))) ().toResult
|
||||
else canUpdateRecordSet(auth, existing.name, existing.typ, zone, existing.ownerGroupId, superUserCanUpdateOwnerGroup).toResult
|
||||
ownerGroup <- getGroupIfProvided(rsForValidations.ownerGroupId)
|
||||
_ <- canUseOwnerGroup(rsForValidations.ownerGroupId, ownerGroup, auth).toResult
|
||||
_ <- if(requestorOwnerShipTransferStatus.contains(recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("<none>"))
|
||||
&& !auth.isSuper && !auth.isGroupMember(existing.ownerGroupId.getOrElse("None")))
|
||||
canUseOwnerGroup(rsForValidations.recordSetGroupChange.map(_.requestedOwnerGroupId).get, ownerGroup, auth).toResult
|
||||
else if(approverOwnerShipTransferStatus.contains(recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("<none>"))
|
||||
&& !auth.isSuper) canUseOwnerGroup(existing.ownerGroupId, ownerGroup, auth).toResult
|
||||
else canUseOwnerGroup(rsForValidations.ownerGroupId, ownerGroup, auth).toResult
|
||||
_ <- notPending(existing).toResult
|
||||
existingRecordsWithName <- recordSetRepository
|
||||
.getRecordSetsByName(zone.id, rsForValidations.name)
|
||||
@ -183,6 +213,11 @@ class RecordSetService(
|
||||
_ <- 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]
|
||||
_ <- if(recordSet.recordSetGroupChange != None &&
|
||||
recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("<none>") != OwnerShipTransferStatus.None &&
|
||||
recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("<none>") != OwnerShipTransferStatus.AutoApproved)
|
||||
notifiers.notify(Notification(change)).toResult
|
||||
else ().toResult
|
||||
} yield change
|
||||
|
||||
def deleteRecordSet(
|
||||
@ -201,6 +236,65 @@ class RecordSetService(
|
||||
_ <- messageQueue.send(change).toResult[Unit]
|
||||
} yield change
|
||||
|
||||
//update ownership transfer is zone is shared
|
||||
def updateRecordSetGroupChangeStatus(recordSet: RecordSet, existing: RecordSet, zone: Zone): Result[RecordSet] = {
|
||||
val existingOwnerShipTransfer = existing.recordSetGroupChange.getOrElse(OwnerShipTransfer.apply(OwnerShipTransferStatus.None, Some("none")))
|
||||
val ownerShipTransfer = recordSet.recordSetGroupChange.getOrElse(OwnerShipTransfer.apply(OwnerShipTransferStatus.None, Some("none")))
|
||||
if (recordSet.recordSetGroupChange != None &&
|
||||
ownerShipTransfer.ownerShipTransferStatus != OwnerShipTransferStatus.None)
|
||||
if (zone.shared){
|
||||
if (approverOwnerShipTransferStatus.contains(ownerShipTransfer.ownerShipTransferStatus)) {
|
||||
val recordSetOwnerApproval =
|
||||
ownerShipTransfer.ownerShipTransferStatus match {
|
||||
case OwnerShipTransferStatus.ManuallyApproved =>
|
||||
recordSet.copy(ownerGroupId = existingOwnerShipTransfer.requestedOwnerGroupId,
|
||||
recordSetGroupChange = Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyApproved,
|
||||
requestedOwnerGroupId = existingOwnerShipTransfer.requestedOwnerGroupId)))
|
||||
case OwnerShipTransferStatus.ManuallyRejected =>
|
||||
recordSet.copy(
|
||||
recordSetGroupChange = Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyRejected,
|
||||
requestedOwnerGroupId = existingOwnerShipTransfer.requestedOwnerGroupId)))
|
||||
case OwnerShipTransferStatus.AutoApproved =>
|
||||
recordSet.copy(
|
||||
ownerGroupId = ownerShipTransfer.requestedOwnerGroupId,
|
||||
recordSetGroupChange = Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.AutoApproved,
|
||||
requestedOwnerGroupId = ownerShipTransfer.requestedOwnerGroupId)))
|
||||
|
||||
case _ => recordSet.copy(
|
||||
recordSetGroupChange = Some(ownerShipTransfer.copy(
|
||||
ownerShipTransferStatus = OwnerShipTransferStatus.None,
|
||||
requestedOwnerGroupId = Some("null"))))
|
||||
}
|
||||
for {
|
||||
recordSet <- recordSetOwnerApproval.toResult
|
||||
} yield recordSet
|
||||
}
|
||||
else {
|
||||
val recordSetOwnerRequest =
|
||||
ownerShipTransfer.ownerShipTransferStatus match {
|
||||
case OwnerShipTransferStatus.Cancelled =>
|
||||
recordSet.copy(recordSetGroupChange = Some(ownerShipTransfer.copy(
|
||||
ownerShipTransferStatus = OwnerShipTransferStatus.Cancelled,
|
||||
requestedOwnerGroupId = existingOwnerShipTransfer.requestedOwnerGroupId)))
|
||||
case OwnerShipTransferStatus.Requested | OwnerShipTransferStatus.PendingReview => recordSet.copy(
|
||||
recordSetGroupChange = Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.PendingReview)))
|
||||
}
|
||||
for {
|
||||
recordSet <- recordSetOwnerRequest.toResult
|
||||
} yield recordSet
|
||||
}
|
||||
} else for {
|
||||
_ <- unchangedRecordSetOwnershipStatus(recordSet, existing).toResult
|
||||
} yield recordSet.copy(
|
||||
recordSetGroupChange = Some(ownerShipTransfer.copy(
|
||||
ownerShipTransferStatus = OwnerShipTransferStatus.None,
|
||||
requestedOwnerGroupId = Some("null"))))
|
||||
else recordSet.copy(
|
||||
recordSetGroupChange = Some(ownerShipTransfer.copy(
|
||||
ownerShipTransferStatus = OwnerShipTransferStatus.None,
|
||||
requestedOwnerGroupId = Some("null")))).toResult
|
||||
}
|
||||
|
||||
// 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
|
||||
@ -382,6 +476,14 @@ class RecordSetService(
|
||||
groupName <- getGroupName(recordSet.ownerGroupId)
|
||||
} yield RecordSetInfo(recordSet, groupName)
|
||||
|
||||
def getRecordSetCount(zoneId: String, authPrincipal: AuthPrincipal): Result[RecordSetCount] = {
|
||||
for {
|
||||
zone <- getZone(zoneId)
|
||||
_ <- canSeeZone(authPrincipal, zone).toResult
|
||||
count <- recordSetRepository.getRecordSetCount(zoneId).toResult
|
||||
} yield RecordSetCount(count)
|
||||
}
|
||||
|
||||
def getRecordSetByZone(
|
||||
recordSetId: String,
|
||||
zoneId: String,
|
||||
@ -407,7 +509,8 @@ class RecordSetService(
|
||||
recordTypeFilter: Option[Set[RecordType]],
|
||||
recordOwnerGroupFilter: Option[String],
|
||||
nameSort: NameSort,
|
||||
authPrincipal: AuthPrincipal
|
||||
authPrincipal: AuthPrincipal,
|
||||
recordTypeSort: RecordTypeSort
|
||||
): Result[ListGlobalRecordSetsResponse] =
|
||||
for {
|
||||
_ <- validRecordNameFilterLength(recordNameFilter).toResult
|
||||
@ -420,7 +523,8 @@ class RecordSetService(
|
||||
Some(formattedRecordNameFilter),
|
||||
recordTypeFilter,
|
||||
recordOwnerGroupFilter,
|
||||
nameSort
|
||||
nameSort,
|
||||
recordTypeSort
|
||||
)
|
||||
.toResult[ListRecordSetResults]
|
||||
rsOwnerGroupIds = recordSetResults.recordSets.flatMap(_.ownerGroupId).toSet
|
||||
@ -458,7 +562,8 @@ class RecordSetService(
|
||||
recordTypeFilter: Option[Set[RecordType]],
|
||||
recordOwnerGroupFilter: Option[String],
|
||||
nameSort: NameSort,
|
||||
authPrincipal: AuthPrincipal
|
||||
authPrincipal: AuthPrincipal,
|
||||
recordTypeSort: RecordTypeSort
|
||||
): Result[ListGlobalRecordSetsResponse] = {
|
||||
for {
|
||||
_ <- validRecordNameFilterLength(recordNameFilter).toResult
|
||||
@ -483,7 +588,8 @@ class RecordSetService(
|
||||
Some(formattedRecordNameFilter),
|
||||
recordTypeFilter,
|
||||
recordOwnerGroupFilter,
|
||||
nameSort
|
||||
nameSort,
|
||||
recordTypeSort
|
||||
).toResult[ListRecordSetResults]
|
||||
}
|
||||
rsOwnerGroupIds = recordSetResults.recordSets.flatMap(_.ownerGroupId).toSet
|
||||
@ -511,7 +617,8 @@ class RecordSetService(
|
||||
recordTypeFilter: Option[Set[RecordType]],
|
||||
recordOwnerGroupFilter: Option[String],
|
||||
nameSort: NameSort,
|
||||
authPrincipal: AuthPrincipal
|
||||
authPrincipal: AuthPrincipal,
|
||||
recordTypeSort: RecordTypeSort
|
||||
): Result[ListRecordSetsByZoneResponse] =
|
||||
for {
|
||||
zone <- getZone(zoneId)
|
||||
@ -524,7 +631,8 @@ class RecordSetService(
|
||||
recordNameFilter,
|
||||
recordTypeFilter,
|
||||
recordOwnerGroupFilter,
|
||||
nameSort
|
||||
nameSort,
|
||||
recordTypeSort
|
||||
)
|
||||
.toResult[ListRecordSetResults]
|
||||
rsOwnerGroupIds = recordSetResults.recordSets.flatMap(_.ownerGroupId).toSet
|
||||
@ -539,7 +647,8 @@ class RecordSetService(
|
||||
recordSetResults.recordNameFilter,
|
||||
recordSetResults.recordTypeFilter,
|
||||
recordSetResults.recordOwnerGroupFilter,
|
||||
recordSetResults.nameSort
|
||||
recordSetResults.nameSort,
|
||||
recordSetResults.recordTypeSort
|
||||
)
|
||||
|
||||
def getRecordSetChange(
|
||||
@ -568,18 +677,61 @@ class RecordSetService(
|
||||
|
||||
def listRecordSetChanges(
|
||||
zoneId: String,
|
||||
startFrom: Option[String] = None,
|
||||
startFrom: Option[Int] = None,
|
||||
maxItems: Int = 100,
|
||||
authPrincipal: AuthPrincipal
|
||||
): Result[ListRecordSetChangesResponse] =
|
||||
for {
|
||||
zone <- getZone(zoneId)
|
||||
_ <- canSeeZone(authPrincipal, zone).toResult
|
||||
recordSetChangesResults <- recordChangeRepository
|
||||
.listRecordSetChanges(Some(zone.id), startFrom, maxItems, None, None)
|
||||
.toResult[ListRecordSetChangesResults]
|
||||
recordSetChangesInfo <- buildRecordSetChangeInfo(recordSetChangesResults.items)
|
||||
} yield ListRecordSetChangesResponse(zoneId, recordSetChangesResults, recordSetChangesInfo)
|
||||
|
||||
def listRecordSetChangeHistory(
|
||||
zoneId: Option[String] = None,
|
||||
startFrom: Option[Int] = None,
|
||||
maxItems: Int = 100,
|
||||
fqdn: Option[String] = None,
|
||||
recordType: Option[RecordType] = None,
|
||||
authPrincipal: AuthPrincipal
|
||||
): Result[ListRecordSetHistoryResponse] =
|
||||
for {
|
||||
zone <- getZone(zoneId)
|
||||
zone <- getZone(zoneId.get)
|
||||
_ <- canSeeZone(authPrincipal, zone).toResult
|
||||
recordSetChangesResults <- recordChangeRepository
|
||||
.listRecordSetChanges(zone.id, startFrom, maxItems)
|
||||
.listRecordSetChanges(zoneId, startFrom, maxItems, fqdn, recordType)
|
||||
.toResult[ListRecordSetChangesResults]
|
||||
recordSetChangesInfo <- buildRecordSetChangeInfo(recordSetChangesResults.items)
|
||||
} yield ListRecordSetChangesResponse(zoneId, recordSetChangesResults, recordSetChangesInfo)
|
||||
} yield ListRecordSetHistoryResponse(zoneId, recordSetChangesResults, recordSetChangesInfo)
|
||||
|
||||
def listFailedRecordSetChanges(
|
||||
authPrincipal: AuthPrincipal,
|
||||
zoneId: Option[String] = None,
|
||||
startFrom: Int= 0,
|
||||
maxItems: Int = 100
|
||||
): Result[ListFailedRecordSetChangesResponse] =
|
||||
for {
|
||||
recordSetChangesFailedResults <- recordChangeRepository
|
||||
.listFailedRecordSetChanges(zoneId, maxItems, startFrom)
|
||||
.toResult[ListFailedRecordSetChangesResults]
|
||||
_ <- zoneAccess(recordSetChangesFailedResults.items, authPrincipal).toResult
|
||||
} yield
|
||||
ListFailedRecordSetChangesResponse(
|
||||
recordSetChangesFailedResults.items,
|
||||
recordSetChangesFailedResults.nextId,
|
||||
startFrom,
|
||||
maxItems)
|
||||
|
||||
def zoneAccess(
|
||||
RecordSetCh: List[RecordSetChange],
|
||||
auth: AuthPrincipal
|
||||
): List[Result[Unit]] =
|
||||
RecordSetCh.map { zn =>
|
||||
canSeeZone(auth, zn.zone).toResult
|
||||
}
|
||||
|
||||
def getZone(zoneId: String): Result[Zone] =
|
||||
zoneRepository
|
||||
|
@ -17,12 +17,13 @@
|
||||
package vinyldns.api.domain.record
|
||||
|
||||
import vinyldns.api.Interfaces.Result
|
||||
import vinyldns.api.domain.zone.RecordSetInfo
|
||||
import vinyldns.api.domain.zone.{RecordSetCount, RecordSetInfo}
|
||||
import vinyldns.core.domain.auth.AuthPrincipal
|
||||
import vinyldns.core.domain.zone.ZoneCommandResult
|
||||
import vinyldns.api.route.{ListGlobalRecordSetsResponse, ListRecordSetsByZoneResponse}
|
||||
import vinyldns.core.domain.record.NameSort.NameSort
|
||||
import vinyldns.core.domain.record.RecordType.RecordType
|
||||
import vinyldns.core.domain.record.RecordTypeSort.RecordTypeSort
|
||||
import vinyldns.core.domain.record.{RecordSet, RecordSetChange}
|
||||
|
||||
trait RecordSetServiceAlgebra {
|
||||
@ -54,7 +55,8 @@ trait RecordSetServiceAlgebra {
|
||||
recordTypeFilter: Option[Set[RecordType]],
|
||||
recordOwnerGroupId: Option[String],
|
||||
nameSort: NameSort,
|
||||
authPrincipal: AuthPrincipal
|
||||
authPrincipal: AuthPrincipal,
|
||||
recordTypeSort: RecordTypeSort
|
||||
): Result[ListGlobalRecordSetsResponse]
|
||||
|
||||
/**
|
||||
@ -76,7 +78,8 @@ trait RecordSetServiceAlgebra {
|
||||
recordTypeFilter: Option[Set[RecordType]],
|
||||
recordOwnerGroupId: Option[String],
|
||||
nameSort: NameSort,
|
||||
authPrincipal: AuthPrincipal
|
||||
authPrincipal: AuthPrincipal,
|
||||
recordTypeSort: RecordTypeSort
|
||||
): Result[ListGlobalRecordSetsResponse]
|
||||
|
||||
def listRecordSetsByZone(
|
||||
@ -87,7 +90,8 @@ trait RecordSetServiceAlgebra {
|
||||
recordTypeFilter: Option[Set[RecordType]],
|
||||
recordOwnerGroupId: Option[String],
|
||||
nameSort: NameSort,
|
||||
authPrincipal: AuthPrincipal
|
||||
authPrincipal: AuthPrincipal,
|
||||
recordTypeSort: RecordTypeSort
|
||||
): Result[ListRecordSetsByZoneResponse]
|
||||
|
||||
def getRecordSetChange(
|
||||
@ -98,9 +102,27 @@ trait RecordSetServiceAlgebra {
|
||||
|
||||
def listRecordSetChanges(
|
||||
zoneId: String,
|
||||
startFrom: Option[String],
|
||||
startFrom: Option[Int],
|
||||
maxItems: Int,
|
||||
authPrincipal: AuthPrincipal
|
||||
): Result[ListRecordSetChangesResponse]
|
||||
|
||||
def listRecordSetChangeHistory(
|
||||
zoneId: Option[String],
|
||||
startFrom: Option[Int],
|
||||
maxItems: Int,
|
||||
fqdn: Option[String],
|
||||
recordType: Option[RecordType],
|
||||
authPrincipal: AuthPrincipal
|
||||
): Result[ListRecordSetHistoryResponse]
|
||||
|
||||
def listFailedRecordSetChanges(
|
||||
authPrincipal: AuthPrincipal,
|
||||
zoneId: Option[String],
|
||||
startFrom: Int,
|
||||
maxItems: Int
|
||||
): Result[ListFailedRecordSetChangesResponse]
|
||||
|
||||
def getRecordSetCount(zoneId: String, authPrincipal: AuthPrincipal): Result[RecordSetCount]
|
||||
|
||||
}
|
||||
|
@ -20,13 +20,14 @@ import cats.syntax.either._
|
||||
import vinyldns.api.Interfaces._
|
||||
import vinyldns.api.backend.dns.DnsConversions
|
||||
import vinyldns.api.config.HighValueDomainConfig
|
||||
import vinyldns.api.domain.DomainValidations.validateIpv4Address
|
||||
import vinyldns.api.domain._
|
||||
import vinyldns.core.domain.DomainHelpers._
|
||||
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.{RecordSet, RecordType}
|
||||
import vinyldns.core.domain.record.{OwnerShipTransferStatus, RecordSet, RecordType}
|
||||
import vinyldns.core.domain.zone.Zone
|
||||
import vinyldns.core.Messages._
|
||||
|
||||
@ -236,6 +237,16 @@ object RecordSetValidations {
|
||||
)
|
||||
}
|
||||
|
||||
val isNotIPv4inCname = {
|
||||
ensuring(
|
||||
RecordSetValidation(
|
||||
s"""Invalid CNAME: ${newRecordSet.records.head.toString.dropRight(1)}, valid CNAME record data cannot be an IP address."""
|
||||
)
|
||||
)(
|
||||
validateIpv4Address(newRecordSet.records.head.toString.dropRight(1)).isInvalid
|
||||
)
|
||||
}
|
||||
|
||||
for {
|
||||
_ <- isNotOrigin(
|
||||
newRecordSet,
|
||||
@ -243,6 +254,7 @@ object RecordSetValidations {
|
||||
"CNAME RecordSet cannot have name '@' because it points to zone origin"
|
||||
)
|
||||
_ <- noRecordWithName
|
||||
_ <- isNotIPv4inCname
|
||||
_ <- RDataWithConsecutiveDots
|
||||
_ <- checkForDot(newRecordSet, zone, existingRecordSet, recordFqdnDoesNotExist, dottedHostZoneConfig, isRecordTypeAndUserAllowed, allowedDotsLimit)
|
||||
} yield ()
|
||||
@ -425,10 +437,66 @@ object RecordSetValidations {
|
||||
InvalidRequest("Cannot update RecordSet's zone ID.")
|
||||
)
|
||||
|
||||
/**
|
||||
* Checks of the user is a superuser, the zone is shared, and the only record attribute being changed
|
||||
* is the record owner group.
|
||||
*/
|
||||
def canSuperUserUpdateOwnerGroup(
|
||||
existing: RecordSet,
|
||||
updates: RecordSet,
|
||||
zone: Zone,
|
||||
auth: AuthPrincipal
|
||||
): Boolean =
|
||||
(updates.ownerGroupId != existing.ownerGroupId
|
||||
&& updates.zoneId == existing.zoneId
|
||||
&& updates.name == existing.name
|
||||
&& updates.typ == existing.typ
|
||||
&& updates.ttl == existing.ttl
|
||||
&& updates.records == existing.records
|
||||
&& zone.shared
|
||||
&& auth.isSuper)
|
||||
|
||||
def validRecordNameFilterLength(recordNameFilter: String): Either[Throwable, Unit] =
|
||||
ensuring(onError = InvalidRequest(RecordNameFilterError)) {
|
||||
val searchRegex = "[a-zA-Z0-9].*[a-zA-Z0-9]+".r
|
||||
val wildcardRegex = raw"^\s*[*%].*[*%]\s*$$".r
|
||||
searchRegex.findFirstIn(recordNameFilter).isDefined && wildcardRegex.findFirstIn(recordNameFilter).isEmpty
|
||||
}
|
||||
|
||||
def unchangedRecordSet(
|
||||
existing: RecordSet,
|
||||
updates: RecordSet
|
||||
): Either[Throwable, Unit] =
|
||||
Either.cond(
|
||||
updates.typ == existing.typ &&
|
||||
updates.records == existing.records &&
|
||||
updates.id == existing.id &&
|
||||
updates.zoneId == existing.zoneId &&
|
||||
updates.name == existing.name &&
|
||||
updates.ownerGroupId == existing.ownerGroupId &&
|
||||
updates.ttl == existing.ttl,
|
||||
(),
|
||||
InvalidRequest("Cannot update RecordSet's if user not a member of ownership group. User can only request for ownership transfer")
|
||||
)
|
||||
|
||||
def recordSetOwnerShipApproveStatus(
|
||||
updates: RecordSet,
|
||||
): Either[Throwable, Unit] =
|
||||
Either.cond(
|
||||
updates.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("<none>") != OwnerShipTransferStatus.ManuallyApproved &&
|
||||
updates.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("<none>") != OwnerShipTransferStatus.AutoApproved &&
|
||||
updates.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("<none>") != OwnerShipTransferStatus.ManuallyRejected,
|
||||
(),
|
||||
InvalidRequest("Cannot update RecordSet OwnerShip Status when request is cancelled.")
|
||||
)
|
||||
|
||||
def unchangedRecordSetOwnershipStatus(
|
||||
updates: RecordSet,
|
||||
existing: RecordSet
|
||||
): Either[Throwable, Unit] =
|
||||
Either.cond(
|
||||
updates.recordSetGroupChange == existing.recordSetGroupChange || existing.recordSetGroupChange.isEmpty,
|
||||
(),
|
||||
InvalidRequest("Cannot update RecordSet OwnerShip Status when zone is not shared.")
|
||||
)
|
||||
}
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
package vinyldns.api.domain.zone
|
||||
|
||||
import com.aaronbedra.orchard.CIDR
|
||||
import com.comcast.ip4s.Cidr
|
||||
import vinyldns.core.domain.record.RecordType
|
||||
import vinyldns.core.domain.zone.ACLRule
|
||||
|
||||
@ -61,7 +61,7 @@ object ACLRuleOrdering extends ACLRuleOrdering {
|
||||
object PTRACLRuleOrdering extends ACLRuleOrdering {
|
||||
def sortableRecordMaskValue(rule: ACLRule): Int = {
|
||||
val slash = rule.recordMask match {
|
||||
case Some(cidrRule) => CIDR.valueOf(cidrRule).getMask
|
||||
case Some(cidrRule) => Cidr.fromString(cidrRule).get.prefixBits
|
||||
case None => 0
|
||||
}
|
||||
128 - slash
|
||||
|
@ -17,7 +17,7 @@
|
||||
package vinyldns.api.domain.zone
|
||||
|
||||
import vinyldns.api.domain.zone
|
||||
import vinyldns.core.domain.zone.{ListZoneChangesResults, ZoneChange}
|
||||
import vinyldns.core.domain.zone.{ListFailedZoneChangesResults, ListZoneChangesResults, ZoneChange}
|
||||
|
||||
case class ListZoneChangesResponse(
|
||||
zoneId: String,
|
||||
@ -37,3 +37,29 @@ object ListZoneChangesResponse {
|
||||
listResults.maxItems
|
||||
)
|
||||
}
|
||||
|
||||
case class ListDeletedZoneChangesResponse(
|
||||
zonesDeletedInfo: List[ZoneChangeDeletedInfo],
|
||||
zoneChangeFilter: Option[String] = None,
|
||||
nextId: Option[String] = None,
|
||||
startFrom: Option[String] = None,
|
||||
maxItems: Int = 100,
|
||||
ignoreAccess: Boolean = false
|
||||
)
|
||||
|
||||
case class ListFailedZoneChangesResponse(
|
||||
failedZoneChanges: List[ZoneChange] = Nil,
|
||||
nextId: Int,
|
||||
startFrom: Int,
|
||||
maxItems: Int
|
||||
)
|
||||
|
||||
object ListFailedZoneChangesResponse {
|
||||
def apply(listResults: ListFailedZoneChangesResults): ListFailedZoneChangesResponse =
|
||||
zone.ListFailedZoneChangesResponse(
|
||||
listResults.items,
|
||||
listResults.nextId,
|
||||
listResults.startFrom,
|
||||
listResults.maxItems
|
||||
)
|
||||
}
|
||||
|
@ -61,6 +61,14 @@ object ZoneChangeGenerator {
|
||||
ZoneChangeStatus.Pending
|
||||
)
|
||||
|
||||
def forSyncs(zone: Zone): ZoneChange =
|
||||
ZoneChange(
|
||||
zone.copy(updated = Some(Instant.now.truncatedTo(ChronoUnit.MILLIS)), status = ZoneStatus.Syncing),
|
||||
zone.scheduleRequestor.get,
|
||||
ZoneChangeType.AutomatedSync,
|
||||
ZoneChangeStatus.Pending
|
||||
)
|
||||
|
||||
def forDelete(zone: Zone, authPrincipal: AuthPrincipal): ZoneChange =
|
||||
ZoneChange(
|
||||
zone.copy(updated = Some(Instant.now.truncatedTo(ChronoUnit.MILLIS)), status = ZoneStatus.Deleted),
|
||||
|
@ -21,8 +21,8 @@ import vinyldns.core.domain.record.RecordSetChangeStatus.RecordSetChangeStatus
|
||||
import vinyldns.core.domain.record.RecordSetChangeType.RecordSetChangeType
|
||||
import vinyldns.core.domain.record.RecordSetStatus.RecordSetStatus
|
||||
import vinyldns.core.domain.record.RecordType.RecordType
|
||||
import vinyldns.core.domain.record.{RecordData, RecordSet, RecordSetChange}
|
||||
import vinyldns.core.domain.zone.{ACLRuleInfo, AccessLevel, Zone, ZoneACL, ZoneConnection}
|
||||
import vinyldns.core.domain.record.{RecordData, RecordSet, RecordSetChange, OwnerShipTransfer}
|
||||
import vinyldns.core.domain.zone.{ACLRuleInfo, AccessLevel, Zone, ZoneACL, ZoneChange, ZoneConnection}
|
||||
import vinyldns.core.domain.zone.AccessLevel.AccessLevel
|
||||
import vinyldns.core.domain.zone.ZoneStatus.ZoneStatus
|
||||
|
||||
@ -44,6 +44,8 @@ case class ZoneInfo(
|
||||
adminGroupName: String,
|
||||
latestSync: Option[Instant],
|
||||
backendId: Option[String],
|
||||
recurrenceSchedule: Option[String],
|
||||
scheduleRequestor: Option[String],
|
||||
accessLevel: AccessLevel
|
||||
)
|
||||
|
||||
@ -70,10 +72,34 @@ object ZoneInfo {
|
||||
adminGroupName = groupName,
|
||||
latestSync = zone.latestSync,
|
||||
backendId = zone.backendId,
|
||||
recurrenceSchedule = zone.recurrenceSchedule,
|
||||
scheduleRequestor = zone.scheduleRequestor,
|
||||
accessLevel = accessLevel
|
||||
)
|
||||
}
|
||||
|
||||
case class ZoneDetails(
|
||||
name: String,
|
||||
email: String,
|
||||
status: ZoneStatus,
|
||||
adminGroupId: String,
|
||||
adminGroupName: String,
|
||||
)
|
||||
|
||||
object ZoneDetails {
|
||||
def apply(
|
||||
zone: Zone,
|
||||
groupName: String,
|
||||
): ZoneDetails =
|
||||
ZoneDetails(
|
||||
name = zone.name,
|
||||
email = zone.email,
|
||||
status = zone.status,
|
||||
adminGroupId = zone.adminGroupId,
|
||||
adminGroupName = groupName,
|
||||
)
|
||||
}
|
||||
|
||||
case class ZoneSummaryInfo(
|
||||
name: String,
|
||||
email: String,
|
||||
@ -90,6 +116,8 @@ case class ZoneSummaryInfo(
|
||||
adminGroupName: String,
|
||||
latestSync: Option[Instant],
|
||||
backendId: Option[String],
|
||||
recurrenceSchedule: Option[String],
|
||||
scheduleRequestor: Option[String],
|
||||
accessLevel: AccessLevel
|
||||
)
|
||||
|
||||
@ -111,6 +139,29 @@ object ZoneSummaryInfo {
|
||||
adminGroupName = groupName,
|
||||
latestSync = zone.latestSync,
|
||||
zone.backendId,
|
||||
recurrenceSchedule = zone.recurrenceSchedule,
|
||||
scheduleRequestor = zone.scheduleRequestor,
|
||||
accessLevel = accessLevel
|
||||
)
|
||||
}
|
||||
|
||||
case class ZoneChangeDeletedInfo(
|
||||
zoneChange: ZoneChange,
|
||||
adminGroupName: String,
|
||||
userName: String,
|
||||
accessLevel: AccessLevel
|
||||
)
|
||||
|
||||
object ZoneChangeDeletedInfo {
|
||||
def apply(zoneChange: List[ZoneChange],
|
||||
groupName: String,
|
||||
userName: String,
|
||||
accessLevel: AccessLevel)
|
||||
: ZoneChangeDeletedInfo =
|
||||
ZoneChangeDeletedInfo(
|
||||
zoneChange= zoneChange,
|
||||
groupName = groupName,
|
||||
userName = userName,
|
||||
accessLevel = accessLevel
|
||||
)
|
||||
}
|
||||
@ -129,6 +180,7 @@ case class RecordSetListInfo(
|
||||
accessLevel: AccessLevel,
|
||||
ownerGroupId: Option[String],
|
||||
ownerGroupName: Option[String],
|
||||
recordSetGroupChange: Option[OwnerShipTransfer],
|
||||
fqdn: Option[String]
|
||||
)
|
||||
|
||||
@ -148,6 +200,7 @@ object RecordSetListInfo {
|
||||
accessLevel = accessLevel,
|
||||
ownerGroupId = recordSet.ownerGroupId,
|
||||
ownerGroupName = recordSet.ownerGroupName,
|
||||
recordSetGroupChange = recordSet.recordSetGroupChange,
|
||||
fqdn = recordSet.fqdn
|
||||
)
|
||||
}
|
||||
@ -165,6 +218,7 @@ case class RecordSetInfo(
|
||||
account: String,
|
||||
ownerGroupId: Option[String],
|
||||
ownerGroupName: Option[String],
|
||||
recordSetGroupChange: Option[OwnerShipTransfer],
|
||||
fqdn: Option[String]
|
||||
)
|
||||
|
||||
@ -183,6 +237,7 @@ object RecordSetInfo {
|
||||
account = recordSet.account,
|
||||
ownerGroupId = recordSet.ownerGroupId,
|
||||
ownerGroupName = groupName,
|
||||
recordSetGroupChange = recordSet.recordSetGroupChange,
|
||||
fqdn = recordSet.fqdn
|
||||
)
|
||||
}
|
||||
@ -200,6 +255,7 @@ case class RecordSetGlobalInfo(
|
||||
account: String,
|
||||
ownerGroupId: Option[String],
|
||||
ownerGroupName: Option[String],
|
||||
recordSetGroupChange: Option[OwnerShipTransfer],
|
||||
fqdn: Option[String],
|
||||
zoneName: String,
|
||||
zoneShared: Boolean
|
||||
@ -225,6 +281,7 @@ object RecordSetGlobalInfo {
|
||||
account = recordSet.account,
|
||||
ownerGroupId = recordSet.ownerGroupId,
|
||||
ownerGroupName = groupName,
|
||||
recordSetGroupChange = recordSet.recordSetGroupChange,
|
||||
fqdn = recordSet.fqdn,
|
||||
zoneName = zoneName,
|
||||
zoneShared = zoneShared
|
||||
@ -266,9 +323,12 @@ case class ListZonesResponse(
|
||||
startFrom: Option[String] = None,
|
||||
nextId: Option[String] = None,
|
||||
maxItems: Int = 100,
|
||||
ignoreAccess: Boolean = false
|
||||
ignoreAccess: Boolean = false,
|
||||
includeReverse: Boolean = true
|
||||
)
|
||||
|
||||
case class RecordSetCount( count: Int = 0 )
|
||||
|
||||
// Errors
|
||||
case class InvalidRequest(msg: String) extends Throwable(msg)
|
||||
|
||||
|
@ -19,13 +19,7 @@ package vinyldns.api.domain.zone
|
||||
import cats.implicits._
|
||||
import cats.data._
|
||||
import com.comcast.ip4s.IpAddress
|
||||
import com.comcast.ip4s.interop.cats.implicits._
|
||||
import vinyldns.core.domain.{
|
||||
DomainHelpers,
|
||||
DomainValidationError,
|
||||
HighValueDomainError,
|
||||
RecordRequiresManualReview
|
||||
}
|
||||
import vinyldns.core.domain.{DomainHelpers, DomainValidationError, HighValueDomainError, RecordRequiresManualReview}
|
||||
import vinyldns.core.domain.record.{NSData, RecordSet}
|
||||
|
||||
import scala.util.matching.Regex
|
||||
@ -41,7 +35,7 @@ object ZoneRecordValidations {
|
||||
|
||||
/* Checks to see if an ip address is part of the ip address list */
|
||||
def isIpInIpList(ipList: List[IpAddress], ipToTest: String): Boolean =
|
||||
IpAddress(ipToTest).exists(ip => ipList.exists(_ === ip))
|
||||
IpAddress.fromString(ipToTest).exists(ip => ipList.exists(_ === ip))
|
||||
|
||||
/* Checks to see if an individual ns data is part of the approved server list */
|
||||
def isApprovedNameServer(
|
||||
|
@ -23,11 +23,16 @@ import vinyldns.api.Interfaces
|
||||
import vinyldns.core.domain.auth.AuthPrincipal
|
||||
import vinyldns.api.repository.ApiDataAccessor
|
||||
import vinyldns.core.crypto.CryptoAlgebra
|
||||
import vinyldns.core.domain.membership.{Group, GroupRepository, User, UserRepository}
|
||||
import vinyldns.core.domain.membership.{Group, GroupRepository, ListUsersResults, User, UserRepository}
|
||||
import vinyldns.core.domain.zone._
|
||||
import vinyldns.core.queue.MessageQueue
|
||||
import vinyldns.core.domain.DomainHelpers.ensureTrailingDot
|
||||
import vinyldns.core.domain.backend.BackendResolver
|
||||
import com.cronutils.model.definition.CronDefinition
|
||||
import com.cronutils.model.definition.CronDefinitionBuilder
|
||||
import com.cronutils.parser.CronParser
|
||||
import com.cronutils.model.CronType
|
||||
import vinyldns.api.domain.membership.MembershipService
|
||||
|
||||
object ZoneService {
|
||||
def apply(
|
||||
@ -37,7 +42,8 @@ object ZoneService {
|
||||
zoneValidations: ZoneValidations,
|
||||
accessValidation: AccessValidationsAlgebra,
|
||||
backendResolver: BackendResolver,
|
||||
crypto: CryptoAlgebra
|
||||
crypto: CryptoAlgebra,
|
||||
membershipService:MembershipService
|
||||
): ZoneService =
|
||||
new ZoneService(
|
||||
dataAccessor.zoneRepository,
|
||||
@ -49,7 +55,8 @@ object ZoneService {
|
||||
zoneValidations,
|
||||
accessValidation,
|
||||
backendResolver,
|
||||
crypto
|
||||
crypto,
|
||||
membershipService
|
||||
)
|
||||
}
|
||||
|
||||
@ -63,7 +70,8 @@ class ZoneService(
|
||||
zoneValidations: ZoneValidations,
|
||||
accessValidation: AccessValidationsAlgebra,
|
||||
backendResolver: BackendResolver,
|
||||
crypto: CryptoAlgebra
|
||||
crypto: CryptoAlgebra,
|
||||
membershipService:MembershipService
|
||||
) extends ZoneServiceAlgebra {
|
||||
|
||||
import accessValidation._
|
||||
@ -76,12 +84,17 @@ class ZoneService(
|
||||
): Result[ZoneCommandResult] =
|
||||
for {
|
||||
_ <- isValidZoneAcl(createZoneInput.acl).toResult
|
||||
_ <- membershipService.emailValidation(createZoneInput.email)
|
||||
_ <- connectionValidator.isValidBackendId(createZoneInput.backendId).toResult
|
||||
_ <- validateSharedZoneAuthorized(createZoneInput.shared, auth.signedInUser).toResult
|
||||
_ <- zoneDoesNotExist(createZoneInput.name)
|
||||
_ <- adminGroupExists(createZoneInput.adminGroupId)
|
||||
_ <- if(createZoneInput.recurrenceSchedule.isDefined) canScheduleZoneSync(auth).toResult else IO.unit.toResult
|
||||
isCronStringValid = if(createZoneInput.recurrenceSchedule.isDefined) isValidCronString(createZoneInput.recurrenceSchedule.get) else true
|
||||
_ <- validateCronString(isCronStringValid).toResult
|
||||
_ <- canChangeZone(auth, createZoneInput.name, createZoneInput.adminGroupId).toResult
|
||||
zoneToCreate = Zone(createZoneInput, auth.isTestUser)
|
||||
createdZoneInput = if(createZoneInput.recurrenceSchedule.isDefined) createZoneInput.copy(scheduleRequestor = Some(auth.signedInUser.userName)) else createZoneInput
|
||||
zoneToCreate = Zone(createdZoneInput, auth.isTestUser)
|
||||
_ <- connectionValidator.validateZoneConnections(zoneToCreate)
|
||||
createZoneChange <- ZoneChangeGenerator.forAdd(zoneToCreate, auth).toResult
|
||||
_ <- messageQueue.send(createZoneChange).toResult[Unit]
|
||||
@ -90,6 +103,7 @@ class ZoneService(
|
||||
def updateZone(updateZoneInput: UpdateZoneInput, auth: AuthPrincipal): Result[ZoneCommandResult] =
|
||||
for {
|
||||
_ <- isValidZoneAcl(updateZoneInput.acl).toResult
|
||||
_ <- membershipService.emailValidation(updateZoneInput.email)
|
||||
_ <- connectionValidator.isValidBackendId(updateZoneInput.backendId).toResult
|
||||
existingZone <- getZoneOrFail(updateZoneInput.id)
|
||||
_ <- validateSharedZoneAuthorized(
|
||||
@ -98,10 +112,14 @@ class ZoneService(
|
||||
auth.signedInUser
|
||||
).toResult
|
||||
_ <- canChangeZone(auth, existingZone.name, existingZone.adminGroupId).toResult
|
||||
_ <- if(updateZoneInput.recurrenceSchedule.isDefined) canScheduleZoneSync(auth).toResult else IO.unit.toResult
|
||||
isCronStringValid = if(updateZoneInput.recurrenceSchedule.isDefined) isValidCronString(updateZoneInput.recurrenceSchedule.get) else true
|
||||
_ <- validateCronString(isCronStringValid).toResult
|
||||
_ <- adminGroupExists(updateZoneInput.adminGroupId)
|
||||
// if admin group changes, this confirms user has access to new group
|
||||
_ <- canChangeZone(auth, updateZoneInput.name, updateZoneInput.adminGroupId).toResult
|
||||
zoneWithUpdates = Zone(updateZoneInput, existingZone)
|
||||
updatedZoneInput = if(updateZoneInput.recurrenceSchedule.isDefined) updateZoneInput.copy(scheduleRequestor = Some(auth.signedInUser.userName)) else updateZoneInput
|
||||
zoneWithUpdates = Zone(updatedZoneInput, existingZone)
|
||||
_ <- validateZoneConnectionIfChanged(zoneWithUpdates, existingZone)
|
||||
updateZoneChange <- ZoneChangeGenerator
|
||||
.forUpdate(zoneWithUpdates, existingZone, auth, crypto)
|
||||
@ -135,6 +153,12 @@ class ZoneService(
|
||||
accessLevel = getZoneAccess(auth, zone)
|
||||
} yield ZoneInfo(zone, aclInfo, groupName, accessLevel)
|
||||
|
||||
def getCommonZoneDetails(zoneId: String, auth: AuthPrincipal): Result[ZoneDetails] =
|
||||
for {
|
||||
zone <- getZoneOrFail(zoneId)
|
||||
groupName <- getGroupName(zone.adminGroupId)
|
||||
} yield ZoneDetails(zone, groupName)
|
||||
|
||||
def getZoneByName(zoneName: String, auth: AuthPrincipal): Result[ZoneInfo] =
|
||||
for {
|
||||
zone <- getZoneByNameOrFail(ensureTrailingDot(zoneName))
|
||||
@ -150,7 +174,8 @@ class ZoneService(
|
||||
startFrom: Option[String] = None,
|
||||
maxItems: Int = 100,
|
||||
searchByAdminGroup: Boolean = false,
|
||||
ignoreAccess: Boolean = false
|
||||
ignoreAccess: Boolean = false,
|
||||
includeReverse: Boolean = true
|
||||
): Result[ListZonesResponse] = {
|
||||
if(!searchByAdminGroup || nameFilter.isEmpty){
|
||||
for {
|
||||
@ -159,21 +184,22 @@ class ZoneService(
|
||||
nameFilter,
|
||||
startFrom,
|
||||
maxItems,
|
||||
ignoreAccess
|
||||
)
|
||||
zones = listZonesResult.zones
|
||||
groupIds = zones.map(_.adminGroupId).toSet
|
||||
groups <- groupRepository.getGroups(groupIds)
|
||||
zoneSummaryInfos = zoneSummaryInfoMapping(zones, authPrincipal, groups)
|
||||
} yield ListZonesResponse(
|
||||
zoneSummaryInfos,
|
||||
listZonesResult.zonesFilter,
|
||||
listZonesResult.startFrom,
|
||||
listZonesResult.nextId,
|
||||
listZonesResult.maxItems,
|
||||
listZonesResult.ignoreAccess
|
||||
ignoreAccess,
|
||||
includeReverse
|
||||
)
|
||||
}
|
||||
zones = listZonesResult.zones
|
||||
groupIds = zones.map(_.adminGroupId).toSet
|
||||
groups <- groupRepository.getGroups(groupIds)
|
||||
zoneSummaryInfos = zoneSummaryInfoMapping(zones, authPrincipal, groups)
|
||||
} yield ListZonesResponse(
|
||||
zoneSummaryInfos,
|
||||
listZonesResult.zonesFilter,
|
||||
listZonesResult.startFrom,
|
||||
listZonesResult.nextId,
|
||||
listZonesResult.maxItems,
|
||||
listZonesResult.ignoreAccess,
|
||||
listZonesResult.includeReverse
|
||||
)}
|
||||
else {
|
||||
for {
|
||||
groupIds <- getGroupsIdsByName(nameFilter.get)
|
||||
@ -182,7 +208,8 @@ class ZoneService(
|
||||
startFrom,
|
||||
maxItems,
|
||||
groupIds,
|
||||
ignoreAccess
|
||||
ignoreAccess,
|
||||
includeReverse
|
||||
)
|
||||
zones = listZonesResult.zones
|
||||
groups <- groupRepository.getGroups(groupIds)
|
||||
@ -193,11 +220,64 @@ class ZoneService(
|
||||
listZonesResult.startFrom,
|
||||
listZonesResult.nextId,
|
||||
listZonesResult.maxItems,
|
||||
listZonesResult.ignoreAccess
|
||||
listZonesResult.ignoreAccess,
|
||||
listZonesResult.includeReverse
|
||||
)
|
||||
}
|
||||
}.toResult
|
||||
|
||||
def listDeletedZones(
|
||||
authPrincipal: AuthPrincipal,
|
||||
nameFilter: Option[String] = None,
|
||||
startFrom: Option[String] = None,
|
||||
maxItems: Int = 100,
|
||||
ignoreAccess: Boolean = false
|
||||
): Result[ListDeletedZoneChangesResponse] = {
|
||||
for {
|
||||
listZonesChangeResult <- zoneChangeRepository.listDeletedZones(
|
||||
authPrincipal,
|
||||
nameFilter,
|
||||
startFrom,
|
||||
maxItems,
|
||||
ignoreAccess
|
||||
)
|
||||
zoneChanges = listZonesChangeResult.zoneDeleted
|
||||
groupIds = zoneChanges.map(_.zone.adminGroupId).toSet
|
||||
groups <- groupRepository.getGroups(groupIds)
|
||||
userId = zoneChanges.map(_.userId).toSet
|
||||
users <- userRepository.getUsers(userId,None,None)
|
||||
zoneDeleteSummaryInfos = ZoneChangeDeletedInfoMapping(zoneChanges, authPrincipal, groups, users)
|
||||
} yield {
|
||||
ListDeletedZoneChangesResponse(
|
||||
zoneDeleteSummaryInfos,
|
||||
listZonesChangeResult.zoneChangeFilter,
|
||||
listZonesChangeResult.nextId,
|
||||
listZonesChangeResult.startFrom,
|
||||
listZonesChangeResult.maxItems,
|
||||
listZonesChangeResult.ignoreAccess
|
||||
)
|
||||
}
|
||||
}.toResult
|
||||
|
||||
private def ZoneChangeDeletedInfoMapping(
|
||||
zoneChange: List[ZoneChange],
|
||||
auth: AuthPrincipal,
|
||||
groups: Set[Group],
|
||||
users: ListUsersResults
|
||||
): List[ZoneChangeDeletedInfo] =
|
||||
zoneChange.map { zc =>
|
||||
val groupName = groups.find(_.id == zc.zone.adminGroupId) match {
|
||||
case Some(group) => group.name
|
||||
case None => "Unknown group name"
|
||||
}
|
||||
val userName = users.users.find(_.id == zc.userId) match {
|
||||
case Some(user) => user.userName
|
||||
case None => "Unknown user name"
|
||||
}
|
||||
val zoneAccess = getZoneAccess(auth, zc.zone)
|
||||
ZoneChangeDeletedInfo(zc, groupName,userName, zoneAccess)
|
||||
}
|
||||
|
||||
def zoneSummaryInfoMapping(
|
||||
zones: List[Zone],
|
||||
auth: AuthPrincipal,
|
||||
@ -220,12 +300,38 @@ class ZoneService(
|
||||
): Result[ListZoneChangesResponse] =
|
||||
for {
|
||||
zone <- getZoneOrFail(zoneId)
|
||||
_ <- canSeeZone(authPrincipal, zone).toResult
|
||||
_ <- canSeeZoneChange(authPrincipal, zone).toResult
|
||||
zoneChangesResults <- zoneChangeRepository
|
||||
.listZoneChanges(zone.id, startFrom, maxItems)
|
||||
.toResult[ListZoneChangesResults]
|
||||
} yield ListZoneChangesResponse(zone.id, zoneChangesResults)
|
||||
|
||||
def listFailedZoneChanges(
|
||||
authPrincipal: AuthPrincipal,
|
||||
startFrom: Int= 0,
|
||||
maxItems: Int = 100
|
||||
): Result[ListFailedZoneChangesResponse] =
|
||||
for {
|
||||
zoneChangesFailedResults <- zoneChangeRepository
|
||||
.listFailedZoneChanges(maxItems, startFrom)
|
||||
.toResult[ListFailedZoneChangesResults]
|
||||
_ <- zoneAccess(zoneChangesFailedResults.items, authPrincipal).toResult
|
||||
} yield
|
||||
ListFailedZoneChangesResponse(
|
||||
zoneChangesFailedResults.items,
|
||||
zoneChangesFailedResults.nextId,
|
||||
startFrom,
|
||||
maxItems
|
||||
)
|
||||
|
||||
def zoneAccess(
|
||||
zoneCh: List[ZoneChange],
|
||||
auth: AuthPrincipal
|
||||
): List[Result[Unit]] =
|
||||
zoneCh.map { zn =>
|
||||
canSeeZone(auth, zn.zone).toResult
|
||||
}
|
||||
|
||||
def addACLRule(
|
||||
zoneId: String,
|
||||
aclRuleInfo: ACLRuleInfo,
|
||||
@ -276,6 +382,28 @@ class ZoneService(
|
||||
def getBackendIds(): Result[List[String]] =
|
||||
backendResolver.ids.toList.toResult
|
||||
|
||||
def isValidCronString(maybeString: String): Boolean = {
|
||||
val isValid = try {
|
||||
val cronDefinition: CronDefinition = CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ)
|
||||
val parser: CronParser = new CronParser(cronDefinition)
|
||||
val quartzCron = parser.parse(maybeString)
|
||||
quartzCron.validate
|
||||
true
|
||||
}
|
||||
catch {
|
||||
case _: Exception =>
|
||||
false
|
||||
}
|
||||
isValid
|
||||
}
|
||||
|
||||
def validateCronString(isValid: Boolean): Either[Throwable, Unit] =
|
||||
ensuring(
|
||||
InvalidRequest("Invalid cron expression. Please enter a valid cron expression in 'recurrenceSchedule'.")
|
||||
)(
|
||||
isValid
|
||||
)
|
||||
|
||||
def zoneDoesNotExist(zoneName: String): Result[Unit] =
|
||||
zoneRepository
|
||||
.getZoneByName(zoneName)
|
||||
@ -289,6 +417,13 @@ class ZoneService(
|
||||
}
|
||||
.toResult
|
||||
|
||||
def canScheduleZoneSync(auth: AuthPrincipal): Either[Throwable, Unit] =
|
||||
ensuring(
|
||||
NotAuthorizedError(s"User '${auth.signedInUser.userName}' is not authorized to schedule zone sync in this zone.")
|
||||
)(
|
||||
auth.isSystemAdmin
|
||||
)
|
||||
|
||||
def adminGroupExists(groupId: String): Result[Unit] =
|
||||
groupRepository
|
||||
.getGroup(groupId)
|
||||
|
@ -35,6 +35,8 @@ trait ZoneServiceAlgebra {
|
||||
|
||||
def getZone(zoneId: String, auth: AuthPrincipal): Result[ZoneInfo]
|
||||
|
||||
def getCommonZoneDetails(zoneId: String, auth: AuthPrincipal): Result[ZoneDetails]
|
||||
|
||||
def getZoneByName(zoneName: String, auth: AuthPrincipal): Result[ZoneInfo]
|
||||
|
||||
def listZones(
|
||||
@ -43,9 +45,18 @@ trait ZoneServiceAlgebra {
|
||||
startFrom: Option[String],
|
||||
maxItems: Int,
|
||||
searchByAdminGroup: Boolean,
|
||||
ignoreAccess: Boolean
|
||||
ignoreAccess: Boolean,
|
||||
includeReverse: Boolean
|
||||
): Result[ListZonesResponse]
|
||||
|
||||
def listDeletedZones(
|
||||
authPrincipal: AuthPrincipal,
|
||||
nameFilter: Option[String],
|
||||
startFrom: Option[String],
|
||||
maxItems: Int,
|
||||
ignoreAccess: Boolean
|
||||
): Result[ListDeletedZoneChangesResponse]
|
||||
|
||||
def listZoneChanges(
|
||||
zoneId: String,
|
||||
authPrincipal: AuthPrincipal,
|
||||
@ -67,4 +78,9 @@ trait ZoneServiceAlgebra {
|
||||
|
||||
def getBackendIds(): Result[List[String]]
|
||||
|
||||
def listFailedZoneChanges(
|
||||
authPrincipal: AuthPrincipal,
|
||||
startFrom: Int,
|
||||
maxItems: Int
|
||||
): Result[ListFailedZoneChangesResponse]
|
||||
}
|
||||
|
@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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.domain.zone
|
||||
|
||||
import cats.effect.IO
|
||||
import com.cronutils.model.CronType
|
||||
import com.cronutils.model.definition.{CronDefinition, CronDefinitionBuilder}
|
||||
import com.cronutils.model.time.ExecutionTime
|
||||
import com.cronutils.parser.CronParser
|
||||
import org.slf4j.LoggerFactory
|
||||
import vinyldns.core.domain.zone.{Zone, ZoneChange, ZoneRepository}
|
||||
import java.time.{Instant, ZoneId}
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
object ZoneSyncScheduleHandler {
|
||||
private val logger = LoggerFactory.getLogger("ZoneSyncScheduleHandler")
|
||||
|
||||
def zoneSyncScheduler(zoneRepository: ZoneRepository): IO[Set[ZoneChange]] = {
|
||||
for {
|
||||
zones <- zoneRepository.getAllZonesWithSyncSchedule
|
||||
zoneScheduleIds = getZonesWithSchedule(zones.toList)
|
||||
zoneChanges <- getZoneChanges(zoneRepository, zoneScheduleIds)
|
||||
} yield zoneChanges
|
||||
}
|
||||
|
||||
def getZoneChanges(zoneRepository: ZoneRepository, zoneScheduleIds: List[String]): IO[Set[ZoneChange]] = {
|
||||
if(zoneScheduleIds.nonEmpty) {
|
||||
for{
|
||||
getZones <- zoneRepository.getZones(zoneScheduleIds.toSet)
|
||||
syncZoneChange = getZones.map(zone => ZoneChangeGenerator.forSyncs(zone))
|
||||
} yield syncZoneChange
|
||||
} else {
|
||||
IO(Set.empty)
|
||||
}
|
||||
}
|
||||
|
||||
def getZonesWithSchedule(zone: List[Zone]): List[String] = {
|
||||
var zonesWithSchedule: List[String] = List.empty
|
||||
for(z <- zone) {
|
||||
if (z.recurrenceSchedule.isDefined) {
|
||||
val now = Instant.now().atZone(ZoneId.of("UTC"))
|
||||
val cronDefinition: CronDefinition = CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ)
|
||||
val parser: CronParser = new CronParser(cronDefinition)
|
||||
val executionTime: ExecutionTime = ExecutionTime.forCron(parser.parse(z.recurrenceSchedule.get))
|
||||
val nextExecution = executionTime.nextExecution(now).get()
|
||||
val diff = ChronoUnit.SECONDS.between(now, nextExecution)
|
||||
if (diff == 1) {
|
||||
zonesWithSchedule = zonesWithSchedule :+ z.id
|
||||
logger.info("Zones with sync schedule: " + zonesWithSchedule)
|
||||
} else {
|
||||
List.empty
|
||||
}
|
||||
} else {
|
||||
List.empty
|
||||
}
|
||||
}
|
||||
zonesWithSchedule
|
||||
}
|
||||
|
||||
}
|
@ -17,7 +17,7 @@
|
||||
package vinyldns.api.domain.zone
|
||||
|
||||
import cats.syntax.either._
|
||||
import com.aaronbedra.orchard.CIDR
|
||||
import com.comcast.ip4s.Cidr
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
import vinyldns.api.Interfaces.ensuring
|
||||
@ -57,10 +57,10 @@ class ZoneValidations(syncDelayMillis: Int) {
|
||||
def aclRuleMaskIsValid(rule: ACLRule): Either[Throwable, Unit] =
|
||||
rule.recordMask match {
|
||||
case Some(mask) if rule.recordTypes == Set(RecordType.PTR) =>
|
||||
Try(CIDR.valueOf(mask)) match {
|
||||
Try(Cidr.fromString(mask).get) match {
|
||||
case Success(_) => Right(())
|
||||
case Failure(e) =>
|
||||
InvalidRequest(s"PTR types must have no mask or a valid CIDR mask: ${e.getMessage}").asLeft
|
||||
case Failure(_) =>
|
||||
InvalidRequest(s"PTR types must have no mask or a valid CIDR mask: Invalid CIDR block").asLeft
|
||||
}
|
||||
case Some(_) if rule.recordTypes.contains(RecordType.PTR) =>
|
||||
InvalidRequest("Multiple record types including PTR must have no mask").asLeft
|
||||
|
@ -20,7 +20,7 @@ import cats.effect._
|
||||
import org.slf4j.LoggerFactory
|
||||
import vinyldns.api.backend.dns.DnsConversions
|
||||
import vinyldns.core.domain.backend.Backend
|
||||
import vinyldns.core.domain.record.{NameSort, RecordSetCacheRepository, RecordSetRepository}
|
||||
import vinyldns.core.domain.record.{NameSort, RecordSetCacheRepository, RecordSetRepository, RecordTypeSort}
|
||||
import vinyldns.core.domain.zone.Zone
|
||||
import vinyldns.core.route.Monitored
|
||||
|
||||
@ -69,7 +69,8 @@ case class VinylDNSZoneViewLoader(
|
||||
recordNameFilter = None,
|
||||
recordTypeFilter = None,
|
||||
recordOwnerGroupFilter = None,
|
||||
nameSort = NameSort.ASC
|
||||
nameSort = NameSort.ASC,
|
||||
recordTypeSort = RecordTypeSort.ASC
|
||||
)
|
||||
.map { result =>
|
||||
VinylDNSZoneViewLoader.logger.info(
|
||||
|
@ -23,6 +23,7 @@ import scalikejdbc.DB
|
||||
import vinyldns.api.backend.dns.DnsProtocol.TryAgain
|
||||
import vinyldns.api.domain.record.RecordSetChangeGenerator
|
||||
import vinyldns.api.domain.record.RecordSetHelpers._
|
||||
import vinyldns.core.Messages.{nonExistentRecordDataDeleteMessage, nonExistentRecordDeleteMessage}
|
||||
import vinyldns.core.domain.backend.{Backend, BackendResponse}
|
||||
import vinyldns.core.domain.batch.{BatchChangeRepository, SingleChange}
|
||||
import vinyldns.core.domain.record._
|
||||
@ -35,6 +36,11 @@ object RecordSetChangeHandler extends TransactionProvider {
|
||||
private implicit val cs: ContextShift[IO] =
|
||||
IO.contextShift(scala.concurrent.ExecutionContext.global)
|
||||
|
||||
private val outOfSyncFailureMessage: String = "This record set is out of sync with the DNS backend; sync this zone before attempting to update this record set."
|
||||
private val incompatibleRecordFailureMessage: String = "Incompatible record in DNS."
|
||||
private val syncZoneMessage: String = "This record set is out of sync with the DNS backend. Sync this zone before attempting to update this record set."
|
||||
private val recordConflictMessage: String = "Conflict due to the record having the same name as an NS record in the same zone. Please create the record using the DNS service the NS record has been delegated to (ex. AWS r53), or use a different record name."
|
||||
|
||||
final case class Requeue(change: RecordSetChange) extends Throwable
|
||||
|
||||
def apply(
|
||||
@ -87,15 +93,31 @@ object RecordSetChangeHandler extends TransactionProvider {
|
||||
): IO[Unit] =
|
||||
executeWithinTransaction { db: DB =>
|
||||
for {
|
||||
_ <- recordSetRepository.apply(db, changeSet)
|
||||
_ <- recordChangeRepository.save(db, changeSet)
|
||||
_ <- recordSetCacheRepository.save(db, changeSet)
|
||||
// Update single changes within this transaction to rollback the changes made to recordset and record change repo
|
||||
// when exception occurs while updating single changes
|
||||
singleBatchChanges <- batchChangeRepository.getSingleChanges(
|
||||
recordSetChange.singleBatchChangeIds
|
||||
)
|
||||
singleChangeStatusUpdates = updateBatchStatuses(singleBatchChanges, completedState.change)
|
||||
updatedChangeSet = if (singleChangeStatusUpdates.size == 1) {
|
||||
// Filter out RecordSetChange from changeSet if systemMessage matches
|
||||
val filteredChangeSetChanges = changeSet.changes.filterNot { recordSetChange =>
|
||||
// Find the corresponding singleChangeStatusUpdate by recordChangeId
|
||||
singleChangeStatusUpdates.exists { singleChange =>
|
||||
singleChange.recordChangeId.contains(recordSetChange.id) &&
|
||||
singleChange.systemMessage.exists(msg =>
|
||||
msg == nonExistentRecordDeleteMessage || msg == nonExistentRecordDataDeleteMessage
|
||||
)
|
||||
}
|
||||
}
|
||||
// Create a new ChangeSet with filtered changes
|
||||
changeSet.copy(changes = filteredChangeSetChanges)
|
||||
} else {
|
||||
changeSet
|
||||
}
|
||||
_ <- recordSetRepository.apply(db, updatedChangeSet)
|
||||
_ <- recordChangeRepository.save(db, updatedChangeSet)
|
||||
_ <- recordSetCacheRepository.save(db, updatedChangeSet)
|
||||
_ <- batchChangeRepository.updateSingleChanges(singleChangeStatusUpdates)
|
||||
} yield ()
|
||||
}
|
||||
@ -106,7 +128,7 @@ object RecordSetChangeHandler extends TransactionProvider {
|
||||
): List[SingleChange] =
|
||||
recordSetChange.status match {
|
||||
case RecordSetChangeStatus.Complete =>
|
||||
singleChanges.map(_.complete(recordSetChange.systemMessage, recordSetChange.id, recordSetChange.recordSet.id))
|
||||
singleChanges.map(_.complete(recordSetChange.id, recordSetChange.recordSet.id))
|
||||
case RecordSetChangeStatus.Failed =>
|
||||
singleChanges.map(_.withProcessingError(recordSetChange.systemMessage, recordSetChange.id))
|
||||
case _ => singleChanges
|
||||
@ -157,15 +179,6 @@ 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,
|
||||
@ -174,9 +187,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) || isRecordExist(existingRecords,change))
|
||||
AlreadyApplied(change) //Record exists in DNS
|
||||
else Failure(change, "Incompatible record in DNS.")
|
||||
else if (isDnsMatch(existingRecords, change.recordSet, change.zone.name))
|
||||
AlreadyApplied(change)
|
||||
else Failure(change, incompatibleRecordFailureMessage)
|
||||
|
||||
case RecordSetChangeType.Update =>
|
||||
if (isDnsMatch(existingRecords, change.recordSet, change.zone.name))
|
||||
@ -190,14 +203,23 @@ object RecordSetChangeHandler extends TransactionProvider {
|
||||
else
|
||||
Failure(
|
||||
change,
|
||||
"This record set is out of sync with the DNS backend; " +
|
||||
"sync this zone before attempting to update this record set."
|
||||
outOfSyncFailureMessage
|
||||
)
|
||||
}
|
||||
|
||||
case RecordSetChangeType.Delete =>
|
||||
if (existingRecords.nonEmpty) ReadyToApply(change) // we have a record set, move forward
|
||||
else AlreadyApplied(change) // we did not find the record set, so already applied
|
||||
|
||||
case RecordSetChangeType.Sync =>
|
||||
if (existingRecords.nonEmpty) {
|
||||
Failure(
|
||||
change,
|
||||
outOfSyncFailureMessage
|
||||
)
|
||||
} else {
|
||||
AlreadyApplied(change)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -397,11 +419,25 @@ object RecordSetChangeHandler extends TransactionProvider {
|
||||
case AlreadyApplied(_) => Completed(change.successful)
|
||||
case ReadyToApply(_) => Validated(change)
|
||||
case Failure(_, message) =>
|
||||
Completed(
|
||||
change.failed(
|
||||
s"""Failed validating update to DNS for change "${change.id}": "${change.recordSet.name}": """ + message
|
||||
if(message == outOfSyncFailureMessage || message == incompatibleRecordFailureMessage){
|
||||
Completed(
|
||||
change.failed(
|
||||
syncZoneMessage
|
||||
)
|
||||
)
|
||||
)
|
||||
} else if (message == "referral") {
|
||||
Completed(
|
||||
change.failed(
|
||||
recordConflictMessage
|
||||
)
|
||||
)
|
||||
} else {
|
||||
Completed(
|
||||
change.failed(
|
||||
s"""Failed validating update to DNS for change "${change.id}": "${change.recordSet.name}": """ + message
|
||||
)
|
||||
)
|
||||
}
|
||||
case Retry(_) => Retrying(change)
|
||||
}
|
||||
|
||||
|
@ -17,12 +17,16 @@
|
||||
package vinyldns.api.engine
|
||||
|
||||
import cats.effect.IO
|
||||
import org.slf4j.{Logger, LoggerFactory}
|
||||
import scalikejdbc.DB
|
||||
import vinyldns.api.engine.ZoneSyncHandler.executeWithinTransaction
|
||||
import vinyldns.core.domain.record.{RecordSetCacheRepository, RecordSetRepository}
|
||||
import vinyldns.core.domain.zone._
|
||||
|
||||
object ZoneChangeHandler {
|
||||
|
||||
private implicit val logger: Logger = LoggerFactory.getLogger("vinyldns.engine.ZoneChangeHandler")
|
||||
|
||||
def apply(
|
||||
zoneRepository: ZoneRepository,
|
||||
zoneChangeRepository: ZoneChangeRepository,
|
||||
@ -54,6 +58,7 @@ object ZoneChangeHandler {
|
||||
zoneChangeRepository.save(zoneChange.copy(status = ZoneChangeStatus.Synced))
|
||||
}
|
||||
case Right(_) =>
|
||||
logger.info(s"Saving zone change with id: '${zoneChange.id}', zone name: '${zoneChange.zone.name}'")
|
||||
zoneChangeRepository.save(zoneChange.copy(status = ZoneChangeStatus.Synced))
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ import vinyldns.core.domain.record._
|
||||
import vinyldns.core.domain.zone._
|
||||
import vinyldns.core.route.Monitored
|
||||
import vinyldns.mysql.TransactionProvider
|
||||
import java.io.{PrintWriter, StringWriter}
|
||||
|
||||
object ZoneSyncHandler extends DnsConversions with Monitored with TransactionProvider {
|
||||
|
||||
@ -78,6 +79,7 @@ object ZoneSyncHandler extends DnsConversions with Monitored with TransactionPro
|
||||
)
|
||||
)
|
||||
case Right(_) =>
|
||||
logger.info(s"Saving zone sync details for zone change with id: '${zoneChange.id}', zone name: '${zoneChange.zone.name}'")
|
||||
zoneChangeRepository.save(zoneChange)
|
||||
}
|
||||
|
||||
@ -169,9 +171,10 @@ object ZoneSyncHandler extends DnsConversions with Monitored with TransactionPro
|
||||
}
|
||||
}.attempt.map {
|
||||
case Left(e: Throwable) =>
|
||||
val errorMessage = new StringWriter
|
||||
e.printStackTrace(new PrintWriter(errorMessage))
|
||||
logger.error(
|
||||
s"Encountered error syncing ; zoneName='${zoneChange.zone.name}'; zoneChange='${zoneChange.id}'",
|
||||
e
|
||||
s"Encountered error syncing ; zoneName='${zoneChange.zone.name}'; zoneChange='${zoneChange.id}'. Error: ${errorMessage.toString.replaceAll("\n",";").replaceAll("\t"," ")}"
|
||||
)
|
||||
// We want to just move back to an active status, do not update latest sync
|
||||
zoneChange.copy(
|
||||
|
@ -18,33 +18,25 @@ package vinyldns.api.notifier.email
|
||||
|
||||
import vinyldns.core.notifier.{Notification, Notifier}
|
||||
import cats.effect.IO
|
||||
import vinyldns.core.domain.batch.{
|
||||
BatchChange,
|
||||
BatchChangeApprovalStatus,
|
||||
SingleAddChange,
|
||||
SingleChange,
|
||||
SingleDeleteRRSetChange
|
||||
}
|
||||
import vinyldns.core.domain.membership.UserRepository
|
||||
import vinyldns.core.domain.membership.User
|
||||
import cats.implicits._
|
||||
import cats.effect.IO
|
||||
import vinyldns.core.domain.batch.{BatchChange, BatchChangeApprovalStatus, SingleAddChange, SingleChange, SingleDeleteRRSetChange}
|
||||
import vinyldns.core.domain.membership.{GroupRepository, User, UserRepository}
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
import javax.mail.internet.{InternetAddress, MimeMessage}
|
||||
import javax.mail.{Address, Message, Session}
|
||||
|
||||
import scala.util.Try
|
||||
import vinyldns.core.domain.record.AData
|
||||
import vinyldns.core.domain.record.AAAAData
|
||||
import vinyldns.core.domain.record.CNAMEData
|
||||
import vinyldns.core.domain.record.MXData
|
||||
import vinyldns.core.domain.record.TXTData
|
||||
import vinyldns.core.domain.record.PTRData
|
||||
import vinyldns.core.domain.record.RecordData
|
||||
import vinyldns.core.domain.record.{AAAAData, AData, CNAMEData, MXData, OwnerShipTransferStatus, PTRData, RecordData, RecordSetChange, TXTData}
|
||||
import vinyldns.core.domain.record.OwnerShipTransferStatus.OwnerShipTransferStatus
|
||||
|
||||
import java.time.format.{DateTimeFormatter, FormatStyle}
|
||||
import vinyldns.core.domain.batch.BatchChangeStatus._
|
||||
import vinyldns.core.domain.batch.BatchChangeApprovalStatus._
|
||||
|
||||
import java.time.ZoneId
|
||||
|
||||
class EmailNotifier(config: EmailNotifierConfig, session: Session, userRepository: UserRepository)
|
||||
class EmailNotifier(config: EmailNotifierConfig, session: Session, userRepository: UserRepository, groupRepository: GroupRepository)
|
||||
extends Notifier {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(classOf[EmailNotifier])
|
||||
@ -52,12 +44,15 @@ class EmailNotifier(config: EmailNotifierConfig, session: Session, userRepositor
|
||||
def notify(notification: Notification[_]): IO[Unit] =
|
||||
notification.change match {
|
||||
case bc: BatchChange => sendBatchChangeNotification(bc)
|
||||
case rsc: RecordSetChange => sendRecordSetOwnerTransferNotification(rsc)
|
||||
case _ => IO.unit
|
||||
}
|
||||
|
||||
def send(addresses: Address*)(buildMessage: Message => Message): IO[Unit] = IO {
|
||||
|
||||
def send(toAddresses: Address*)(ccAddresses: Address*)(buildMessage: Message => Message): IO[Unit] = IO {
|
||||
val message = new MimeMessage(session)
|
||||
message.setRecipients(Message.RecipientType.TO, addresses.toArray)
|
||||
message.setRecipients(Message.RecipientType.TO, toAddresses.toArray)
|
||||
message.setRecipients(Message.RecipientType.CC, ccAddresses.toArray)
|
||||
message.setFrom(config.from)
|
||||
buildMessage(message)
|
||||
message.saveChanges()
|
||||
@ -67,10 +62,10 @@ class EmailNotifier(config: EmailNotifierConfig, session: Session, userRepositor
|
||||
transport.close()
|
||||
}
|
||||
|
||||
def sendBatchChangeNotification(bc: BatchChange): IO[Unit] =
|
||||
def sendBatchChangeNotification(bc: BatchChange): IO[Unit] = {
|
||||
userRepository.getUser(bc.userId).flatMap {
|
||||
case Some(UserWithEmail(email)) =>
|
||||
send(email) { message =>
|
||||
case Some(UserWithEmail(email)) =>
|
||||
send(email)() { message =>
|
||||
message.setSubject(s"VinylDNS Batch change ${bc.id} results")
|
||||
message.setContent(formatBatchChange(bc), "text/html")
|
||||
message
|
||||
@ -81,9 +76,58 @@ class EmailNotifier(config: EmailNotifierConfig, session: Session, userRepositor
|
||||
s"Unable to properly parse email for ${user.id}: ${user.email.getOrElse("<none>")}"
|
||||
)
|
||||
}
|
||||
case None => IO { logger.warn(s"Unable to find user: ${bc.userId}") }
|
||||
case None => IO {
|
||||
logger.warn(s"Unable to find user: ${bc.userId}")
|
||||
}
|
||||
case _ => IO.unit
|
||||
}
|
||||
}
|
||||
|
||||
def sendRecordSetOwnerTransferNotification(rsc: RecordSetChange): IO[Unit] = {
|
||||
for {
|
||||
toGroup <- groupRepository.getGroup(rsc.recordSet.ownerGroupId.getOrElse("<none>"))
|
||||
ccGroup <- groupRepository.getGroup(rsc.recordSet.recordSetGroupChange.map(_.requestedOwnerGroupId.getOrElse("<none>")).getOrElse("<none>"))
|
||||
_ <- toGroup match {
|
||||
case Some(group) =>
|
||||
group.memberIds.toList.traverse { id =>
|
||||
userRepository.getUser(id).flatMap {
|
||||
case Some(UserWithEmail(toEmail)) =>
|
||||
ccGroup match {
|
||||
case Some(ccg) =>
|
||||
ccg.memberIds.toList.traverse { id =>
|
||||
userRepository.getUser(id).flatMap {
|
||||
case Some(ccUser) =>
|
||||
val ccEmail = ccUser.email.getOrElse("<none>")
|
||||
send(toEmail)(new InternetAddress(ccEmail)) { message =>
|
||||
message.setSubject(s"VinylDNS RecordSet change ${rsc.id} results")
|
||||
message.setContent(formatRecordSetChange(rsc), "text/html")
|
||||
message
|
||||
}
|
||||
case None =>
|
||||
IO.unit
|
||||
}
|
||||
}
|
||||
case None => IO.unit
|
||||
}
|
||||
case Some(user: User) if user.email.isDefined =>
|
||||
IO {
|
||||
logger.warn(
|
||||
s"Unable to properly parse email for ${user.id}: ${user.email.getOrElse("<none>")}"
|
||||
)
|
||||
}
|
||||
case None =>
|
||||
IO {
|
||||
logger.warn(s"Unable to find user: ${rsc.userId}")
|
||||
}
|
||||
case _ =>
|
||||
IO.unit
|
||||
}
|
||||
}
|
||||
case None => IO.unit // Handle case where toGroup is None
|
||||
}
|
||||
} yield ()
|
||||
}
|
||||
|
||||
|
||||
def formatBatchChange(bc: BatchChange): String = {
|
||||
val sb = new StringBuilder
|
||||
@ -93,7 +137,7 @@ class EmailNotifier(config: EmailNotifierConfig, session: Session, userRepositor
|
||||
| ${bc.comments.map(comments => s"<b>Description:</b> $comments</br>").getOrElse("")}
|
||||
| <b>Created:</b> ${DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL).withZone(ZoneId.systemDefault()).format(bc.createdTimestamp)} <br/>
|
||||
| <b>Id:</b> ${bc.id}<br/>
|
||||
| <b>Status:</b> ${formatStatus(bc.approvalStatus, bc.status)}<br/>""".stripMargin)
|
||||
| <b>Status:</b> ${formatBatchStatus(bc.approvalStatus, bc.status)}<br/>""".stripMargin)
|
||||
|
||||
// For manually reviewed e-mails, add additional info; e-mails are not sent for pending batch changes
|
||||
if (bc.approvalStatus != AutoApproved) {
|
||||
@ -125,7 +169,8 @@ class EmailNotifier(config: EmailNotifierConfig, session: Session, userRepositor
|
||||
sb.toString
|
||||
}
|
||||
|
||||
def formatStatus(approval: BatchChangeApprovalStatus, status: BatchChangeStatus): String =
|
||||
|
||||
def formatBatchStatus(approval: BatchChangeApprovalStatus, status: BatchChangeStatus): String =
|
||||
(approval, status) match {
|
||||
case (ManuallyRejected, _) => "Rejected"
|
||||
case (BatchChangeApprovalStatus.PendingReview, _) => "Pending Review"
|
||||
@ -133,6 +178,28 @@ class EmailNotifier(config: EmailNotifierConfig, session: Session, userRepositor
|
||||
case (_, status) => status.toString
|
||||
}
|
||||
|
||||
def formatRecordSetChange(rsc: RecordSetChange): String = {
|
||||
|
||||
val sb = new StringBuilder
|
||||
sb.append(s"""<h1>RecordSet Ownership Transfer</h1>
|
||||
| <b>Submitter:</b> ${ userRepository.getUser(rsc.userId).map(_.get.userName)}
|
||||
| <b>Id:</b> ${rsc.id}<br/>
|
||||
| <b>Submitted time:</b> ${DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL).withZone(ZoneId.systemDefault()).format(rsc.created)} <br/>
|
||||
| <b>OwnerShip Current Group:</b> ${rsc.recordSet.ownerGroupId.getOrElse("none")} <br/>
|
||||
| <b>OwnerShip Transfer Group:</b> ${rsc.recordSet.recordSetGroupChange.map(_.requestedOwnerGroupId.getOrElse("none")).getOrElse("none")} <br/>
|
||||
| <b>OwnerShip Transfer Status:</b> ${formatOwnerShipStatus(rsc.recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).get)}<br/>
|
||||
""".stripMargin)
|
||||
sb.toString
|
||||
}
|
||||
|
||||
def formatOwnerShipStatus(status: OwnerShipTransferStatus): String =
|
||||
status match {
|
||||
case OwnerShipTransferStatus.ManuallyRejected => "Rejected"
|
||||
case OwnerShipTransferStatus.PendingReview => "Pending Review"
|
||||
case OwnerShipTransferStatus.ManuallyApproved => "Approved"
|
||||
case OwnerShipTransferStatus.Cancelled => "Cancelled"
|
||||
}
|
||||
|
||||
def formatSingleChange(sc: SingleChange, index: Int): String = sc match {
|
||||
case SingleAddChange(
|
||||
_,
|
||||
|
@ -17,7 +17,7 @@
|
||||
package vinyldns.api.notifier.email
|
||||
|
||||
import vinyldns.core.notifier.{Notifier, NotifierConfig, NotifierProvider}
|
||||
import vinyldns.core.domain.membership.UserRepository
|
||||
import vinyldns.core.domain.membership.{GroupRepository, UserRepository}
|
||||
import pureconfig._
|
||||
import pureconfig.generic.auto._
|
||||
import pureconfig.module.catseffect.syntax._
|
||||
@ -30,13 +30,13 @@ class EmailNotifierProvider extends NotifierProvider {
|
||||
private implicit val cs: ContextShift[IO] =
|
||||
IO.contextShift(scala.concurrent.ExecutionContext.global)
|
||||
|
||||
def load(config: NotifierConfig, userRepository: UserRepository): IO[Notifier] =
|
||||
def load(config: NotifierConfig, userRepository: UserRepository, groupRepository: GroupRepository): IO[Notifier] =
|
||||
for {
|
||||
emailConfig <- Blocker[IO].use(
|
||||
ConfigSource.fromConfig(config.settings).loadF[IO, EmailNotifierConfig](_)
|
||||
)
|
||||
session <- createSession(emailConfig)
|
||||
} yield new EmailNotifier(emailConfig, session, userRepository)
|
||||
} yield new EmailNotifier(emailConfig, session, userRepository, groupRepository)
|
||||
|
||||
def createSession(config: EmailNotifierConfig): IO[Session] = IO {
|
||||
Session.getInstance(config.smtp)
|
||||
|
@ -25,6 +25,7 @@ import org.slf4j.LoggerFactory
|
||||
import vinyldns.api.route.VinylDNSJsonProtocol
|
||||
import vinyldns.core.domain.batch.BatchChange
|
||||
import vinyldns.core.notifier.{Notification, Notifier}
|
||||
import java.io.{PrintWriter, StringWriter}
|
||||
|
||||
class SnsNotifier(config: SnsNotifierConfig, sns: AmazonSNS)
|
||||
extends Notifier
|
||||
@ -52,6 +53,8 @@ class SnsNotifier(config: SnsNotifierConfig, sns: AmazonSNS)
|
||||
sns.publish(request)
|
||||
logger.info(s"Sending batch change success; batchChange='${bc.id}'")
|
||||
}.handleErrorWith { e =>
|
||||
IO(logger.error(s"Failed sending batch change; batchChange='${bc.id}'", e))
|
||||
val errorMessage = new StringWriter
|
||||
e.printStackTrace(new PrintWriter(errorMessage))
|
||||
IO(logger.error(s"Failed sending batch change; batchChange='${bc.id}'. Error: ${errorMessage.toString.replaceAll("\n",";").replaceAll("\t"," ")}"))
|
||||
}.void
|
||||
}
|
||||
|
@ -17,7 +17,7 @@
|
||||
package vinyldns.api.notifier.sns
|
||||
|
||||
import vinyldns.core.notifier.{Notifier, NotifierConfig, NotifierProvider}
|
||||
import vinyldns.core.domain.membership.UserRepository
|
||||
import vinyldns.core.domain.membership.{GroupRepository, UserRepository}
|
||||
import pureconfig._
|
||||
import pureconfig.generic.auto._
|
||||
import pureconfig.module.catseffect.syntax._
|
||||
@ -35,7 +35,7 @@ class SnsNotifierProvider extends NotifierProvider {
|
||||
IO.contextShift(scala.concurrent.ExecutionContext.global)
|
||||
private val logger = LoggerFactory.getLogger(classOf[SnsNotifierProvider])
|
||||
|
||||
def load(config: NotifierConfig, userRepository: UserRepository): IO[Notifier] =
|
||||
def load(config: NotifierConfig, userRepository: UserRepository, groupRepository: GroupRepository): IO[Notifier] =
|
||||
for {
|
||||
snsConfig <- Blocker[IO].use(
|
||||
ConfigSource.fromConfig(config.settings).loadF[IO, SnsNotifierConfig](_)
|
||||
|
@ -22,6 +22,7 @@ import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
import org.slf4j.{Logger, LoggerFactory}
|
||||
import scalikejdbc.DB
|
||||
import vinyldns.core.domain.Encrypted
|
||||
import vinyldns.core.domain.membership._
|
||||
import vinyldns.core.domain.zone._
|
||||
import vinyldns.mysql.TransactionProvider
|
||||
@ -39,7 +40,7 @@ object TestDataLoader extends TransactionProvider {
|
||||
id = "testuser",
|
||||
created = Instant.now.truncatedTo(ChronoUnit.SECONDS),
|
||||
accessKey = "testUserAccessKey",
|
||||
secretKey = "testUserSecretKey",
|
||||
secretKey = Encrypted("testUserSecretKey"),
|
||||
firstName = Some("Test"),
|
||||
lastName = Some("User"),
|
||||
email = Some("test@test.com"),
|
||||
@ -50,7 +51,7 @@ object TestDataLoader extends TransactionProvider {
|
||||
id = "ok",
|
||||
created = Instant.now.truncatedTo(ChronoUnit.SECONDS),
|
||||
accessKey = "okAccessKey",
|
||||
secretKey = "okSecretKey",
|
||||
secretKey = Encrypted("okSecretKey"),
|
||||
firstName = Some("ok"),
|
||||
lastName = Some("ok"),
|
||||
email = Some("test@test.com"),
|
||||
@ -61,7 +62,7 @@ object TestDataLoader extends TransactionProvider {
|
||||
id = "dummy",
|
||||
created = Instant.now.truncatedTo(ChronoUnit.SECONDS),
|
||||
accessKey = "dummyAccessKey",
|
||||
secretKey = "dummySecretKey",
|
||||
secretKey = Encrypted("dummySecretKey"),
|
||||
isTest = true
|
||||
)
|
||||
final val sharedZoneUser = User(
|
||||
@ -69,7 +70,7 @@ object TestDataLoader extends TransactionProvider {
|
||||
id = "sharedZoneUser",
|
||||
created = Instant.now.truncatedTo(ChronoUnit.SECONDS),
|
||||
accessKey = "sharedZoneUserAccessKey",
|
||||
secretKey = "sharedZoneUserSecretKey",
|
||||
secretKey = Encrypted("sharedZoneUserSecretKey"),
|
||||
firstName = Some("sharedZoneUser"),
|
||||
lastName = Some("sharedZoneUser"),
|
||||
email = Some("test@test.com"),
|
||||
@ -80,7 +81,7 @@ object TestDataLoader extends TransactionProvider {
|
||||
id = "locked",
|
||||
created = Instant.now.truncatedTo(ChronoUnit.SECONDS),
|
||||
accessKey = "lockedAccessKey",
|
||||
secretKey = "lockedSecretKey",
|
||||
secretKey = Encrypted("lockedSecretKey"),
|
||||
firstName = Some("Locked"),
|
||||
lastName = Some("User"),
|
||||
email = Some("testlocked@test.com"),
|
||||
@ -93,7 +94,7 @@ object TestDataLoader extends TransactionProvider {
|
||||
id = "dummy%03d".format(runner),
|
||||
created = Instant.now.truncatedTo(ChronoUnit.SECONDS),
|
||||
accessKey = "dummy",
|
||||
secretKey = "dummy",
|
||||
secretKey = Encrypted("dummy"),
|
||||
isTest = true
|
||||
)
|
||||
}
|
||||
@ -102,7 +103,7 @@ object TestDataLoader extends TransactionProvider {
|
||||
id = "list-group-user",
|
||||
created = Instant.now.truncatedTo(ChronoUnit.SECONDS),
|
||||
accessKey = "listGroupAccessKey",
|
||||
secretKey = "listGroupSecretKey",
|
||||
secretKey = Encrypted("listGroupSecretKey"),
|
||||
firstName = Some("list-group"),
|
||||
lastName = Some("list-group"),
|
||||
email = Some("test@test.com"),
|
||||
@ -114,7 +115,7 @@ object TestDataLoader extends TransactionProvider {
|
||||
id = "list-zones-user",
|
||||
created = Instant.now.truncatedTo(ChronoUnit.SECONDS),
|
||||
accessKey = "listZonesAccessKey",
|
||||
secretKey = "listZonesSecretKey",
|
||||
secretKey = Encrypted("listZonesSecretKey"),
|
||||
firstName = Some("list-zones"),
|
||||
lastName = Some("list-zones"),
|
||||
email = Some("test@test.com"),
|
||||
@ -126,7 +127,7 @@ object TestDataLoader extends TransactionProvider {
|
||||
id = "history-id",
|
||||
created = Instant.now.truncatedTo(ChronoUnit.SECONDS),
|
||||
accessKey = "history-key",
|
||||
secretKey = "history-secret",
|
||||
secretKey = Encrypted("history-secret"),
|
||||
firstName = Some("history-first"),
|
||||
lastName = Some("history-last"),
|
||||
email = Some("history@history.com"),
|
||||
@ -138,7 +139,7 @@ object TestDataLoader extends TransactionProvider {
|
||||
id = "list-records-user",
|
||||
created = Instant.now.truncatedTo(ChronoUnit.SECONDS),
|
||||
accessKey = "listRecordsAccessKey",
|
||||
secretKey = "listRecordsSecretKey",
|
||||
secretKey = Encrypted("listRecordsSecretKey"),
|
||||
firstName = Some("list-records"),
|
||||
lastName = Some("list-records"),
|
||||
email = Some("test@test.com"),
|
||||
@ -150,7 +151,7 @@ object TestDataLoader extends TransactionProvider {
|
||||
id = "list-batch-summaries-id",
|
||||
created = Instant.now.truncatedTo(ChronoUnit.SECONDS),
|
||||
accessKey = "listBatchSummariesAccessKey",
|
||||
secretKey = "listBatchSummariesSecretKey",
|
||||
secretKey = Encrypted("listBatchSummariesSecretKey"),
|
||||
firstName = Some("list-batch-summaries"),
|
||||
lastName = Some("list-batch-summaries"),
|
||||
email = Some("test@test.com"),
|
||||
@ -170,7 +171,7 @@ object TestDataLoader extends TransactionProvider {
|
||||
id = "list-zero-summaries-id",
|
||||
created = Instant.now.truncatedTo(ChronoUnit.SECONDS),
|
||||
accessKey = "listZeroSummariesAccessKey",
|
||||
secretKey = "listZeroSummariesSecretKey",
|
||||
secretKey = Encrypted("listZeroSummariesSecretKey"),
|
||||
firstName = Some("list-zero-summaries"),
|
||||
lastName = Some("list-zero-summaries"),
|
||||
email = Some("test@test.com"),
|
||||
@ -182,7 +183,7 @@ object TestDataLoader extends TransactionProvider {
|
||||
id = "support-user-id",
|
||||
created = Instant.now.truncatedTo(ChronoUnit.SECONDS),
|
||||
accessKey = "supportUserAccessKey",
|
||||
secretKey = "supportUserSecretKey",
|
||||
secretKey = Encrypted("supportUserSecretKey"),
|
||||
firstName = Some("support-user"),
|
||||
lastName = Some("support-user"),
|
||||
email = Some("test@test.com"),
|
||||
@ -190,6 +191,19 @@ object TestDataLoader extends TransactionProvider {
|
||||
isTest = true
|
||||
)
|
||||
|
||||
final val superUser = User(
|
||||
userName = "super-user",
|
||||
id = "super-user-id",
|
||||
created = Instant.now.truncatedTo(ChronoUnit.SECONDS),
|
||||
accessKey = "superUserAccessKey",
|
||||
secretKey = Encrypted("superUserSecretKey"),
|
||||
firstName = Some("super-user"),
|
||||
lastName = Some("super-user"),
|
||||
email = Some("test@test.com"),
|
||||
isSuper = true,
|
||||
isTest = true
|
||||
)
|
||||
|
||||
final val sharedZoneGroup = Group(
|
||||
name = "testSharedZoneGroup",
|
||||
id = "shared-zone-group",
|
||||
@ -276,7 +290,7 @@ object TestDataLoader extends TransactionProvider {
|
||||
for {
|
||||
_ <- (testUser :: okUser :: dummyUser :: sharedZoneUser :: lockedUser :: listGroupUser :: listZonesUser ::
|
||||
listBatchChangeSummariesUser :: listZeroBatchChangeSummariesUser :: zoneHistoryUser :: supportUser ::
|
||||
listRecordsUser :: listOfDummyUsers).map { user =>
|
||||
superUser :: listRecordsUser :: listOfDummyUsers).map { user =>
|
||||
userRepo.save(user)
|
||||
}.parSequence
|
||||
// if the test shared zones exist already, clean them out
|
||||
|
@ -88,6 +88,7 @@ trait BatchChangeJsonProtocol extends JsonValidation {
|
||||
(
|
||||
(js \ "inputName").required[String]("Missing BatchChangeInput.changes.inputName"),
|
||||
recordType,
|
||||
(js \ "systemMessage").optional[String],
|
||||
(js \ "ttl").optional[Long],
|
||||
recordType.andThen(extractRecord(_, js \ "record"))
|
||||
).mapN(AddChangeInput.apply)
|
||||
@ -114,6 +115,7 @@ trait BatchChangeJsonProtocol extends JsonValidation {
|
||||
(
|
||||
(js \ "inputName").required[String]("Missing BatchChangeInput.changes.inputName"),
|
||||
recordType,
|
||||
(js \ "systemMessage").optional[String],
|
||||
recordData
|
||||
).mapN(DeleteRRSetChangeInput.apply)
|
||||
}
|
||||
@ -249,8 +251,23 @@ trait BatchChangeJsonProtocol extends JsonValidation {
|
||||
js.required[MXData](
|
||||
"Missing BatchChangeInput.changes.record.preference and BatchChangeInput.changes.record.exchange"
|
||||
)
|
||||
case NS => js.required[NSData]("Missing BatchChangeInput.changes.record.nsdname")
|
||||
case SRV => js.required[SRVData](
|
||||
"Missing BatchChangeInput.changes.record.priority and " +
|
||||
"Missing BatchChangeInput.changes.record.weight and " +
|
||||
"Missing BatchChangeInput.changes.record.port and " +
|
||||
"Missing BatchChangeInput.changes.record.target"
|
||||
)
|
||||
case NAPTR => js.required[NAPTRData](
|
||||
"Missing BatchChangeInput.changes.record.order and " +
|
||||
"Missing BatchChangeInput.changes.record.preference and " +
|
||||
"Missing BatchChangeInput.changes.record.flags and " +
|
||||
"Missing BatchChangeInput.changes.record.service and " +
|
||||
"Missing BatchChangeInput.changes.record.regexp and " +
|
||||
"Missing BatchChangeInput.changes.record.replacement"
|
||||
)
|
||||
case _ =>
|
||||
s"Unsupported type $typ, valid types include: A, AAAA, CNAME, PTR, TXT, and MX".invalidNel
|
||||
s"Unsupported type $typ, valid types include: A, AAAA, CNAME, PTR, TXT, MX, NS, SRV and NAPTR".invalidNel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,13 +16,12 @@
|
||||
|
||||
package vinyldns.api.route
|
||||
|
||||
import akka.http.scaladsl.model.StatusCodes
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.server.{RejectionHandler, Route, ValidationRejection}
|
||||
import vinyldns.api.config.LimitsConfig
|
||||
import org.slf4j.{Logger, LoggerFactory}
|
||||
import vinyldns.api.config.ManualReviewConfig
|
||||
import vinyldns.core.domain.batch._
|
||||
import vinyldns.api.config.{LimitsConfig, ManualReviewConfig}
|
||||
import vinyldns.api.domain.batch._
|
||||
import vinyldns.core.domain.batch._
|
||||
|
||||
class BatchChangeRoute(
|
||||
batchChangeService: BatchChangeServiceAlgebra,
|
||||
@ -71,22 +70,28 @@ class BatchChangeRoute(
|
||||
}
|
||||
}
|
||||
} ~
|
||||
(get & monitor("Endpoint.listBatchChangeSummaries")) {
|
||||
parameters(
|
||||
"startFrom".as[Int].?,
|
||||
"maxItems".as[Int].?(MAX_ITEMS_LIMIT),
|
||||
"ignoreAccess".as[Boolean].?(false),
|
||||
"approvalStatus".as[String].?
|
||||
) {
|
||||
(
|
||||
startFrom: Option[Int],
|
||||
maxItems: Int,
|
||||
ignoreAccess: Boolean,
|
||||
approvalStatus: Option[String]
|
||||
) =>
|
||||
{
|
||||
val convertApprovalStatus = approvalStatus.flatMap(BatchChangeApprovalStatus.find)
|
||||
|
||||
(get & monitor("Endpoint.listBatchChangeSummaries")) {
|
||||
parameters(
|
||||
"userName".as[String].?,
|
||||
"dateTimeRangeStart".as[String].?,
|
||||
"dateTimeRangeEnd".as[String].?,
|
||||
"startFrom".as[Int].?,
|
||||
"maxItems".as[Int].?(MAX_ITEMS_LIMIT),
|
||||
"ignoreAccess".as[Boolean].?(false),
|
||||
"approvalStatus".as[String].?
|
||||
) {
|
||||
(
|
||||
userName: Option[String],
|
||||
dateTimeRangeStart: Option[String],
|
||||
dateTimeRangeEnd: Option[String],
|
||||
startFrom: Option[Int],
|
||||
maxItems: Int,
|
||||
ignoreAccess: Boolean,
|
||||
approvalStatus: Option[String]
|
||||
) =>
|
||||
{
|
||||
val convertApprovalStatus = approvalStatus.flatMap(BatchChangeApprovalStatus.find)
|
||||
|
||||
handleRejections(invalidQueryHandler) {
|
||||
validate(
|
||||
0 < maxItems && maxItems <= MAX_ITEMS_LIMIT,
|
||||
@ -95,9 +100,14 @@ class BatchChangeRoute(
|
||||
authenticateAndExecute(
|
||||
batchChangeService.listBatchChangeSummaries(
|
||||
_,
|
||||
userName,
|
||||
dateTimeRangeStart,
|
||||
dateTimeRangeEnd,
|
||||
startFrom,
|
||||
maxItems,
|
||||
ignoreAccess,
|
||||
// TODO: Update batch status from None to its actual value when the feature is ready for release
|
||||
None,
|
||||
convertApprovalStatus
|
||||
)
|
||||
) { summaries =>
|
||||
@ -106,9 +116,9 @@ class BatchChangeRoute(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} ~
|
||||
} ~
|
||||
path("zones" / "batchrecordchanges" / Segment) { id =>
|
||||
(get & monitor("Endpoint.getBatchChange")) {
|
||||
authenticateAndExecute(batchChangeService.getBatchChange(id, _)) { chg =>
|
||||
|
@ -27,10 +27,12 @@ import scodec.bits.{Bases, ByteVector}
|
||||
import vinyldns.api.domain.zone.{RecordSetGlobalInfo, RecordSetInfo, RecordSetListInfo}
|
||||
import vinyldns.core.domain.DomainHelpers.ensureTrailingDot
|
||||
import vinyldns.core.domain.DomainHelpers.removeWhitespace
|
||||
import vinyldns.core.domain.Fqdn
|
||||
import vinyldns.core.domain.{EncryptFromJson, Encrypted, Fqdn}
|
||||
import vinyldns.core.domain.record._
|
||||
import vinyldns.core.domain.zone._
|
||||
import vinyldns.core.Messages._
|
||||
import vinyldns.core.domain.record.OwnerShipTransferStatus
|
||||
import vinyldns.core.domain.record.OwnerShipTransferStatus.OwnerShipTransferStatus
|
||||
|
||||
trait DnsJsonProtocol extends JsonValidation {
|
||||
import vinyldns.core.domain.record.RecordType._
|
||||
@ -40,12 +42,15 @@ trait DnsJsonProtocol extends JsonValidation {
|
||||
UpdateZoneInputSerializer,
|
||||
ZoneConnectionSerializer,
|
||||
AlgorithmSerializer,
|
||||
EncryptedSerializer,
|
||||
RecordSetSerializer,
|
||||
ownerShipTransferSerializer,
|
||||
RecordSetListInfoSerializer,
|
||||
RecordSetGlobalInfoSerializer,
|
||||
RecordSetInfoSerializer,
|
||||
RecordSetChangeSerializer,
|
||||
JsonEnumV(ZoneStatus),
|
||||
JsonEnumV(OwnerShipTransferStatus),
|
||||
JsonEnumV(ZoneChangeStatus),
|
||||
JsonEnumV(RecordSetStatus),
|
||||
JsonEnumV(RecordSetChangeStatus),
|
||||
@ -53,6 +58,7 @@ trait DnsJsonProtocol extends JsonValidation {
|
||||
JsonEnumV(ZoneChangeType),
|
||||
JsonEnumV(RecordSetChangeType),
|
||||
JsonEnumV(NameSort),
|
||||
JsonEnumV(RecordTypeSort),
|
||||
ASerializer,
|
||||
AAAASerializer,
|
||||
CNAMESerializer,
|
||||
@ -84,6 +90,18 @@ trait DnsJsonProtocol extends JsonValidation {
|
||||
(js \ "id").default[String](UUID.randomUUID.toString),
|
||||
(js \ "singleBatchChangeIds").default[List[String]](List())
|
||||
).mapN(RecordSetChange.apply)
|
||||
|
||||
override def toJson(rs: RecordSetChange): JValue =
|
||||
("zone" -> Extraction.decompose(rs.zone)) ~
|
||||
("recordSet" -> Extraction.decompose(rs.recordSet)) ~
|
||||
("userId" -> rs.userId) ~
|
||||
("changeType" -> Extraction.decompose(rs.changeType)) ~
|
||||
("status" -> Extraction.decompose(rs.status)) ~
|
||||
("created" -> Extraction.decompose(rs.created)) ~
|
||||
("systemMessage" -> rs.systemMessage) ~
|
||||
("updates" -> Extraction.decompose(rs.updates)) ~
|
||||
("id" -> rs.id) ~
|
||||
("singleBatchChangeIds" -> Extraction.decompose(rs.singleBatchChangeIds))
|
||||
}
|
||||
|
||||
case object CreateZoneInputSerializer extends ValidationSerializer[CreateZoneInput] {
|
||||
@ -99,7 +117,9 @@ trait DnsJsonProtocol extends JsonValidation {
|
||||
(js \ "shared").default[Boolean](false),
|
||||
(js \ "acl").default[ZoneACL](ZoneACL()),
|
||||
(js \ "adminGroupId").required[String]("Missing Zone.adminGroupId"),
|
||||
(js \ "backendId").optional[String]
|
||||
(js \ "backendId").optional[String],
|
||||
(js \ "recurrenceSchedule").optional[String],
|
||||
(js \ "scheduleRequestor").optional[String],
|
||||
).mapN(CreateZoneInput.apply)
|
||||
}
|
||||
|
||||
@ -116,7 +136,9 @@ trait DnsJsonProtocol extends JsonValidation {
|
||||
(js \ "shared").default[Boolean](false),
|
||||
(js \ "acl").default[ZoneACL](ZoneACL()),
|
||||
(js \ "adminGroupId").required[String]("Missing Zone.adminGroupId"),
|
||||
(js \ "backendId").optional[String]
|
||||
(js \ "recurrenceSchedule").optional[String],
|
||||
(js \ "scheduleRequestor").optional[String],
|
||||
(js \ "backendId").optional[String],
|
||||
).mapN(UpdateZoneInput.apply)
|
||||
}
|
||||
|
||||
@ -130,18 +152,30 @@ trait DnsJsonProtocol extends JsonValidation {
|
||||
override def toJson(a: Algorithm): JValue = JString(a.name)
|
||||
}
|
||||
|
||||
case object EncryptedSerializer extends ValidationSerializer[Encrypted] {
|
||||
override def fromJson(js: JValue): ValidatedNel[String, Encrypted] =
|
||||
js match {
|
||||
case JString(value) => EncryptFromJson.fromString(value).toValidatedNel
|
||||
case _ => "Unsupported type for zone connection key, must be a string".invalidNel
|
||||
}
|
||||
|
||||
override def toJson(a: Encrypted): JValue = JString(a.value)
|
||||
}
|
||||
|
||||
case object ZoneConnectionSerializer extends ValidationSerializer[ZoneConnection] {
|
||||
override def fromJson(js: JValue): ValidatedNel[String, ZoneConnection] =
|
||||
(
|
||||
(js \ "name").required[String]("Missing ZoneConnection.name"),
|
||||
(js \ "keyName").required[String]("Missing ZoneConnection.keyName"),
|
||||
(js \ "key").required[String]("Missing ZoneConnection.key"),
|
||||
(js \ "key").required[Encrypted]("Missing ZoneConnection.key"),
|
||||
(js \ "primaryServer").required[String]("Missing ZoneConnection.primaryServer"),
|
||||
(js \ "algorithm").default[Algorithm](Algorithm.HMAC_MD5)
|
||||
).mapN(ZoneConnection.apply)
|
||||
}
|
||||
|
||||
def checkDomainNameLen(s: String): Boolean = s.length <= 255
|
||||
def validateNaptrFlag(flag: String): Boolean = flag == "U" || flag == "S" || flag == "A" || flag == "P"
|
||||
def validateNaptrRegexp(regexp: String): Boolean = regexp.startsWith("!") && regexp.endsWith("!") || regexp == ""
|
||||
def nameContainsDots(s: String): Boolean = s.contains(".")
|
||||
def nameDoesNotContainSpaces(s: String): Boolean = !s.contains(" ")
|
||||
|
||||
@ -202,6 +236,7 @@ trait DnsJsonProtocol extends JsonValidation {
|
||||
(js \ "id").default[String](UUID.randomUUID().toString),
|
||||
(js \ "account").default[String]("system"),
|
||||
(js \ "ownerGroupId").optional[String],
|
||||
(js \ "recordSetGroupChange").optional[OwnerShipTransfer],
|
||||
(js \ "fqdn").optional[String]
|
||||
).mapN(RecordSet.apply)
|
||||
|
||||
@ -226,9 +261,23 @@ trait DnsJsonProtocol extends JsonValidation {
|
||||
("id" -> rs.id) ~
|
||||
("account" -> rs.account) ~
|
||||
("ownerGroupId" -> rs.ownerGroupId) ~
|
||||
("recordSetGroupChange" -> Extraction.decompose(rs.recordSetGroupChange)) ~
|
||||
("fqdn" -> rs.fqdn)
|
||||
}
|
||||
|
||||
|
||||
case object ownerShipTransferSerializer extends ValidationSerializer[OwnerShipTransfer] {
|
||||
override def fromJson(js: JValue): ValidatedNel[String, OwnerShipTransfer] =
|
||||
(
|
||||
(js \ "ownerShipTransferStatus").required[OwnerShipTransferStatus]("Missing ownerShipTransfer.ownerShipTransferStatus"),
|
||||
(js \ "requestedOwnerGroupId").optional[String],
|
||||
).mapN(OwnerShipTransfer.apply)
|
||||
|
||||
override def toJson(rsa: OwnerShipTransfer): JValue =
|
||||
("ownerShipTransferStatus" -> Extraction.decompose(rsa.ownerShipTransferStatus)) ~
|
||||
("requestedOwnerGroupId" -> Extraction.decompose(rsa.requestedOwnerGroupId))
|
||||
}
|
||||
|
||||
case object RecordSetListInfoSerializer extends ValidationSerializer[RecordSetListInfo] {
|
||||
override def fromJson(js: JValue): ValidatedNel[String, RecordSetListInfo] =
|
||||
(
|
||||
@ -250,6 +299,7 @@ trait DnsJsonProtocol extends JsonValidation {
|
||||
("accessLevel" -> rs.accessLevel.toString) ~
|
||||
("ownerGroupId" -> rs.ownerGroupId) ~
|
||||
("ownerGroupName" -> rs.ownerGroupName) ~
|
||||
("recordSetGroupChange" -> Extraction.decompose(rs.recordSetGroupChange)) ~
|
||||
("fqdn" -> rs.fqdn)
|
||||
}
|
||||
|
||||
@ -271,6 +321,7 @@ trait DnsJsonProtocol extends JsonValidation {
|
||||
("account" -> rs.account) ~
|
||||
("ownerGroupId" -> rs.ownerGroupId) ~
|
||||
("ownerGroupName" -> rs.ownerGroupName) ~
|
||||
("recordSetGroupChange" -> Extraction.decompose(rs.recordSetGroupChange)) ~
|
||||
("fqdn" -> rs.fqdn)
|
||||
}
|
||||
|
||||
@ -296,6 +347,7 @@ trait DnsJsonProtocol extends JsonValidation {
|
||||
("account" -> rs.account) ~
|
||||
("ownerGroupId" -> rs.ownerGroupId) ~
|
||||
("ownerGroupName" -> rs.ownerGroupName) ~
|
||||
("recordSetGroupChange" -> Extraction.decompose(rs.recordSetGroupChange)) ~
|
||||
("fqdn" -> rs.fqdn) ~
|
||||
("zoneName" -> rs.zoneName) ~
|
||||
("zoneShared" -> rs.zoneShared)
|
||||
@ -487,7 +539,7 @@ trait DnsJsonProtocol extends JsonValidation {
|
||||
(js \ "flags")
|
||||
.required[String]("Missing NAPTR.flags")
|
||||
.check(
|
||||
"NAPTR.flags must be less than 2 characters" -> (_.length < 2)
|
||||
"Invalid NAPTR.flag. Valid NAPTR flag value must be U, S, A or P" -> validateNaptrFlag
|
||||
),
|
||||
(js \ "service")
|
||||
.required[String]("Missing NAPTR.service")
|
||||
@ -497,7 +549,7 @@ trait DnsJsonProtocol extends JsonValidation {
|
||||
(js \ "regexp")
|
||||
.required[String]("Missing NAPTR.regexp")
|
||||
.check(
|
||||
"NAPTR.regexp must be less than 255 characters" -> checkDomainNameLen
|
||||
"Invalid NAPTR.regexp. Valid NAPTR regexp value must start and end with '!' or can be empty" -> validateNaptrRegexp
|
||||
),
|
||||
|
||||
(js \ "replacement")
|
||||
|
@ -49,6 +49,7 @@ class MembershipRoute(
|
||||
case InvalidGroupError(msg) => complete(StatusCodes.BadRequest, msg)
|
||||
case UserNotFoundError(msg) => complete(StatusCodes.NotFound, msg)
|
||||
case InvalidGroupRequestError(msg) => complete(StatusCodes.BadRequest, msg)
|
||||
case EmailValidationError(msg) => complete(StatusCodes.BadRequest, msg)
|
||||
}
|
||||
|
||||
val membershipRoute: Route = path("groups" / Segment) { groupId =>
|
||||
@ -161,8 +162,8 @@ class MembershipRoute(
|
||||
} ~
|
||||
path("groups" / Segment / "activity") { groupId =>
|
||||
(get & monitor("Endpoint.groupActivity")) {
|
||||
parameters("startFrom".?, "maxItems".as[Int].?(DEFAULT_MAX_ITEMS)) {
|
||||
(startFrom: Option[String], maxItems: Int) =>
|
||||
parameters("startFrom".as[Int].?, "maxItems".as[Int].?(DEFAULT_MAX_ITEMS)) {
|
||||
(startFrom: Option[Int], maxItems: Int) =>
|
||||
handleRejections(invalidQueryHandler) {
|
||||
validate(
|
||||
0 < maxItems && maxItems <= MAX_ITEMS_LIMIT,
|
||||
@ -186,6 +187,13 @@ class MembershipRoute(
|
||||
}
|
||||
}
|
||||
} ~
|
||||
path("groups" / "valid" / "domains") {
|
||||
(get & monitor("Endpoint.validdomains")) {
|
||||
authenticateAndExecute(membershipService.listEmailDomains) { emailDomains =>
|
||||
complete(StatusCodes.OK, emailDomains)
|
||||
}
|
||||
}
|
||||
} ~
|
||||
path("users" / Segment / "lock") { id =>
|
||||
(put & monitor("Endpoint.lockUser")) {
|
||||
authenticateAndExecute(membershipService.updateUserLockStatus(id, LockStatus.Locked, _)) {
|
||||
@ -204,9 +212,9 @@ class MembershipRoute(
|
||||
} ~
|
||||
path("users" / Segment) { id =>
|
||||
(get & monitor("Endpoint.getUser")) {
|
||||
authenticateAndExecute(membershipService.getUser(id, _)) {
|
||||
authenticateAndExecute(membershipService.getUserDetails(id, _)) {
|
||||
user =>
|
||||
complete(StatusCodes.OK, UserResponseInfo(user))
|
||||
complete(StatusCodes.OK, user)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,8 @@ import vinyldns.api.config.LimitsConfig
|
||||
import vinyldns.api.domain.zone._
|
||||
import vinyldns.core.domain.record.NameSort.NameSort
|
||||
import vinyldns.core.domain.record.RecordType.RecordType
|
||||
import vinyldns.core.domain.record.{NameSort, RecordSet, RecordType}
|
||||
import vinyldns.core.domain.record.RecordTypeSort.RecordTypeSort
|
||||
import vinyldns.core.domain.record.{NameSort, RecordSet, RecordType, RecordTypeSort}
|
||||
import vinyldns.core.domain.zone.ZoneCommandResult
|
||||
|
||||
import scala.concurrent.duration._
|
||||
@ -52,7 +53,8 @@ case class ListRecordSetsByZoneResponse(
|
||||
recordNameFilter: Option[String] = None,
|
||||
recordTypeFilter: Option[Set[RecordType]] = None,
|
||||
recordOwnerGroupFilter: Option[String] = None,
|
||||
nameSort: NameSort
|
||||
nameSort: NameSort,
|
||||
recordTypeSort: RecordTypeSort
|
||||
)
|
||||
|
||||
class RecordSetRoute(
|
||||
@ -100,7 +102,8 @@ class RecordSetRoute(
|
||||
"recordNameFilter".?,
|
||||
"recordTypeFilter".?,
|
||||
"recordOwnerGroupFilter".?,
|
||||
"nameSort".as[String].?("ASC")
|
||||
"nameSort".as[String].?("ASC"),
|
||||
"recordTypeSort".as[String].?("None")
|
||||
) {
|
||||
(
|
||||
startFrom: Option[String],
|
||||
@ -108,7 +111,8 @@ class RecordSetRoute(
|
||||
recordNameFilter: Option[String],
|
||||
recordTypeFilter: Option[String],
|
||||
recordOwnerGroupFilter: Option[String],
|
||||
nameSort: String
|
||||
nameSort: String,
|
||||
recordTypeSort: String
|
||||
) =>
|
||||
val convertedRecordTypeFilter = convertRecordTypeFilter(recordTypeFilter)
|
||||
handleRejections(invalidQueryHandler) {
|
||||
@ -126,8 +130,9 @@ class RecordSetRoute(
|
||||
convertedRecordTypeFilter,
|
||||
recordOwnerGroupFilter,
|
||||
NameSort.find(nameSort),
|
||||
_
|
||||
)
|
||||
_,
|
||||
RecordTypeSort.find(recordTypeSort),
|
||||
)
|
||||
) { rsResponse =>
|
||||
complete(StatusCodes.OK, rsResponse)
|
||||
}
|
||||
@ -144,7 +149,8 @@ class RecordSetRoute(
|
||||
"recordNameFilter".as[String],
|
||||
"recordTypeFilter".?,
|
||||
"recordOwnerGroupFilter".?,
|
||||
"nameSort".as[String].?("ASC")
|
||||
"nameSort".as[String].?("ASC"),
|
||||
"recordTypeSort".as[String].?("NONE")
|
||||
) {
|
||||
(
|
||||
startFrom: Option[String],
|
||||
@ -152,7 +158,8 @@ class RecordSetRoute(
|
||||
recordNameFilter: String,
|
||||
recordTypeFilter: Option[String],
|
||||
recordOwnerGroupFilter: Option[String],
|
||||
nameSort: String
|
||||
nameSort: String,
|
||||
recordTypeSort: String
|
||||
) =>
|
||||
val convertedRecordTypeFilter = convertRecordTypeFilter(recordTypeFilter)
|
||||
handleRejections(invalidQueryHandler) {
|
||||
@ -169,7 +176,8 @@ class RecordSetRoute(
|
||||
convertedRecordTypeFilter,
|
||||
recordOwnerGroupFilter,
|
||||
NameSort.find(nameSort),
|
||||
_
|
||||
_,
|
||||
RecordTypeSort.find(recordTypeSort)
|
||||
)
|
||||
) { rsResponse =>
|
||||
complete(StatusCodes.OK, rsResponse)
|
||||
@ -179,6 +187,13 @@ class RecordSetRoute(
|
||||
}
|
||||
}
|
||||
} ~
|
||||
path("zones" / Segment / "recordsetcount") { zoneId =>
|
||||
(get & monitor("Endpoint.getRecordSetCount")) {
|
||||
authenticateAndExecute(recordSetService.getRecordSetCount(zoneId, _)) { count =>
|
||||
complete(StatusCodes.OK, count)
|
||||
}
|
||||
}
|
||||
} ~
|
||||
path("zones" / Segment / "recordsets" / Segment) { (zoneId, rsId) =>
|
||||
(get & monitor("Endpoint.getRecordSetByZone")) {
|
||||
authenticateAndExecute(recordSetService.getRecordSetByZone(rsId, zoneId, _)) { rs =>
|
||||
@ -215,8 +230,8 @@ class RecordSetRoute(
|
||||
} ~
|
||||
path("zones" / Segment / "recordsetchanges") { zoneId =>
|
||||
(get & monitor("Endpoint.listRecordSetChanges")) {
|
||||
parameters("startFrom".?, "maxItems".as[Int].?(DEFAULT_MAX_ITEMS)) {
|
||||
(startFrom: Option[String], maxItems: Int) =>
|
||||
parameters("startFrom".as[Int].?, "maxItems".as[Int].?(DEFAULT_MAX_ITEMS)) {
|
||||
(startFrom: Option[Int], maxItems: Int) =>
|
||||
handleRejections(invalidQueryHandler) {
|
||||
validate(
|
||||
check = 0 < maxItems && maxItems <= DEFAULT_MAX_ITEMS,
|
||||
@ -233,6 +248,52 @@ class RecordSetRoute(
|
||||
}
|
||||
}
|
||||
}
|
||||
} ~
|
||||
path("recordsetchange" / "history") {
|
||||
(get & monitor("Endpoint.listRecordSetChangeHistory")) {
|
||||
parameters("zoneId".as[String].?, "startFrom".as[Int].?, "maxItems".as[Int].?(DEFAULT_MAX_ITEMS), "fqdn".as[String].?, "recordType".as[String].?) {
|
||||
(zoneId: Option[String], startFrom: Option[Int], maxItems: Int, fqdn: Option[String], recordType: Option[String]) =>
|
||||
handleRejections(invalidQueryHandler) {
|
||||
val errorMessage = if(fqdn.isEmpty || recordType.isEmpty || zoneId.isEmpty) {
|
||||
"recordType, fqdn and zoneId cannot be empty"
|
||||
} else {
|
||||
s"maxItems was $maxItems, maxItems must be between 0 exclusive " +
|
||||
s"and $DEFAULT_MAX_ITEMS inclusive"
|
||||
}
|
||||
val isValid = (0 < maxItems && maxItems <= DEFAULT_MAX_ITEMS) && (fqdn.nonEmpty && recordType.nonEmpty && zoneId.nonEmpty)
|
||||
validate(
|
||||
check = isValid,
|
||||
errorMsg = errorMessage
|
||||
){
|
||||
authenticateAndExecute(
|
||||
recordSetService
|
||||
.listRecordSetChangeHistory(zoneId, startFrom, maxItems, fqdn, RecordType.find(recordType.get), _)
|
||||
) { changes =>
|
||||
complete(StatusCodes.OK, changes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} ~
|
||||
path("metrics" / "health" / "zones" / Segment / "recordsetchangesfailure") {zoneId =>
|
||||
(get & monitor("Endpoint.listFailedRecordSetChanges")) {
|
||||
parameters("startFrom".as[Int].?(0), "maxItems".as[Int].?(DEFAULT_MAX_ITEMS)) {
|
||||
(startFrom: Int, maxItems: Int) =>
|
||||
handleRejections(invalidQueryHandler) {
|
||||
validate(
|
||||
check = 0 < maxItems && maxItems <= DEFAULT_MAX_ITEMS,
|
||||
errorMsg = s"maxItems was $maxItems, maxItems must be between 0 exclusive " +
|
||||
s"and $DEFAULT_MAX_ITEMS inclusive"
|
||||
){
|
||||
authenticateAndExecute(recordSetService.listFailedRecordSetChanges(_, Some(zoneId), startFrom, maxItems)) {
|
||||
changes =>
|
||||
complete(StatusCodes.OK, changes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val invalidQueryHandler = RejectionHandler
|
||||
|
@ -21,6 +21,7 @@ import akka.http.scaladsl.server._
|
||||
import akka.util.Timeout
|
||||
import org.slf4j.{Logger, LoggerFactory}
|
||||
import vinyldns.api.config.LimitsConfig
|
||||
import vinyldns.api.domain.membership.EmailValidationError
|
||||
import vinyldns.api.domain.zone._
|
||||
import vinyldns.core.crypto.CryptoAlgebra
|
||||
import vinyldns.core.domain.zone._
|
||||
@ -28,6 +29,7 @@ import vinyldns.core.domain.zone._
|
||||
import scala.concurrent.duration._
|
||||
|
||||
case class GetZoneResponse(zone: ZoneInfo)
|
||||
case class GetZoneDetailsResponse(zone: ZoneDetails)
|
||||
case class ZoneRejected(zone: Zone, errors: List[String])
|
||||
|
||||
class ZoneRoute(
|
||||
@ -62,6 +64,7 @@ class ZoneRoute(
|
||||
case RecentSyncError(msg) => complete(StatusCodes.Forbidden, msg)
|
||||
case ZoneInactiveError(msg) => complete(StatusCodes.BadRequest, msg)
|
||||
case InvalidRequest(msg) => complete(StatusCodes.BadRequest, msg)
|
||||
case EmailValidationError(msg) => complete(StatusCodes.BadRequest, msg)
|
||||
}
|
||||
|
||||
val zoneRoute: Route = path("zones") {
|
||||
@ -79,14 +82,16 @@ class ZoneRoute(
|
||||
"startFrom".as[String].?,
|
||||
"maxItems".as[Int].?(DEFAULT_MAX_ITEMS),
|
||||
"searchByAdminGroup".as[Boolean].?(false),
|
||||
"ignoreAccess".as[Boolean].?(false)
|
||||
"ignoreAccess".as[Boolean].?(false),
|
||||
"includeReverse".as[Boolean].?(true)
|
||||
) {
|
||||
(
|
||||
nameFilter: Option[String],
|
||||
startFrom: Option[String],
|
||||
maxItems: Int,
|
||||
searchByAdminGroup: Boolean,
|
||||
ignoreAccess: Boolean
|
||||
ignoreAccess: Boolean,
|
||||
includeReverse: Boolean
|
||||
) =>
|
||||
{
|
||||
handleRejections(invalidQueryHandler) {
|
||||
@ -96,7 +101,7 @@ class ZoneRoute(
|
||||
) {
|
||||
authenticateAndExecute(
|
||||
zoneService
|
||||
.listZones(_, nameFilter, startFrom, maxItems, searchByAdminGroup, ignoreAccess)
|
||||
.listZones(_, nameFilter, startFrom, maxItems, searchByAdminGroup, ignoreAccess, includeReverse)
|
||||
) { result =>
|
||||
complete(StatusCodes.OK, result)
|
||||
}
|
||||
@ -106,6 +111,38 @@ class ZoneRoute(
|
||||
}
|
||||
}
|
||||
} ~
|
||||
path("zones" / "deleted" / "changes") {
|
||||
(get & monitor("Endpoint.listDeletedZones")) {
|
||||
parameters(
|
||||
"nameFilter".?,
|
||||
"startFrom".as[String].?,
|
||||
"maxItems".as[Int].?(DEFAULT_MAX_ITEMS),
|
||||
"ignoreAccess".as[Boolean].?(false)
|
||||
) {
|
||||
(
|
||||
nameFilter: Option[String],
|
||||
startFrom: Option[String],
|
||||
maxItems: Int,
|
||||
ignoreAccess: Boolean
|
||||
) =>
|
||||
{
|
||||
handleRejections(invalidQueryHandler) {
|
||||
validate(
|
||||
0 < maxItems && maxItems <= MAX_ITEMS_LIMIT,
|
||||
s"maxItems was $maxItems, maxItems must be between 0 and $MAX_ITEMS_LIMIT"
|
||||
) {
|
||||
authenticateAndExecute(
|
||||
zoneService
|
||||
.listDeletedZones(_, nameFilter, startFrom, maxItems, ignoreAccess)
|
||||
) { result =>
|
||||
complete(StatusCodes.OK, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} ~
|
||||
path("zones" / "backendids") {
|
||||
(get & monitor("Endpoint.getBackendIds")) {
|
||||
authenticateAndExecute(_ => zoneService.getBackendIds()) { ids =>
|
||||
@ -138,6 +175,13 @@ class ZoneRoute(
|
||||
}
|
||||
}
|
||||
} ~
|
||||
path("zones" / Segment / "details") { id =>
|
||||
(get & monitor("Endpoint.getCommonZoneDetails")) {
|
||||
authenticateAndExecute(zoneService.getCommonZoneDetails(id, _)) { zone =>
|
||||
complete(StatusCodes.OK, GetZoneDetailsResponse(zone))
|
||||
}
|
||||
}
|
||||
} ~
|
||||
path("zones" / Segment / "sync") { id =>
|
||||
(post & monitor("Endpoint.syncZone")) {
|
||||
authenticateAndExecute(zoneService.syncZone(id, _)) { chg =>
|
||||
@ -163,6 +207,24 @@ class ZoneRoute(
|
||||
}
|
||||
}
|
||||
} ~
|
||||
path("metrics" / "health" / "zonechangesfailure") {
|
||||
(get & monitor("Endpoint.listFailedZoneChanges")) {
|
||||
parameters("startFrom".as[Int].?(0), "maxItems".as[Int].?(DEFAULT_MAX_ITEMS)) {
|
||||
(startFrom: Int, maxItems: Int) =>
|
||||
handleRejections(invalidQueryHandler) {
|
||||
validate(
|
||||
0 < maxItems && maxItems <= DEFAULT_MAX_ITEMS,
|
||||
s"maxItems was $maxItems, maxItems must be between 0 exclusive and $DEFAULT_MAX_ITEMS inclusive"
|
||||
) {
|
||||
authenticateAndExecute(zoneService.listFailedZoneChanges(_, startFrom, maxItems)) {
|
||||
changes =>
|
||||
complete(StatusCodes.OK, changes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} ~
|
||||
path("zones" / Segment / "acl" / "rules") { id =>
|
||||
(put & monitor("Endpoint.addZoneACLRule")) {
|
||||
authenticateAndExecuteWithEntity[ZoneCommandResult, ACLRuleInfo](
|
||||
|
@ -2,11 +2,11 @@ pyhamcrest==2.0.2
|
||||
pytz>=2014
|
||||
pytest==6.2.5
|
||||
mock==4.0.3
|
||||
dnspython==2.1.0
|
||||
dnspython==2.6.1
|
||||
boto3==1.18.51
|
||||
botocore==1.21.51
|
||||
requests==2.26.0
|
||||
requests==2.32.3
|
||||
pytest-xdist==2.4.0
|
||||
python-dateutil==2.8.2
|
||||
filelock==3.2.0
|
||||
pytest-custom_exit_code==0.3.0
|
||||
pytest-custom_exit_code==0.3.0
|
||||
|
@ -14,7 +14,7 @@ def validate_change_error_response_basics(input_json, change_type, input_name, r
|
||||
assert_that(input_json["changeType"], is_(change_type))
|
||||
assert_that(input_json["inputName"], is_(input_name))
|
||||
assert_that(input_json["type"], is_(record_type))
|
||||
assert_that(record_type, is_in(["A", "AAAA", "CNAME", "PTR", "TXT", "MX"]))
|
||||
assert_that(record_type, is_in(["A", "AAAA", "CNAME", "PTR", "TXT", "MX", "NS", "NAPTR", "SRV"]))
|
||||
if change_type == "Add":
|
||||
assert_that(input_json["ttl"], is_(ttl))
|
||||
if record_type in ["A", "AAAA"]:
|
||||
@ -28,6 +28,20 @@ def validate_change_error_response_basics(input_json, change_type, input_name, r
|
||||
elif record_type == "MX":
|
||||
assert_that(input_json["record"]["preference"], is_(record_data["preference"]))
|
||||
assert_that(input_json["record"]["exchange"], is_(record_data["exchange"]))
|
||||
elif record_type == "NS" and change_type == "Add":
|
||||
assert_that(input_json["record"]["nsdname"], is_(record_data))
|
||||
elif record_type == "NAPTR" and change_type == "Add":
|
||||
assert_that(input_json["record"]["order"], is_(record_data["order"]))
|
||||
assert_that(input_json["record"]["preference"], is_(record_data["preference"]))
|
||||
assert_that(input_json["record"]["flags"], is_(record_data["flags"]))
|
||||
assert_that(input_json["record"]["service"], is_(record_data["service"]))
|
||||
assert_that(input_json["record"]["regexp"], is_(record_data["regexp"]))
|
||||
assert_that(input_json["record"]["replacement"], is_(record_data["replacement"]))
|
||||
elif record_type == "SRV" and change_type == "Add":
|
||||
assert_that(input_json["record"]["priority"], is_(record_data["priority"]))
|
||||
assert_that(input_json["record"]["weight"], is_(record_data["weight"]))
|
||||
assert_that(input_json["record"]["port"], is_(record_data["port"]))
|
||||
assert_that(input_json["record"]["target"], is_(record_data["target"]))
|
||||
return
|
||||
|
||||
|
||||
@ -56,7 +70,7 @@ def assert_change_success(changes_json, zone, index, record_name, input_name, re
|
||||
assert_that(changes_json[index]["type"], is_(record_type))
|
||||
assert_that(changes_json[index]["id"], is_not(none()))
|
||||
assert_that(changes_json[index]["changeType"], is_(change_type))
|
||||
assert_that(record_type, is_in(["A", "AAAA", "CNAME", "PTR", "TXT", "MX"]))
|
||||
assert_that(record_type, is_in(["A", "AAAA", "CNAME", "PTR", "TXT", "MX", "NS", "NAPTR", "SRV"]))
|
||||
if record_type in ["A", "AAAA"] and change_type == "Add":
|
||||
assert_that(changes_json[index]["record"]["address"], is_(record_data))
|
||||
elif record_type == "CNAME" and change_type == "Add":
|
||||
@ -68,6 +82,20 @@ def assert_change_success(changes_json, zone, index, record_name, input_name, re
|
||||
elif record_type == "MX" and change_type == "Add":
|
||||
assert_that(changes_json[index]["record"]["preference"], is_(record_data["preference"]))
|
||||
assert_that(changes_json[index]["record"]["exchange"], is_(record_data["exchange"]))
|
||||
elif record_type == "NS" and change_type == "Add":
|
||||
assert_that(changes_json[index]["record"]["nsdname"], is_(record_data))
|
||||
elif record_type == "NAPTR" and change_type == "Add":
|
||||
assert_that(changes_json[index]["record"]["order"], is_(record_data["order"]))
|
||||
assert_that(changes_json[index]["record"]["preference"], is_(record_data["preference"]))
|
||||
assert_that(changes_json[index]["record"]["flags"], is_(record_data["flags"]))
|
||||
assert_that(changes_json[index]["record"]["service"], is_(record_data["service"]))
|
||||
assert_that(changes_json[index]["record"]["regexp"], is_(record_data["regexp"]))
|
||||
assert_that(changes_json[index]["record"]["replacement"], is_(record_data["replacement"]))
|
||||
elif record_type == "SRV" and change_type == "Add":
|
||||
assert_that(changes_json[index]["record"]["priority"], is_(record_data["priority"]))
|
||||
assert_that(changes_json[index]["record"]["weight"], is_(record_data["weight"]))
|
||||
assert_that(changes_json[index]["record"]["port"], is_(record_data["port"]))
|
||||
assert_that(changes_json[index]["record"]["target"], is_(record_data["target"]))
|
||||
return
|
||||
|
||||
|
||||
@ -113,7 +141,9 @@ def test_create_batch_change_with_adds_success(shared_zone_test_context):
|
||||
get_change_TXT_json(f"txt-unique-characters.{ok_zone_name}", text='a\\\\`=` =\\"Cat\\"\nattr=val'),
|
||||
get_change_TXT_json(f"txt.{ip4_zone_name}"),
|
||||
get_change_MX_json(f"mx.{ok_zone_name}", preference=0),
|
||||
get_change_MX_json(f"{ok_zone_name}", preference=1000, exchange="bar.foo.")
|
||||
get_change_MX_json(f"{ok_zone_name}", preference=1000, exchange="bar.foo."),
|
||||
get_change_NAPTR_json(f"naptr.{ok_zone_name}", order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
|
||||
get_change_SRV_json(f"srv.{ok_zone_name}", priority=1000, weight=5, port=20, target="bar.foo.")
|
||||
]
|
||||
}
|
||||
|
||||
@ -161,6 +191,10 @@ def test_create_batch_change_with_adds_success(shared_zone_test_context):
|
||||
record_name="mx", input_name=f"mx.{ok_zone_name}", record_data={"preference": 0, "exchange": "foo.bar."}, record_type="MX")
|
||||
assert_change_success(result["changes"], zone=ok_zone, index=14,
|
||||
record_name=f"{ok_zone_name}", input_name=f"{ok_zone_name}", record_data={"preference": 1000, "exchange": "bar.foo."}, record_type="MX")
|
||||
assert_change_success(result["changes"], zone=ok_zone, index=15,
|
||||
record_name=f"naptr", input_name=f"naptr.{ok_zone_name}", record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."}, record_type="NAPTR")
|
||||
assert_change_success(result["changes"], zone=ok_zone, index=16,
|
||||
record_name=f"srv", input_name=f"srv.{ok_zone_name}", record_data={"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."}, record_type="SRV")
|
||||
|
||||
completed_status = [change["status"] == "Complete" for change in completed_batch["changes"]]
|
||||
assert_that(all(completed_status), is_(True))
|
||||
@ -285,6 +319,22 @@ def test_create_batch_change_with_adds_success(shared_zone_test_context):
|
||||
"ttl": 200,
|
||||
"records": [{"preference": 1000, "exchange": "bar.foo."}]}
|
||||
verify_recordset(rs16, expected16)
|
||||
|
||||
rs17 = client.get_recordset(record_set_list[15][0], record_set_list[15][1])["recordSet"]
|
||||
expected17 = {"name": f"naptr",
|
||||
"zoneId": ok_zone["id"],
|
||||
"type": "NAPTR",
|
||||
"ttl": 200,
|
||||
"records": [{"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."}]}
|
||||
verify_recordset(rs17, expected17)
|
||||
|
||||
rs18 = client.get_recordset(record_set_list[16][0], record_set_list[16][1])["recordSet"]
|
||||
expected18 = {"name": f"srv",
|
||||
"zoneId": ok_zone["id"],
|
||||
"type": "SRV",
|
||||
"ttl": 200,
|
||||
"records": [{"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."}]}
|
||||
verify_recordset(rs18, expected18)
|
||||
finally:
|
||||
clear_zoneid_rsid_tuple_list(to_delete, client)
|
||||
|
||||
@ -316,6 +366,30 @@ def test_create_batch_change_with_scheduled_time_and_owner_group_succeeds(shared
|
||||
rejecter.reject_batch_change(result["id"], status=200)
|
||||
|
||||
|
||||
@pytest.mark.manual_batch_review
|
||||
def test_create_batch_change_with_ns_record_goes_to_review(shared_zone_test_context):
|
||||
"""
|
||||
Test creating a batch change with ns record goes to review
|
||||
"""
|
||||
client = shared_zone_test_context.ok_vinyldns_client
|
||||
ok_zone_name = shared_zone_test_context.ok_zone["name"]
|
||||
batch_change_input = {
|
||||
"comments": "this is optional",
|
||||
"changes": [
|
||||
get_change_NS_json(f"ns.{ok_zone_name}", nsdname="ns1.parent.com."),
|
||||
],
|
||||
"ownerGroupId": shared_zone_test_context.ok_group["id"]
|
||||
}
|
||||
result = None
|
||||
try:
|
||||
result = client.create_batch_change(batch_change_input, status=202)
|
||||
assert_that(result["status"], "Pending Review")
|
||||
finally:
|
||||
if result:
|
||||
rejecter = shared_zone_test_context.support_user_client
|
||||
rejecter.reject_batch_change(result["id"], status=200)
|
||||
|
||||
|
||||
@pytest.mark.manual_batch_review
|
||||
def test_create_scheduled_batch_change_with_zone_discovery_error_without_owner_group_fails(shared_zone_test_context):
|
||||
"""
|
||||
@ -890,7 +964,7 @@ def test_create_batch_change_with_unsupported_record_type_fails(shared_zone_test
|
||||
errors = client.create_batch_change(batch_change_input, status=400)
|
||||
|
||||
assert_error(errors,
|
||||
error_messages=["Unsupported type UNKNOWN, valid types include: A, AAAA, CNAME, PTR, TXT, and MX"])
|
||||
error_messages=["Unsupported type UNKNOWN, valid types include: A, AAAA, CNAME, PTR, TXT, MX, NS, SRV and NAPTR"])
|
||||
|
||||
|
||||
def test_create_batch_change_with_high_value_domain_fails(shared_zone_test_context):
|
||||
@ -1481,8 +1555,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"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."])
|
||||
error_messages=[f"RecordName \"{existing_a_fqdn}\" already exists. "
|
||||
f"If you intended to update this record, submit 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. '
|
||||
@ -1938,7 +2012,8 @@ def test_cname_recordtype_add_checks(shared_zone_test_context):
|
||||
get_change_CNAME_json(existing_forward_fqdn),
|
||||
get_change_CNAME_json(existing_cname_fqdn),
|
||||
get_change_CNAME_json(f"0.{ip4_zone_name}", cname="duplicate.in.db."),
|
||||
get_change_CNAME_json(f"user-add-unauthorized.{dummy_zone_name}")
|
||||
get_change_CNAME_json(f"user-add-unauthorized.{dummy_zone_name}"),
|
||||
get_change_CNAME_json(f"invalid-ipv4-{parent_zone_name}", cname="1.2.3.4")
|
||||
]
|
||||
}
|
||||
|
||||
@ -2003,8 +2078,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"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.",
|
||||
error_messages=[f"RecordName \"{existing_cname_fqdn}\" already exists. "
|
||||
f"If you intended to update this record, submit 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",
|
||||
@ -2014,6 +2089,9 @@ def test_cname_recordtype_add_checks(shared_zone_test_context):
|
||||
assert_failed_change_in_error_response(response[16], input_name=f"user-add-unauthorized.{dummy_zone_name}",
|
||||
record_type="CNAME", record_data="test.com.",
|
||||
error_messages=[f"User \"ok\" is not authorized. Contact zone owner group: {dummy_group_name} at test@test.com to make DNS changes."])
|
||||
assert_failed_change_in_error_response(response[17], input_name=f"invalid-ipv4-{parent_zone_name}", record_type="CNAME", record_data="1.2.3.4.",
|
||||
error_messages=[f'Invalid Cname: "Fqdn(1.2.3.4.)", Valid CNAME record data should not be an IP address'])
|
||||
|
||||
finally:
|
||||
clear_recordset_list(to_delete, client)
|
||||
|
||||
@ -2291,9 +2369,9 @@ def test_ipv4_ptr_recordtype_add_checks(shared_zone_test_context):
|
||||
|
||||
# delegated and non-delegated PTR duplicate name checks
|
||||
assert_successful_change_in_error_response(response[4], input_name=f"{ip4_prefix}.196", record_type="PTR", record_data="test.com.")
|
||||
assert_successful_change_in_error_response(response[5], input_name=f"196.{ip4_zone_name}", record_type="CNAME", record_data="test.com.")
|
||||
assert_failed_change_in_error_response(response[6], input_name=f"196.192/30.{ip4_zone_name}", record_type="CNAME", record_data="test.com.",
|
||||
error_messages=[f'Record Name "196.192/30.{ip4_zone_name}" Not Unique In Batch Change: cannot have multiple "CNAME" records with the same name.'])
|
||||
assert_failed_change_in_error_response(response[5], input_name=f"196.{ip4_zone_name}", record_type="CNAME", record_data="test.com.",
|
||||
error_messages=[f'Record Name "196.{ip4_zone_name}" Not Unique In Batch Change: cannot have multiple "CNAME" records with the same name.'])
|
||||
assert_successful_change_in_error_response(response[6], input_name=f"196.192/30.{ip4_zone_name}", record_type="CNAME", record_data="test.com.")
|
||||
assert_successful_change_in_error_response(response[7], input_name=f"{ip4_prefix}.55", record_type="PTR", record_data="test.com.")
|
||||
assert_failed_change_in_error_response(response[8], input_name=f"55.{ip4_zone_name}", record_type="CNAME", record_data="test.com.",
|
||||
error_messages=[f'Record Name "55.{ip4_zone_name}" Not Unique In Batch Change: cannot have multiple "CNAME" records with the same name.'])
|
||||
@ -2306,8 +2384,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'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.'])
|
||||
error_messages=[f'RecordName "{ip4_prefix}.193" already exists. '
|
||||
f'If you intended to update this record, submit 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.'])
|
||||
@ -2516,8 +2594,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"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."])
|
||||
error_messages=[f"RecordName \"{ip6_prefix}:1000::bbbb\" already exists. "
|
||||
f"If you intended to update this record, submit a DeleteRecordSet entry followed by an Add."])
|
||||
finally:
|
||||
clear_recordset_list(to_delete, client)
|
||||
|
||||
@ -3725,7 +3803,7 @@ def test_create_batch_delete_record_that_does_not_exists_completes(shared_zone_t
|
||||
ok_zone_name = shared_zone_test_context.ok_zone["name"]
|
||||
|
||||
batch_change_input = {
|
||||
"comments": "test delete record failures",
|
||||
"comments": "test delete record",
|
||||
"changes": [
|
||||
get_change_A_AAAA_json(f"delete-non-existent-record.{ok_zone_name}", change_type="DeleteRecordSet")
|
||||
]
|
||||
@ -3734,12 +3812,46 @@ def test_create_batch_delete_record_that_does_not_exists_completes(shared_zone_t
|
||||
response = client.create_batch_change(batch_change_input, status=202)
|
||||
get_batch = client.get_batch_change(response["id"])
|
||||
|
||||
assert_that(get_batch["changes"][0]["systemMessage"], is_("This record does not exist." +
|
||||
assert_that(get_batch["changes"][0]["systemMessage"], is_("This record does not exist. " +
|
||||
"No further action is required."))
|
||||
|
||||
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")
|
||||
|
||||
|
||||
def test_create_batch_delete_record_data_that_does_not_exists_completes(shared_zone_test_context):
|
||||
"""
|
||||
Test delete record set completes for non-existent record data
|
||||
"""
|
||||
client = shared_zone_test_context.ok_vinyldns_client
|
||||
ok_zone_name = shared_zone_test_context.ok_zone["name"]
|
||||
batch_change_input = {
|
||||
"comments": "this is optional",
|
||||
"changes": [
|
||||
get_change_A_AAAA_json(f"delete-non-existent-record-data.{ok_zone_name}", address="4.5.6.7"),
|
||||
]
|
||||
}
|
||||
batch_change_delete_input = {
|
||||
"comments": "test delete record",
|
||||
"changes": [
|
||||
get_change_A_AAAA_json(f"delete-non-existent-record-data.{ok_zone_name}", address="1.1.1.1", change_type="DeleteRecordSet")
|
||||
]
|
||||
}
|
||||
|
||||
to_delete = []
|
||||
try:
|
||||
result = client.create_batch_change(batch_change_input, status=202)
|
||||
completed_batch = client.wait_until_batch_change_completed(result)
|
||||
record_set_list = [(change["zoneId"], change["recordSetId"]) for change in completed_batch["changes"]]
|
||||
to_delete = set(record_set_list)
|
||||
response = client.create_batch_change(batch_change_delete_input, status=202)
|
||||
get_batch = client.get_batch_change(response["id"])
|
||||
assert_that(get_batch["changes"][0]["systemMessage"], is_("Record data entered does not exist. " +
|
||||
"No further action is required."))
|
||||
assert_successful_change_in_error_response(response["changes"][0], input_name=f"delete-non-existent-record-data.{ok_zone_name}", record_data="1.1.1.1", change_type="DeleteRecordSet")
|
||||
finally:
|
||||
clear_zoneid_rsid_tuple_list(to_delete, client)
|
||||
|
||||
|
||||
@pytest.mark.serial
|
||||
def test_create_batch_delete_record_access_checks(shared_zone_test_context):
|
||||
"""
|
||||
@ -4109,7 +4221,7 @@ def test_create_batch_deletes_succeeds(shared_zone_test_context):
|
||||
@pytest.mark.skip_production
|
||||
def test_create_batch_change_with_multi_record_adds_with_multi_record_support(shared_zone_test_context):
|
||||
"""
|
||||
Test new recordsets with multiple records can be added in batch, but existing recordsets cannot be added to
|
||||
Test new recordsets with multiple records can be added in batch.
|
||||
"""
|
||||
client = shared_zone_test_context.ok_vinyldns_client
|
||||
ok_zone = shared_zone_test_context.ok_zone
|
||||
@ -4134,7 +4246,7 @@ def test_create_batch_change_with_multi_record_adds_with_multi_record_support(sh
|
||||
get_change_TXT_json(f"multi-txt.{ok_zone_name}", text="more-multi-text"),
|
||||
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")
|
||||
get_change_A_AAAA_json(rs_fqdn, address="1.2.3.4")
|
||||
],
|
||||
"ownerGroupId": shared_zone_test_context.ok_group["id"]
|
||||
}
|
||||
@ -4152,6 +4264,637 @@ def test_create_batch_change_with_multi_record_adds_with_multi_record_support(sh
|
||||
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")
|
||||
assert_successful_change_in_error_response(response["changes"][8], input_name=rs_fqdn, record_data="1.2.3.4")
|
||||
finally:
|
||||
clear_recordset_list(to_delete, client)
|
||||
|
||||
|
||||
def test_ns_recordtype_add_checks(shared_zone_test_context):
|
||||
"""
|
||||
Test all add validations performed on NS records submitted in batch changes
|
||||
"""
|
||||
client = shared_zone_test_context.ok_vinyldns_client
|
||||
dummy_zone_name = shared_zone_test_context.dummy_zone["name"]
|
||||
dummy_group_name = shared_zone_test_context.dummy_group["name"]
|
||||
ok_zone_name = shared_zone_test_context.ok_zone["name"]
|
||||
|
||||
existing_ns_name = generate_record_name()
|
||||
existing_ns_fqdn = existing_ns_name + f".{ok_zone_name}"
|
||||
existing_ns = create_recordset(shared_zone_test_context.ok_zone, existing_ns_name, "NS", [{"nsdname": "ns1.parent.com."}], 100)
|
||||
|
||||
existing_cname_name = generate_record_name()
|
||||
existing_cname_fqdn = existing_cname_name + f".{ok_zone_name}"
|
||||
existing_cname = create_recordset(shared_zone_test_context.ok_zone, existing_cname_name, "CNAME",
|
||||
[{"cname": "test."}], 100)
|
||||
|
||||
batch_change_input = {
|
||||
"changes": [
|
||||
# valid change
|
||||
get_change_NS_json(f"ns.{ok_zone_name}", nsdname="ns1.parent.com."),
|
||||
|
||||
# input validation failures
|
||||
get_change_NS_json(f"bad-ttl-and-invalid-name$.{ok_zone_name}", ttl=29, nsdname="ns1.parent.com."),
|
||||
|
||||
# zone discovery failures
|
||||
get_change_NS_json(f"no.zone.at.all.", nsdname="ns1.parent.com."),
|
||||
|
||||
# context validation failures
|
||||
get_change_CNAME_json(f"cname-duplicate.{ok_zone_name}"),
|
||||
get_change_NS_json(f"cname-duplicate.{ok_zone_name}", nsdname="ns1.parent.com."),
|
||||
get_change_NS_json(existing_ns_fqdn, nsdname="ns1.parent.com."),
|
||||
get_change_NS_json(existing_cname_fqdn, nsdname="ns1.parent.com."),
|
||||
get_change_NS_json(f"unapproved.{ok_zone_name}", nsdname="unapproved.name.server."),
|
||||
get_change_NS_json(f"user-add-unauthorized.{dummy_zone_name}", nsdname="ns1.parent.com.")
|
||||
]
|
||||
}
|
||||
|
||||
to_create = [existing_ns, existing_cname]
|
||||
to_delete = []
|
||||
try:
|
||||
for create_json in to_create:
|
||||
create_result = client.create_recordset(create_json, status=202)
|
||||
to_delete.append(client.wait_until_recordset_change_status(create_result, "Complete"))
|
||||
|
||||
response = client.create_batch_change(batch_change_input, status=400)
|
||||
|
||||
# successful changes
|
||||
assert_successful_change_in_error_response(response[0], input_name=f"ns.{ok_zone_name}", record_type="NS",
|
||||
record_data="ns1.parent.com.")
|
||||
|
||||
# ttl, domain name, record data
|
||||
assert_failed_change_in_error_response(response[1], input_name=f"bad-ttl-and-invalid-name$.{ok_zone_name}", ttl=29,
|
||||
record_type="NS", record_data="ns1.parent.com.",
|
||||
error_messages=[
|
||||
'Invalid TTL: "29", must be a number between 30 and 2147483647.',
|
||||
f'Invalid domain name: "bad-ttl-and-invalid-name$.{ok_zone_name}", '
|
||||
"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[2], input_name="no.zone.at.all.", record_type="NS",
|
||||
record_data="ns1.parent.com.",
|
||||
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 validations: cname duplicate
|
||||
assert_failed_change_in_error_response(response[3], input_name=f"cname-duplicate.{ok_zone_name}", record_type="CNAME",
|
||||
record_data="test.com.",
|
||||
error_messages=[f"Record Name \"cname-duplicate.{ok_zone_name}\" Not Unique In Batch Change: "
|
||||
f"cannot have multiple \"CNAME\" records with the same name."])
|
||||
|
||||
# context validations: conflicting recordsets, unauthorized error
|
||||
assert_successful_change_in_error_response(response[5], input_name=existing_ns_fqdn, record_type="NS",
|
||||
record_data="ns1.parent.com.")
|
||||
assert_failed_change_in_error_response(response[6], input_name=existing_cname_fqdn, record_type="NS",
|
||||
record_data="ns1.parent.com.",
|
||||
error_messages=[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[7], input_name=f"unapproved.{ok_zone_name}",
|
||||
record_type="NS", record_data="unapproved.name.server.",
|
||||
error_messages=[f"Name Server unapproved.name.server. is not an approved name server."])
|
||||
assert_failed_change_in_error_response(response[8], input_name=f"user-add-unauthorized.{dummy_zone_name}",
|
||||
record_type="NS", record_data="ns1.parent.com.",
|
||||
error_messages=[f"User \"ok\" is not authorized. Contact zone owner group: {dummy_group_name} at test@test.com to make DNS changes."])
|
||||
finally:
|
||||
clear_recordset_list(to_delete, client)
|
||||
|
||||
|
||||
def test_ns_recordtype_update_delete_checks(shared_zone_test_context):
|
||||
"""
|
||||
Test all update and delete validations performed on NS records submitted in batch changes
|
||||
"""
|
||||
ok_client = shared_zone_test_context.ok_vinyldns_client
|
||||
dummy_client = shared_zone_test_context.dummy_vinyldns_client
|
||||
ok_zone = shared_zone_test_context.ok_zone
|
||||
dummy_zone = shared_zone_test_context.dummy_zone
|
||||
ok_zone_name = shared_zone_test_context.ok_zone["name"]
|
||||
dummy_zone_name = shared_zone_test_context.dummy_zone["name"]
|
||||
dummy_group_name = shared_zone_test_context.dummy_group["name"]
|
||||
|
||||
rs_delete_name = generate_record_name()
|
||||
rs_delete_fqdn = rs_delete_name + f".{ok_zone_name}"
|
||||
rs_delete_ok = create_recordset(ok_zone, rs_delete_name, "NS", [{"nsdname": "ns1.parent.com."}], 200)
|
||||
|
||||
rs_update_name = generate_record_name()
|
||||
rs_update_fqdn = rs_update_name + f".{ok_zone_name}"
|
||||
rs_update_ok = create_recordset(ok_zone, rs_update_name, "NS", [{"nsdname": "ns1.parent.com."}], 200)
|
||||
|
||||
rs_delete_dummy_name = generate_record_name()
|
||||
rs_delete_dummy_fqdn = rs_delete_dummy_name + f".{dummy_zone_name}"
|
||||
rs_delete_dummy = create_recordset(dummy_zone, rs_delete_dummy_name, "NS", [{"nsdname": "ns1.parent.com."}], 200)
|
||||
|
||||
rs_update_dummy_name = generate_record_name()
|
||||
rs_update_dummy_fqdn = rs_update_dummy_name + f".{dummy_zone_name}"
|
||||
rs_update_dummy = create_recordset(dummy_zone, rs_update_dummy_name, "NS", [{"nsdname": "ns1.parent.com."}], 200)
|
||||
|
||||
batch_change_input = {
|
||||
"comments": "this is optional",
|
||||
"changes": [
|
||||
# valid changes
|
||||
get_change_NS_json(rs_delete_fqdn, change_type="DeleteRecordSet", nsdname="ns1.parent.com."),
|
||||
get_change_NS_json(rs_update_fqdn, change_type="DeleteRecordSet", nsdname="ns1.parent.com."),
|
||||
get_change_NS_json(rs_update_fqdn, ttl=300, nsdname="ns1.parent.com."),
|
||||
get_change_NS_json(f"delete-nonexistent.{ok_zone_name}", change_type="DeleteRecordSet", nsdname="ns1.parent.com."),
|
||||
get_change_NS_json(f"update-nonexistent.{ok_zone_name}", change_type="DeleteRecordSet", nsdname="ns1.parent.com."),
|
||||
|
||||
# input validations failures
|
||||
get_change_NS_json(f"invalid-name$.{ok_zone_name}", change_type="DeleteRecordSet", nsdname="ns1.parent.com."),
|
||||
get_change_NS_json(f"invalid-ttl.{ok_zone_name}", ttl=29, nsdname="ns1.parent.com."),
|
||||
|
||||
# zone discovery failure
|
||||
get_change_NS_json("no.zone.at.all.", change_type="DeleteRecordSet", nsdname="ns1.parent.com."),
|
||||
|
||||
# context validation failures
|
||||
get_change_NS_json(f"update-nonexistent.{ok_zone_name}", nsdname="ns1.parent.com."),
|
||||
get_change_NS_json(rs_delete_dummy_fqdn, change_type="DeleteRecordSet", nsdname="ns1.parent.com."),
|
||||
get_change_NS_json(rs_update_dummy_fqdn, nsdname="ns1.parent.com."),
|
||||
get_change_NS_json(f"unapproved.{ok_zone_name}", nsdname="unapproved.name.server."),
|
||||
get_change_NS_json(rs_update_dummy_fqdn, change_type="DeleteRecordSet", nsdname="ns1.parent.com.")
|
||||
]
|
||||
}
|
||||
|
||||
to_create = [rs_delete_ok, rs_update_ok, rs_delete_dummy, rs_update_dummy]
|
||||
to_delete = []
|
||||
|
||||
try:
|
||||
for rs in to_create:
|
||||
if rs["zoneId"] == dummy_zone["id"]:
|
||||
create_client = dummy_client
|
||||
else:
|
||||
create_client = ok_client
|
||||
|
||||
create_rs = create_client.create_recordset(rs, status=202)
|
||||
to_delete.append(create_client.wait_until_recordset_change_status(create_rs, "Complete"))
|
||||
|
||||
# Confirm that record set doesn't already exist
|
||||
ok_client.get_recordset(ok_zone["id"], "delete-nonexistent", status=404)
|
||||
|
||||
response = ok_client.create_batch_change(batch_change_input, status=400)
|
||||
|
||||
# successful changes
|
||||
assert_successful_change_in_error_response(response[0], input_name=rs_delete_fqdn, record_type="NS", record_data="ns1.parent.com.", change_type="DeleteRecordSet")
|
||||
assert_successful_change_in_error_response(response[1], input_name=rs_update_fqdn, record_type="NS", record_data="ns1.parent.com.", change_type="DeleteRecordSet")
|
||||
assert_successful_change_in_error_response(response[2], ttl=300, input_name=rs_update_fqdn, record_type="NS", record_data="ns1.parent.com.")
|
||||
assert_successful_change_in_error_response(response[3], input_name=f"delete-nonexistent.{ok_zone_name}", record_type="NS", record_data="ns1.parent.com.", change_type="DeleteRecordSet")
|
||||
assert_successful_change_in_error_response(response[4], input_name=f"update-nonexistent.{ok_zone_name}", record_type="NS", record_data="ns1.parent.com.", change_type="DeleteRecordSet")
|
||||
|
||||
# input validations failures: invalid input name, reverse zone error, invalid ttl
|
||||
assert_failed_change_in_error_response(response[5], input_name=f"invalid-name$.{ok_zone_name}", record_type="NS", record_data="ns1.parent.com.", 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[6], input_name=f"invalid-ttl.{ok_zone_name}", ttl=29, record_type="NS", record_data="ns1.parent.com.",
|
||||
error_messages=['Invalid TTL: "29", must be a number between 30 and 2147483647.'])
|
||||
|
||||
# zone discovery failure
|
||||
assert_failed_change_in_error_response(response[7], input_name="no.zone.at.all.", record_type="NS", record_data="ns1.parent.com.", 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_successful_change_in_error_response(response[8], input_name=f"update-nonexistent.{ok_zone_name}", record_type="NS", record_data="ns1.parent.com.")
|
||||
assert_failed_change_in_error_response(response[9], input_name=rs_delete_dummy_fqdn, record_type="NS", record_data="ns1.parent.com.", 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."])
|
||||
assert_failed_change_in_error_response(response[10], input_name=rs_update_dummy_fqdn, record_type="NS", record_data="ns1.parent.com.",
|
||||
error_messages=[f"User \"ok\" is not authorized. Contact zone owner group: {dummy_group_name} at test@test.com to make DNS changes."])
|
||||
assert_failed_change_in_error_response(response[11], input_name=f"unapproved.{ok_zone_name}",
|
||||
record_type="NS", record_data="unapproved.name.server.",
|
||||
error_messages=[f"Name Server unapproved.name.server. is not an approved name server."])
|
||||
assert_failed_change_in_error_response(response[12], input_name=rs_update_dummy_fqdn, record_type="NS", record_data="ns1.parent.com.", 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."])
|
||||
finally:
|
||||
# Clean up updates
|
||||
dummy_deletes = [rs for rs in to_delete if rs["zone"]["id"] == dummy_zone["id"]]
|
||||
ok_deletes = [rs for rs in to_delete if rs["zone"]["id"] != dummy_zone["id"]]
|
||||
clear_recordset_list(dummy_deletes, dummy_client)
|
||||
clear_recordset_list(ok_deletes, ok_client)
|
||||
|
||||
|
||||
def test_naptr_recordtype_add_checks(shared_zone_test_context):
|
||||
"""
|
||||
Test all add validations performed on NAPTR records submitted in batch changes
|
||||
"""
|
||||
client = shared_zone_test_context.ok_vinyldns_client
|
||||
ok_zone_name = shared_zone_test_context.ok_zone["name"]
|
||||
dummy_zone_name = shared_zone_test_context.dummy_zone["name"]
|
||||
dummy_group_name = shared_zone_test_context.dummy_group["name"]
|
||||
ip4_zone_name = shared_zone_test_context.classless_base_zone["name"]
|
||||
|
||||
existing_naptr_name = generate_record_name()
|
||||
existing_naptr_fqdn = f"{existing_naptr_name}.{ok_zone_name}"
|
||||
existing_naptr = create_recordset(shared_zone_test_context.ok_zone, existing_naptr_name, "NAPTR", [{"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."}], 100)
|
||||
|
||||
existing_cname_name = generate_record_name()
|
||||
existing_cname_fqdn = f"{existing_cname_name}.{ok_zone_name}"
|
||||
existing_cname = create_recordset(shared_zone_test_context.ok_zone, existing_cname_name, "CNAME", [{"cname": "test."}], 100)
|
||||
|
||||
good_record_fqdn = generate_record_name(ok_zone_name)
|
||||
batch_change_input = {
|
||||
"changes": [
|
||||
# valid change
|
||||
get_change_NAPTR_json(good_record_fqdn, order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
|
||||
|
||||
# input validation failures
|
||||
get_change_NAPTR_json(f"bad-ttl-and-invalid-name$.{ok_zone_name}", ttl=29, order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
|
||||
get_change_NAPTR_json(f"naptr.{ip4_zone_name}", order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
|
||||
|
||||
# zone discovery failures
|
||||
get_change_NAPTR_json(f"no.subzone.{ok_zone_name}", order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
|
||||
get_change_NAPTR_json("no.zone.at.all.", order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
|
||||
|
||||
# context validation failures
|
||||
get_change_CNAME_json(f"cname-duplicate.{ok_zone_name}"),
|
||||
get_change_NAPTR_json(f"cname-duplicate.{ok_zone_name}", order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
|
||||
get_change_NAPTR_json(existing_naptr_fqdn, order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
|
||||
get_change_NAPTR_json(existing_cname_fqdn, order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
|
||||
get_change_NAPTR_json(f"user-add-unauthorized.{dummy_zone_name}", order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns.")
|
||||
]
|
||||
}
|
||||
|
||||
to_create = [existing_naptr, existing_cname]
|
||||
to_delete = []
|
||||
try:
|
||||
for create_json in to_create:
|
||||
create_result = client.create_recordset(create_json, status=202)
|
||||
to_delete.append(client.wait_until_recordset_change_status(create_result, "Complete"))
|
||||
|
||||
response = client.create_batch_change(batch_change_input, status=400)
|
||||
|
||||
# successful changes
|
||||
assert_successful_change_in_error_response(response[0], input_name=good_record_fqdn, record_type="NAPTR", record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."})
|
||||
|
||||
# ttl, domain name, record data
|
||||
assert_failed_change_in_error_response(response[1], input_name=f"bad-ttl-and-invalid-name$.{ok_zone_name}", ttl=29, record_type="NAPTR",
|
||||
record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."},
|
||||
error_messages=['Invalid TTL: "29", must be a number between 30 and 2147483647.',
|
||||
f'Invalid domain name: "bad-ttl-and-invalid-name$.{ok_zone_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[2], input_name=f"naptr.{ip4_zone_name}", record_type="NAPTR",
|
||||
record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."},
|
||||
error_messages=[f'Invalid Record Type In Reverse Zone: record with name "naptr.{ip4_zone_name}" and type "NAPTR" is not allowed in a reverse zone.'])
|
||||
|
||||
# zone discovery failures
|
||||
assert_failed_change_in_error_response(response[3], input_name=f"no.subzone.{ok_zone_name}", record_type="NAPTR",
|
||||
record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."},
|
||||
error_messages=[f'Zone Discovery Failed: zone for "no.subzone.{ok_zone_name}" does not exist in VinylDNS. '
|
||||
f'If zone exists, then it must be connected to in VinylDNS.'])
|
||||
assert_failed_change_in_error_response(response[4], input_name="no.zone.at.all.", record_type="NAPTR",
|
||||
record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."},
|
||||
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 validations: cname duplicate
|
||||
assert_failed_change_in_error_response(response[5], input_name=f"cname-duplicate.{ok_zone_name}", record_type="CNAME",
|
||||
record_data="test.com.",
|
||||
error_messages=[f"Record Name \"cname-duplicate.{ok_zone_name}\" Not Unique In Batch Change: "
|
||||
f"cannot have multiple \"CNAME\" records with the same name."])
|
||||
|
||||
# context validations: conflicting recordsets, unauthorized error
|
||||
assert_successful_change_in_error_response(response[7], input_name=existing_naptr_fqdn, record_type="NAPTR",
|
||||
record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."})
|
||||
assert_failed_change_in_error_response(response[8], input_name=existing_cname_fqdn, record_type="NAPTR",
|
||||
record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."},
|
||||
error_messages=["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[9], input_name=f"user-add-unauthorized.{dummy_zone_name}", record_type="NAPTR",
|
||||
record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."},
|
||||
error_messages=[f"User \"ok\" is not authorized. Contact zone owner group: {dummy_group_name} at test@test.com to make DNS changes."])
|
||||
finally:
|
||||
clear_recordset_list(to_delete, client)
|
||||
|
||||
|
||||
def test_naptr_recordtype_update_delete_checks(shared_zone_test_context):
|
||||
"""
|
||||
Test all update and delete validations performed on NAPTR records submitted in batch changes
|
||||
"""
|
||||
ok_client = shared_zone_test_context.ok_vinyldns_client
|
||||
dummy_client = shared_zone_test_context.dummy_vinyldns_client
|
||||
ok_zone = shared_zone_test_context.ok_zone
|
||||
dummy_zone = shared_zone_test_context.dummy_zone
|
||||
dummy_zone_name = shared_zone_test_context.dummy_zone["name"]
|
||||
|
||||
dummy_group_name = shared_zone_test_context.dummy_group["name"]
|
||||
ok_zone_name = shared_zone_test_context.ok_zone["name"]
|
||||
ip4_zone_name = shared_zone_test_context.classless_base_zone["name"]
|
||||
|
||||
rs_delete_name = generate_record_name()
|
||||
rs_delete_fqdn = rs_delete_name + f".{ok_zone_name}"
|
||||
rs_delete_ok = create_recordset(ok_zone, rs_delete_name, "NAPTR", [{"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."}], 200)
|
||||
|
||||
rs_update_name = generate_record_name()
|
||||
rs_update_fqdn = rs_update_name + f".{ok_zone_name}"
|
||||
rs_update_ok = create_recordset(ok_zone, rs_update_name, "NAPTR", [{"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."}], 200)
|
||||
|
||||
rs_delete_dummy_name = generate_record_name()
|
||||
rs_delete_dummy_fqdn = rs_delete_dummy_name + f".{dummy_zone_name}"
|
||||
rs_delete_dummy = create_recordset(dummy_zone, rs_delete_dummy_name, "NAPTR", [{"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."}], 200)
|
||||
|
||||
rs_update_dummy_name = generate_record_name()
|
||||
rs_update_dummy_fqdn = rs_update_dummy_name + f".{dummy_zone_name}"
|
||||
rs_update_dummy = create_recordset(dummy_zone, rs_update_dummy_name, "NAPTR", [{"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."}], 200)
|
||||
|
||||
batch_change_input = {
|
||||
"comments": "this is optional",
|
||||
"changes": [
|
||||
# valid changes
|
||||
get_change_NAPTR_json(rs_delete_fqdn, change_type="DeleteRecordSet", order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
|
||||
get_change_NAPTR_json(rs_update_fqdn, change_type="DeleteRecordSet", order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
|
||||
get_change_NAPTR_json(rs_update_fqdn, ttl=300, order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
|
||||
get_change_NAPTR_json(f"delete-nonexistent.{ok_zone_name}", change_type="DeleteRecordSet", order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
|
||||
get_change_NAPTR_json(f"update-nonexistent.{ok_zone_name}", change_type="DeleteRecordSet", order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
|
||||
|
||||
# input validations failures
|
||||
get_change_NAPTR_json(f"invalid-name$.{ok_zone_name}", change_type="DeleteRecordSet", order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
|
||||
get_change_NAPTR_json(f"delete.{ok_zone_name}", ttl=29, order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
|
||||
get_change_NAPTR_json(f"naptr.{ip4_zone_name}", order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
|
||||
|
||||
# zone discovery failures
|
||||
get_change_NAPTR_json("no.zone.at.all.", change_type="DeleteRecordSet", order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
|
||||
|
||||
# context validation failures
|
||||
get_change_NAPTR_json(f"update-nonexistent.{ok_zone_name}", order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
|
||||
get_change_NAPTR_json(rs_delete_dummy_fqdn, change_type="DeleteRecordSet", order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
|
||||
get_change_NAPTR_json(rs_update_dummy_fqdn, order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
|
||||
get_change_NAPTR_json(rs_update_dummy_fqdn, change_type="DeleteRecordSet", order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns.")
|
||||
]
|
||||
}
|
||||
|
||||
to_create = [rs_delete_ok, rs_update_ok, rs_delete_dummy, rs_update_dummy]
|
||||
to_delete = []
|
||||
|
||||
try:
|
||||
for rs in to_create:
|
||||
if rs["zoneId"] == dummy_zone["id"]:
|
||||
create_client = dummy_client
|
||||
else:
|
||||
create_client = ok_client
|
||||
|
||||
create_rs = create_client.create_recordset(rs, status=202)
|
||||
to_delete.append(create_client.wait_until_recordset_change_status(create_rs, "Complete"))
|
||||
|
||||
# Confirm that record set doesn't already exist
|
||||
ok_client.get_recordset(ok_zone["id"], "delete-nonexistent", status=404)
|
||||
|
||||
response = ok_client.create_batch_change(batch_change_input, status=400)
|
||||
|
||||
# successful changes
|
||||
assert_successful_change_in_error_response(response[0], input_name=rs_delete_fqdn, record_type="NAPTR", record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."}, change_type="DeleteRecordSet")
|
||||
assert_successful_change_in_error_response(response[1], input_name=rs_update_fqdn, record_type="NAPTR", record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."}, change_type="DeleteRecordSet")
|
||||
assert_successful_change_in_error_response(response[2], ttl=300, input_name=rs_update_fqdn, record_type="NAPTR", record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."})
|
||||
assert_successful_change_in_error_response(response[3], input_name=f"delete-nonexistent.{ok_zone_name}", record_type="NAPTR",
|
||||
record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."}, change_type="DeleteRecordSet")
|
||||
assert_successful_change_in_error_response(response[4], input_name=f"update-nonexistent.{ok_zone_name}", record_type="NAPTR",
|
||||
record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."}, change_type="DeleteRecordSet")
|
||||
|
||||
# input validations failures: invalid input name, reverse zone error, invalid ttl
|
||||
assert_failed_change_in_error_response(response[5], input_name=f"invalid-name$.{ok_zone_name}", record_type="NAPTR", record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."},
|
||||
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[6], input_name=f"delete.{ok_zone_name}", ttl=29, record_type="NAPTR",
|
||||
record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."},
|
||||
error_messages=['Invalid TTL: "29", must be a number between 30 and 2147483647.'])
|
||||
assert_failed_change_in_error_response(response[7], input_name=f"naptr.{ip4_zone_name}", record_type="NAPTR",
|
||||
record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."},
|
||||
error_messages=[f'Invalid Record Type In Reverse Zone: record with name "naptr.{ip4_zone_name}" '
|
||||
f'and type "NAPTR" is not allowed in a reverse zone.'])
|
||||
|
||||
# zone discovery failure
|
||||
assert_failed_change_in_error_response(response[8], input_name="no.zone.at.all.", record_type="NAPTR",
|
||||
record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."}, 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_successful_change_in_error_response(response[9], input_name=f"update-nonexistent.{ok_zone_name}", record_type="NAPTR",
|
||||
record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."})
|
||||
assert_failed_change_in_error_response(response[10], input_name=rs_delete_dummy_fqdn, record_type="NAPTR",
|
||||
record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."}, 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."])
|
||||
assert_failed_change_in_error_response(response[11], input_name=rs_update_dummy_fqdn, record_type="NAPTR",
|
||||
record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."},
|
||||
error_messages=[f"User \"ok\" is not authorized. Contact zone owner group: {dummy_group_name} at test@test.com to make DNS changes."])
|
||||
assert_failed_change_in_error_response(response[12], input_name=rs_update_dummy_fqdn, record_type="NAPTR",
|
||||
record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."}, 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."])
|
||||
finally:
|
||||
# Clean up updates
|
||||
dummy_deletes = [rs for rs in to_delete if rs["zone"]["id"] == dummy_zone["id"]]
|
||||
ok_deletes = [rs for rs in to_delete if rs["zone"]["id"] != dummy_zone["id"]]
|
||||
clear_recordset_list(dummy_deletes, dummy_client)
|
||||
clear_recordset_list(ok_deletes, ok_client)
|
||||
|
||||
|
||||
def test_srv_recordtype_add_checks(shared_zone_test_context):
|
||||
"""
|
||||
Test all add validations performed on SRV records submitted in batch changes
|
||||
"""
|
||||
client = shared_zone_test_context.ok_vinyldns_client
|
||||
ok_zone_name = shared_zone_test_context.ok_zone["name"]
|
||||
dummy_zone_name = shared_zone_test_context.dummy_zone["name"]
|
||||
dummy_group_name = shared_zone_test_context.dummy_group["name"]
|
||||
ip4_zone_name = shared_zone_test_context.classless_base_zone["name"]
|
||||
|
||||
existing_srv_name = generate_record_name()
|
||||
existing_srv_fqdn = f"{existing_srv_name}.{ok_zone_name}"
|
||||
existing_srv = create_recordset(shared_zone_test_context.ok_zone, existing_srv_name, "SRV", [{"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."}], 100)
|
||||
|
||||
existing_cname_name = generate_record_name()
|
||||
existing_cname_fqdn = f"{existing_cname_name}.{ok_zone_name}"
|
||||
existing_cname = create_recordset(shared_zone_test_context.ok_zone, existing_cname_name, "CNAME", [{"cname": "test."}], 100)
|
||||
|
||||
good_record_fqdn = generate_record_name(ok_zone_name)
|
||||
batch_change_input = {
|
||||
"changes": [
|
||||
# valid change
|
||||
get_change_SRV_json(good_record_fqdn, priority=1000, weight=5, port=20, target="bar.foo."),
|
||||
|
||||
# input validation failures
|
||||
get_change_SRV_json(f"bad-ttl-and-invalid-name$.{ok_zone_name}", ttl=29, priority=1000, weight=5, port=20, target="bar.foo."),
|
||||
get_change_SRV_json(f"srv.{ip4_zone_name}", priority=1000, weight=5, port=20, target="bar.foo."),
|
||||
|
||||
# zone discovery failures
|
||||
get_change_SRV_json(f"no.subzone.{ok_zone_name}", priority=1000, weight=5, port=20, target="bar.foo."),
|
||||
get_change_SRV_json("no.zone.at.all.", priority=1000, weight=5, port=20, target="bar.foo."),
|
||||
|
||||
# context validation failures
|
||||
get_change_CNAME_json(f"cname-duplicate.{ok_zone_name}"),
|
||||
get_change_SRV_json(f"cname-duplicate.{ok_zone_name}", priority=1000, weight=5, port=20, target="bar.foo."),
|
||||
get_change_SRV_json(existing_srv_fqdn, priority=1000, weight=5, port=20, target="bar.foo."),
|
||||
get_change_SRV_json(existing_cname_fqdn, priority=1000, weight=5, port=20, target="bar.foo."),
|
||||
get_change_SRV_json(f"user-add-unauthorized.{dummy_zone_name}", priority=1000, weight=5, port=20, target="bar.foo.")
|
||||
]
|
||||
}
|
||||
|
||||
to_create = [existing_srv, existing_cname]
|
||||
to_delete = []
|
||||
try:
|
||||
for create_json in to_create:
|
||||
create_result = client.create_recordset(create_json, status=202)
|
||||
to_delete.append(client.wait_until_recordset_change_status(create_result, "Complete"))
|
||||
|
||||
response = client.create_batch_change(batch_change_input, status=400)
|
||||
|
||||
# successful changes
|
||||
assert_successful_change_in_error_response(response[0], input_name=good_record_fqdn, record_type="SRV", record_data={"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."})
|
||||
|
||||
# ttl, domain name, record data
|
||||
assert_failed_change_in_error_response(response[1], input_name=f"bad-ttl-and-invalid-name$.{ok_zone_name}", ttl=29, record_type="SRV",
|
||||
record_data={"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."},
|
||||
error_messages=['Invalid TTL: "29", must be a number between 30 and 2147483647.',
|
||||
f'Invalid domain name: "bad-ttl-and-invalid-name$.{ok_zone_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[2], input_name=f"srv.{ip4_zone_name}", record_type="SRV",
|
||||
record_data={"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."},
|
||||
error_messages=[f'Invalid Record Type In Reverse Zone: record with name "srv.{ip4_zone_name}" and type "SRV" is not allowed in a reverse zone.'])
|
||||
|
||||
# zone discovery failures
|
||||
assert_failed_change_in_error_response(response[3], input_name=f"no.subzone.{ok_zone_name}", record_type="SRV",
|
||||
record_data={"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."},
|
||||
error_messages=[f'Zone Discovery Failed: zone for "no.subzone.{ok_zone_name}" does not exist in VinylDNS. '
|
||||
f'If zone exists, then it must be connected to in VinylDNS.'])
|
||||
assert_failed_change_in_error_response(response[4], input_name="no.zone.at.all.", record_type="SRV",
|
||||
record_data={"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."},
|
||||
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 validations: cname duplicate
|
||||
assert_failed_change_in_error_response(response[5], input_name=f"cname-duplicate.{ok_zone_name}", record_type="CNAME",
|
||||
record_data="test.com.",
|
||||
error_messages=[f"Record Name \"cname-duplicate.{ok_zone_name}\" Not Unique In Batch Change: "
|
||||
f"cannot have multiple \"CNAME\" records with the same name."])
|
||||
|
||||
# context validations: conflicting recordsets, unauthorized error
|
||||
assert_successful_change_in_error_response(response[7], input_name=existing_srv_fqdn, record_type="SRV",
|
||||
record_data={"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."})
|
||||
assert_failed_change_in_error_response(response[8], input_name=existing_cname_fqdn, record_type="SRV",
|
||||
record_data={"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."},
|
||||
error_messages=["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[9], input_name=f"user-add-unauthorized.{dummy_zone_name}", record_type="SRV",
|
||||
record_data={"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."},
|
||||
error_messages=[f"User \"ok\" is not authorized. Contact zone owner group: {dummy_group_name} at test@test.com to make DNS changes."])
|
||||
finally:
|
||||
clear_recordset_list(to_delete, client)
|
||||
|
||||
|
||||
def test_srv_recordtype_update_delete_checks(shared_zone_test_context):
|
||||
"""
|
||||
Test all update and delete validations performed on SRV records submitted in batch changes
|
||||
"""
|
||||
ok_client = shared_zone_test_context.ok_vinyldns_client
|
||||
dummy_client = shared_zone_test_context.dummy_vinyldns_client
|
||||
ok_zone = shared_zone_test_context.ok_zone
|
||||
dummy_zone = shared_zone_test_context.dummy_zone
|
||||
dummy_zone_name = shared_zone_test_context.dummy_zone["name"]
|
||||
|
||||
dummy_group_name = shared_zone_test_context.dummy_group["name"]
|
||||
ok_zone_name = shared_zone_test_context.ok_zone["name"]
|
||||
ip4_zone_name = shared_zone_test_context.classless_base_zone["name"]
|
||||
|
||||
rs_delete_name = generate_record_name()
|
||||
rs_delete_fqdn = rs_delete_name + f".{ok_zone_name}"
|
||||
rs_delete_ok = create_recordset(ok_zone, rs_delete_name, "SRV", [{"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."}], 200)
|
||||
|
||||
rs_update_name = generate_record_name()
|
||||
rs_update_fqdn = rs_update_name + f".{ok_zone_name}"
|
||||
rs_update_ok = create_recordset(ok_zone, rs_update_name, "SRV", [{"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."}], 200)
|
||||
|
||||
rs_delete_dummy_name = generate_record_name()
|
||||
rs_delete_dummy_fqdn = rs_delete_dummy_name + f".{dummy_zone_name}"
|
||||
rs_delete_dummy = create_recordset(dummy_zone, rs_delete_dummy_name, "SRV", [{"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."}], 200)
|
||||
|
||||
rs_update_dummy_name = generate_record_name()
|
||||
rs_update_dummy_fqdn = rs_update_dummy_name + f".{dummy_zone_name}"
|
||||
rs_update_dummy = create_recordset(dummy_zone, rs_update_dummy_name, "SRV", [{"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."}], 200)
|
||||
|
||||
batch_change_input = {
|
||||
"comments": "this is optional",
|
||||
"changes": [
|
||||
# valid changes
|
||||
get_change_SRV_json(rs_delete_fqdn, change_type="DeleteRecordSet", priority=1000, weight=5, port=20, target="bar.foo."),
|
||||
get_change_SRV_json(rs_update_fqdn, change_type="DeleteRecordSet", priority=1000, weight=5, port=20, target="bar.foo."),
|
||||
get_change_SRV_json(rs_update_fqdn, ttl=300, priority=1000, weight=5, port=20, target="bar.foo."),
|
||||
get_change_SRV_json(f"delete-nonexistent.{ok_zone_name}", change_type="DeleteRecordSet", priority=1000, weight=5, port=20, target="bar.foo."),
|
||||
get_change_SRV_json(f"update-nonexistent.{ok_zone_name}", change_type="DeleteRecordSet", priority=1000, weight=5, port=20, target="bar.foo."),
|
||||
|
||||
# input validations failures
|
||||
get_change_SRV_json(f"invalid-name$.{ok_zone_name}", change_type="DeleteRecordSet", priority=1000, weight=5, port=20, target="bar.foo."),
|
||||
get_change_SRV_json(f"delete.{ok_zone_name}", ttl=29, priority=1000, weight=5, port=20, target="bar.foo."),
|
||||
get_change_SRV_json(f"srv.{ip4_zone_name}", priority=1000, weight=5, port=20, target="bar.foo."),
|
||||
|
||||
# zone discovery failures
|
||||
get_change_SRV_json("no.zone.at.all.", change_type="DeleteRecordSet", priority=1000, weight=5, port=20, target="bar.foo."),
|
||||
|
||||
# context validation failures
|
||||
get_change_SRV_json(f"update-nonexistent.{ok_zone_name}", priority=1000, weight=5, port=20, target="bar.foo."),
|
||||
get_change_SRV_json(rs_delete_dummy_fqdn, change_type="DeleteRecordSet", priority=1000, weight=5, port=20, target="bar.foo."),
|
||||
get_change_SRV_json(rs_update_dummy_fqdn, priority=1000, weight=5, port=20, target="bar.foo."),
|
||||
get_change_SRV_json(rs_update_dummy_fqdn, change_type="DeleteRecordSet", priority=1000, weight=5, port=20, target="bar.foo.")
|
||||
]
|
||||
}
|
||||
|
||||
to_create = [rs_delete_ok, rs_update_ok, rs_delete_dummy, rs_update_dummy]
|
||||
to_delete = []
|
||||
|
||||
try:
|
||||
for rs in to_create:
|
||||
if rs["zoneId"] == dummy_zone["id"]:
|
||||
create_client = dummy_client
|
||||
else:
|
||||
create_client = ok_client
|
||||
|
||||
create_rs = create_client.create_recordset(rs, status=202)
|
||||
to_delete.append(create_client.wait_until_recordset_change_status(create_rs, "Complete"))
|
||||
|
||||
# Confirm that record set doesn't already exist
|
||||
ok_client.get_recordset(ok_zone["id"], "delete-nonexistent", status=404)
|
||||
|
||||
response = ok_client.create_batch_change(batch_change_input, status=400)
|
||||
|
||||
# successful changes
|
||||
assert_successful_change_in_error_response(response[0], input_name=rs_delete_fqdn, record_type="SRV", record_data={"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."}, change_type="DeleteRecordSet")
|
||||
assert_successful_change_in_error_response(response[1], input_name=rs_update_fqdn, record_type="SRV", record_data={"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."}, change_type="DeleteRecordSet")
|
||||
assert_successful_change_in_error_response(response[2], ttl=300, input_name=rs_update_fqdn, record_type="SRV", record_data={"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."})
|
||||
assert_successful_change_in_error_response(response[3], input_name=f"delete-nonexistent.{ok_zone_name}", record_type="SRV",
|
||||
record_data={"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."}, change_type="DeleteRecordSet")
|
||||
assert_successful_change_in_error_response(response[4], input_name=f"update-nonexistent.{ok_zone_name}", record_type="SRV",
|
||||
record_data={"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."}, change_type="DeleteRecordSet")
|
||||
|
||||
# input validations failures: invalid input name, reverse zone error, invalid ttl
|
||||
assert_failed_change_in_error_response(response[5], input_name=f"invalid-name$.{ok_zone_name}", record_type="SRV", record_data={"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."},
|
||||
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[6], input_name=f"delete.{ok_zone_name}", ttl=29, record_type="SRV",
|
||||
record_data={"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."},
|
||||
error_messages=['Invalid TTL: "29", must be a number between 30 and 2147483647.'])
|
||||
assert_failed_change_in_error_response(response[7], input_name=f"srv.{ip4_zone_name}", record_type="SRV",
|
||||
record_data={"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."},
|
||||
error_messages=[f'Invalid Record Type In Reverse Zone: record with name "srv.{ip4_zone_name}" '
|
||||
f'and type "SRV" is not allowed in a reverse zone.'])
|
||||
|
||||
# zone discovery failure
|
||||
assert_failed_change_in_error_response(response[8], input_name="no.zone.at.all.", record_type="SRV",
|
||||
record_data={"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."}, 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_successful_change_in_error_response(response[9], input_name=f"update-nonexistent.{ok_zone_name}", record_type="SRV",
|
||||
record_data={"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."})
|
||||
assert_failed_change_in_error_response(response[10], input_name=rs_delete_dummy_fqdn, record_type="SRV",
|
||||
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."])
|
||||
assert_failed_change_in_error_response(response[11], input_name=rs_update_dummy_fqdn, record_type="SRV",
|
||||
record_data={"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."},
|
||||
error_messages=[f"User \"ok\" is not authorized. Contact zone owner group: {dummy_group_name} at test@test.com to make DNS changes."])
|
||||
assert_failed_change_in_error_response(response[12], input_name=rs_update_dummy_fqdn, record_type="SRV",
|
||||
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."])
|
||||
finally:
|
||||
# Clean up updates
|
||||
dummy_deletes = [rs for rs in to_delete if rs["zone"]["id"] == dummy_zone["id"]]
|
||||
ok_deletes = [rs for rs in to_delete if rs["zone"]["id"] != dummy_zone["id"]]
|
||||
clear_recordset_list(dummy_deletes, dummy_client)
|
||||
clear_recordset_list(ok_deletes, ok_client)
|
||||
|
||||
|
@ -77,7 +77,7 @@ def test_toggle_processing(shared_zone_test_context):
|
||||
|
||||
# Create changes to make sure we can process after the toggle
|
||||
# attempt to perform an update
|
||||
ok_zone["email"] = "foo@bar.com"
|
||||
ok_zone["email"] = "test@test.com"
|
||||
zone_change_result = client.update_zone(ok_zone, status=202)
|
||||
|
||||
# attempt to a create a record
|
||||
|
@ -10,7 +10,7 @@ def test_create_group_success(shared_zone_test_context):
|
||||
|
||||
try:
|
||||
new_group = {
|
||||
"name": f"test-create-group-success{shared_zone_test_context.partition_id}",
|
||||
"name": "test-create-group-success{shared_zone_test_context.partition_id}",
|
||||
"email": "test@test.com",
|
||||
"description": "this is a description",
|
||||
"members": [{"id": "ok"}],
|
||||
@ -32,6 +32,67 @@ def test_create_group_success(shared_zone_test_context):
|
||||
if result:
|
||||
client.delete_group(result["id"], status=(200, 404))
|
||||
|
||||
def test_create_group_success_wildcard(shared_zone_test_context):
|
||||
"""
|
||||
Tests that creating a group works
|
||||
"""
|
||||
client = shared_zone_test_context.ok_vinyldns_client
|
||||
result = None
|
||||
|
||||
try:
|
||||
new_group = {
|
||||
"name": "test-create-group-success_wildcard{shared_zone_test_context.partition_id}",
|
||||
"email": "test@ok.dummy.com",
|
||||
"description": "this is a description",
|
||||
"members": [{"id": "ok"}],
|
||||
"admins": [{"id": "ok"}]
|
||||
}
|
||||
result = client.create_group(new_group, status=200)
|
||||
|
||||
assert_that(result["name"], is_(new_group["name"]))
|
||||
assert_that(result["email"], is_(new_group["email"]))
|
||||
assert_that(result["description"], is_(new_group["description"]))
|
||||
assert_that(result["status"], is_("Active"))
|
||||
assert_that(result["created"], not_none())
|
||||
assert_that(result["id"], not_none())
|
||||
assert_that(result["members"], has_length(1))
|
||||
assert_that(result["members"][0]["id"], is_("ok"))
|
||||
assert_that(result["admins"], has_length(1))
|
||||
assert_that(result["admins"][0]["id"], is_("ok"))
|
||||
finally:
|
||||
if result:
|
||||
client.delete_group(result["id"], status=(200, 404))
|
||||
|
||||
def test_create_group_success_number_of_dots(shared_zone_test_context):
|
||||
"""
|
||||
Tests that creating a group works
|
||||
"""
|
||||
client = shared_zone_test_context.ok_vinyldns_client
|
||||
result = None
|
||||
|
||||
try:
|
||||
new_group = {
|
||||
"name": "test-create-group-success_wildcard{shared_zone_test_context.partition_id}",
|
||||
"email": "test@ok.dummy.com",
|
||||
"description": "this is a description",
|
||||
"members": [{"id": "ok"}],
|
||||
"admins": [{"id": "ok"}]
|
||||
}
|
||||
result = client.create_group(new_group, status=200)
|
||||
|
||||
assert_that(result["name"], is_(new_group["name"]))
|
||||
assert_that(result["email"], is_(new_group["email"]))
|
||||
assert_that(result["description"], is_(new_group["description"]))
|
||||
assert_that(result["status"], is_("Active"))
|
||||
assert_that(result["created"], not_none())
|
||||
assert_that(result["id"], not_none())
|
||||
assert_that(result["members"], has_length(1))
|
||||
assert_that(result["members"][0]["id"], is_("ok"))
|
||||
assert_that(result["admins"], has_length(1))
|
||||
assert_that(result["admins"][0]["id"], is_("ok"))
|
||||
finally:
|
||||
if result:
|
||||
client.delete_group(result["id"], status=(200, 404))
|
||||
|
||||
def test_creator_is_an_admin(shared_zone_test_context):
|
||||
"""
|
||||
@ -114,7 +175,52 @@ def test_create_group_without_name_or_email(shared_zone_test_context):
|
||||
"Missing Group.name",
|
||||
"Missing Group.email"
|
||||
))
|
||||
def test_create_group_with_invalid_email_domain(shared_zone_test_context):
|
||||
"""
|
||||
Tests that creating a group With Invalid email fails
|
||||
"""
|
||||
client = shared_zone_test_context.ok_vinyldns_client
|
||||
|
||||
new_group = {
|
||||
"name": "invalid-email",
|
||||
"email": "test@abc.com",
|
||||
"description": "this is a description",
|
||||
"members": [{"id": "ok"}],
|
||||
"admins": [{"id": "ok"}]
|
||||
}
|
||||
error = client.create_group(new_group, status=400)
|
||||
assert_that(error, is_("Please enter a valid Email. Valid domains should end with test.com,dummy.com"))
|
||||
def test_create_group_with_invalid_email(shared_zone_test_context):
|
||||
"""
|
||||
Tests that creating a group With Invalid email fails
|
||||
"""
|
||||
client = shared_zone_test_context.ok_vinyldns_client
|
||||
|
||||
new_group = {
|
||||
"name": "invalid-email",
|
||||
"email": "test.abc.com",
|
||||
"description": "this is a description",
|
||||
"members": [{"id": "ok"}],
|
||||
"admins": [{"id": "ok"}]
|
||||
}
|
||||
error = client.create_group(new_group, status=400)
|
||||
assert_that(error, is_("Please enter a valid Email."))
|
||||
|
||||
def test_create_group_with_invalid_email_number_of_dots(shared_zone_test_context):
|
||||
"""
|
||||
Tests that creating a group With Invalid email fails
|
||||
"""
|
||||
client = shared_zone_test_context.ok_vinyldns_client
|
||||
|
||||
new_group = {
|
||||
"name": "invalid-email",
|
||||
"email": "test@ok.ok.dummy.com",
|
||||
"description": "this is a description",
|
||||
"members": [{"id": "ok"}],
|
||||
"admins": [{"id": "ok"}]
|
||||
}
|
||||
error = client.create_group(new_group, status=400)
|
||||
assert_that(error, is_("Please enter a valid Email. Number of dots allowed after @ is 2"))
|
||||
|
||||
def test_create_group_without_members_or_admins(shared_zone_test_context):
|
||||
"""
|
||||
|
@ -26,12 +26,6 @@ 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
|
||||
# using epoch time to start from a timestamp
|
||||
epoch = datetime.utcfromtimestamp(0)
|
||||
# start from a known good timestamp
|
||||
start_from = str(int((datetime.strptime(page_one["changes"][start_from_index]["created"], "%Y-%m-%dT%H:%M:%S.%fZ") - epoch).total_seconds() * 1000))
|
||||
# now, we say give me all changes since the start_from, which should yield 8-7-6-5-4
|
||||
result = client.get_group_changes(created_group["id"], start_from=page_one["nextId"], max_items=5, status=200)
|
||||
|
||||
@ -50,26 +44,19 @@ def test_list_group_activity_start_from_success(group_activity_context, shared_z
|
||||
assert_that(result["changes"][i]["oldGroup"], is_(updated_groups[expected_start - i - 1]))
|
||||
|
||||
|
||||
def test_list_group_activity_start_from_fake_time(group_activity_context, shared_zone_test_context):
|
||||
def test_list_group_activity_start_from_random_number(group_activity_context, shared_zone_test_context):
|
||||
"""
|
||||
Test that we can start from a fake time stamp
|
||||
Test that we can start from a random number, but it returns no changes
|
||||
"""
|
||||
client = shared_zone_test_context.ok_vinyldns_client
|
||||
created_group = group_activity_context["created_group"]
|
||||
updated_groups = group_activity_context["updated_groups"]
|
||||
start_from = "9999999999999" # start from a random timestamp far in the future
|
||||
|
||||
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=999, max_items=5, status=200)
|
||||
|
||||
# there are 10 updates, proceeded by 1 create
|
||||
assert_that(result["changes"], has_length(5))
|
||||
assert_that(result["changes"], has_length(0))
|
||||
assert_that(result["maxItems"], is_(5))
|
||||
assert_that(result["startFrom"], is_(start_from))
|
||||
assert_that(result["nextId"], is_not(none()))
|
||||
|
||||
for i in range(0, 5):
|
||||
assert_that(result["changes"][i]["newGroup"], is_(updated_groups[9 - i]))
|
||||
assert_that(result["changes"][i]["oldGroup"], is_(updated_groups[9 - i - 1]))
|
||||
|
||||
|
||||
def test_list_group_activity_max_item_success(group_activity_context, shared_zone_test_context):
|
||||
@ -151,9 +138,9 @@ def test_get_group_changes_paging(group_activity_context, shared_zone_test_conte
|
||||
assert_that(page_three["changes"][0]["newGroup"], is_(created_group))
|
||||
|
||||
|
||||
def test_get_group_changes_unauthed(shared_zone_test_context):
|
||||
def test_get_group_changes_unauthorized(shared_zone_test_context):
|
||||
"""
|
||||
Tests that non-group members can still get group changes
|
||||
Tests that non-group members cannot get group changes
|
||||
"""
|
||||
client = shared_zone_test_context.ok_vinyldns_client
|
||||
dummy_client = shared_zone_test_context.dummy_vinyldns_client
|
||||
@ -167,7 +154,7 @@ def test_get_group_changes_unauthed(shared_zone_test_context):
|
||||
}
|
||||
saved_group = client.create_group(new_group, status=200)
|
||||
|
||||
dummy_client.get_group_changes(saved_group["id"], status=200)
|
||||
dummy_client.get_group_changes(saved_group["id"], status=403)
|
||||
client.get_group_changes(saved_group["id"], status=200)
|
||||
finally:
|
||||
if saved_group:
|
||||
|
@ -681,6 +681,24 @@ def test_create_dotted_cname_record_succeeds_if_all_dotted_hosts_config_satisfie
|
||||
client.wait_until_recordset_change_status(delete_result, "Complete")
|
||||
|
||||
|
||||
def test_create_IPv4_cname_record_fails(shared_zone_test_context):
|
||||
"""
|
||||
Test that creating a CNAME record set as IPv4 address records returns an error.
|
||||
"""
|
||||
client = shared_zone_test_context.ok_vinyldns_client
|
||||
zone = shared_zone_test_context.parent_zone
|
||||
apex_cname_rs = {
|
||||
"zoneId": zone["id"],
|
||||
"name": "test_create_cname_with_ipaddress",
|
||||
"type": "CNAME",
|
||||
"ttl": 500,
|
||||
"records": [{"cname": "1.2.3.4"}]
|
||||
}
|
||||
|
||||
error = client.create_recordset(apex_cname_rs, status=400)
|
||||
assert_that(error, is_(f'Invalid CNAME: 1.2.3.4, valid CNAME record data cannot be an IP address.'))
|
||||
|
||||
|
||||
def test_create_cname_with_multiple_records(shared_zone_test_context):
|
||||
"""
|
||||
Test that creating a CNAME record set with multiple records returns an error
|
||||
@ -1770,7 +1788,7 @@ def test_create_ipv4_ptr_recordset_with_verify_in_classless(shared_zone_test_con
|
||||
try:
|
||||
new_rs = {
|
||||
"zoneId": reverse4_zone["id"],
|
||||
"name": "196",
|
||||
"name": "193",
|
||||
"type": "PTR",
|
||||
"ttl": 100,
|
||||
"records": [
|
||||
|
@ -88,7 +88,60 @@ def test_get_recordset_doesnt_exist(shared_zone_test_context):
|
||||
client.get_recordset(shared_zone_test_context.ok_zone["id"], "123", status=404)
|
||||
|
||||
|
||||
@pytest.mark.serial
|
||||
def test_get_recordset_count_status_code(shared_zone_test_context):
|
||||
"""
|
||||
Test getting recordset count for a valid zoneid should return 200
|
||||
"""
|
||||
client = shared_zone_test_context.ok_vinyldns_client
|
||||
client.get_recordset_count(shared_zone_test_context.ok_zone["id"],status=200)
|
||||
|
||||
|
||||
def test_get_recordset_count_error(shared_zone_test_context):
|
||||
"""
|
||||
Test getting recordset count for a invalid zoneid should return 404
|
||||
"""
|
||||
client = shared_zone_test_context.ok_vinyldns_client
|
||||
client.get_recordset_count("999",status=404)
|
||||
|
||||
@pytest.mark.skip(reason="test is passing and failing with different result_recordset_count values")
|
||||
def test_get_recordset_count_by_zoneid(shared_zone_test_context):
|
||||
"""
|
||||
Test getting a recordset with name @
|
||||
"""
|
||||
client = shared_zone_test_context.ok_vinyldns_client
|
||||
ok_zone = shared_zone_test_context.ok_zone
|
||||
result_rs = None
|
||||
try:
|
||||
new_rs = {
|
||||
"zoneId": ok_zone["id"],
|
||||
"name": "@",
|
||||
"type": "TXT",
|
||||
"ttl": 100,
|
||||
"records": [
|
||||
{
|
||||
"text": "someText"
|
||||
}
|
||||
]
|
||||
}
|
||||
result = client.create_recordset(new_rs, status=202)
|
||||
result_rs = client.wait_until_recordset_change_status(result, "Complete")["recordSet"]
|
||||
|
||||
# Get the recordset we just made and verify
|
||||
result = client.get_recordset(result_rs["zoneId"], result_rs["id"])
|
||||
result_recordset_count = client.get_recordset_count(result_rs["zoneId"],status=200)
|
||||
result_rs = result["recordSet"]
|
||||
|
||||
expected_rs = new_rs
|
||||
expected_rs["name"] = ok_zone["name"]
|
||||
verify_recordset(result_rs, expected_rs)
|
||||
|
||||
assert_that(result_recordset_count, is_({'count': 10}))
|
||||
finally:
|
||||
if result_rs:
|
||||
delete_result = client.delete_recordset(result_rs["zoneId"], result_rs["id"], status=202)
|
||||
client.wait_until_recordset_change_status(delete_result, "Complete")
|
||||
|
||||
|
||||
def test_at_get_recordset(shared_zone_test_context):
|
||||
"""
|
||||
Test getting a recordset with name @
|
||||
|
@ -30,6 +30,39 @@ def check_changes_response(response, recordChanges=False, nextId=False, startFro
|
||||
assert_that(change["userName"], is_("history-user"))
|
||||
|
||||
|
||||
def check_change_history_response(response, fqdn, type, recordChanges=False, nextId=False, startFrom=False,
|
||||
maxItems=100):
|
||||
"""
|
||||
:param type: type of the record
|
||||
:param fqdn: fqdn of the record
|
||||
:param response: return value of list_recordset_changes()
|
||||
:param recordChanges: true if not empty or False if empty, cannot check exact values because don't have access to all attributes
|
||||
:param nextId: true if exists, false if doesn't, wouldn't be able to check exact value
|
||||
:param startFrom: the string for startFrom or false if doesnt exist
|
||||
:param maxItems: maxItems is defined as an Int by default so will always return an Int
|
||||
"""
|
||||
assert_that(response, has_key("zoneId")) # always defined as random string
|
||||
if recordChanges:
|
||||
assert_that(response["recordSetChanges"], is_not(has_length(0)))
|
||||
else:
|
||||
assert_that(response["recordSetChanges"], has_length(0))
|
||||
if nextId:
|
||||
assert_that(response, has_key("nextId"))
|
||||
else:
|
||||
assert_that(response, is_not(has_key("nextId")))
|
||||
if startFrom:
|
||||
assert_that(response["startFrom"], is_(startFrom))
|
||||
else:
|
||||
assert_that(response, is_not(has_key("startFrom")))
|
||||
assert_that(response["maxItems"], is_(maxItems))
|
||||
|
||||
for change in response["recordSetChanges"]:
|
||||
assert_that(change["userName"], is_("history-user"))
|
||||
for recordset in change["recordSet"]:
|
||||
assert_that(change["recordSet"]["type"], is_(type))
|
||||
assert_that(change["recordSet"]["name"] + "." + response["recordSetChanges"][0]["zone"]["name"], is_(fqdn))
|
||||
|
||||
|
||||
def test_list_recordset_changes_no_authorization(shared_zone_test_context):
|
||||
"""
|
||||
Test that recordset changes without authorization fails
|
||||
@ -143,12 +176,12 @@ def test_list_recordset_changes_exhausted(shared_zone_test_context):
|
||||
|
||||
def test_list_recordset_returning_no_changes(shared_zone_test_context):
|
||||
"""
|
||||
Pass in startFrom of 0 should return empty list because start key is created time
|
||||
Pass in startFrom of "2000" should return empty list because start key exceeded number of recordset changes
|
||||
"""
|
||||
client = shared_zone_test_context.history_client
|
||||
original_zone = shared_zone_test_context.history_zone
|
||||
response = client.list_recordset_changes(original_zone["id"], start_from="0", max_items=None)
|
||||
check_changes_response(response, recordChanges=False, startFrom="0", nextId=False)
|
||||
response = client.list_recordset_changes(original_zone["id"], start_from=2000, max_items=None)
|
||||
check_changes_response(response, recordChanges=False, startFrom=2000, nextId=False)
|
||||
|
||||
|
||||
def test_list_recordset_changes_default_max_items(shared_zone_test_context):
|
||||
@ -174,3 +207,109 @@ def test_list_recordset_changes_max_items_boundaries(shared_zone_test_context):
|
||||
|
||||
assert_that(too_large, is_("maxItems was 101, maxItems must be between 0 exclusive and 100 inclusive"))
|
||||
assert_that(too_small, is_("maxItems was 0, maxItems must be between 0 exclusive and 100 inclusive"))
|
||||
|
||||
|
||||
def test_list_recordset_history_no_authorization(shared_zone_test_context):
|
||||
"""
|
||||
Test that recordset history without authorization fails
|
||||
"""
|
||||
client = shared_zone_test_context.history_client
|
||||
zone_id = shared_zone_test_context.history_zone["id"]
|
||||
fqdn = "test-create-cname-ok.system-test-history1."
|
||||
type = "CNAME"
|
||||
client.list_recordset_change_history(zone_id, fqdn, type, sign_request=False, status=401)
|
||||
|
||||
|
||||
def test_list_recordset_history_member_auth_success(shared_zone_test_context):
|
||||
"""
|
||||
Test recordset history succeeds with membership auth for member of admin group
|
||||
"""
|
||||
client = shared_zone_test_context.history_client
|
||||
zone_id = shared_zone_test_context.history_zone["id"]
|
||||
fqdn = "test-create-cname-ok.system-test-history1."
|
||||
type = "CNAME"
|
||||
response = client.list_recordset_change_history(zone_id, fqdn, type, status=200)
|
||||
check_change_history_response(response, fqdn, type, recordChanges=True, startFrom=False, nextId=False)
|
||||
|
||||
|
||||
def test_list_recordset_history_member_auth_no_access(shared_zone_test_context):
|
||||
"""
|
||||
Test recordset history fails for user not in admin group with no acl rules
|
||||
"""
|
||||
client = shared_zone_test_context.ok_vinyldns_client
|
||||
zone_id = shared_zone_test_context.history_zone["id"]
|
||||
fqdn = "test-create-cname-ok.system-test-history1."
|
||||
type = "CNAME"
|
||||
client.list_recordset_change_history(zone_id, fqdn, type, status=403)
|
||||
|
||||
|
||||
def test_list_recordset_history_success(shared_zone_test_context):
|
||||
"""
|
||||
Test recordset history succeeds with membership auth for member of admin group
|
||||
"""
|
||||
client = shared_zone_test_context.history_client
|
||||
zone_id = shared_zone_test_context.history_zone["id"]
|
||||
fqdn = "test-create-cname-ok.system-test-history1."
|
||||
type = "CNAME"
|
||||
response = client.list_recordset_change_history(zone_id, fqdn, type, status=200)
|
||||
check_change_history_response(response, fqdn, type, recordChanges=True, startFrom=False, nextId=False)
|
||||
|
||||
|
||||
def test_list_recordset_history_paging(shared_zone_test_context):
|
||||
"""
|
||||
Test paging for recordset history can use previous nextId as start key of next page
|
||||
"""
|
||||
client = shared_zone_test_context.history_client
|
||||
|
||||
zone_id = shared_zone_test_context.history_zone["id"]
|
||||
fqdn = "test-create-cname-ok.system-test-history1."
|
||||
type = "CNAME"
|
||||
|
||||
response_1 = client.list_recordset_change_history(zone_id, fqdn, type, start_from=None, max_items=1)
|
||||
response_2 = client.list_recordset_change_history(zone_id, fqdn, type, start_from=response_1["nextId"], max_items=1)
|
||||
|
||||
check_change_history_response(response_1, fqdn, type, recordChanges=True, nextId=True, startFrom=False, maxItems=1)
|
||||
check_change_history_response(response_2, fqdn, type, recordChanges=True, nextId=True, startFrom=response_1["nextId"], maxItems=1)
|
||||
|
||||
|
||||
def test_list_recordset_history_returning_no_changes(shared_zone_test_context):
|
||||
"""
|
||||
Pass in startFrom of "2000" should return empty list because start key exceeded number of recordset change history
|
||||
"""
|
||||
client = shared_zone_test_context.history_client
|
||||
zone_id = shared_zone_test_context.history_zone["id"]
|
||||
fqdn = "test-create-cname-ok.system-test-history1."
|
||||
type = "CNAME"
|
||||
response = client.list_recordset_change_history(zone_id, fqdn, type, start_from=2000, max_items=None)
|
||||
assert_that(response["recordSetChanges"], has_length(0))
|
||||
assert_that(response["startFrom"], is_(2000))
|
||||
assert_that(response["maxItems"], is_(100))
|
||||
|
||||
|
||||
def test_list_recordset_history_default_max_items(shared_zone_test_context):
|
||||
"""
|
||||
Test default max items is 100
|
||||
"""
|
||||
client = shared_zone_test_context.history_client
|
||||
zone_id = shared_zone_test_context.history_zone["id"]
|
||||
fqdn = "test-create-cname-ok.system-test-history1."
|
||||
type = "CNAME"
|
||||
|
||||
response = client.list_recordset_change_history(zone_id, fqdn, type, start_from=None, max_items=None)
|
||||
check_change_history_response(response, fqdn, type, recordChanges=True, startFrom=False, nextId=False, maxItems=100)
|
||||
|
||||
|
||||
def test_list_recordset_history_max_items_boundaries(shared_zone_test_context):
|
||||
"""
|
||||
Test 0 < max_items <= 100
|
||||
"""
|
||||
client = shared_zone_test_context.history_client
|
||||
zone_id = shared_zone_test_context.history_zone["id"]
|
||||
fqdn = "test-create-cname-ok.system-test-history1."
|
||||
type = "CNAME"
|
||||
|
||||
too_large = client.list_recordset_change_history(zone_id, fqdn, type, start_from=None, max_items=101, status=400)
|
||||
too_small = client.list_recordset_change_history(zone_id, fqdn, type, start_from=None, max_items=0, status=400)
|
||||
|
||||
assert_that(too_large, is_("maxItems was 101, maxItems must be between 0 exclusive and 100 inclusive"))
|
||||
assert_that(too_small, is_("maxItems was 0, maxItems must be between 0 exclusive and 100 inclusive"))
|
||||
|
@ -303,7 +303,7 @@ def test_update_recordset_replace_2_records_with_1_different_record(shared_zone_
|
||||
]
|
||||
}
|
||||
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()))
|
||||
@ -373,7 +373,7 @@ def test_update_existing_record_set_add_record(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()))
|
||||
@ -1593,8 +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']}\": "
|
||||
f"This record set is out of sync with the DNS backend; sync this zone before attempting to update this record set."))
|
||||
assert_that(response["systemMessage"], is_(f"This record set is out of sync with the DNS backend. Sync this zone before attempting to update this record set."))
|
||||
finally:
|
||||
try:
|
||||
delete_result = client.delete_recordset(zone["id"], create_rs["id"], status=202)
|
||||
@ -1789,6 +1788,115 @@ def test_update_from_unassociated_user_in_shared_zone_fails(shared_zone_test_con
|
||||
delete_result = shared_client.delete_recordset(zone["id"], create_rs["id"], status=202)
|
||||
shared_client.wait_until_recordset_change_status(delete_result, "Complete")
|
||||
|
||||
def test_update_from_super_user_in_shared_zone_passes_when_owner_group_is_only_update(shared_zone_test_context):
|
||||
"""
|
||||
Test that updating with a superuser passes when the zone is set to shared and the owner group is the only change
|
||||
"""
|
||||
super_user_client = shared_zone_test_context.super_user_client
|
||||
shared_record_group = shared_zone_test_context.shared_record_group
|
||||
dummy_group = shared_zone_test_context.dummy_group
|
||||
shared_client = shared_zone_test_context.shared_zone_vinyldns_client
|
||||
shared_zone = shared_zone_test_context.shared_zone
|
||||
update_rs = None
|
||||
|
||||
try:
|
||||
record_json = create_recordset(shared_zone, "test_shared_success", "A", [{"address": "1.1.1.1"}])
|
||||
record_json["ownerGroupId"] = shared_record_group["id"]
|
||||
create_response = shared_client.create_recordset(record_json, status=202)
|
||||
update = shared_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
|
||||
assert_that(update["ownerGroupId"], is_(shared_record_group["id"]))
|
||||
|
||||
update["ownerGroupId"] = dummy_group["id"]
|
||||
update_response = super_user_client.update_recordset(update, status=202)
|
||||
update_rs = shared_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"]
|
||||
assert_that(update_rs["ownerGroupId"], is_(dummy_group["id"]))
|
||||
finally:
|
||||
if update_rs:
|
||||
delete_result = shared_client.delete_recordset(shared_zone["id"], update_rs["id"], status=202)
|
||||
shared_client.wait_until_recordset_change_status(delete_result, "Complete")
|
||||
|
||||
def test_update_from_unassociated_user_in_shared_zone_fails_when_owner_group_is_only_update(shared_zone_test_context):
|
||||
"""
|
||||
Test that updating with a user not in the record owner group fails when the zone is set to shared and
|
||||
the owner group is the only change
|
||||
"""
|
||||
ok_client = shared_zone_test_context.ok_vinyldns_client
|
||||
shared_record_group = shared_zone_test_context.shared_record_group
|
||||
dummy_group = shared_zone_test_context.dummy_group
|
||||
shared_client = shared_zone_test_context.shared_zone_vinyldns_client
|
||||
shared_zone = shared_zone_test_context.shared_zone
|
||||
create_rs = None
|
||||
|
||||
try:
|
||||
record_json = create_recordset(shared_zone, "test_shared_fail", "A", [{"address": "1.1.1.1"}])
|
||||
record_json["ownerGroupId"] = shared_record_group["id"]
|
||||
create_response = shared_client.create_recordset(record_json, status=202)
|
||||
create_rs = shared_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
|
||||
assert_that(create_rs["ownerGroupId"], is_(shared_record_group["id"]))
|
||||
|
||||
update = create_rs
|
||||
update["ownerGroupId"] = dummy_group["id"]
|
||||
error = ok_client.update_recordset(update, status=422)
|
||||
assert_that(error, is_(f"User not in record owner group with id \"{dummy_group['id']}\""))
|
||||
finally:
|
||||
if create_rs:
|
||||
delete_result = shared_client.delete_recordset(shared_zone["id"], create_rs["id"], status=202)
|
||||
shared_client.wait_until_recordset_change_status(delete_result, "Complete")
|
||||
|
||||
def test_update_from_super_user_in_private_zone_fails_when_owner_group_is_only_update(shared_zone_test_context):
|
||||
"""
|
||||
Test that updating with a superuser fails when the zone is set to private and the owner group is the only change
|
||||
"""
|
||||
ok_client = shared_zone_test_context.ok_vinyldns_client
|
||||
super_user_client = shared_zone_test_context.super_user_client
|
||||
ok_record_group = shared_zone_test_context.ok_group
|
||||
dummy_group = shared_zone_test_context.dummy_group
|
||||
ok_zone = shared_zone_test_context.ok_zone
|
||||
create_rs = None
|
||||
|
||||
try:
|
||||
record_json = create_recordset(ok_zone, "test_private_fail", "A", [{"address": "1.1.1.1"}])
|
||||
record_json["ownerGroupId"] = ok_record_group["id"]
|
||||
create_response = ok_client.create_recordset(record_json, status=202)
|
||||
create_rs = ok_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
|
||||
assert_that(create_rs["ownerGroupId"], is_(ok_record_group["id"]))
|
||||
|
||||
update = create_rs
|
||||
update["ownerGroupId"] = dummy_group["id"]
|
||||
error = super_user_client.update_recordset(update, status=403)
|
||||
assert_that(error, is_(f'User super-user does not have access to update test-private-fail.{ok_zone["name"]}'))
|
||||
finally:
|
||||
if create_rs:
|
||||
delete_result = ok_client.delete_recordset(ok_zone["id"], create_rs["id"], status=202)
|
||||
ok_client.wait_until_recordset_change_status(delete_result, "Complete")
|
||||
|
||||
def test_update_from_super_user_in_shared_zone_fails_when_owner_group_is_not_the_only_update(shared_zone_test_context):
|
||||
"""
|
||||
Test that updating with a superuser fails when the zone is set to shared and the owner group is not the only change
|
||||
"""
|
||||
super_user_client = shared_zone_test_context.super_user_client
|
||||
shared_record_group = shared_zone_test_context.shared_record_group
|
||||
dummy_group = shared_zone_test_context.dummy_group
|
||||
shared_client = shared_zone_test_context.shared_zone_vinyldns_client
|
||||
shared_zone = shared_zone_test_context.shared_zone
|
||||
create_rs = None
|
||||
|
||||
try:
|
||||
record_json = create_recordset(shared_zone, "test_shared_fail", "A", [{"address": "1.1.1.1"}])
|
||||
record_json["ownerGroupId"] = shared_record_group["id"]
|
||||
create_response = shared_client.create_recordset(record_json, status=202)
|
||||
create_rs = shared_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
|
||||
assert_that(create_rs["ownerGroupId"], is_(shared_record_group["id"]))
|
||||
|
||||
update = create_rs
|
||||
update["ownerGroupId"] = dummy_group["id"]
|
||||
update["ttl"] = update["ttl"] + 100
|
||||
error = super_user_client.update_recordset(update, status=403)
|
||||
assert_that(error, is_(f'User super-user does not have access to update test-shared-fail.{shared_zone["name"]}'))
|
||||
finally:
|
||||
if create_rs:
|
||||
delete_result = shared_client.delete_recordset(shared_zone["id"], create_rs["id"], status=202)
|
||||
shared_client.wait_until_recordset_change_status(delete_result, "Complete")
|
||||
|
||||
@pytest.mark.serial
|
||||
def test_update_from_acl_for_shared_zone_passes(shared_zone_test_context):
|
||||
@ -1819,6 +1927,572 @@ def test_update_from_acl_for_shared_zone_passes(shared_zone_test_context):
|
||||
delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
|
||||
shared_client.wait_until_recordset_change_status(delete_result, "Complete")
|
||||
|
||||
def test_update_owner_group_transfer_auto_approved(shared_zone_test_context):
|
||||
"""
|
||||
Test auto approve ownerShip transfer, for shared zones
|
||||
"""
|
||||
shared_client = shared_zone_test_context.shared_zone_vinyldns_client
|
||||
zone = shared_zone_test_context.shared_zone
|
||||
shared_group = shared_zone_test_context.shared_record_group
|
||||
ok_group = shared_zone_test_context.ok_group
|
||||
|
||||
update_rs = None
|
||||
|
||||
try:
|
||||
record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}])
|
||||
record_json["ownerGroupId"] = shared_group["id"]
|
||||
create_response = shared_client.create_recordset(record_json, status=202)
|
||||
update = shared_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
|
||||
assert_that(update["ownerGroupId"], is_(shared_group["id"]))
|
||||
|
||||
recordset_group_change_json = {"ownerShipTransferStatus": "AutoApproved",
|
||||
"requestedOwnerGroupId": ok_group["id"]}
|
||||
|
||||
update["recordSetGroupChange"] = recordset_group_change_json
|
||||
update_response = shared_client.update_recordset(update, status=202)
|
||||
update_rs = shared_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"]
|
||||
assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_json))
|
||||
assert_that(update_rs["ownerGroupId"], is_(ok_group["id"]))
|
||||
finally:
|
||||
if update_rs:
|
||||
delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
|
||||
shared_client.wait_until_recordset_change_status(delete_result, "Complete")
|
||||
|
||||
|
||||
def test_update_owner_group_transfer_request(shared_zone_test_context):
|
||||
"""
|
||||
Test requesting ownerShip transfer, for shared zones
|
||||
"""
|
||||
shared_client = shared_zone_test_context.shared_zone_vinyldns_client
|
||||
dummy_client = shared_zone_test_context.dummy_vinyldns_client
|
||||
zone = shared_zone_test_context.shared_zone
|
||||
shared_group = shared_zone_test_context.shared_record_group
|
||||
dummy_group = shared_zone_test_context.dummy_group
|
||||
|
||||
update_rs = None
|
||||
|
||||
try:
|
||||
record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}])
|
||||
record_json["ownerGroupId"] = dummy_group["id"]
|
||||
|
||||
create_response = dummy_client.create_recordset(record_json, status=202)
|
||||
update = dummy_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
|
||||
assert_that(update["ownerGroupId"], is_(dummy_group["id"]))
|
||||
|
||||
recordset_group_change_json = {"ownerShipTransferStatus": "Requested",
|
||||
"requestedOwnerGroupId": shared_group["id"]}
|
||||
recordset_group_change_pending_review_json = {"ownerShipTransferStatus": "PendingReview",
|
||||
"requestedOwnerGroupId": shared_group["id"]}
|
||||
update["recordSetGroupChange"] = recordset_group_change_json
|
||||
|
||||
update_response = shared_client.update_recordset(update, status=202)
|
||||
update_rs = shared_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"]
|
||||
assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_pending_review_json))
|
||||
assert_that(update_rs["ownerGroupId"], is_(dummy_group["id"]))
|
||||
|
||||
finally:
|
||||
if update_rs:
|
||||
delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
|
||||
shared_client.wait_until_recordset_change_status(delete_result, "Complete")
|
||||
|
||||
|
||||
def test_update_request_owner_group_transfer_manually_approved(shared_zone_test_context):
|
||||
"""
|
||||
Test approving ownerShip transfer request, for shared zones
|
||||
"""
|
||||
shared_client = shared_zone_test_context.shared_zone_vinyldns_client
|
||||
ok_client = shared_zone_test_context.ok_vinyldns_client
|
||||
zone = shared_zone_test_context.shared_zone
|
||||
shared_group = shared_zone_test_context.shared_record_group
|
||||
ok_group = shared_zone_test_context.ok_group
|
||||
|
||||
update_rs = None
|
||||
|
||||
try:
|
||||
record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}])
|
||||
record_json["ownerGroupId"] = shared_group["id"]
|
||||
|
||||
create_response = shared_client.create_recordset(record_json, status=202)
|
||||
update = shared_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
|
||||
assert_that(update["ownerGroupId"], is_(shared_group["id"]))
|
||||
|
||||
recordset_group_change_json = {"ownerShipTransferStatus": "Requested",
|
||||
"requestedOwnerGroupId": ok_group["id"]}
|
||||
recordset_group_change_pending_review_json = {"ownerShipTransferStatus": "PendingReview",
|
||||
"requestedOwnerGroupId": ok_group["id"]}
|
||||
update["recordSetGroupChange"] = recordset_group_change_json
|
||||
|
||||
update_response = ok_client.update_recordset(update, status=202)
|
||||
update_rs = ok_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"]
|
||||
assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_pending_review_json))
|
||||
assert_that(update_rs["ownerGroupId"], is_(shared_group["id"]))
|
||||
|
||||
recordset_group_change_json = {"ownerShipTransferStatus": "ManuallyApproved"}
|
||||
recordset_group_change_manually_approved_json = {"ownerShipTransferStatus": "ManuallyApproved",
|
||||
"requestedOwnerGroupId": ok_group["id"]}
|
||||
update_rs["recordSetGroupChange"] = recordset_group_change_json
|
||||
update_rs_response = shared_client.update_recordset(update_rs, status=202)
|
||||
update_rs_ownership = shared_client.wait_until_recordset_change_status(update_rs_response, "Complete")[
|
||||
"recordSet"]
|
||||
assert_that(update_rs_ownership["recordSetGroupChange"], is_(recordset_group_change_manually_approved_json))
|
||||
assert_that(update_rs_ownership["ownerGroupId"], is_(ok_group["id"]))
|
||||
|
||||
finally:
|
||||
if update_rs:
|
||||
delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
|
||||
shared_client.wait_until_recordset_change_status(delete_result, "Complete")
|
||||
|
||||
|
||||
def test_update_request_owner_group_transfer_manually_rejected(shared_zone_test_context):
|
||||
"""
|
||||
Test rejecting ownerShip transfer request, for shared zones
|
||||
"""
|
||||
shared_client = shared_zone_test_context.shared_zone_vinyldns_client
|
||||
ok_client = shared_zone_test_context.ok_vinyldns_client
|
||||
zone = shared_zone_test_context.shared_zone
|
||||
shared_group = shared_zone_test_context.shared_record_group
|
||||
ok_group = shared_zone_test_context.ok_group
|
||||
|
||||
update_rs = None
|
||||
|
||||
try:
|
||||
record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}])
|
||||
record_json["ownerGroupId"] = shared_group["id"]
|
||||
|
||||
create_response = shared_client.create_recordset(record_json, status=202)
|
||||
update = shared_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
|
||||
assert_that(update["ownerGroupId"], is_(shared_group["id"]))
|
||||
|
||||
recordset_group_change_json = {"ownerShipTransferStatus": "Requested",
|
||||
"requestedOwnerGroupId": ok_group["id"]}
|
||||
recordset_group_change_pending_review_json = {"ownerShipTransferStatus": "PendingReview",
|
||||
"requestedOwnerGroupId": ok_group["id"]}
|
||||
update["recordSetGroupChange"] = recordset_group_change_json
|
||||
|
||||
update_response = ok_client.update_recordset(update, status=202)
|
||||
update_rs = ok_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"]
|
||||
assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_pending_review_json))
|
||||
assert_that(update_rs["ownerGroupId"], is_(shared_group["id"]))
|
||||
|
||||
recordset_group_change_json = {"ownerShipTransferStatus": "ManuallyRejected"}
|
||||
recordset_group_change_manually_rejected_json = {"ownerShipTransferStatus": "ManuallyRejected",
|
||||
"requestedOwnerGroupId": ok_group["id"]}
|
||||
update_rs["recordSetGroupChange"] = recordset_group_change_json
|
||||
update_rs_response = shared_client.update_recordset(update_rs, status=202)
|
||||
update_rs_ownership = shared_client.wait_until_recordset_change_status(update_rs_response, "Complete")[
|
||||
"recordSet"]
|
||||
assert_that(update_rs_ownership["recordSetGroupChange"], is_(recordset_group_change_manually_rejected_json))
|
||||
assert_that(update_rs_ownership["ownerGroupId"], is_(shared_group["id"]))
|
||||
|
||||
finally:
|
||||
if update_rs:
|
||||
delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
|
||||
shared_client.wait_until_recordset_change_status(delete_result, "Complete")
|
||||
|
||||
|
||||
def test_update_request_owner_group_transfer_cancelled(shared_zone_test_context):
|
||||
"""
|
||||
Test cancelling ownerShip transfer request
|
||||
"""
|
||||
shared_client = shared_zone_test_context.shared_zone_vinyldns_client
|
||||
ok_client = shared_zone_test_context.ok_vinyldns_client
|
||||
zone = shared_zone_test_context.shared_zone
|
||||
shared_group = shared_zone_test_context.shared_record_group
|
||||
ok_group = shared_zone_test_context.ok_group
|
||||
|
||||
update_rs = None
|
||||
|
||||
try:
|
||||
record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}])
|
||||
record_json["ownerGroupId"] = shared_group["id"]
|
||||
|
||||
create_response = shared_client.create_recordset(record_json, status=202)
|
||||
update = shared_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
|
||||
assert_that(update["ownerGroupId"], is_(shared_group["id"]))
|
||||
|
||||
recordset_group_change_json = {"ownerShipTransferStatus": "Requested",
|
||||
"requestedOwnerGroupId": ok_group["id"]}
|
||||
recordset_group_change_pending_review_json = {"ownerShipTransferStatus": "PendingReview",
|
||||
"requestedOwnerGroupId": ok_group["id"]}
|
||||
update["recordSetGroupChange"] = recordset_group_change_json
|
||||
|
||||
update_response = ok_client.update_recordset(update, status=202)
|
||||
update_rs = ok_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"]
|
||||
assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_pending_review_json))
|
||||
assert_that(update_rs["ownerGroupId"], is_(shared_group["id"]))
|
||||
|
||||
recordset_group_change_json = {"ownerShipTransferStatus": "Cancelled"}
|
||||
recordset_group_change_cancelled_json = {"ownerShipTransferStatus": "Cancelled",
|
||||
"requestedOwnerGroupId": ok_group["id"]}
|
||||
update_rs["recordSetGroupChange"] = recordset_group_change_json
|
||||
update_rs_response = ok_client.update_recordset(update_rs, status=202)
|
||||
update_rs_ownership = ok_client.wait_until_recordset_change_status(update_rs_response, "Complete")["recordSet"]
|
||||
assert_that(update_rs_ownership["recordSetGroupChange"], is_(recordset_group_change_cancelled_json))
|
||||
assert_that(update_rs_ownership["ownerGroupId"], is_(shared_group["id"]))
|
||||
|
||||
finally:
|
||||
if update_rs:
|
||||
delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
|
||||
shared_client.wait_until_recordset_change_status(delete_result, "Complete")
|
||||
|
||||
|
||||
def test_update_owner_group_transfer_approval_to_group_a_user_is_not_in_fails(shared_zone_test_context):
|
||||
"""
|
||||
Test approving ownerShip transfer request, for user not a member of owner group
|
||||
"""
|
||||
shared_client = shared_zone_test_context.shared_zone_vinyldns_client
|
||||
dummy_client = shared_zone_test_context.dummy_vinyldns_client
|
||||
zone = shared_zone_test_context.shared_zone
|
||||
shared_group = shared_zone_test_context.shared_record_group
|
||||
dummy_group = shared_zone_test_context.dummy_group
|
||||
|
||||
update_rs = None
|
||||
|
||||
try:
|
||||
record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}])
|
||||
record_json["ownerGroupId"] = dummy_group["id"]
|
||||
|
||||
create_response = dummy_client.create_recordset(record_json, status=202)
|
||||
update = dummy_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
|
||||
assert_that(update["ownerGroupId"], is_(dummy_group["id"]))
|
||||
|
||||
recordset_group_change_json = {"ownerShipTransferStatus": "Requested",
|
||||
"requestedOwnerGroupId": shared_group["id"]}
|
||||
recordset_group_change_pending_review_json = {"ownerShipTransferStatus": "PendingReview",
|
||||
"requestedOwnerGroupId": shared_group["id"]}
|
||||
update["recordSetGroupChange"] = recordset_group_change_json
|
||||
|
||||
update_response = shared_client.update_recordset(update, status=202)
|
||||
update_rs = shared_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"]
|
||||
assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_pending_review_json))
|
||||
assert_that(update_rs["ownerGroupId"], is_(dummy_group["id"]))
|
||||
|
||||
recordset_group_change_approved_json = {"ownerShipTransferStatus": "ManuallyApproved"}
|
||||
|
||||
update_rs["recordSetGroupChange"] = recordset_group_change_approved_json
|
||||
error = shared_client.update_recordset(update_rs, status=422)
|
||||
assert_that(error, is_(f"User not in record owner group with id \"{dummy_group['id']}\""))
|
||||
|
||||
finally:
|
||||
if update_rs:
|
||||
delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
|
||||
shared_client.wait_until_recordset_change_status(delete_result, "Complete")
|
||||
|
||||
|
||||
def test_update_owner_group_transfer_reject_to_group_a_user_is_not_in_fails(shared_zone_test_context):
|
||||
"""
|
||||
Test rejecting ownerShip transfer request, for user not a member of owner group
|
||||
"""
|
||||
shared_client = shared_zone_test_context.shared_zone_vinyldns_client
|
||||
dummy_client = shared_zone_test_context.dummy_vinyldns_client
|
||||
zone = shared_zone_test_context.shared_zone
|
||||
shared_group = shared_zone_test_context.shared_record_group
|
||||
dummy_group = shared_zone_test_context.dummy_group
|
||||
|
||||
update_rs = None
|
||||
|
||||
try:
|
||||
record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}])
|
||||
record_json["ownerGroupId"] = dummy_group["id"]
|
||||
|
||||
create_response = dummy_client.create_recordset(record_json, status=202)
|
||||
update = dummy_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
|
||||
assert_that(update["ownerGroupId"], is_(dummy_group["id"]))
|
||||
|
||||
recordset_group_change_json = {"ownerShipTransferStatus": "Requested",
|
||||
"requestedOwnerGroupId": shared_group["id"]}
|
||||
recordset_group_change_pending_review_json = {"ownerShipTransferStatus": "PendingReview",
|
||||
"requestedOwnerGroupId": shared_group["id"]}
|
||||
update["recordSetGroupChange"] = recordset_group_change_json
|
||||
|
||||
update_response = shared_client.update_recordset(update, status=202)
|
||||
update_rs = shared_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"]
|
||||
assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_pending_review_json))
|
||||
assert_that(update_rs["ownerGroupId"], is_(dummy_group["id"]))
|
||||
|
||||
recordset_group_change_approved_json = {"ownerShipTransferStatus": "ManuallyRejected"}
|
||||
update_rs["recordSetGroupChange"] = recordset_group_change_approved_json
|
||||
error = shared_client.update_recordset(update_rs, status=422)
|
||||
assert_that(error, is_(f"User not in record owner group with id \"{dummy_group['id']}\""))
|
||||
|
||||
finally:
|
||||
if update_rs:
|
||||
delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
|
||||
shared_client.wait_until_recordset_change_status(delete_result, "Complete")
|
||||
|
||||
|
||||
def test_update_owner_group_transfer_auto_approved_to_group_a_user_is_not_in_fails(shared_zone_test_context):
|
||||
"""
|
||||
Test approving ownerShip transfer request, for user not a member of owner group
|
||||
"""
|
||||
shared_client = shared_zone_test_context.shared_zone_vinyldns_client
|
||||
dummy_client = shared_zone_test_context.dummy_vinyldns_client
|
||||
zone = shared_zone_test_context.shared_zone
|
||||
shared_group = shared_zone_test_context.shared_record_group
|
||||
dummy_group = shared_zone_test_context.dummy_group
|
||||
|
||||
update_rs = None
|
||||
|
||||
try:
|
||||
record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}])
|
||||
record_json["ownerGroupId"] = dummy_group["id"]
|
||||
|
||||
create_response = dummy_client.create_recordset(record_json, status=202)
|
||||
update = dummy_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
|
||||
assert_that(update["ownerGroupId"], is_(dummy_group["id"]))
|
||||
|
||||
recordset_group_change_json = {"ownerShipTransferStatus": "Requested",
|
||||
"requestedOwnerGroupId": shared_group["id"]}
|
||||
recordset_group_change_pending_review_json = {"ownerShipTransferStatus": "PendingReview",
|
||||
"requestedOwnerGroupId": shared_group["id"]}
|
||||
update["recordSetGroupChange"] = recordset_group_change_json
|
||||
|
||||
update_response = shared_client.update_recordset(update, status=202)
|
||||
update_rs = shared_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"]
|
||||
assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_pending_review_json))
|
||||
assert_that(update_rs["ownerGroupId"], is_(dummy_group["id"]))
|
||||
|
||||
recordset_group_change_approved_json = {"ownerShipTransferStatus": "AutoApproved"}
|
||||
|
||||
update_rs["recordSetGroupChange"] = recordset_group_change_approved_json
|
||||
error = shared_client.update_recordset(update_rs, status=422)
|
||||
assert_that(error, is_(f"Record owner group with id \"{dummy_group['id']}\" not found"))
|
||||
|
||||
finally:
|
||||
if update_rs:
|
||||
delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
|
||||
shared_client.wait_until_recordset_change_status(delete_result, "Complete")
|
||||
|
||||
|
||||
def test_update_owner_group_transfer_approved_when_request_cancelled_in_fails(shared_zone_test_context):
|
||||
"""
|
||||
Test approving ownerShip transfer, for cancelled request
|
||||
"""
|
||||
shared_client = shared_zone_test_context.shared_zone_vinyldns_client
|
||||
ok_client = shared_zone_test_context.ok_vinyldns_client
|
||||
zone = shared_zone_test_context.shared_zone
|
||||
shared_group = shared_zone_test_context.shared_record_group
|
||||
ok_group = shared_zone_test_context.ok_group
|
||||
|
||||
update_rs = None
|
||||
|
||||
try:
|
||||
record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}])
|
||||
record_json["ownerGroupId"] = shared_group["id"]
|
||||
|
||||
create_response = shared_client.create_recordset(record_json, status=202)
|
||||
update = shared_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
|
||||
assert_that(update["ownerGroupId"], is_(shared_group["id"]))
|
||||
|
||||
recordset_group_change_json = {"ownerShipTransferStatus": "Requested",
|
||||
"requestedOwnerGroupId": ok_group["id"]}
|
||||
recordset_group_change_pending_review_json = {"ownerShipTransferStatus": "PendingReview",
|
||||
"requestedOwnerGroupId": ok_group["id"]}
|
||||
update["recordSetGroupChange"] = recordset_group_change_json
|
||||
|
||||
update_response = ok_client.update_recordset(update, status=202)
|
||||
update_rs = ok_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"]
|
||||
assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_pending_review_json))
|
||||
assert_that(update_rs["ownerGroupId"], is_(shared_group["id"]))
|
||||
|
||||
recordset_group_change_json = {"ownerShipTransferStatus": "Cancelled"}
|
||||
recordset_group_change_cancelled_json = {"ownerShipTransferStatus": "Cancelled",
|
||||
"requestedOwnerGroupId": ok_group["id"]}
|
||||
update_rs["recordSetGroupChange"] = recordset_group_change_json
|
||||
update_rs_response = ok_client.update_recordset(update_rs, status=202)
|
||||
update_rs_ownership = ok_client.wait_until_recordset_change_status(update_rs_response, "Complete")["recordSet"]
|
||||
assert_that(update_rs_ownership["recordSetGroupChange"], is_(recordset_group_change_cancelled_json))
|
||||
assert_that(update_rs_ownership["ownerGroupId"], is_(shared_group["id"]))
|
||||
|
||||
recordset_group_change_json = {"ownerShipTransferStatus": "ManuallyApproved"}
|
||||
|
||||
update_rs["recordSetGroupChange"] = recordset_group_change_json
|
||||
error = ok_client.update_recordset(update_rs, status=422)
|
||||
assert_that(error, is_("Cannot update RecordSet OwnerShip Status when request is cancelled."))
|
||||
|
||||
finally:
|
||||
if update_rs:
|
||||
delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
|
||||
shared_client.wait_until_recordset_change_status(delete_result, "Complete")
|
||||
|
||||
|
||||
def test_update_owner_group_transfer_rejected_when_request_cancelled_in_fails(shared_zone_test_context):
|
||||
"""
|
||||
Test rejecting ownerShip transfer, for cancelled request
|
||||
"""
|
||||
shared_client = shared_zone_test_context.shared_zone_vinyldns_client
|
||||
ok_client = shared_zone_test_context.ok_vinyldns_client
|
||||
zone = shared_zone_test_context.shared_zone
|
||||
shared_group = shared_zone_test_context.shared_record_group
|
||||
ok_group = shared_zone_test_context.ok_group
|
||||
|
||||
update_rs = None
|
||||
|
||||
try:
|
||||
record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}])
|
||||
record_json["ownerGroupId"] = shared_group["id"]
|
||||
|
||||
create_response = shared_client.create_recordset(record_json, status=202)
|
||||
update = shared_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
|
||||
assert_that(update["ownerGroupId"], is_(shared_group["id"]))
|
||||
|
||||
recordset_group_change_json = {"ownerShipTransferStatus": "Requested",
|
||||
"requestedOwnerGroupId": ok_group["id"]}
|
||||
recordset_group_change_pending_review_json = {"ownerShipTransferStatus": "PendingReview",
|
||||
"requestedOwnerGroupId": ok_group["id"]}
|
||||
update["recordSetGroupChange"] = recordset_group_change_json
|
||||
|
||||
update_response = ok_client.update_recordset(update, status=202)
|
||||
update_rs = ok_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"]
|
||||
assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_pending_review_json))
|
||||
assert_that(update_rs["ownerGroupId"], is_(shared_group["id"]))
|
||||
|
||||
recordset_group_change_json = {"ownerShipTransferStatus": "Cancelled"}
|
||||
recordset_group_change_cancelled_json = {"ownerShipTransferStatus": "Cancelled",
|
||||
"requestedOwnerGroupId": ok_group["id"]}
|
||||
update_rs["recordSetGroupChange"] = recordset_group_change_json
|
||||
update_rs_response = ok_client.update_recordset(update_rs, status=202)
|
||||
update_rs_ownership = ok_client.wait_until_recordset_change_status(update_rs_response, "Complete")["recordSet"]
|
||||
assert_that(update_rs_ownership["recordSetGroupChange"], is_(recordset_group_change_cancelled_json))
|
||||
assert_that(update_rs_ownership["ownerGroupId"], is_(shared_group["id"]))
|
||||
|
||||
recordset_group_change_json = {"ownerShipTransferStatus": "ManuallyRejected"}
|
||||
|
||||
update_rs["recordSetGroupChange"] = recordset_group_change_json
|
||||
error = ok_client.update_recordset(update_rs, status=422)
|
||||
assert_that(error, is_("Cannot update RecordSet OwnerShip Status when request is cancelled."))
|
||||
|
||||
finally:
|
||||
if update_rs:
|
||||
delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
|
||||
shared_client.wait_until_recordset_change_status(delete_result, "Complete")
|
||||
|
||||
|
||||
def test_update_owner_group_transfer_auto_approved_when_request_cancelled_in_fails(shared_zone_test_context):
|
||||
"""
|
||||
Test auto_approving ownerShip transfer, for cancelled request
|
||||
"""
|
||||
shared_client = shared_zone_test_context.shared_zone_vinyldns_client
|
||||
ok_client = shared_zone_test_context.ok_vinyldns_client
|
||||
zone = shared_zone_test_context.shared_zone
|
||||
shared_group = shared_zone_test_context.shared_record_group
|
||||
ok_group = shared_zone_test_context.ok_group
|
||||
|
||||
update_rs = None
|
||||
|
||||
try:
|
||||
record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}])
|
||||
record_json["ownerGroupId"] = shared_group["id"]
|
||||
|
||||
create_response = shared_client.create_recordset(record_json, status=202)
|
||||
update = shared_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
|
||||
assert_that(update["ownerGroupId"], is_(shared_group["id"]))
|
||||
|
||||
recordset_group_change_json = {"ownerShipTransferStatus": "Requested",
|
||||
"requestedOwnerGroupId": ok_group["id"]}
|
||||
recordset_group_change_pending_review_json = {"ownerShipTransferStatus": "PendingReview",
|
||||
"requestedOwnerGroupId": ok_group["id"]}
|
||||
update["recordSetGroupChange"] = recordset_group_change_json
|
||||
|
||||
update_response = ok_client.update_recordset(update, status=202)
|
||||
update_rs = ok_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"]
|
||||
assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_pending_review_json))
|
||||
assert_that(update_rs["ownerGroupId"], is_(shared_group["id"]))
|
||||
|
||||
recordset_group_change_json = {"ownerShipTransferStatus": "Cancelled"}
|
||||
recordset_group_change_cancelled_json = {"ownerShipTransferStatus": "Cancelled",
|
||||
"requestedOwnerGroupId": ok_group["id"]}
|
||||
update_rs["recordSetGroupChange"] = recordset_group_change_json
|
||||
update_rs_response = ok_client.update_recordset(update_rs, status=202)
|
||||
update_rs_ownership = ok_client.wait_until_recordset_change_status(update_rs_response, "Complete")["recordSet"]
|
||||
assert_that(update_rs_ownership["recordSetGroupChange"], is_(recordset_group_change_cancelled_json))
|
||||
assert_that(update_rs_ownership["ownerGroupId"], is_(shared_group["id"]))
|
||||
|
||||
recordset_group_change_json = {"ownerShipTransferStatus": "AutoApproved"}
|
||||
|
||||
update_rs["recordSetGroupChange"] = recordset_group_change_json
|
||||
error = ok_client.update_recordset(update_rs, status=422)
|
||||
assert_that(error, is_("Cannot update RecordSet OwnerShip Status when request is cancelled."))
|
||||
|
||||
finally:
|
||||
if update_rs:
|
||||
delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
|
||||
shared_client.wait_until_recordset_change_status(delete_result, "Complete")
|
||||
|
||||
|
||||
def test_update_owner_group_transfer_on_non_shared_zones_in_fails(shared_zone_test_context):
|
||||
"""
|
||||
Test that requesting ownerShip transfer for non shared zones
|
||||
"""
|
||||
shared_client = shared_zone_test_context.shared_zone_vinyldns_client
|
||||
ok_client = shared_zone_test_context.ok_vinyldns_client
|
||||
shared_group = shared_zone_test_context.shared_record_group
|
||||
ok_zone = shared_zone_test_context.ok_zone
|
||||
|
||||
update_rs = None
|
||||
|
||||
try:
|
||||
# record_json = create_recordset(ok_zone, "test_update_success", "A", [{"address": "1.1.1.1"}], recordSetGroupChange={"ownerShipTransferStatus": None, "requestedOwnerGroupId": None})
|
||||
record_json = {
|
||||
"zoneId": ok_zone["id"],
|
||||
"name": "test_update_success",
|
||||
"type": "A",
|
||||
"ttl": 38400,
|
||||
"records": [
|
||||
{"address": "1.1.1.1"}
|
||||
],
|
||||
"recordSetGroupChange": {"ownerShipTransferStatus": "None",
|
||||
"requestedOwnerGroupId": None}
|
||||
}
|
||||
|
||||
create_response = ok_client.create_recordset(record_json, status=202)
|
||||
update = ok_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
|
||||
|
||||
recordset_group_change_json = {"ownerShipTransferStatus": "Requested",
|
||||
"requestedOwnerGroupId": shared_group["id"]}
|
||||
update["recordSetGroupChange"] = recordset_group_change_json
|
||||
|
||||
error = shared_client.update_recordset(update, status=422)
|
||||
assert_that(error, is_("Cannot update RecordSet OwnerShip Status when zone is not shared."))
|
||||
|
||||
finally:
|
||||
if update_rs:
|
||||
delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
|
||||
shared_client.wait_until_recordset_change_status(delete_result, "Complete")
|
||||
|
||||
|
||||
def test_update_owner_group_transfer_and_ttl_on_user_not_in_owner_group_in_fails(shared_zone_test_context):
|
||||
"""
|
||||
Test that updating record "i.e.ttl" with requesting ownerShip transfer, where user not in the member of the owner group
|
||||
"""
|
||||
shared_client = shared_zone_test_context.shared_zone_vinyldns_client
|
||||
dummy_client = shared_zone_test_context.dummy_vinyldns_client
|
||||
zone = shared_zone_test_context.shared_zone
|
||||
shared_group = shared_zone_test_context.shared_record_group
|
||||
dummy_group = shared_zone_test_context.dummy_group
|
||||
update_rs = None
|
||||
|
||||
try:
|
||||
record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}])
|
||||
record_json["ownerGroupId"] = shared_group["id"]
|
||||
|
||||
create_response = shared_client.create_recordset(record_json, status=202)
|
||||
update = shared_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
|
||||
assert_that(update["ownerGroupId"], is_(shared_group["id"]))
|
||||
|
||||
recordset_group_change_json = {"ownerShipTransferStatus": "Requested",
|
||||
"requestedOwnerGroupId": dummy_group["id"]}
|
||||
update["recordSetGroupChange"] = recordset_group_change_json
|
||||
update["ttl"] = update["ttl"] + 100
|
||||
|
||||
error = dummy_client.update_recordset(update, status=422)
|
||||
assert_that(error, is_(f"Cannot update RecordSet's if user not a member of ownership group. User can only request for ownership transfer"))
|
||||
|
||||
finally:
|
||||
if update_rs:
|
||||
delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
|
||||
shared_client.wait_until_recordset_change_status(delete_result, "Complete")
|
||||
|
||||
|
||||
def test_update_to_no_group_owner_passes(shared_zone_test_context):
|
||||
"""
|
||||
|
@ -28,12 +28,14 @@ class SharedZoneTestContext(object):
|
||||
self.dummy_vinyldns_client = VinylDNSClient(VinylDNSTestContext.vinyldns_url, "dummyAccessKey", "dummySecretKey")
|
||||
self.shared_zone_vinyldns_client = VinylDNSClient(VinylDNSTestContext.vinyldns_url, "sharedZoneUserAccessKey", "sharedZoneUserSecretKey")
|
||||
self.support_user_client = VinylDNSClient(VinylDNSTestContext.vinyldns_url, "supportUserAccessKey", "supportUserSecretKey")
|
||||
self.super_user_client = VinylDNSClient(VinylDNSTestContext.vinyldns_url, "superUserAccessKey", "superUserSecretKey")
|
||||
self.unassociated_client = VinylDNSClient(VinylDNSTestContext.vinyldns_url, "listGroupAccessKey", "listGroupSecretKey")
|
||||
self.test_user_client = VinylDNSClient(VinylDNSTestContext.vinyldns_url, "testUserAccessKey", "testUserSecretKey")
|
||||
self.history_client = VinylDNSClient(VinylDNSTestContext.vinyldns_url, "history-key", "history-secret")
|
||||
self.non_user_client = VinylDNSClient(VinylDNSTestContext.vinyldns_url, "not-exist-key", "not-exist-secret")
|
||||
self.clients = [self.ok_vinyldns_client, self.dummy_vinyldns_client, self.shared_zone_vinyldns_client, self.support_user_client,
|
||||
self.unassociated_client, self.test_user_client, self.history_client, self.non_user_client]
|
||||
self.clients = [self.ok_vinyldns_client, self.dummy_vinyldns_client, self.shared_zone_vinyldns_client,
|
||||
self.support_user_client, self.super_user_client, self.unassociated_client,
|
||||
self.test_user_client, self.history_client, self.non_user_client]
|
||||
self.list_zones = ListZonesTestContext(partition_id)
|
||||
self.list_zones_client = self.list_zones.client
|
||||
self.list_records_context = ListRecordSetsTestContext(partition_id)
|
||||
|
@ -95,6 +95,49 @@ def test_create_zone_success(shared_zone_test_context):
|
||||
client.abandon_zones([result_zone["id"]], status=202)
|
||||
|
||||
|
||||
def test_create_zone_success_number_of_dots(shared_zone_test_context):
|
||||
"""
|
||||
Test successfully creating a zone
|
||||
"""
|
||||
client = shared_zone_test_context.ok_vinyldns_client
|
||||
result_zone = None
|
||||
try:
|
||||
# Include a space in the zone name to verify that it is trimmed and properly formatted
|
||||
zone_name = f"one-time{shared_zone_test_context.partition_id} "
|
||||
|
||||
zone = {
|
||||
"name": zone_name,
|
||||
"email": "test@ok.dummy.com",
|
||||
"adminGroupId": shared_zone_test_context.ok_group["id"],
|
||||
"backendId": "func-test-backend"
|
||||
}
|
||||
result = client.create_zone(zone, status=202)
|
||||
result_zone = result["zone"]
|
||||
client.wait_until_zone_active(result_zone["id"])
|
||||
|
||||
get_result = client.get_zone(result_zone["id"])
|
||||
|
||||
get_zone = get_result["zone"]
|
||||
assert_that(get_zone["name"], is_(zone["name"].strip() + "."))
|
||||
assert_that(get_zone["email"], is_(zone["email"]))
|
||||
assert_that(get_zone["adminGroupId"], is_(zone["adminGroupId"]))
|
||||
assert_that(get_zone["latestSync"], is_not(none()))
|
||||
assert_that(get_zone["status"], is_("Active"))
|
||||
assert_that(get_zone["backendId"], is_("func-test-backend"))
|
||||
|
||||
# confirm that the recordsets in DNS have been saved in vinyldns
|
||||
recordsets = client.list_recordsets_by_zone(result_zone["id"])["recordSets"]
|
||||
|
||||
assert_that(len(recordsets), is_(7))
|
||||
for rs in recordsets:
|
||||
small_rs = dict((k, rs[k]) for k in ["name", "type", "records"])
|
||||
small_rs["records"] = small_rs["records"]
|
||||
assert_that(retrieve_dns_records(shared_zone_test_context), has_item(small_rs))
|
||||
finally:
|
||||
if result_zone:
|
||||
client.abandon_zones([result_zone["id"]], status=202)
|
||||
|
||||
|
||||
@pytest.mark.skip_production
|
||||
def test_create_zone_without_transfer_connection_leaves_it_empty(shared_zone_test_context):
|
||||
"""
|
||||
@ -179,6 +222,59 @@ def test_create_invalid_zone_data(shared_zone_test_context):
|
||||
errors = client.create_zone(zone, status=400)["errors"]
|
||||
assert_that(errors, contains_inanyorder("Do not know how to convert JString(invalid_value) into boolean"))
|
||||
|
||||
def test_create_invalid_email(shared_zone_test_context):
|
||||
"""
|
||||
Test that creating a zone with invalid email
|
||||
"""
|
||||
client = shared_zone_test_context.ok_vinyldns_client
|
||||
|
||||
zone_name = f"one-time{shared_zone_test_context.partition_id} "
|
||||
|
||||
zone = {
|
||||
"name": zone_name,
|
||||
"email": "test.abc.com",
|
||||
"adminGroupId": shared_zone_test_context.ok_group["id"],
|
||||
"backendId": "func-test-backend"
|
||||
}
|
||||
|
||||
errors = client.create_zone(zone, status=400)
|
||||
assert_that(errors, is_("Please enter a valid Email."))
|
||||
|
||||
def test_create_invalid_email_number_of_dots(shared_zone_test_context):
|
||||
"""
|
||||
Test that creating a zone with invalid email
|
||||
"""
|
||||
client = shared_zone_test_context.ok_vinyldns_client
|
||||
|
||||
zone_name = f"one-time{shared_zone_test_context.partition_id} "
|
||||
|
||||
zone = {
|
||||
"name": zone_name,
|
||||
"email": "test@abc.ok.dummy.com",
|
||||
"adminGroupId": shared_zone_test_context.ok_group["id"],
|
||||
"backendId": "func-test-backend"
|
||||
}
|
||||
|
||||
errors = client.create_zone(zone, status=400)
|
||||
assert_that(errors, is_("Please enter a valid Email. Number of dots allowed after @ is 2"))
|
||||
|
||||
def test_create_invalid_domain(shared_zone_test_context):
|
||||
"""
|
||||
Test that creating a zone with invalid domain
|
||||
"""
|
||||
client = shared_zone_test_context.ok_vinyldns_client
|
||||
|
||||
zone_name = f"one-time{shared_zone_test_context.partition_id} "
|
||||
|
||||
zone = {
|
||||
"name": zone_name,
|
||||
"email": "test@abc.com",
|
||||
"adminGroupId": shared_zone_test_context.ok_group["id"],
|
||||
"backendId": "func-test-backend"
|
||||
}
|
||||
|
||||
errors = client.create_zone(zone, status=400)
|
||||
assert_that(errors, is_("Please enter a valid Email. Valid domains should end with test.com,dummy.com"))
|
||||
|
||||
@pytest.mark.serial
|
||||
def test_create_zone_with_connection_failure(shared_zone_test_context):
|
||||
|
@ -46,6 +46,7 @@ def test_get_zone_shared_by_id_non_owner(shared_zone_test_context):
|
||||
assert_that(retrieved["shared"], is_(True))
|
||||
assert_that(retrieved["accessLevel"], is_("Read"))
|
||||
|
||||
|
||||
def test_get_zone_private_by_id_fails_without_access(shared_zone_test_context):
|
||||
"""
|
||||
Test get an existing zone by id without access
|
||||
@ -72,6 +73,29 @@ def test_get_zone_by_id_no_authorization(shared_zone_test_context):
|
||||
client.get_zone("123456", sign_request=False, status=401)
|
||||
|
||||
|
||||
def test_get_common_zone_details_by_id(shared_zone_test_context):
|
||||
"""
|
||||
Test get an existing zone's common details by id
|
||||
"""
|
||||
client = shared_zone_test_context.ok_vinyldns_client
|
||||
|
||||
result = client.get_common_zone_details(shared_zone_test_context.system_test_zone["id"], status=200)
|
||||
retrieved = result["zone"]
|
||||
|
||||
assert_that(retrieved["name"], is_(shared_zone_test_context.system_test_zone["name"]))
|
||||
assert_that(retrieved["email"], is_(shared_zone_test_context.system_test_zone["email"]))
|
||||
assert_that(retrieved["adminGroupName"], is_(shared_zone_test_context.ok_group["name"]))
|
||||
assert_that(retrieved["adminGroupId"], is_(shared_zone_test_context.ok_group["id"]))
|
||||
|
||||
|
||||
def test_get_common_zone_details_by_id_returns_404_when_not_found(shared_zone_test_context):
|
||||
"""
|
||||
Test get an existing zone returns a 404 when the zone is not found
|
||||
"""
|
||||
client = shared_zone_test_context.ok_vinyldns_client
|
||||
client.get_common_zone_details(str(uuid.uuid4()), status=404)
|
||||
|
||||
|
||||
@pytest.mark.serial
|
||||
def test_get_zone_by_id_includes_acl_display_name(shared_zone_test_context):
|
||||
"""
|
||||
|
@ -53,7 +53,7 @@ def test_list_zone_changes_member_auth_no_access(shared_zone_test_context):
|
||||
@pytest.mark.serial
|
||||
def test_list_zone_changes_member_auth_with_acl(shared_zone_test_context):
|
||||
"""
|
||||
Test list zone changes succeeds for user with acl rules
|
||||
Test list zone changes fails for user with acl rules
|
||||
"""
|
||||
zone = shared_zone_test_context.ok_zone
|
||||
acl_rule = generate_acl_rule("Write", userId="dummy")
|
||||
@ -62,7 +62,7 @@ def test_list_zone_changes_member_auth_with_acl(shared_zone_test_context):
|
||||
|
||||
client.list_zone_changes(zone["id"], status=403)
|
||||
add_ok_acl_rules(shared_zone_test_context, [acl_rule])
|
||||
client.list_zone_changes(zone["id"], status=200)
|
||||
client.list_zone_changes(zone["id"], status=403)
|
||||
finally:
|
||||
clear_ok_acl_rules(shared_zone_test_context)
|
||||
|
||||
|
@ -4,6 +4,7 @@ import pytest
|
||||
|
||||
from utils import *
|
||||
from vinyldns_context import VinylDNSTestContext
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
|
||||
@pytest.mark.serial
|
||||
@ -45,7 +46,7 @@ def test_update_zone_success(shared_zone_test_context):
|
||||
result_zone = result["zone"]
|
||||
client.wait_until_zone_active(result_zone["id"])
|
||||
|
||||
result_zone["email"] = "foo@bar.com"
|
||||
result_zone["email"] = "test@dummy.com"
|
||||
result_zone["acl"]["rules"] = [acl_rule]
|
||||
update_result = client.update_zone(result_zone, status=202)
|
||||
client.wait_until_zone_change_status_synced(update_result)
|
||||
@ -57,7 +58,7 @@ def test_update_zone_success(shared_zone_test_context):
|
||||
get_result = client.get_zone(result_zone["id"])
|
||||
|
||||
uz = get_result["zone"]
|
||||
assert_that(uz["email"], is_("foo@bar.com"))
|
||||
assert_that(uz["email"], is_("test@dummy.com"))
|
||||
assert_that(uz["updated"], is_not(none()))
|
||||
|
||||
acl = uz["acl"]
|
||||
@ -66,6 +67,378 @@ def test_update_zone_success(shared_zone_test_context):
|
||||
if result_zone:
|
||||
client.abandon_zones([result_zone["id"]], status=202)
|
||||
|
||||
def test_update_zone_success_wildcard(shared_zone_test_context):
|
||||
"""
|
||||
Test updating a zone for email validation wildcard
|
||||
"""
|
||||
client = shared_zone_test_context.ok_vinyldns_client
|
||||
result_zone = None
|
||||
try:
|
||||
zone_name = f"one-time{shared_zone_test_context.partition_id}"
|
||||
|
||||
acl_rule = {
|
||||
"accessLevel": "Read",
|
||||
"description": "test-acl-updated-by-updatezn",
|
||||
"userId": "ok",
|
||||
"recordMask": "www-*",
|
||||
"recordTypes": ["A", "AAAA", "CNAME"]
|
||||
}
|
||||
|
||||
zone = {
|
||||
"name": zone_name,
|
||||
"email": "test@test.com",
|
||||
"adminGroupId": shared_zone_test_context.ok_group["id"],
|
||||
"connection": {
|
||||
"name": "vinyldns.",
|
||||
"keyName": VinylDNSTestContext.dns_key_name,
|
||||
"key": VinylDNSTestContext.dns_key,
|
||||
"primaryServer": VinylDNSTestContext.name_server_ip
|
||||
},
|
||||
"transferConnection": {
|
||||
"name": "vinyldns.",
|
||||
"keyName": VinylDNSTestContext.dns_key_name,
|
||||
"key": VinylDNSTestContext.dns_key,
|
||||
"primaryServer": VinylDNSTestContext.name_server_ip
|
||||
}
|
||||
}
|
||||
result = client.create_zone(zone, status=202)
|
||||
result_zone = result["zone"]
|
||||
client.wait_until_zone_active(result_zone["id"])
|
||||
|
||||
result_zone["email"] = "test@ok.dummy.com"
|
||||
result_zone["acl"]["rules"] = [acl_rule]
|
||||
update_result = client.update_zone(result_zone, status=202)
|
||||
client.wait_until_zone_change_status_synced(update_result)
|
||||
|
||||
assert_that(update_result["changeType"], is_("Update"))
|
||||
assert_that(update_result["userId"], is_("ok"))
|
||||
assert_that(update_result, has_key("created"))
|
||||
|
||||
get_result = client.get_zone(result_zone["id"])
|
||||
|
||||
uz = get_result["zone"]
|
||||
assert_that(uz["email"], is_("test@ok.dummy.com"))
|
||||
assert_that(uz["updated"], is_not(none()))
|
||||
|
||||
acl = uz["acl"]
|
||||
verify_acl_rule_is_present_once(acl_rule, acl)
|
||||
finally:
|
||||
if result_zone:
|
||||
client.abandon_zones([result_zone["id"]], status=202)
|
||||
|
||||
def test_update_zone_success_number_of_dots(shared_zone_test_context):
|
||||
"""
|
||||
Test updating a zone for email validation wildcard
|
||||
"""
|
||||
client = shared_zone_test_context.ok_vinyldns_client
|
||||
result_zone = None
|
||||
try:
|
||||
zone_name = f"one-time{shared_zone_test_context.partition_id}"
|
||||
|
||||
acl_rule = {
|
||||
"accessLevel": "Read",
|
||||
"description": "test-acl-updated-by-updatezn",
|
||||
"userId": "ok",
|
||||
"recordMask": "www-*",
|
||||
"recordTypes": ["A", "AAAA", "CNAME"]
|
||||
}
|
||||
|
||||
zone = {
|
||||
"name": zone_name,
|
||||
"email": "test@test.com",
|
||||
"adminGroupId": shared_zone_test_context.ok_group["id"],
|
||||
"connection": {
|
||||
"name": "vinyldns.",
|
||||
"keyName": VinylDNSTestContext.dns_key_name,
|
||||
"key": VinylDNSTestContext.dns_key,
|
||||
"primaryServer": VinylDNSTestContext.name_server_ip
|
||||
},
|
||||
"transferConnection": {
|
||||
"name": "vinyldns.",
|
||||
"keyName": VinylDNSTestContext.dns_key_name,
|
||||
"key": VinylDNSTestContext.dns_key,
|
||||
"primaryServer": VinylDNSTestContext.name_server_ip
|
||||
}
|
||||
}
|
||||
result = client.create_zone(zone, status=202)
|
||||
result_zone = result["zone"]
|
||||
client.wait_until_zone_active(result_zone["id"])
|
||||
|
||||
result_zone["email"] = "test@ok.dummy.com"
|
||||
result_zone["acl"]["rules"] = [acl_rule]
|
||||
update_result = client.update_zone(result_zone, status=202)
|
||||
client.wait_until_zone_change_status_synced(update_result)
|
||||
|
||||
assert_that(update_result["changeType"], is_("Update"))
|
||||
assert_that(update_result["userId"], is_("ok"))
|
||||
assert_that(update_result, has_key("created"))
|
||||
|
||||
get_result = client.get_zone(result_zone["id"])
|
||||
|
||||
uz = get_result["zone"]
|
||||
assert_that(uz["email"], is_("test@ok.dummy.com"))
|
||||
assert_that(uz["updated"], is_not(none()))
|
||||
|
||||
acl = uz["acl"]
|
||||
verify_acl_rule_is_present_once(acl_rule, acl)
|
||||
finally:
|
||||
if result_zone:
|
||||
client.abandon_zones([result_zone["id"]], status=202)
|
||||
|
||||
def test_update_invalid_email(shared_zone_test_context):
|
||||
"""
|
||||
Test that updating a zone with invalid email
|
||||
"""
|
||||
client = shared_zone_test_context.ok_vinyldns_client
|
||||
result_zone = None
|
||||
try:
|
||||
zone_name = f"one-time{shared_zone_test_context.partition_id}"
|
||||
|
||||
acl_rule = {
|
||||
"accessLevel": "Read",
|
||||
"description": "test-acl-updated-by-updatezn",
|
||||
"userId": "ok",
|
||||
"recordMask": "www-*",
|
||||
"recordTypes": ["A", "AAAA", "CNAME"]
|
||||
}
|
||||
|
||||
zone = {
|
||||
"name": zone_name,
|
||||
"email": "test@test.com",
|
||||
"adminGroupId": shared_zone_test_context.ok_group["id"],
|
||||
"connection": {
|
||||
"name": "vinyldns.",
|
||||
"keyName": VinylDNSTestContext.dns_key_name,
|
||||
"key": VinylDNSTestContext.dns_key,
|
||||
"primaryServer": VinylDNSTestContext.name_server_ip
|
||||
},
|
||||
"transferConnection": {
|
||||
"name": "vinyldns.",
|
||||
"keyName": VinylDNSTestContext.dns_key_name,
|
||||
"key": VinylDNSTestContext.dns_key,
|
||||
"primaryServer": VinylDNSTestContext.name_server_ip
|
||||
}
|
||||
}
|
||||
result = client.create_zone(zone, status=202)
|
||||
result_zone = result["zone"]
|
||||
client.wait_until_zone_active(result_zone["id"])
|
||||
|
||||
result_zone["email"] = "test.trial.com"
|
||||
errors = client.update_zone(result_zone, status=400)
|
||||
assert_that(errors, is_("Please enter a valid Email."))
|
||||
finally:
|
||||
if result_zone:
|
||||
client.abandon_zones([result_zone["id"]], status=202)
|
||||
|
||||
def test_update_invalid_domain(shared_zone_test_context):
|
||||
"""
|
||||
Test that updating a zone with invalid domain
|
||||
"""
|
||||
client = shared_zone_test_context.ok_vinyldns_client
|
||||
result_zone = None
|
||||
try:
|
||||
zone_name = f"one-time{shared_zone_test_context.partition_id}"
|
||||
|
||||
acl_rule = {
|
||||
"accessLevel": "Read",
|
||||
"description": "test-acl-updated-by-updatezn",
|
||||
"userId": "ok",
|
||||
"recordMask": "www-*",
|
||||
"recordTypes": ["A", "AAAA", "CNAME"]
|
||||
}
|
||||
|
||||
zone = {
|
||||
"name": zone_name,
|
||||
"email": "test@test.com",
|
||||
"adminGroupId": shared_zone_test_context.ok_group["id"],
|
||||
"connection": {
|
||||
"name": "vinyldns.",
|
||||
"keyName": VinylDNSTestContext.dns_key_name,
|
||||
"key": VinylDNSTestContext.dns_key,
|
||||
"primaryServer": VinylDNSTestContext.name_server_ip
|
||||
},
|
||||
"transferConnection": {
|
||||
"name": "vinyldns.",
|
||||
"keyName": VinylDNSTestContext.dns_key_name,
|
||||
"key": VinylDNSTestContext.dns_key,
|
||||
"primaryServer": VinylDNSTestContext.name_server_ip
|
||||
}
|
||||
}
|
||||
result = client.create_zone(zone, status=202)
|
||||
result_zone = result["zone"]
|
||||
client.wait_until_zone_active(result_zone["id"])
|
||||
|
||||
result_zone["email"] = "test@trial.com"
|
||||
errors = client.update_zone(result_zone, status=400)
|
||||
assert_that(errors, is_("Please enter a valid Email. Valid domains should end with test.com,dummy.com"))
|
||||
|
||||
finally:
|
||||
if result_zone:
|
||||
client.abandon_zones([result_zone["id"]], status=202)
|
||||
|
||||
def test_update_invalid_email_number_of_dots(shared_zone_test_context):
|
||||
"""
|
||||
Test that updating a zone with invalid domain
|
||||
"""
|
||||
client = shared_zone_test_context.ok_vinyldns_client
|
||||
result_zone = None
|
||||
try:
|
||||
zone_name = f"one-time{shared_zone_test_context.partition_id}"
|
||||
|
||||
acl_rule = {
|
||||
"accessLevel": "Read",
|
||||
"description": "test-acl-updated-by-updatezn",
|
||||
"userId": "ok",
|
||||
"recordMask": "www-*",
|
||||
"recordTypes": ["A", "AAAA", "CNAME"]
|
||||
}
|
||||
|
||||
zone = {
|
||||
"name": zone_name,
|
||||
"email": "test@test.com",
|
||||
"adminGroupId": shared_zone_test_context.ok_group["id"],
|
||||
"connection": {
|
||||
"name": "vinyldns.",
|
||||
"keyName": VinylDNSTestContext.dns_key_name,
|
||||
"key": VinylDNSTestContext.dns_key,
|
||||
"primaryServer": VinylDNSTestContext.name_server_ip
|
||||
},
|
||||
"transferConnection": {
|
||||
"name": "vinyldns.",
|
||||
"keyName": VinylDNSTestContext.dns_key_name,
|
||||
"key": VinylDNSTestContext.dns_key,
|
||||
"primaryServer": VinylDNSTestContext.name_server_ip
|
||||
}
|
||||
}
|
||||
result = client.create_zone(zone, status=202)
|
||||
result_zone = result["zone"]
|
||||
client.wait_until_zone_active(result_zone["id"])
|
||||
|
||||
result_zone["email"] = "test@ok.ok.dummy.com"
|
||||
errors = client.update_zone(result_zone, status=400)
|
||||
assert_that(errors, is_("Please enter a valid Email. Number of dots allowed after @ is 2"))
|
||||
|
||||
finally:
|
||||
if result_zone:
|
||||
client.abandon_zones([result_zone["id"]], status=202)
|
||||
|
||||
def test_update_zone_sync_schedule_fails(shared_zone_test_context):
|
||||
"""
|
||||
Test updating a zone with a schedule for zone sync fails when the user is not an admin user
|
||||
"""
|
||||
client = shared_zone_test_context.ok_vinyldns_client
|
||||
result_zone = None
|
||||
try:
|
||||
zone_name = f"one-time{shared_zone_test_context.partition_id}"
|
||||
|
||||
zone = {
|
||||
"name": zone_name,
|
||||
"email": "test@test.com",
|
||||
"adminGroupId": shared_zone_test_context.ok_group["id"],
|
||||
"connection": {
|
||||
"name": "vinyldns.",
|
||||
"keyName": VinylDNSTestContext.dns_key_name,
|
||||
"key": VinylDNSTestContext.dns_key,
|
||||
"primaryServer": VinylDNSTestContext.name_server_ip
|
||||
},
|
||||
"transferConnection": {
|
||||
"name": "vinyldns.",
|
||||
"keyName": VinylDNSTestContext.dns_key_name,
|
||||
"key": VinylDNSTestContext.dns_key,
|
||||
"primaryServer": VinylDNSTestContext.name_server_ip
|
||||
}
|
||||
}
|
||||
result = client.create_zone(zone, status=202)
|
||||
result_zone = result["zone"]
|
||||
client.wait_until_zone_active(result_zone["id"])
|
||||
|
||||
# schedule zone sync every 5 seconds
|
||||
result_zone["recurrenceSchedule"] = "0/5 0 0 ? * * *"
|
||||
error = client.update_zone(result_zone, status=403)
|
||||
|
||||
assert_that(error, contains_string("User 'ok' is not authorized to schedule zone sync in this zone."))
|
||||
|
||||
finally:
|
||||
if result_zone:
|
||||
client.abandon_zones([result_zone["id"]], status=202)
|
||||
|
||||
|
||||
def test_update_zone_sync_schedule_success(shared_zone_test_context):
|
||||
"""
|
||||
Test updating a zone with a schedule for zone sync successfully sync zone at scheduled time
|
||||
"""
|
||||
client = shared_zone_test_context.ok_vinyldns_client
|
||||
result_zone = None
|
||||
try:
|
||||
zone_name = f"one-time{shared_zone_test_context.partition_id}"
|
||||
|
||||
zone = {
|
||||
"name": zone_name,
|
||||
"email": "test@test.com",
|
||||
"adminGroupId": shared_zone_test_context.ok_group["id"],
|
||||
"connection": {
|
||||
"name": "vinyldns.",
|
||||
"keyName": VinylDNSTestContext.dns_key_name,
|
||||
"key": VinylDNSTestContext.dns_key,
|
||||
"primaryServer": VinylDNSTestContext.name_server_ip
|
||||
},
|
||||
"transferConnection": {
|
||||
"name": "vinyldns.",
|
||||
"keyName": VinylDNSTestContext.dns_key_name,
|
||||
"key": VinylDNSTestContext.dns_key,
|
||||
"primaryServer": VinylDNSTestContext.name_server_ip
|
||||
}
|
||||
}
|
||||
result = client.create_zone(zone, status=202)
|
||||
result_zone = result["zone"]
|
||||
client.wait_until_zone_active(result_zone["id"])
|
||||
|
||||
# schedule zone sync every 5 seconds
|
||||
result_zone["recurrenceSchedule"] = "0/5 0 0 ? * * *"
|
||||
|
||||
# Get the current time in the local timezone
|
||||
now = datetime.now()
|
||||
|
||||
# Convert the time to the UTC timezone
|
||||
utc_time = now.astimezone(timezone.utc)
|
||||
|
||||
super_user_client = shared_zone_test_context.support_user_client
|
||||
|
||||
update_result = super_user_client.update_zone(result_zone, status=202)
|
||||
super_user_client.wait_until_zone_change_status_synced(update_result)
|
||||
|
||||
assert_that(update_result["changeType"], is_("Update"))
|
||||
assert_that(update_result["userId"], is_("support-user-id"))
|
||||
assert_that(update_result, has_key("created"))
|
||||
|
||||
get_result = client.get_zone(result_zone["id"])
|
||||
|
||||
uz = get_result["zone"]
|
||||
assert_that(uz["recurrenceSchedule"], is_("0/5 0 0 ? * * *"))
|
||||
assert_that(uz["updated"], is_not(none()))
|
||||
|
||||
# Add + or - 2 seconds to the current time as there may be a slight change than the exact scheduled time
|
||||
utc_time_1 = utc_time - timedelta(seconds=1)
|
||||
utc_time_2 = utc_time - timedelta(seconds=2)
|
||||
utc_time_3 = utc_time + timedelta(seconds=1)
|
||||
utc_time_4 = utc_time + timedelta(seconds=2)
|
||||
|
||||
# Format the time as a string in the desired format
|
||||
time_str = utc_time.strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
time_str_1 = utc_time_1.strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
time_str_2 = utc_time_2.strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
time_str_3 = utc_time_3.strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
time_str_4 = utc_time_4.strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
|
||||
time_list = [time_str, time_str_1, time_str_2, time_str_3, time_str_4]
|
||||
|
||||
# Check if zone sync was performed at scheduled time
|
||||
assert_that(time_list, has_item(uz["latestSync"]))
|
||||
|
||||
finally:
|
||||
if result_zone:
|
||||
client.abandon_zones([result_zone["id"]], status=202)
|
||||
|
||||
|
||||
def test_update_bad_acl_fails(shared_zone_test_context):
|
||||
"""
|
||||
@ -318,12 +691,12 @@ def test_create_acl_user_rule_invalid_cidr_failure(shared_zone_test_context):
|
||||
"accessLevel": "Read",
|
||||
"description": "test-acl-user-id",
|
||||
"userId": "789",
|
||||
"recordMask": "10.0.0.0/50",
|
||||
"recordMask": "10.0.0/50",
|
||||
"recordTypes": ["PTR"]
|
||||
}
|
||||
|
||||
errors = client.add_zone_acl_rule(shared_zone_test_context.ip4_reverse_zone["id"], acl_rule, status=400)
|
||||
assert_that(errors, contains_string("PTR types must have no mask or a valid CIDR mask: IPv4 mask must be between 0 and 32"))
|
||||
assert_that(errors, contains_string("PTR types must have no mask or a valid CIDR mask: Invalid CIDR block"))
|
||||
|
||||
|
||||
@pytest.mark.serial
|
||||
@ -626,7 +999,7 @@ def test_update_reverse_v4_zone(shared_zone_test_context):
|
||||
client = shared_zone_test_context.ok_vinyldns_client
|
||||
|
||||
zone = copy.deepcopy(shared_zone_test_context.ip4_reverse_zone)
|
||||
zone["email"] = "update-test@bar.com"
|
||||
zone["email"] = "test@test.com"
|
||||
|
||||
update_result = client.update_zone(zone, status=202)
|
||||
client.wait_until_zone_change_status_synced(update_result)
|
||||
@ -638,7 +1011,7 @@ def test_update_reverse_v4_zone(shared_zone_test_context):
|
||||
get_result = client.get_zone(zone["id"])
|
||||
|
||||
uz = get_result["zone"]
|
||||
assert_that(uz["email"], is_("update-test@bar.com"))
|
||||
assert_that(uz["email"], is_("test@test.com"))
|
||||
assert_that(uz["updated"], is_not(none()))
|
||||
|
||||
|
||||
@ -649,7 +1022,7 @@ def test_update_reverse_v6_zone(shared_zone_test_context):
|
||||
client = shared_zone_test_context.ok_vinyldns_client
|
||||
|
||||
zone = copy.deepcopy(shared_zone_test_context.ip6_reverse_zone)
|
||||
zone["email"] = "update-test@bar.com"
|
||||
zone["email"] = "test@test.com"
|
||||
|
||||
update_result = client.update_zone(zone, status=202)
|
||||
client.wait_until_zone_change_status_synced(update_result)
|
||||
@ -661,7 +1034,7 @@ def test_update_reverse_v6_zone(shared_zone_test_context):
|
||||
get_result = client.get_zone(zone["id"])
|
||||
|
||||
uz = get_result["zone"]
|
||||
assert_that(uz["email"], is_("update-test@bar.com"))
|
||||
assert_that(uz["email"], is_("test@test.com"))
|
||||
assert_that(uz["updated"], is_not(none()))
|
||||
|
||||
|
||||
@ -831,6 +1204,7 @@ def test_normal_user_cannot_update_shared_zone_flag(shared_zone_test_context):
|
||||
error = shared_zone_test_context.ok_vinyldns_client.update_zone(zone_update, status=403)
|
||||
assert_that(error, contains_string("Not authorized to update zone shared status from false to true."))
|
||||
|
||||
|
||||
@pytest.mark.serial
|
||||
def test_update_connection_info_success(shared_zone_test_context):
|
||||
"""
|
||||
|
@ -509,6 +509,57 @@ def get_change_MX_json(input_name, ttl=200, preference=None, exchange=None, chan
|
||||
return json
|
||||
|
||||
|
||||
def get_change_NS_json(input_name, ttl=200, nsdname=None, change_type="Add"):
|
||||
json = {
|
||||
"changeType": change_type,
|
||||
"inputName": input_name,
|
||||
"type": "NS"
|
||||
}
|
||||
if change_type == "Add":
|
||||
json["ttl"] = ttl
|
||||
json["record"] = {
|
||||
"nsdname": nsdname
|
||||
}
|
||||
return json
|
||||
|
||||
|
||||
def get_change_SRV_json(input_name, ttl=200, priority=None, weight=None, port=None, target=None, change_type="Add"):
|
||||
json = {
|
||||
"changeType": change_type,
|
||||
"inputName": input_name,
|
||||
"type": "SRV",
|
||||
}
|
||||
if change_type == "Add":
|
||||
json["ttl"] = ttl
|
||||
json["record"] = {
|
||||
"priority": priority,
|
||||
"weight": weight,
|
||||
"port": port,
|
||||
"target": target
|
||||
}
|
||||
return json
|
||||
|
||||
|
||||
def get_change_NAPTR_json(input_name, ttl=200, order=None, preference=None, flags=None, service=None, regexp=None, replacement=None, change_type="Add"):
|
||||
json = {
|
||||
"changeType": change_type,
|
||||
"inputName": input_name,
|
||||
"type": "NAPTR",
|
||||
|
||||
}
|
||||
if change_type == "Add":
|
||||
json["ttl"] = ttl
|
||||
json["record"] = {
|
||||
"order": order,
|
||||
"preference": preference,
|
||||
"flags": flags,
|
||||
"service": service,
|
||||
"regexp": regexp,
|
||||
"replacement": replacement
|
||||
}
|
||||
return json
|
||||
|
||||
|
||||
def create_recordset(zone, rname, recordset_type, rdata_list, ttl=200, ownergroup_id=None):
|
||||
recordset_data = {
|
||||
"zoneId": zone["id"],
|
||||
|
@ -51,7 +51,8 @@ class VinylDNSClient(object):
|
||||
self.session.close()
|
||||
self.session_not_found_ok.close()
|
||||
|
||||
def requests_retry_not_found_ok_session(self, retries=20, backoff_factor=0.1, status_forcelist=(500, 502, 504), session=None):
|
||||
def requests_retry_not_found_ok_session(self, retries=20, backoff_factor=0.1, status_forcelist=(500, 502, 504),
|
||||
session=None):
|
||||
session = session or requests.Session()
|
||||
retry = Retry(
|
||||
total=retries,
|
||||
@ -79,7 +80,8 @@ class VinylDNSClient(object):
|
||||
session.mount("https://", adapter)
|
||||
return session
|
||||
|
||||
def make_request(self, url, method="GET", headers=None, body_string=None, sign_request=True, not_found_ok=False, **kwargs):
|
||||
def make_request(self, url, method="GET", headers=None, body_string=None, sign_request=True, not_found_ok=False,
|
||||
**kwargs):
|
||||
|
||||
# pull out status or None
|
||||
status_code = kwargs.pop("status", None)
|
||||
@ -100,13 +102,15 @@ class VinylDNSClient(object):
|
||||
for k, v in query.items())
|
||||
|
||||
if sign_request:
|
||||
signed_headers, signed_body = self.sign_request(method, path, body_string, query, with_headers=headers or {}, **kwargs)
|
||||
signed_headers, signed_body = self.sign_request(method, path, body_string, query,
|
||||
with_headers=headers or {}, **kwargs)
|
||||
else:
|
||||
signed_headers = headers or {}
|
||||
signed_body = body_string
|
||||
|
||||
if not_found_ok:
|
||||
response = self.session_not_found_ok.request(method, url, data=signed_body, headers=signed_headers, **kwargs)
|
||||
response = self.session_not_found_ok.request(method, url, data=signed_body, headers=signed_headers,
|
||||
**kwargs)
|
||||
else:
|
||||
response = self.session.request(method, url, data=signed_body, headers=signed_headers, **kwargs)
|
||||
|
||||
@ -388,6 +392,17 @@ class VinylDNSClient(object):
|
||||
|
||||
return data
|
||||
|
||||
def get_common_zone_details(self, zone_id, **kwargs):
|
||||
"""
|
||||
Gets common zone details which can be seen by all users for the given zone id
|
||||
:param zone_id: the id of the zone to retrieve
|
||||
:return: the zone, or will 404 if not found
|
||||
"""
|
||||
url = urljoin(self.index_url, "/zones/{0}/details".format(zone_id))
|
||||
response, data = self.make_request(url, "GET", self.headers, not_found_ok=True, **kwargs)
|
||||
|
||||
return data
|
||||
|
||||
def get_zone_by_name(self, zone_name, **kwargs):
|
||||
"""
|
||||
Gets a zone for the given zone name
|
||||
@ -445,7 +460,31 @@ 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, search_by_admin_group=False, ignore_access=False, **kwargs):
|
||||
def list_recordset_change_history(self, zone_id, fqdn, record_type, start_from=None, max_items=None, **kwargs):
|
||||
"""
|
||||
Gets the record's change history for the given zone, record fqdn and record type
|
||||
:param zone_id: the id of the zone to retrieve
|
||||
:param fqdn: the record's fqdn
|
||||
:param record_type: the record's type
|
||||
:param start_from: the start key of the page
|
||||
:param max_items: the page limit
|
||||
:return: the zone, or will 404 if not found
|
||||
"""
|
||||
args = []
|
||||
if start_from:
|
||||
args.append("startFrom={0}".format(start_from))
|
||||
if max_items is not None:
|
||||
args.append("maxItems={0}".format(max_items))
|
||||
args.append("zoneId={0}".format(zone_id))
|
||||
args.append("fqdn={0}".format(fqdn))
|
||||
args.append("recordType={0}".format(record_type))
|
||||
url = urljoin(self.index_url, "recordsetchange/history") + "?" + "&".join(args)
|
||||
|
||||
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, search_by_admin_group=False,
|
||||
ignore_access=False, **kwargs):
|
||||
"""
|
||||
Gets a list of zones that currently exist
|
||||
:return: a list of zones
|
||||
@ -522,6 +561,17 @@ class VinylDNSClient(object):
|
||||
response, data = self.make_request(url, "GET", self.headers, None, not_found_ok=True, **kwargs)
|
||||
return data
|
||||
|
||||
def get_recordset_count(self, zone_id,**kwargs):
|
||||
"""
|
||||
Get count of record set in managed records tab
|
||||
:param zone_id: the zone id the recordset belongs to
|
||||
:return: the value of count
|
||||
"""
|
||||
url = urljoin(self.index_url, "/zones/{0}/recordsetcount".format(zone_id))
|
||||
|
||||
response, data = self.make_request(url, "GET", self.headers, None, not_found_ok=True, **kwargs)
|
||||
return data
|
||||
|
||||
def get_recordset_change(self, zone_id, rs_id, change_id, **kwargs):
|
||||
"""
|
||||
Gets an existing recordset change
|
||||
@ -535,7 +585,8 @@ class VinylDNSClient(object):
|
||||
response, data = self.make_request(url, "GET", self.headers, None, not_found_ok=True, **kwargs)
|
||||
return data
|
||||
|
||||
def list_recordsets_by_zone(self, zone_id, start_from=None, max_items=None, record_name_filter=None, record_type_filter=None, name_sort=None, **kwargs):
|
||||
def list_recordsets_by_zone(self, zone_id, start_from=None, max_items=None, record_name_filter=None,
|
||||
record_type_filter=None, name_sort=None, **kwargs):
|
||||
"""
|
||||
Retrieves all recordsets in a zone
|
||||
:param zone_id: the zone to retrieve
|
||||
@ -618,7 +669,8 @@ class VinylDNSClient(object):
|
||||
_, data = self.make_request(url, "POST", self.headers, **kwargs)
|
||||
return data
|
||||
|
||||
def list_batch_change_summaries(self, start_from=None, max_items=None, ignore_access=False, approval_status=None, **kwargs):
|
||||
def list_batch_change_summaries(self, start_from=None, max_items=None, ignore_access=False, approval_status=None,
|
||||
**kwargs):
|
||||
"""
|
||||
Gets list of user's batch change summaries
|
||||
:return: the content of the response
|
||||
@ -660,7 +712,8 @@ class VinylDNSClient(object):
|
||||
:return: the content of the response
|
||||
"""
|
||||
url = urljoin(self.index_url, "/zones/{0}/acl/rules".format(zone_id))
|
||||
response, data = self.make_request(url, "PUT", self.headers, json.dumps(acl_rule), sign_request=sign_request, **kwargs)
|
||||
response, data = self.make_request(url, "PUT", self.headers, json.dumps(acl_rule), sign_request=sign_request,
|
||||
**kwargs)
|
||||
|
||||
return data
|
||||
|
||||
@ -804,16 +857,11 @@ class VinylDNSClient(object):
|
||||
while change["status"] != expected_status and retries > 0:
|
||||
time.sleep(RETRY_WAIT)
|
||||
retries -= 1
|
||||
latest_change = self.get_recordset_change(change["recordSet"]["zoneId"], change["recordSet"]["id"], change["id"], status=(200, 404))
|
||||
latest_change = self.get_recordset_change(change["recordSet"]["zoneId"], change["recordSet"]["id"],
|
||||
change["id"], status=(200, 404))
|
||||
if type(latest_change) != str:
|
||||
change = latest_change
|
||||
|
||||
if change["status"] != expected_status:
|
||||
print("Failed waiting for record change status")
|
||||
print(json.dumps(change, indent=3))
|
||||
if "systemMessage" in change:
|
||||
print("systemMessage is " + change["systemMessage"])
|
||||
|
||||
assert_that(change["status"], is_(expected_status))
|
||||
return change
|
||||
|
||||
@ -838,10 +886,6 @@ class VinylDNSClient(object):
|
||||
else:
|
||||
change = latest_change
|
||||
|
||||
if not self.batch_is_completed(change):
|
||||
print("Failed waiting for record change status")
|
||||
print(change)
|
||||
|
||||
assert_that(self.batch_is_completed(change), is_(True))
|
||||
return change
|
||||
|
||||
|
@ -127,7 +127,10 @@ vinyldns {
|
||||
from = ${?EMAIL_FROM}
|
||||
}
|
||||
}
|
||||
|
||||
valid-email-config{
|
||||
email-domains = ["test.com","*dummy.com"]
|
||||
number-of-dots= 2
|
||||
}
|
||||
sns {
|
||||
class-name = "vinyldns.apadi.notifier.sns.SnsNotifierProvider"
|
||||
class-name = ${?SNS_CLASS_NAME}
|
||||
|
@ -16,46 +16,14 @@
|
||||
|
||||
package vinyldns.api
|
||||
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
import vinyldns.api.domain.batch.BatchChangeInterfaces.ValidatedBatch
|
||||
import vinyldns.api.domain.batch.BatchTransformations.ChangeForValidation
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import org.scalatest.Assertions._
|
||||
import org.scalatest.matchers.{MatchResult, Matcher}
|
||||
|
||||
import scala.concurrent.ExecutionContext
|
||||
|
||||
trait CatsHelpers {
|
||||
private implicit val timer: Timer[IO] = IO.timer(ExecutionContext.global)
|
||||
private implicit val cs: ContextShift[IO] =
|
||||
IO.contextShift(scala.concurrent.ExecutionContext.global)
|
||||
|
||||
def await[E, T](f: => IO[T], duration: FiniteDuration = 60.seconds): T = {
|
||||
val i: IO[Either[E, T]] = f.attempt.map {
|
||||
case Right(ok) => Right(ok.asInstanceOf[T])
|
||||
case Left(e) => Left(e.asInstanceOf[E])
|
||||
}
|
||||
awaitResultOf[E, T](i, duration).toOption.get
|
||||
}
|
||||
|
||||
// Waits for the future to complete, then returns the value as an Either[Throwable, T]
|
||||
def awaitResultOf[E, T](
|
||||
f: => IO[Either[E, T]],
|
||||
duration: FiniteDuration = 60.seconds
|
||||
): Either[E, T] = {
|
||||
val timeOut = IO.sleep(duration) *> IO(new RuntimeException("Timed out waiting for result"))
|
||||
IO.race(timeOut, f).unsafeRunSync().toOption.get
|
||||
}
|
||||
|
||||
// Assumes that the result of the future operation will be successful, this will fail on a left disjunction
|
||||
def rightResultOf[E, T](f: => IO[Either[E, T]], duration: FiniteDuration = 60.seconds): T =
|
||||
rightValue(awaitResultOf[E, T](f, duration))
|
||||
|
||||
// Assumes that the result of the future operation will fail, this will error on a right disjunction
|
||||
def leftResultOf[E, T](f: => IO[Either[E, T]], duration: FiniteDuration = 60.seconds): E =
|
||||
leftValue(awaitResultOf(f, duration))
|
||||
|
||||
def leftValue[E, T](t: Either[E, T]): E = t match {
|
||||
case Right(x) => fail(s"expected left value, got right: $x")
|
||||
|
@ -18,54 +18,15 @@ package vinyldns.api
|
||||
|
||||
import cats.data.Validated.{Invalid, Valid}
|
||||
import cats.data.ValidatedNel
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
import cats.scalatest.ValidatedMatchers
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.propspec.AnyPropSpec
|
||||
|
||||
import scala.concurrent.ExecutionContext
|
||||
import scala.concurrent.duration._
|
||||
import scala.reflect.ClassTag
|
||||
|
||||
final case class TimeoutException(message: String) extends Throwable(message)
|
||||
|
||||
trait ResultHelpers {
|
||||
private implicit val timer: Timer[IO] = IO.timer(ExecutionContext.global)
|
||||
private implicit val cs: ContextShift[IO] =
|
||||
IO.contextShift(scala.concurrent.ExecutionContext.global)
|
||||
|
||||
def await[T](f: => IO[_], duration: FiniteDuration = 60.seconds): T =
|
||||
awaitResultOf[T](f.map(_.asInstanceOf[T]).attempt, duration).toOption.get
|
||||
|
||||
// Waits for the future to complete, then returns the value as an Either[Throwable, T]
|
||||
def awaitResultOf[T](
|
||||
f: => IO[Either[Throwable, T]],
|
||||
duration: FiniteDuration = 60.seconds
|
||||
): Either[Throwable, T] = {
|
||||
|
||||
val timeOut = IO.sleep(duration) *> IO(
|
||||
TimeoutException("Timed out waiting for result").asInstanceOf[Throwable]
|
||||
)
|
||||
|
||||
IO.race(timeOut, f.handleError(e => Left(e))).unsafeRunSync() match {
|
||||
case Left(e) => Left(e)
|
||||
case Right(ok) => ok
|
||||
}
|
||||
}
|
||||
|
||||
// Assumes that the result of the future operation will be successful, this will fail on a left disjunction
|
||||
def rightResultOf[T](f: => IO[Either[Throwable, T]], duration: FiniteDuration = 60.seconds): T =
|
||||
awaitResultOf[T](f, duration) match {
|
||||
case Right(result) => result
|
||||
case Left(error) => throw error
|
||||
}
|
||||
|
||||
// Assumes that the result of the future operation will fail, this will error on a right disjunction
|
||||
def leftResultOf[T](
|
||||
f: => IO[Either[Throwable, T]],
|
||||
duration: FiniteDuration = 60.seconds
|
||||
): Throwable = awaitResultOf(f, duration).swap.toOption.get
|
||||
|
||||
def leftValue[T](t: Either[Throwable, T]): Throwable = t.swap.toOption.get
|
||||
|
||||
|
@ -38,9 +38,9 @@ trait VinylDNSTestHelpers {
|
||||
|
||||
val highValueDomainRegexList: List[Regex] = List(new Regex("high-value-domain.*"))
|
||||
val highValueDomainIpList: List[IpAddress] =
|
||||
(IpAddress("192.0.2.252") ++ IpAddress("192.0.2.253") ++ IpAddress(
|
||||
(IpAddress.fromString("192.0.2.252") ++ IpAddress.fromString("192.0.2.253") ++ IpAddress.fromString(
|
||||
"fd69:27cc:fe91:0:0:0:0:ffff"
|
||||
) ++ IpAddress(
|
||||
) ++ IpAddress.fromString(
|
||||
"fd69:27cc:fe91:0:0:0:ffff:0"
|
||||
)).toList
|
||||
|
||||
@ -58,9 +58,9 @@ trait VinylDNSTestHelpers {
|
||||
val manualReviewDomainList: List[Regex] = List(new Regex("needs-review.*"))
|
||||
|
||||
val manualReviewIpList: List[IpAddress] =
|
||||
(IpAddress("192.0.2.254") ++ IpAddress("192.0.2.255") ++ IpAddress(
|
||||
(IpAddress.fromString("192.0.2.254") ++ IpAddress.fromString("192.0.2.255") ++ IpAddress.fromString(
|
||||
"fd69:27cc:fe91:0:0:0:ffff:1"
|
||||
) ++ IpAddress("fd69:27cc:fe91:0:0:0:ffff:2")).toList
|
||||
) ++ IpAddress.fromString("fd69:27cc:fe91:0:0:0:ffff:2")).toList
|
||||
|
||||
val manualReviewZoneNameList: Set[String] = Set("zone.needs.review.")
|
||||
|
||||
@ -85,7 +85,7 @@ trait VinylDNSTestHelpers {
|
||||
LimitsConfig(100,100,1000,1500,100,100,100)
|
||||
|
||||
val testServerConfig: ServerConfig =
|
||||
ServerConfig(100, 100, 100, 100, true, approvedNameServers, "blue", "unset", "vinyldns.", false, true, true)
|
||||
ServerConfig(100, 100, 100, 100, true, approvedNameServers, "blue", "unset", "vinyldns.", false, true, true, true)
|
||||
|
||||
val batchChangeConfig: BatchChangeConfig =
|
||||
BatchChangeConfig(batchChangeLimit, sharedApprovedTypes, v6DiscoveryNibbleBoundaries)
|
||||
|
@ -30,6 +30,7 @@ import org.xbill.DNS
|
||||
import org.xbill.DNS.{Lookup, Name, TSIG}
|
||||
import vinyldns.api.backend.dns.DnsProtocol._
|
||||
import vinyldns.core.crypto.{CryptoAlgebra, NoOpCrypto}
|
||||
import vinyldns.core.domain.Encrypted
|
||||
import vinyldns.core.domain.backend.BackendResponse
|
||||
import vinyldns.core.domain.record.RecordType._
|
||||
import vinyldns.core.domain.record._
|
||||
@ -47,7 +48,7 @@ class DnsBackendSpec
|
||||
with EitherValues {
|
||||
|
||||
private val zoneConnection =
|
||||
ZoneConnection("vinyldns.", "vinyldns.", "nzisn+4G2ldMn0q1CV3vsg==", "10.1.1.1")
|
||||
ZoneConnection("vinyldns.", "vinyldns.", Encrypted("nzisn+4G2ldMn0q1CV3vsg=="), "10.1.1.1")
|
||||
private val testZone = Zone("vinyldns", "test@test.com")
|
||||
private val testA = RecordSet(
|
||||
testZone.id,
|
||||
|
@ -21,6 +21,7 @@ import org.scalatest.BeforeAndAfterAll
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpec
|
||||
import vinyldns.api.backend.dns.DnsBackendProviderConfig
|
||||
import vinyldns.core.domain.Encrypted
|
||||
import vinyldns.core.domain.zone.ZoneConnection
|
||||
import vinyldns.core.repository.RepositoryName._
|
||||
|
||||
@ -68,7 +69,7 @@ class VinylDNSConfigSpec extends AnyWordSpec with Matchers with BeforeAndAfterAl
|
||||
}
|
||||
|
||||
"load specified backends" in {
|
||||
val zc = ZoneConnection("vinyldns.", "vinyldns.", "nzisn+4G2ldMn0q1CV3vsg==", sys.env.getOrElse("DEFAULT_DNS_ADDRESS", "127.0.0.1:19001"))
|
||||
val zc = ZoneConnection("vinyldns.", "vinyldns.", Encrypted("nzisn+4G2ldMn0q1CV3vsg=="), sys.env.getOrElse("DEFAULT_DNS_ADDRESS", "127.0.0.1:19001"))
|
||||
val tc = zc.copy()
|
||||
|
||||
val backends = underTest.backendConfigs.backendProviders
|
||||
|
@ -22,7 +22,7 @@ import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks
|
||||
import org.scalatest.propspec.AnyPropSpec
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import vinyldns.api.ValidationTestImprovements._
|
||||
import vinyldns.core.domain.{InvalidDomainName, InvalidCname, InvalidLength}
|
||||
import vinyldns.core.domain.{InvalidDomainName, Fqdn, InvalidCname, InvalidLength}
|
||||
|
||||
class DomainValidationsSpec
|
||||
extends AnyPropSpec
|
||||
@ -77,6 +77,54 @@ class DomainValidationsSpec
|
||||
validateHostName("asterisk.domain*.name.") shouldBe invalid
|
||||
}
|
||||
|
||||
property("Shortest fqdn name should be valid") {
|
||||
val fqdn = Fqdn("a.")
|
||||
validateCname(fqdn, false) shouldBe valid
|
||||
}
|
||||
|
||||
property("Ip address in cname should be invalid") {
|
||||
val fqdn = Fqdn("1.2.3.4")
|
||||
validateCname(fqdn, false) shouldBe invalid
|
||||
}
|
||||
|
||||
property("Longest fqdn name should be valid") {
|
||||
val fqdn = Fqdn(("a" * 50 + ".") * 5)
|
||||
validateCname(fqdn, false) shouldBe valid
|
||||
}
|
||||
|
||||
property("fqdn name should pass property-based testing") {
|
||||
forAll(domainGenerator) { domain: String =>
|
||||
val domains= Fqdn(domain)
|
||||
whenever(validateHostName(domains).isValid) {
|
||||
domains.fqdn.length should be > 0
|
||||
domains.fqdn.length should be < 256
|
||||
(domains.fqdn should fullyMatch).regex(validFQDNRegex)
|
||||
domains.fqdn should endWith(".")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property("fqdn names beginning with invalid characters should fail with InvalidCname") {
|
||||
validateCname(Fqdn("/slash.domain.name."), false).failWith[InvalidCname]
|
||||
validateCname(Fqdn("-hyphen.domain.name."), false).failWith[InvalidCname]
|
||||
}
|
||||
|
||||
property("fqdn names with underscores should pass property-based testing") {
|
||||
validateCname(Fqdn("_underscore.domain.name."), false).isValid
|
||||
validateCname(Fqdn("under_score.domain.name."), false).isValid
|
||||
validateCname(Fqdn("underscore._domain.name."), false).isValid
|
||||
}
|
||||
|
||||
// For wildcard records. '*' can only be in the beginning followed by '.' and domain name
|
||||
property("fqdn names beginning with asterisk should pass property-based testing") {
|
||||
validateCname(Fqdn("*.domain.name."), false) shouldBe valid
|
||||
validateCname(Fqdn("aste*risk.domain.name."),false) shouldBe invalid
|
||||
validateCname(Fqdn("*asterisk.domain.name."),false) shouldBe invalid
|
||||
validateCname(Fqdn("asterisk*.domain.name."),false) shouldBe invalid
|
||||
validateCname(Fqdn("asterisk.*domain.name."),false)shouldBe invalid
|
||||
validateCname(Fqdn("asterisk.domain*.name."),false) shouldBe invalid
|
||||
}
|
||||
|
||||
property("Valid Ipv4 addresses should pass property-based testing") {
|
||||
forAll(validIpv4Gen) { validIp: String =>
|
||||
val res = validateIpv4Address(validIp)
|
||||
@ -112,50 +160,50 @@ class DomainValidationsSpec
|
||||
}
|
||||
|
||||
property("Shortest cname should be valid") {
|
||||
validateCname("a.",true) shouldBe valid
|
||||
validateCname("a.",false) shouldBe valid
|
||||
validateIsReverseCname("a.",true) shouldBe valid
|
||||
validateIsReverseCname("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
|
||||
validateIsReverseCname(name,true) shouldBe valid
|
||||
validateIsReverseCname(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
|
||||
validateIsReverseCname("_underscore.domain.name.",true).isValid
|
||||
validateIsReverseCname("under_score.domain.name.",true).isValid
|
||||
validateIsReverseCname("underscore._domain.name.",true).isValid
|
||||
validateIsReverseCname("_underscore.domain.name.",false).isValid
|
||||
validateIsReverseCname("under_score.domain.name.",false).isValid
|
||||
validateIsReverseCname("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
|
||||
validateIsReverseCname("*.domain.name.",true) shouldBe valid
|
||||
validateIsReverseCname("aste*risk.domain.name.",true) shouldBe invalid
|
||||
validateIsReverseCname("*asterisk.domain.name.",true) shouldBe invalid
|
||||
validateIsReverseCname("asterisk*.domain.name.",true) shouldBe invalid
|
||||
validateIsReverseCname("asterisk.*domain.name.",true) shouldBe invalid
|
||||
validateIsReverseCname("asterisk.domain*.name.",true) shouldBe invalid
|
||||
validateIsReverseCname("*.domain.name.",false) shouldBe valid
|
||||
validateIsReverseCname("aste*risk.domain.name.",false) shouldBe invalid
|
||||
validateIsReverseCname("*asterisk.domain.name.",false) shouldBe invalid
|
||||
validateIsReverseCname("asterisk*.domain.name.",false) shouldBe invalid
|
||||
validateIsReverseCname("asterisk.*domain.name.",false) shouldBe invalid
|
||||
validateIsReverseCname("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
|
||||
validateIsReverseCname("/slash.cname.name.",true).isValid
|
||||
validateIsReverseCname("slash./cname.name.",true).isValid
|
||||
validateIsReverseCname("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]
|
||||
validateIsReverseCname("/slash.cname.name.",false).failWith[InvalidCname]
|
||||
validateIsReverseCname("slash./cname.name.",false).failWith[InvalidCname]
|
||||
validateIsReverseCname("slash.cname./name.",false).failWith[InvalidCname]
|
||||
}
|
||||
}
|
||||
|
@ -137,8 +137,8 @@ class ReverseZoneHelpersSpec
|
||||
}
|
||||
"recordsetIsWithinCidrMask" should {
|
||||
"when testing IPv4" should {
|
||||
"filter in/out record set based on CIDR rule of 0 (lower bound for ip4 CIDR rules)" in {
|
||||
val mask = "120.1.2.0/0"
|
||||
"filter in/out record set based on CIDR rule of 1 (lower bound for ip4 CIDR rules)" in {
|
||||
val mask = "120.1.2.0/1"
|
||||
val znTrue = Zone("40.120.in-addr.arpa.", "email")
|
||||
val rsTrue =
|
||||
RecordSet("id", "20.3", RecordType.PTR, 200, RecordSetStatus.Active, Instant.now.truncatedTo(ChronoUnit.MILLIS))
|
||||
@ -151,7 +151,7 @@ class ReverseZoneHelpersSpec
|
||||
}
|
||||
|
||||
"filter in/out record set based on CIDR rule of 8" in {
|
||||
val mask = "10.10.32/19"
|
||||
val mask = "10.10.32.0/19"
|
||||
val zone = Zone("10.10.in-addr.arpa.", "email")
|
||||
val recordSet =
|
||||
RecordSet("id", "90.44", RecordType.PTR, 200, RecordSetStatus.Active, Instant.now.truncatedTo(ChronoUnit.MILLIS))
|
||||
|
@ -26,7 +26,7 @@ import vinyldns.api.domain.zone.{NotAuthorizedError, RecordSetInfo, RecordSetLis
|
||||
import vinyldns.core.TestMembershipData._
|
||||
import vinyldns.core.TestRecordSetData._
|
||||
import vinyldns.core.TestZoneData._
|
||||
import vinyldns.core.domain.Fqdn
|
||||
import vinyldns.core.domain.{Encrypted, Fqdn}
|
||||
import vinyldns.core.domain.auth.AuthPrincipal
|
||||
import vinyldns.core.domain.membership.User
|
||||
import vinyldns.core.domain.record._
|
||||
@ -95,7 +95,7 @@ class AccessValidationsSpec
|
||||
VinylDNSTestHelpers.sharedApprovedTypes
|
||||
)
|
||||
|
||||
private val testUser = User("test", "test", "test", isTest = true)
|
||||
private val testUser = User("test", "test", Encrypted("test"), isTest = true)
|
||||
|
||||
"canSeeZone" should {
|
||||
"return a NotAuthorizedError if the user is not admin or super user with no acl rules" in {
|
||||
@ -131,6 +131,41 @@ class AccessValidationsSpec
|
||||
}
|
||||
}
|
||||
|
||||
"canSeeZoneChange" should {
|
||||
"return a NotAuthorizedError if the user is not admin or super user with no acl rules" in {
|
||||
val error = leftValue(accessValidationTest.canSeeZoneChange(okAuth, zoneNotAuthorized))
|
||||
error shouldBe a[NotAuthorizedError]
|
||||
}
|
||||
|
||||
"return true if the user is an admin or super user" in {
|
||||
val auth = okAuth.copy(
|
||||
signedInUser = okAuth.signedInUser.copy(isSuper = true),
|
||||
memberGroupIds = Seq.empty
|
||||
)
|
||||
accessValidationTest.canSeeZoneChange(auth, okZone) should be(right)
|
||||
}
|
||||
|
||||
"return false if there is an acl rule for the user in the zone" in {
|
||||
val rule = ACLRule(AccessLevel.Read, userId = Some(okAuth.userId))
|
||||
val zoneIn = zoneNotAuthorized.copy(acl = ZoneACL(Set(rule)))
|
||||
|
||||
val error = leftValue(accessValidationTest.canSeeZoneChange(okAuth, zoneIn))
|
||||
error shouldBe a[NotAuthorizedError]
|
||||
}
|
||||
|
||||
"return true if the user is a support admin" in {
|
||||
val supportAuth = okAuth.copy(
|
||||
signedInUser = okAuth.signedInUser.copy(isSupport = true),
|
||||
memberGroupIds = Seq.empty
|
||||
)
|
||||
accessValidationTest.canSeeZone(supportAuth, okZone) should be(right)
|
||||
}
|
||||
|
||||
"return true if the zone is shared and user does not have other access" in {
|
||||
accessValidationTest.canSeeZone(okAuth, sharedZone) should be(right)
|
||||
}
|
||||
}
|
||||
|
||||
"canChangeZone" should {
|
||||
"return a NotAuthorizedError if the user is not admin or super user" in {
|
||||
val error = leftValue(
|
||||
@ -255,6 +290,7 @@ class AccessValidationsSpec
|
||||
) should be(right)
|
||||
}
|
||||
}
|
||||
|
||||
"canUpdateRecordSet" should {
|
||||
"return a NotAuthorizedError if the user has AccessLevel.NoAccess" in {
|
||||
val error = leftValue(
|
||||
@ -292,6 +328,13 @@ class AccessValidationsSpec
|
||||
)
|
||||
}
|
||||
|
||||
"return true if the user has AccessLevel.Read or AccessLevel.NoAccess and superUserCanUpdateOwnerGroup is true" in {
|
||||
accessValidationTest.canUpdateRecordSet(userAuthRead, "test", RecordType.A, zoneInRead,
|
||||
None, superUserCanUpdateOwnerGroup = true) should be(right)
|
||||
accessValidationTest.canUpdateRecordSet(userAuthNone, "test", RecordType.A, zoneInNone,
|
||||
None, superUserCanUpdateOwnerGroup = true) should be(right)
|
||||
}
|
||||
|
||||
"return true if the user is in the owner group and the zone is shared" in {
|
||||
val zone = okZone.copy(shared = true)
|
||||
val record = aaaa.copy(zoneId = zone.id, ownerGroupId = Some(oneUserDummyGroup.id))
|
||||
@ -365,7 +408,7 @@ class AccessValidationsSpec
|
||||
RecordType.PTR,
|
||||
zoneIp4,
|
||||
None,
|
||||
List(PTRData(Fqdn("test.foo.comcast.net")))
|
||||
newRecordData = List(PTRData(Fqdn("test.foo.comcast.net")))
|
||||
) should be(right)
|
||||
}
|
||||
}
|
||||
@ -957,8 +1000,8 @@ class AccessValidationsSpec
|
||||
|
||||
"ruleAppliesToRecordNameIPv4" should {
|
||||
|
||||
"filter in/out record set based on CIDR rule of 0 (lower bound for ip4 CIDR rules)" in {
|
||||
val aclRule = userReadAcl.copy(recordMask = Some("120.1.2.0/0"))
|
||||
"filter in/out record set based on CIDR rule of 1 (lower bound for ip4 CIDR rules)" in {
|
||||
val aclRule = userReadAcl.copy(recordMask = Some("120.1.2.0/1"))
|
||||
val znTrue = Zone("40.120.in-addr.arpa.", "email")
|
||||
val rsTrue =
|
||||
RecordSet("id", "20.3", RecordType.PTR, 200, RecordSetStatus.Active, Instant.now.truncatedTo(ChronoUnit.MILLIS))
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user