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

Merge branch 'master' into records_ownership_transfer

This commit is contained in:
Jay 2024-09-23 12:09:08 +05:30 committed by GitHub
commit d6fcd3d726
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
53 changed files with 870 additions and 663 deletions

149
.github/workflows/release-beta.yml vendored Normal file
View 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 }}

View File

@ -34,7 +34,7 @@ jobs:
steps: steps:
- name: Checkout current branch - name: Checkout current branch
if: github.event.inputs.verify-first == 'true' if: github.event.inputs.verify-first == 'true'
uses: actions/checkout@v2 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
@ -53,7 +53,7 @@ jobs:
steps: steps:
- name: Checkout current branch - name: Checkout current branch
uses: actions/checkout@v2 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
@ -67,7 +67,7 @@ jobs:
- name: Create GitHub Release - name: Create GitHub Release
id: create_release id: create_release
uses: softprops/action-gh-release@1e07f4398721186383de40550babbdf2b84acfc5 # v0.1.14 uses: softprops/action-gh-release@v2
with: with:
tag_name: v${{ steps.get-version.outputs.vinyldns_version }} tag_name: v${{ steps.get-version.outputs.vinyldns_version }}
generate_release_notes: true 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')" 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) - name: Checkout current branch (full)
uses: actions/checkout@v2 uses: actions/checkout@v4
with: with:
ref: ${{ steps.get-version.outputs.vinyldns_version }} ref: ${{ steps.get-version.outputs.vinyldns_version }}
fetch-depth: 0 fetch-depth: 0
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v1 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKER_USER }} username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_TOKEN }} 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')" 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) - name: Checkout current branch (full)
uses: actions/checkout@v2 uses: actions/checkout@v4
with: with:
ref: ${{ steps.get-version.outputs.vinyldns_version }} ref: ${{ steps.get-version.outputs.vinyldns_version }}
fetch-depth: 0 fetch-depth: 0
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v1 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKER_USER }} username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_TOKEN }} password: ${{ secrets.DOCKER_TOKEN }}

View File

@ -31,28 +31,28 @@ jobs:
- name: Codecov - name: Codecov
id: codecov0 id: codecov0
uses: codecov/codecov-action@v2 uses: codecov/codecov-action@v4
continue-on-error: true continue-on-error: true
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
- name: Codecov Retry - name: Codecov Retry
id: codecov1 id: codecov1
if: steps.codecov0.outcome=='failure' if: steps.codecov0.outcome=='failure'
uses: codecov/codecov-action@v2 uses: codecov/codecov-action@v4
continue-on-error: true continue-on-error: true
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
- name: Codecov Retry 2 - name: Codecov Retry 2
id: codecov2 id: codecov2
if: steps.codecov1.outcome=='failure' if: steps.codecov1.outcome=='failure'
uses: codecov/codecov-action@v2 uses: codecov/codecov-action@v4
continue-on-error: true continue-on-error: true
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
- name: Codecov Retry 3 - name: Codecov Retry 3
id: codecov3 id: codecov3
if: steps.codecov2.outcome=='failure' if: steps.codecov2.outcome=='failure'
uses: codecov/codecov-action@v2 uses: codecov/codecov-action@v4
continue-on-error: true continue-on-error: true
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}

View File

@ -21,6 +21,7 @@ in any way, but do not see your name here, please open a PR to add yourself (in
- Joe Crowe - Joe Crowe
- Jearvon Dharrie - Jearvon Dharrie
- Andrew Dunn - Andrew Dunn
- Josh Edwards
- Ryan Emerle - Ryan Emerle
- David Grizzanti - David Grizzanti
- Alejandro Guirao - Alejandro Guirao
@ -41,8 +42,9 @@ in any way, but do not see your name here, please open a PR to add yourself (in
- Khalid Reid - Khalid Reid
- Timo Schmid - Timo Schmid
- Trent Schmidt - Trent Schmidt
- Nick Spadaccino - Arpit Shah
- Ghafar Shah - Ghafar Shah
- Nick Spadaccino
- Rebecca Star - Rebecca Star
- Jess Stodola - Jess Stodola
- Juan Valencia - Juan Valencia

View File

@ -146,7 +146,8 @@ See the [Contributing Guide](CONTRIBUTING.md).
The current maintainers (people who can merge pull requests) are: The current maintainers (people who can merge pull requests) are:
- Ryan Emerle ([@remerle](https://github.com/remerle)) - Ryan Emerle ([@remerle](https://github.com/remerle))
- Sriram Ramakrishnan ([@sramakr](https://github.com/sramakr)) - Arpit Shah ([@arpit4ever](https://github.com/arpit4ever))
- Nick Spadaccino ([@nspadaccino](https://github.com/nspadaccino))
- Jim Wakemen ([@jwakemen](https://github.com/jwakemen)) - Jim Wakemen ([@jwakemen](https://github.com/jwakemen))
See [AUTHORS.md](AUTHORS.md) for the full list of contributors to VinylDNS. See [AUTHORS.md](AUTHORS.md) for the full list of contributors to VinylDNS.

View File

@ -351,6 +351,10 @@ akka.http {
# Set to `infinite` to disable. # Set to `infinite` to disable.
bind-timeout = 5s 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 # Show verbose error messages back to the client
verbose-error-messages = on verbose-error-messages = on
} }

View File

@ -14,7 +14,7 @@
<logger name="scalikejdbc.StatementExecutor$$anon$1" level="OFF"/> <logger name="scalikejdbc.StatementExecutor$$anon$1" level="OFF"/>
<logger name="com.zaxxer.hikari" level="TRACE"> <logger name="com.zaxxer.hikari" level="ERROR">
<appender-ref ref="CONSOLE"/> <appender-ref ref="CONSOLE"/>
</logger> </logger>

View File

@ -15,7 +15,7 @@
<logger name="play" level="INFO" /> <logger name="play" level="INFO" />
<logger name="application" level="DEBUG" /> <logger name="application" level="DEBUG" />
<logger name="com.zaxxer.hikari" level="TRACE"> <logger name="com.zaxxer.hikari" level="ERROR">
<appender-ref ref="CONSOLE"/> <appender-ref ref="CONSOLE"/>
</logger> </logger>

View File

@ -348,6 +348,10 @@ akka.http {
# Set to `infinite` to disable. # Set to `infinite` to disable.
bind-timeout = 5s 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 # Show verbose error messages back to the client
verbose-error-messages = on verbose-error-messages = on
} }

View File

@ -367,6 +367,10 @@ akka.http {
# Set to `infinite` to disable. # Set to `infinite` to disable.
bind-timeout = 5s 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 # Show verbose error messages back to the client
verbose-error-messages = on verbose-error-messages = on
} }

View File

@ -7,7 +7,7 @@
</encoder> </encoder>
</appender> </appender>
<logger name="com.zaxxer.hikari" level="TRACE"> <logger name="com.zaxxer.hikari" level="ERROR">
<appender-ref ref="CONSOLE"/> <appender-ref ref="CONSOLE"/>
</logger> </logger>

View File

@ -30,7 +30,6 @@ import vinyldns.core.domain.zone.Zone
import vinyldns.core.domain.batch._ import vinyldns.core.domain.batch._
import vinyldns.core.domain.record.RecordType.{RecordType, UNKNOWN} import vinyldns.core.domain.record.RecordType.{RecordType, UNKNOWN}
import vinyldns.core.queue.MessageQueue import vinyldns.core.queue.MessageQueue
import java.net.InetAddress
class BatchChangeConverter(batchChangeRepo: BatchChangeRepository, messageQueue: MessageQueue) class BatchChangeConverter(batchChangeRepo: BatchChangeRepository, messageQueue: MessageQueue)
extends BatchChangeConverterAlgebra { extends BatchChangeConverterAlgebra {
@ -52,17 +51,16 @@ class BatchChangeConverter(batchChangeRepo: BatchChangeRepository, messageQueue:
s"Converting BatchChange [${batchChange.id}] with SingleChanges [${batchChange.changes.map(_.id)}]" s"Converting BatchChange [${batchChange.id}] with SingleChanges [${batchChange.changes.map(_.id)}]"
) )
for { for {
updatedBatchChange <- updateBatchChange(batchChange, groupedChanges).toRightBatchResult
recordSetChanges <- createRecordSetChangesForBatch( recordSetChanges <- createRecordSetChangesForBatch(
updatedBatchChange.changes, batchChange.changes,
existingZones, existingZones,
groupedChanges, groupedChanges,
batchChange.userId, batchChange.userId,
ownerGroupId ownerGroupId
).toRightBatchResult ).toRightBatchResult
_ <- allChangesWereConverted(updatedBatchChange.changes, recordSetChanges) _ <- allChangesWereConverted(batchChange.changes, recordSetChanges)
_ <- batchChangeRepo _ <- batchChangeRepo
.save(updatedBatchChange) .save(batchChange)
.toBatchResult // need to save the change before queueing, backend processing expects the changes to exist .toBatchResult // need to save the change before queueing, backend processing expects the changes to exist
queued <- putChangesOnQueue(recordSetChanges, batchChange.id) queued <- putChangesOnQueue(recordSetChanges, batchChange.id)
changeToStore = updateWithQueueingFailures(batchChange, queued) changeToStore = updateWithQueueingFailures(batchChange, queued)
@ -129,7 +127,7 @@ class BatchChangeConverter(batchChangeRepo: BatchChangeRepository, messageQueue:
change match { change match {
case _: SingleDeleteRRSetChange if change.recordSetId.isEmpty => case _: SingleDeleteRRSetChange if change.recordSetId.isEmpty =>
// Mark as Complete since we don't want to throw it as an error // Mark as Complete since we don't want to throw it as an error
change.withDoesNotExistMessage(nonExistentRecordDeleteMessage) change.withDoesNotExistMessage
case _ => case _ =>
// Failure here means there was a message queue issue for this change // Failure here means there was a message queue issue for this change
change.withFailureMessage(failedMessage) change.withFailureMessage(failedMessage)
@ -142,35 +140,12 @@ class BatchChangeConverter(batchChangeRepo: BatchChangeRepository, messageQueue:
def storeQueuingFailures(batchChange: BatchChange): BatchResult[Unit] = { def storeQueuingFailures(batchChange: BatchChange): BatchResult[Unit] = {
// Update if Single change is Failed or if a record that does not exist is deleted // Update if Single change is Failed or if a record that does not exist is deleted
val failedAndNotExistsChanges = batchChange.changes.collect { val failedAndNotExistsChanges = batchChange.changes.collect {
case change if change.status == SingleChangeStatus.Failed || change.systemMessage.contains(nonExistentRecordDeleteMessage) => change case change if change.status == SingleChangeStatus.Failed || change.systemMessage.contains(nonExistentRecordDeleteMessage) || change.systemMessage.contains(nonExistentRecordDataDeleteMessage) => change
} }
batchChangeRepo.updateSingleChanges(failedAndNotExistsChanges).as(()) val storeChanges = batchChangeRepo.updateSingleChanges(failedAndNotExistsChanges).as(())
storeChanges
}.toBatchResult }.toBatchResult
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 updateBatchChange(batchChange: BatchChange, groupedChanges: ChangeForValidationMap): BatchChange = {
// Update system message to be display the information if record data doesn't exist for the delete request
val singleChanges = batchChange.changes.map {
case change@(sd: SingleDeleteRRSetChange) =>
if (sd.recordData.isDefined && !groupedChanges.getExistingRecordSet(change.recordKey.get).exists(rs => matchRecordData(rs.records, sd.recordData.get))) {
sd.copy(systemMessage = Some(nonExistentRecordDataDeleteMessage))
}
else change
case change => change
}
batchChange.copy(changes = singleChanges)
}
def createRecordSetChangesForBatch( def createRecordSetChangesForBatch(
changes: List[SingleChange], changes: List[SingleChange],
existingZones: ExistingZones, existingZones: ExistingZones,

View File

@ -44,12 +44,14 @@ object BatchChangeInput {
sealed trait ChangeInput { sealed trait ChangeInput {
val inputName: String val inputName: String
val typ: RecordType val typ: RecordType
val systemMessage: Option[String]
def asNewStoredChange(errors: NonEmptyList[DomainValidationError], defaultTtl: Long): SingleChange def asNewStoredChange(errors: NonEmptyList[DomainValidationError], defaultTtl: Long): SingleChange
} }
final case class AddChangeInput( final case class AddChangeInput(
inputName: String, inputName: String,
typ: RecordType, typ: RecordType,
systemMessage: Option[String],
ttl: Option[Long], ttl: Option[Long],
record: RecordData record: RecordData
) extends ChangeInput { ) extends ChangeInput {
@ -68,7 +70,7 @@ final case class AddChangeInput(
knownTtl, knownTtl,
record, record,
SingleChangeStatus.NeedsReview, SingleChangeStatus.NeedsReview,
None, systemMessage,
None, None,
None, None,
errors.toList.map(SingleChangeError(_)) errors.toList.map(SingleChangeError(_))
@ -79,6 +81,7 @@ final case class AddChangeInput(
final case class DeleteRRSetChangeInput( final case class DeleteRRSetChangeInput(
inputName: String, inputName: String,
typ: RecordType, typ: RecordType,
systemMessage: Option[String],
record: Option[RecordData] record: Option[RecordData]
) extends ChangeInput { ) extends ChangeInput {
def asNewStoredChange( def asNewStoredChange(
@ -93,7 +96,7 @@ final case class DeleteRRSetChangeInput(
typ, typ,
record, record,
SingleChangeStatus.NeedsReview, SingleChangeStatus.NeedsReview,
None, systemMessage,
None, None,
None, None,
errors.toList.map(SingleChangeError(_)) errors.toList.map(SingleChangeError(_))
@ -104,6 +107,7 @@ object AddChangeInput {
def apply( def apply(
inputName: String, inputName: String,
typ: RecordType, typ: RecordType,
systemMessage: Option[String],
ttl: Option[Long], ttl: Option[Long],
record: RecordData record: RecordData
): AddChangeInput = { ): AddChangeInput = {
@ -111,28 +115,29 @@ object AddChangeInput {
case PTR => inputName case PTR => inputName
case _ => ensureTrailingDot(inputName) case _ => ensureTrailingDot(inputName)
} }
new AddChangeInput(transformName, typ, ttl, record) new AddChangeInput(transformName, typ, systemMessage, ttl, record)
} }
def apply(sc: SingleAddChange): AddChangeInput = 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 { object DeleteRRSetChangeInput {
def apply( def apply(
inputName: String, inputName: String,
typ: RecordType, typ: RecordType,
systemMessage: Option[String],
record: Option[RecordData] = None record: Option[RecordData] = None
): DeleteRRSetChangeInput = { ): DeleteRRSetChangeInput = {
val transformName = typ match { val transformName = typ match {
case PTR => inputName case PTR => inputName
case _ => ensureTrailingDot(inputName) case _ => ensureTrailingDot(inputName)
} }
new DeleteRRSetChangeInput(transformName, typ, record) new DeleteRRSetChangeInput(transformName, typ, systemMessage, record)
} }
def apply(sc: SingleDeleteRRSetChange): DeleteRRSetChangeInput = 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 { object ChangeInputType extends Enumeration {

View File

@ -16,7 +16,6 @@
package vinyldns.api.domain.batch package vinyldns.api.domain.batch
import java.net.InetAddress
import java.time.Instant import java.time.Instant
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
import cats.data._ import cats.data._
@ -315,16 +314,34 @@ class BatchChangeValidations(
else else
().validNel ().validNel
def matchRecordData(existingRecordSetData: List[RecordData], recordData: RecordData): Boolean = def matchRecordData(existingRecordSetData: List[RecordData], recordData: RecordData): Boolean = {
existingRecordSetData.exists { rd => existingRecordSetData.par.exists { rd =>
(rd, recordData) match { rd == recordData
case (AAAAData(rdAddress), AAAAData(proposedAddress)) =>
InetAddress.getByName(proposedAddress).getHostName == InetAddress
.getByName(rdAddress)
.getHostName
case _ => rd == recordData
}
} }
}
def ensureRecordExists(
change: ChangeForValidation,
groupedChanges: ChangeForValidationMap
): Boolean = {
change match {
// For DeleteRecord inputs, need to verify that the record data actually exists
case DeleteRRSetChangeForValidation(_, _, DeleteRRSetChangeInput(_, _, _, Some(recordData)))
if !groupedChanges
.getExistingRecordSet(change.recordKey)
.exists(rs => matchRecordData(rs.records, recordData)) =>
false
case _ =>
true
}
}
def updateSystemMessage(changeInput: ChangeInput, systemMessage: String): ChangeInput = {
changeInput match {
case dci: DeleteRRSetChangeInput => dci.copy(systemMessage = Some(systemMessage))
case _ => changeInput
}
}
def validateDeleteWithContext( def validateDeleteWithContext(
change: ChangeForValidation, change: ChangeForValidation,
@ -333,26 +350,37 @@ class BatchChangeValidations(
isApproved: Boolean isApproved: Boolean
): SingleValidation[ChangeForValidation] = { ): SingleValidation[ChangeForValidation] = {
// To handle add and delete for the record with same record data is present in the batch val nonExistentRecordDeleteMessage = "This record does not exist. No further action is required."
val nonExistentRecordDataDeleteMessage = "Record data entered does not exist. No further action is required."
val recordData = change match { val recordData = change match {
case AddChangeForValidation(_, _, inputChange, _, _) => inputChange.record.toString case AddChangeForValidation(_, _, inputChange, _, _) => inputChange.record.toString
case DeleteRRSetChangeForValidation(_, _, inputChange) => if(inputChange.record.isDefined) inputChange.record.get.toString else "" case DeleteRRSetChangeForValidation(_, _, inputChange) => inputChange.record.map(_.toString).getOrElse("")
} }
val addInBatch = groupedChanges.getProposedAdds(change.recordKey) val addInBatch = groupedChanges.getProposedAdds(change.recordKey)
val isSameRecordUpdateInBatch = if(recordData.nonEmpty){ val isSameRecordUpdateInBatch = recordData.nonEmpty && addInBatch.contains(RecordData.fromString(recordData, change.inputChange.typ).get)
if(addInBatch.contains(RecordData.fromString(recordData, change.inputChange.typ).get)) true else false
} else false
val validations = // Perform the system message update based on the condition
groupedChanges.getExistingRecordSet(change.recordKey) match { val updatedChange = if (groupedChanges.getExistingRecordSet(change.recordKey).isEmpty && !isSameRecordUpdateInBatch) {
case Some(rs) => val updatedChangeInput = updateSystemMessage(change.inputChange, nonExistentRecordDeleteMessage)
userCanDeleteRecordSet(change, auth, rs.ownerGroupId, rs.records) |+| change.withUpdatedInputChange(updatedChangeInput)
zoneDoesNotRequireManualReview(change, isApproved) } else if (!ensureRecordExists(change, groupedChanges)) {
case None => val updatedChangeInput = updateSystemMessage(change.inputChange, nonExistentRecordDataDeleteMessage)
if(isSameRecordUpdateInBatch) InvalidUpdateRequest(change.inputChange.inputName).invalidNel else ().validNel change.withUpdatedInputChange(updatedChangeInput)
} } else {
validations.map(_ => change) 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( def validateAddUpdateWithContext(
@ -397,28 +425,40 @@ class BatchChangeValidations(
isApproved: Boolean isApproved: Boolean
): SingleValidation[ChangeForValidation] = { ): SingleValidation[ChangeForValidation] = {
val nonExistentRecordDeleteMessage = "This record does not exist. No further action is required."
val nonExistentRecordDataDeleteMessage = "Record data entered does not exist. No further action is required."
// To handle add and delete for the record with same record data is present in the batch // To handle add and delete for the record with same record data is present in the batch
val recordData = change match { val recordData = change match {
case AddChangeForValidation(_, _, inputChange, _, _) => inputChange.record.toString case AddChangeForValidation(_, _, inputChange, _, _) => inputChange.record.toString
case DeleteRRSetChangeForValidation(_, _, inputChange) => if(inputChange.record.isDefined) inputChange.record.get.toString else "" case DeleteRRSetChangeForValidation(_, _, inputChange) => inputChange.record.map(_.toString).getOrElse("")
} }
val addInBatch = groupedChanges.getProposedAdds(change.recordKey) val addInBatch = groupedChanges.getProposedAdds(change.recordKey)
val isSameRecordUpdateInBatch = if(recordData.nonEmpty){ val isSameRecordUpdateInBatch = recordData.nonEmpty && addInBatch.contains(RecordData.fromString(recordData, change.inputChange.typ).get)
if(addInBatch.contains(RecordData.fromString(recordData, change.inputChange.typ).get)) true else false
} else false // 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 = val validations =
groupedChanges.getExistingRecordSet(change.recordKey) match { groupedChanges.getExistingRecordSet(updatedChange.recordKey) match {
case Some(rs) => case Some(rs) =>
val adds = groupedChanges.getProposedAdds(change.recordKey).toList val adds = groupedChanges.getProposedAdds(updatedChange.recordKey).toList
userCanUpdateRecordSet(change, auth, rs.ownerGroupId, adds) |+| userCanUpdateRecordSet(updatedChange, auth, rs.ownerGroupId, adds) |+|
zoneDoesNotRequireManualReview(change, isApproved) zoneDoesNotRequireManualReview(updatedChange, isApproved)
case None => case None =>
if(isSameRecordUpdateInBatch) InvalidUpdateRequest(change.inputChange.inputName).invalidNel else ().validNel if(isSameRecordUpdateInBatch) InvalidUpdateRequest(updatedChange.inputChange.inputName).invalidNel else ().validNel
} }
validations.map(_ => change) validations.map(_ => updatedChange)
} }
def validateAddWithContext( def validateAddWithContext(

View File

@ -16,7 +16,6 @@
package vinyldns.api.domain.batch package vinyldns.api.domain.batch
import java.net.InetAddress
import java.util.UUID import java.util.UUID
import vinyldns.api.domain.ReverseZoneHelpers import vinyldns.api.domain.ReverseZoneHelpers
@ -24,7 +23,7 @@ import vinyldns.api.domain.batch.BatchChangeInterfaces.ValidatedBatch
import vinyldns.api.domain.batch.BatchTransformations.LogicalChangeType.LogicalChangeType import vinyldns.api.domain.batch.BatchTransformations.LogicalChangeType.LogicalChangeType
import vinyldns.api.backend.dns.DnsConversions.getIPv6FullReverseName import vinyldns.api.backend.dns.DnsConversions.getIPv6FullReverseName
import vinyldns.core.domain.batch._ 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.record.RecordType._
import vinyldns.core.domain.zone.Zone import vinyldns.core.domain.zone.Zone
import vinyldns.core.domain.record.RecordType.RecordType import vinyldns.core.domain.record.RecordType.RecordType
@ -82,6 +81,7 @@ object BatchTransformations {
val recordKey = RecordKey(zone.id, recordName, inputChange.typ) val recordKey = RecordKey(zone.id, recordName, inputChange.typ)
def asStoredChange(changeId: Option[String] = None): SingleChange def asStoredChange(changeId: Option[String] = None): SingleChange
def isAddChangeForValidation: Boolean def isAddChangeForValidation: Boolean
def withUpdatedInputChange(inputChange: ChangeInput): ChangeForValidation
} }
object ChangeForValidation { object ChangeForValidation {
@ -118,7 +118,7 @@ object BatchTransformations {
ttl, ttl,
inputChange.record, inputChange.record,
SingleChangeStatus.Pending, SingleChangeStatus.Pending,
None, inputChange.systemMessage,
None, None,
None, None,
List.empty, List.empty,
@ -127,6 +127,10 @@ object BatchTransformations {
} }
def isAddChangeForValidation: Boolean = true def isAddChangeForValidation: Boolean = true
def withUpdatedInputChange(inputChange: ChangeInput): ChangeForValidation = {
this.copy(inputChange = inputChange.asInstanceOf[AddChangeInput])
}
} }
final case class DeleteRRSetChangeForValidation( final case class DeleteRRSetChangeForValidation(
@ -143,7 +147,7 @@ object BatchTransformations {
inputChange.typ, inputChange.typ,
inputChange.record, inputChange.record,
SingleChangeStatus.Pending, SingleChangeStatus.Pending,
None, inputChange.systemMessage,
None, None,
None, None,
List.empty, List.empty,
@ -151,6 +155,10 @@ object BatchTransformations {
) )
def isAddChangeForValidation: Boolean = false def isAddChangeForValidation: Boolean = false
def withUpdatedInputChange(inputChange: ChangeInput): ChangeForValidation = {
this.copy(inputChange = inputChange.asInstanceOf[DeleteRRSetChangeInput])
}
} }
final case class BatchConversionOutput( final case class BatchConversionOutput(
@ -197,13 +205,6 @@ object BatchTransformations {
} }
object ValidationChanges { 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( def apply(
changes: List[ChangeForValidation], changes: List[ChangeForValidation],
@ -223,16 +224,11 @@ object BatchTransformations {
case DeleteRRSetChangeForValidation( case DeleteRRSetChangeForValidation(
_, _,
_, _,
DeleteRRSetChangeInput(_, AAAA, Some(AAAAData(address))) DeleteRRSetChangeInput(_, _, _, Some(recordData))
) =>
existingRecords.filter(r => matchRecordData(r, address))
case DeleteRRSetChangeForValidation(
_,
_,
DeleteRRSetChangeInput(_, _, Some(recordData))
) => ) =>
Set(recordData) Set(recordData)
case _: DeleteRRSetChangeForValidation => existingRecords case _: DeleteRRSetChangeForValidation =>
existingRecords
} }
.toSet .toSet
.flatten .flatten

View File

@ -670,21 +670,19 @@ class RecordSetService(
} yield change } yield change
def listRecordSetChanges( def listRecordSetChanges(
zoneId: Option[String] = None, zoneId: String,
startFrom: Option[Int] = None, startFrom: Option[Int] = None,
maxItems: Int = 100, maxItems: Int = 100,
fqdn: Option[String] = None,
recordType: Option[RecordType] = None,
authPrincipal: AuthPrincipal authPrincipal: AuthPrincipal
): Result[ListRecordSetChangesResponse] = ): Result[ListRecordSetChangesResponse] =
for { for {
zone <- getZone(zoneId.get) zone <- getZone(zoneId)
_ <- canSeeZone(authPrincipal, zone).toResult _ <- canSeeZone(authPrincipal, zone).toResult
recordSetChangesResults <- recordChangeRepository recordSetChangesResults <- recordChangeRepository
.listRecordSetChanges(Some(zone.id), startFrom, maxItems, fqdn, recordType) .listRecordSetChanges(Some(zone.id), startFrom, maxItems, None, None)
.toResult[ListRecordSetChangesResults] .toResult[ListRecordSetChangesResults]
recordSetChangesInfo <- buildRecordSetChangeInfo(recordSetChangesResults.items) recordSetChangesInfo <- buildRecordSetChangeInfo(recordSetChangesResults.items)
} yield ListRecordSetChangesResponse(zoneId.get, recordSetChangesResults, recordSetChangesInfo) } yield ListRecordSetChangesResponse(zoneId, recordSetChangesResults, recordSetChangesInfo)
def listRecordSetChangeHistory( def listRecordSetChangeHistory(
zoneId: Option[String] = None, zoneId: Option[String] = None,

View File

@ -101,11 +101,9 @@ trait RecordSetServiceAlgebra {
): Result[RecordSetChange] ): Result[RecordSetChange]
def listRecordSetChanges( def listRecordSetChanges(
zoneId: Option[String], zoneId: String,
startFrom: Option[Int], startFrom: Option[Int],
maxItems: Int, maxItems: Int,
fqdn: Option[String],
recordType: Option[RecordType],
authPrincipal: AuthPrincipal authPrincipal: AuthPrincipal
): Result[ListRecordSetChangesResponse] ): Result[ListRecordSetChangesResponse]

View File

@ -38,6 +38,7 @@ object RecordSetChangeHandler extends TransactionProvider {
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 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 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 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 wrongRecordDataMessage: String = "The record data entered doesn't exist. Please enter the correct record data or leave the field empty if it's a delete operation."
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." 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 final case class Requeue(change: RecordSetChange) extends Throwable
@ -392,12 +393,18 @@ object RecordSetChangeHandler extends TransactionProvider {
case AlreadyApplied(_) => Completed(change.successful) case AlreadyApplied(_) => Completed(change.successful)
case ReadyToApply(_) => Validated(change) case ReadyToApply(_) => Validated(change)
case Failure(_, message) => case Failure(_, message) =>
if(message == outOfSyncFailureMessage || message == incompatibleRecordFailureMessage){ if(message == outOfSyncFailureMessage){
Completed( Completed(
change.failed( change.failed(
syncZoneMessage syncZoneMessage
) )
) )
} else if (message == incompatibleRecordFailureMessage) {
Completed(
change.failed(
wrongRecordDataMessage
)
)
} else if (message == "referral") { } else if (message == "referral") {
Completed( Completed(
change.failed( change.failed(

View File

@ -88,6 +88,7 @@ trait BatchChangeJsonProtocol extends JsonValidation {
( (
(js \ "inputName").required[String]("Missing BatchChangeInput.changes.inputName"), (js \ "inputName").required[String]("Missing BatchChangeInput.changes.inputName"),
recordType, recordType,
(js \ "systemMessage").optional[String],
(js \ "ttl").optional[Long], (js \ "ttl").optional[Long],
recordType.andThen(extractRecord(_, js \ "record")) recordType.andThen(extractRecord(_, js \ "record"))
).mapN(AddChangeInput.apply) ).mapN(AddChangeInput.apply)
@ -114,6 +115,7 @@ trait BatchChangeJsonProtocol extends JsonValidation {
( (
(js \ "inputName").required[String]("Missing BatchChangeInput.changes.inputName"), (js \ "inputName").required[String]("Missing BatchChangeInput.changes.inputName"),
recordType, recordType,
(js \ "systemMessage").optional[String],
recordData recordData
).mapN(DeleteRRSetChangeInput.apply) ).mapN(DeleteRRSetChangeInput.apply)
} }

View File

@ -16,13 +16,12 @@
package vinyldns.api.route package vinyldns.api.route
import akka.http.scaladsl.model.StatusCodes import akka.http.scaladsl.model._
import akka.http.scaladsl.server.{RejectionHandler, Route, ValidationRejection} import akka.http.scaladsl.server.{RejectionHandler, Route, ValidationRejection}
import vinyldns.api.config.LimitsConfig
import org.slf4j.{Logger, LoggerFactory} import org.slf4j.{Logger, LoggerFactory}
import vinyldns.api.config.ManualReviewConfig import vinyldns.api.config.{LimitsConfig, ManualReviewConfig}
import vinyldns.core.domain.batch._
import vinyldns.api.domain.batch._ import vinyldns.api.domain.batch._
import vinyldns.core.domain.batch._
class BatchChangeRoute( class BatchChangeRoute(
batchChangeService: BatchChangeServiceAlgebra, batchChangeService: BatchChangeServiceAlgebra,
@ -71,52 +70,52 @@ class BatchChangeRoute(
} }
} }
} ~ } ~
(get & monitor("Endpoint.listBatchChangeSummaries")) { (get & monitor("Endpoint.listBatchChangeSummaries")) {
parameters( parameters(
"userName".as[String].?, "userName".as[String].?,
"dateTimeRangeStart".as[String].?, "dateTimeRangeStart".as[String].?,
"dateTimeRangeEnd".as[String].?, "dateTimeRangeEnd".as[String].?,
"startFrom".as[Int].?, "startFrom".as[Int].?,
"maxItems".as[Int].?(MAX_ITEMS_LIMIT), "maxItems".as[Int].?(MAX_ITEMS_LIMIT),
"ignoreAccess".as[Boolean].?(false), "ignoreAccess".as[Boolean].?(false),
"approvalStatus".as[String].? "approvalStatus".as[String].?
) { ) {
( (
userName: Option[String], userName: Option[String],
dateTimeRangeStart: Option[String], dateTimeRangeStart: Option[String],
dateTimeRangeEnd: Option[String], dateTimeRangeEnd: Option[String],
startFrom: Option[Int], startFrom: Option[Int],
maxItems: Int, maxItems: Int,
ignoreAccess: Boolean, ignoreAccess: Boolean,
approvalStatus: Option[String] approvalStatus: Option[String]
) => ) =>
{ {
val convertApprovalStatus = approvalStatus.flatMap(BatchChangeApprovalStatus.find) val convertApprovalStatus = approvalStatus.flatMap(BatchChangeApprovalStatus.find)
handleRejections(invalidQueryHandler) { handleRejections(invalidQueryHandler) {
validate( validate(
0 < maxItems && maxItems <= MAX_ITEMS_LIMIT, 0 < maxItems && maxItems <= MAX_ITEMS_LIMIT,
s"maxItems was $maxItems, maxItems must be between 1 and $MAX_ITEMS_LIMIT, inclusive." s"maxItems was $maxItems, maxItems must be between 1 and $MAX_ITEMS_LIMIT, inclusive."
) { ) {
authenticateAndExecute( authenticateAndExecute(
batchChangeService.listBatchChangeSummaries( batchChangeService.listBatchChangeSummaries(
_, _,
userName, userName,
dateTimeRangeStart, dateTimeRangeStart,
dateTimeRangeEnd, dateTimeRangeEnd,
startFrom, startFrom,
maxItems, maxItems,
ignoreAccess, ignoreAccess,
convertApprovalStatus convertApprovalStatus
) )
) { summaries => ) { summaries =>
complete(StatusCodes.OK, summaries) complete(StatusCodes.OK, summaries)
}
} }
} }
} }
} }
} }
}
} ~ } ~
path("zones" / "batchrecordchanges" / Segment) { id => path("zones" / "batchrecordchanges" / Segment) { id =>
(get & monitor("Endpoint.getBatchChange")) { (get & monitor("Endpoint.getBatchChange")) {

View File

@ -230,8 +230,8 @@ class RecordSetRoute(
} ~ } ~
path("zones" / Segment / "recordsetchanges") { zoneId => path("zones" / Segment / "recordsetchanges") { zoneId =>
(get & monitor("Endpoint.listRecordSetChanges")) { (get & monitor("Endpoint.listRecordSetChanges")) {
parameters("startFrom".as[Int].?, "maxItems".as[Int].?(DEFAULT_MAX_ITEMS), "fqdn".as[String].?, "recordType".as[String].?) { parameters("startFrom".as[Int].?, "maxItems".as[Int].?(DEFAULT_MAX_ITEMS)) {
(startFrom: Option[Int], maxItems: Int, fqdn: Option[String], _: Option[String]) => (startFrom: Option[Int], maxItems: Int) =>
handleRejections(invalidQueryHandler) { handleRejections(invalidQueryHandler) {
validate( validate(
check = 0 < maxItems && maxItems <= DEFAULT_MAX_ITEMS, check = 0 < maxItems && maxItems <= DEFAULT_MAX_ITEMS,
@ -240,7 +240,7 @@ class RecordSetRoute(
) { ) {
authenticateAndExecute( authenticateAndExecute(
recordSetService recordSetService
.listRecordSetChanges(Some(zoneId), startFrom, maxItems, fqdn, None, _) .listRecordSetChanges(zoneId, startFrom, maxItems, _)
) { changes => ) { changes =>
complete(StatusCodes.OK, changes) complete(StatusCodes.OK, changes)
} }

View File

@ -2,11 +2,11 @@ pyhamcrest==2.0.2
pytz>=2014 pytz>=2014
pytest==6.2.5 pytest==6.2.5
mock==4.0.3 mock==4.0.3
dnspython==2.1.0 dnspython==2.6.1
boto3==1.18.51 boto3==1.18.51
botocore==1.21.51 botocore==1.21.51
requests==2.31.0 requests==2.32.3
pytest-xdist==2.4.0 pytest-xdist==2.4.0
python-dateutil==2.8.2 python-dateutil==2.8.2
filelock==3.2.0 filelock==3.2.0
pytest-custom_exit_code==0.3.0 pytest-custom_exit_code==0.3.0

View File

@ -39,8 +39,6 @@ import vinyldns.core.domain.zone.Zone
class BatchChangeConverterSpec extends AnyWordSpec with Matchers { class BatchChangeConverterSpec extends AnyWordSpec with Matchers {
private val nonExistentRecordDeleteMessage: String = "This record does not exist. " + private val nonExistentRecordDeleteMessage: String = "This record does not exist. " +
"No further action is required." "No further action is required."
private val nonExistentRecordDataDeleteMessage: String = "Record data entered does not exist. " +
"No further action is required."
private def makeSingleAddChange( private def makeSingleAddChange(
name: String, name: String,
@ -64,7 +62,7 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers {
) )
} }
private def makeSingleDeleteRRSetChange(name: String, typ: RecordType, zone: Zone = okZone) = { private def makeSingleDeleteRRSetChange(name: String, typ: RecordType, zone: Zone = okZone, systemMessage: Option[String] = None) = {
val fqdn = s"$name.${zone.name}" val fqdn = s"$name.${zone.name}"
SingleDeleteRRSetChange( SingleDeleteRRSetChange(
Some(zone.id), Some(zone.id),
@ -74,7 +72,7 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers {
typ, typ,
None, None,
SingleChangeStatus.Pending, SingleChangeStatus.Pending,
None, systemMessage,
None, None,
None None
) )
@ -88,18 +86,19 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers {
AddChangeForValidation( AddChangeForValidation(
okZone, okZone,
s"$recordName", s"$recordName",
AddChangeInput(s"$recordName.ok.", typ, Some(123), recordData), AddChangeInput(s"$recordName.ok.", typ, None, Some(123), recordData),
7200L 7200L
) )
private def makeDeleteRRSetChangeForValidation( private def makeDeleteRRSetChangeForValidation(
recordName: String, recordName: String,
typ: RecordType = RecordType.A typ: RecordType = RecordType.A,
systemMessage: Option[String] = None
): DeleteRRSetChangeForValidation = ): DeleteRRSetChangeForValidation =
DeleteRRSetChangeForValidation( DeleteRRSetChangeForValidation(
okZone, okZone,
s"$recordName", s"$recordName",
DeleteRRSetChangeInput(s"$recordName.ok.", typ) DeleteRRSetChangeInput(s"$recordName.ok.", typ, systemMessage, None)
) )
private val addSingleChangesGood = List( private val addSingleChangesGood = List(
@ -165,19 +164,11 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers {
) )
private val singleChangesOneDelete = List( private val singleChangesOneDelete = List(
makeSingleDeleteRRSetChange("DoesNotExistToDelete", A) makeSingleDeleteRRSetChange("DoesNotExistToDelete", A, okZone, Some(nonExistentRecordDeleteMessage))
)
private val singleChangesOneDeleteGood = List(
makeSingleDeleteRRSetChange("aToDelete", A).copy(recordData = Some(AData("2.3.4.6"))),
) )
private val changeForValidationOneDelete = List( private val changeForValidationOneDelete = List(
makeDeleteRRSetChangeForValidation("DoesNotExistToDelete", A) makeDeleteRRSetChangeForValidation("DoesNotExistToDelete", A, Some(nonExistentRecordDeleteMessage))
)
private val changeForValidationOneDeleteGood = List(
makeDeleteRRSetChangeForValidation("aToDelete", A)
) )
private val singleChangesOneBad = List( private val singleChangesOneBad = List(
@ -621,45 +612,6 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers {
} }
} }
"updateBatchChange" should {
"update the batch change system message when there is a delete request with non-existent record data" in {
val batchWithBadChange =
BatchChange(
okUser.id,
okUser.userName,
None,
Instant.now.truncatedTo(ChronoUnit.MILLIS),
singleChangesOneDeleteGood,
approvalStatus = BatchChangeApprovalStatus.AutoApproved
)
val result =
underTest
.updateBatchChange(
batchWithBadChange,
ChangeForValidationMap(changeForValidationOneDeleteGood.map(_.validNel), existingRecordSets),
)
// validate the batch change returned
val receivedChange = result.changes(0)
receivedChange.systemMessage shouldBe Some(nonExistentRecordDataDeleteMessage)
result.changes(0) shouldBe singleChangesOneDeleteGood(0).copy(systemMessage = Some(nonExistentRecordDataDeleteMessage))
}
}
"matchRecordData" should {
"check if the record data given matches the record data present" in {
val recordData = List(AData("1.2.3.5"), AAAAData("caec:cec6:c4ef:bb7b:1a78:d055:216d:3a78"))
val result1 = underTest.matchRecordData(recordData, AData("1.2.3.5"))
result1 shouldBe true
val result2 = underTest.matchRecordData(recordData, AData("1.2.3.4"))
result2 shouldBe false
val result3 = underTest.matchRecordData(recordData, AAAAData("caec:cec6:c4ef:bb7b:1a78:d055:216d:3a78"))
result3 shouldBe true
val result4 = underTest.matchRecordData(recordData, AAAAData("abcd:cec6:c4ef:bb7b:1a78:d055:216d:3a78"))
result4 shouldBe false
}
}
"generateAddChange" should { "generateAddChange" should {
val singleAddChange = makeSingleAddChange("shared-rs", AData("1.2.3.4"), A, sharedZone) val singleAddChange = makeSingleAddChange("shared-rs", AData("1.2.3.4"), A, sharedZone)
val ownerGroupId = Some("some-owner-group-id") val ownerGroupId = Some("some-owner-group-id")

View File

@ -31,16 +31,16 @@ class BatchChangeInputSpec extends AnyWordSpec with Matchers {
"BatchChangeInput" should { "BatchChangeInput" should {
"ensure trailing dot on A, AAAA, and CNAME fqdn" in { "ensure trailing dot on A, AAAA, and CNAME fqdn" in {
val changeA = AddChangeInput("apex.test.com", A, Some(100), AData("1.1.1.1")) val changeA = AddChangeInput("apex.test.com", A, None, Some(100), AData("1.1.1.1"))
val changeAAAA = val changeAAAA =
AddChangeInput("aaaa.test.com", AAAA, Some(3600), AAAAData("1:2:3:4:5:6:7:8")) AddChangeInput("aaaa.test.com", AAAA, None, Some(3600), AAAAData("1:2:3:4:5:6:7:8"))
val changeCname = val changeCname =
AddChangeInput("cname.test.com", CNAME, Some(100), CNAMEData(Fqdn("testing.test.com"))) AddChangeInput("cname.test.com", CNAME, None, Some(100), CNAMEData(Fqdn("testing.test.com")))
val changeADotted = AddChangeInput("adot.test.com.", A, Some(100), AData("1.1.1.1")) val changeADotted = AddChangeInput("adot.test.com.", A, None, Some(100), AData("1.1.1.1"))
val changeAAAADotted = val changeAAAADotted =
AddChangeInput("aaaadot.test.com.", AAAA, Some(3600), AAAAData("1:2:3:4:5:6:7:8")) AddChangeInput("aaaadot.test.com.", AAAA, None, Some(3600), AAAAData("1:2:3:4:5:6:7:8"))
val changeCnameDotted = val changeCnameDotted =
AddChangeInput("cnamedot.test.com.", CNAME, Some(100), CNAMEData(Fqdn("testing.test.com."))) AddChangeInput("cnamedot.test.com.", CNAME, None, Some(100), CNAMEData(Fqdn("testing.test.com.")))
val input = BatchChangeInput( val input = BatchChangeInput(
None, None,
@ -58,7 +58,7 @@ class BatchChangeInputSpec extends AnyWordSpec with Matchers {
} }
"asNewStoredChange" should { "asNewStoredChange" should {
"Convert an AddChangeInput into SingleAddChange" in { "Convert an AddChangeInput into SingleAddChange" in {
val changeA = AddChangeInput("some.test.com", A, None, AData("1.1.1.1")) val changeA = AddChangeInput("some.test.com", A, None, None, AData("1.1.1.1"))
val converted = changeA.asNewStoredChange( val converted = changeA.asNewStoredChange(
NonEmptyList.of(ZoneDiscoveryError("test")), NonEmptyList.of(ZoneDiscoveryError("test")),
VinylDNSTestHelpers.defaultTtl VinylDNSTestHelpers.defaultTtl
@ -80,7 +80,7 @@ class BatchChangeInputSpec extends AnyWordSpec with Matchers {
asAdd.recordSetId shouldBe None asAdd.recordSetId shouldBe None
} }
"Convert a DeleteChangeInput into SingleDeleteRRSetChange" in { "Convert a DeleteChangeInput into SingleDeleteRRSetChange" in {
val changeA = DeleteRRSetChangeInput("some.test.com", A) val changeA = DeleteRRSetChangeInput("some.test.com", A, None)
val converted = changeA.asNewStoredChange( val converted = changeA.asNewStoredChange(
NonEmptyList.of(ZoneDiscoveryError("test")), NonEmptyList.of(ZoneDiscoveryError("test")),
VinylDNSTestHelpers.defaultTtl VinylDNSTestHelpers.defaultTtl
@ -111,14 +111,14 @@ class BatchChangeInputSpec extends AnyWordSpec with Matchers {
1234, 1234,
AData("1.2.3.4"), AData("1.2.3.4"),
SingleChangeStatus.NeedsReview, SingleChangeStatus.NeedsReview,
Some("msg"), None,
None, None,
None, None,
List(SingleChangeError(DomainValidationErrorType.ZoneDiscoveryError, "test err")) List(SingleChangeError(DomainValidationErrorType.ZoneDiscoveryError, "test err"))
) )
val expectedAddChange = val expectedAddChange =
AddChangeInput("testRname.testZoneName.", A, Some(1234), AData("1.2.3.4")) AddChangeInput("testRname.testZoneName.", A, None, Some(1234), AData("1.2.3.4"))
val singleDelChange = SingleDeleteRRSetChange( val singleDelChange = SingleDeleteRRSetChange(
Some("testZoneId"), Some("testZoneId"),
@ -128,14 +128,14 @@ class BatchChangeInputSpec extends AnyWordSpec with Matchers {
A, A,
None, None,
SingleChangeStatus.NeedsReview, SingleChangeStatus.NeedsReview,
Some("msg"), None,
None, None,
None, None,
List(SingleChangeError(DomainValidationErrorType.ZoneDiscoveryError, "test err")) List(SingleChangeError(DomainValidationErrorType.ZoneDiscoveryError, "test err"))
) )
val expectedDelChange = val expectedDelChange =
DeleteRRSetChangeInput("testRname.testZoneName.", A) DeleteRRSetChangeInput("testRname.testZoneName.", A, None)
val change = BatchChange( val change = BatchChange(
"userId", "userId",

View File

@ -81,34 +81,36 @@ class BatchChangeServiceSpec
) )
private val ttl = Some(200L) private val ttl = Some(200L)
private val apexAddA = AddChangeInput("apex.test.com", RecordType.A, ttl, AData("1.1.1.1")) private val apexAddA = AddChangeInput("apex.test.com", RecordType.A, None, ttl, AData("1.1.1.1"))
private val nonApexAddA = private val nonApexAddA =
AddChangeInput("non-apex.test.com", RecordType.A, ttl, AData("1.1.1.1")) AddChangeInput("non-apex.test.com", RecordType.A, None, ttl, AData("1.1.1.1"))
private val onlyApexAddA = private val onlyApexAddA =
AddChangeInput("only.apex.exists", RecordType.A, ttl, AData("1.1.1.1")) AddChangeInput("only.apex.exists", RecordType.A, None, ttl, AData("1.1.1.1"))
private val onlyBaseAddAAAA = private val onlyBaseAddAAAA =
AddChangeInput("have.only.base", RecordType.AAAA, ttl, AAAAData("1:2:3:4:5:6:7:8")) AddChangeInput("have.only.base", RecordType.AAAA, None, ttl, AAAAData("1:2:3:4:5:6:7:8"))
private val noZoneAddA = AddChangeInput("no.zone.match.", RecordType.A, ttl, AData("1.1.1.1")) private val noZoneAddA = AddChangeInput("no.zone.match.", RecordType.A, None, ttl, AData("1.1.1.1"))
private val dottedAddA = private val dottedAddA =
AddChangeInput("dot.ted.apex.test.com", RecordType.A, ttl, AData("1.1.1.1")) AddChangeInput("dot.ted.apex.test.com", RecordType.A, None, ttl, AData("1.1.1.1"))
private val cnameAdd = private val cnameAdd =
AddChangeInput("cname.test.com", RecordType.CNAME, ttl, CNAMEData(Fqdn("testing.test.com."))) AddChangeInput("cname.test.com", RecordType.CNAME, None, ttl, CNAMEData(Fqdn("testing.test.com.")))
private val cnameApexAdd = private val cnameApexAdd =
AddChangeInput("apex.test.com", RecordType.CNAME, ttl, CNAMEData(Fqdn("testing.test.com."))) AddChangeInput("apex.test.com", RecordType.CNAME, None, ttl, CNAMEData(Fqdn("testing.test.com.")))
private val cnameReverseAdd = AddChangeInput( private val cnameReverseAdd = AddChangeInput(
"cname.55.144.10.in-addr.arpa", "cname.55.144.10.in-addr.arpa",
RecordType.CNAME, RecordType.CNAME,
None,
ttl, ttl,
CNAMEData(Fqdn("testing.cname.com.")) CNAMEData(Fqdn("testing.cname.com."))
) )
private val ptrAdd = AddChangeInput("10.144.55.11", RecordType.PTR, ttl, PTRData(Fqdn("ptr"))) private val ptrAdd = AddChangeInput("10.144.55.11", RecordType.PTR, None, ttl, PTRData(Fqdn("ptr")))
private val ptrAdd2 = AddChangeInput("10.144.55.255", RecordType.PTR, ttl, PTRData(Fqdn("ptr"))) private val ptrAdd2 = AddChangeInput("10.144.55.255", RecordType.PTR, None, ttl, PTRData(Fqdn("ptr")))
private val ptrDelegatedAdd = private val ptrDelegatedAdd =
AddChangeInput("192.0.2.193", RecordType.PTR, ttl, PTRData(Fqdn("ptr"))) AddChangeInput("192.0.2.193", RecordType.PTR, None, ttl, PTRData(Fqdn("ptr")))
private val ptrV6Add = private val ptrV6Add =
AddChangeInput( AddChangeInput(
"2001:0000:0000:0000:0000:ff00:0042:8329", "2001:0000:0000:0000:0000:ff00:0042:8329",
RecordType.PTR, RecordType.PTR,
None,
ttl, ttl,
PTRData(Fqdn("ptr")) PTRData(Fqdn("ptr"))
) )
@ -489,6 +491,7 @@ class BatchChangeServiceSpec
val ptr = AddChangeInput( val ptr = AddChangeInput(
"2001:0000:0000:0001:0000:ff00:0042:8329", "2001:0000:0000:0001:0000:ff00:0042:8329",
RecordType.PTR, RecordType.PTR,
None,
ttl, ttl,
PTRData(Fqdn("ptr")) PTRData(Fqdn("ptr"))
) )
@ -520,6 +523,7 @@ class BatchChangeServiceSpec
val ptr = AddChangeInput( val ptr = AddChangeInput(
"2001:0000:0000:0001:0000:ff00:0042:8329", "2001:0000:0000:0001:0000:ff00:0042:8329",
RecordType.PTR, RecordType.PTR,
None,
ttl, ttl,
PTRData(Fqdn("ptr")) PTRData(Fqdn("ptr"))
) )
@ -577,12 +581,12 @@ class BatchChangeServiceSpec
} }
"succeed with excluded TTL" in { "succeed with excluded TTL" in {
val noTtl = AddChangeInput("no-ttl-add.test.com", RecordType.A, None, AData("1.1.1.1")) val noTtl = AddChangeInput("no-ttl-add.test.com", RecordType.A, None, None, AData("1.1.1.1"))
val withTtl = val withTtl =
AddChangeInput("with-ttl-add-2.test.com", RecordType.A, Some(900), AData("1.1.1.1")) AddChangeInput("with-ttl-add-2.test.com", RecordType.A, None, Some(900), AData("1.1.1.1"))
val noTtlDel = DeleteRRSetChangeInput("non-apex.test.com.", RecordType.TXT) val noTtlDel = DeleteRRSetChangeInput("non-apex.test.com.", RecordType.TXT, None)
val noTtlUpdate = val noTtlUpdate =
AddChangeInput("non-apex.test.com.", RecordType.TXT, None, TXTData("hello")) AddChangeInput("non-apex.test.com.", RecordType.TXT, None, None, TXTData("hello"))
val input = BatchChangeInput(None, List(noTtl, withTtl, noTtlDel, noTtlUpdate)) val input = BatchChangeInput(None, List(noTtl, withTtl, noTtlDel, noTtlUpdate))
val result = underTest.applyBatchChange(input, auth, true).value.unsafeRunSync().toOption.get val result = underTest.applyBatchChange(input, auth, true).value.unsafeRunSync().toOption.get
@ -1172,7 +1176,7 @@ class BatchChangeServiceSpec
"0.1.0.0.2.ip6.arpa." "0.1.0.0.2.ip6.arpa."
) )
val ptr = AddChangeInput(ip, RecordType.PTR, ttl, PTRData(Fqdn("ptr."))).validNel val ptr = AddChangeInput(ip, RecordType.PTR, None, ttl, PTRData(Fqdn("ptr."))).validNel
val underTestPTRZonesList: ExistingZones = underTest.getZonesForRequest(List(ptr)).unsafeRunSync() val underTestPTRZonesList: ExistingZones = underTest.getZonesForRequest(List(ptr)).unsafeRunSync()
val zoneNames = underTestPTRZonesList.zones.map(_.name) val zoneNames = underTestPTRZonesList.zones.map(_.name)
@ -1198,7 +1202,7 @@ class BatchChangeServiceSpec
) )
val ip = "2001:0db8:0000:0000:0000:ff00:0042:8329" val ip = "2001:0db8:0000:0000:0000:ff00:0042:8329"
val ptr = AddChangeInput(ip, RecordType.PTR, ttl, PTRData(Fqdn("ptr."))).validNel val ptr = AddChangeInput(ip, RecordType.PTR, None, ttl, PTRData(Fqdn("ptr."))).validNel
val underTestPTRZonesList: ExistingZones = underTest.getZonesForRequest(List(ptr)).unsafeRunSync() val underTestPTRZonesList: ExistingZones = underTest.getZonesForRequest(List(ptr)).unsafeRunSync()
val zoneNames = underTestPTRZonesList.zones.map(_.name) val zoneNames = underTestPTRZonesList.zones.map(_.name)
@ -1245,7 +1249,7 @@ class BatchChangeServiceSpec
val ips = ip1 :: ip2s val ips = ip1 :: ip2s
val ptrs = ips.map { v6Name => val ptrs = ips.map { v6Name =>
AddChangeInput(v6Name, RecordType.PTR, ttl, PTRData(Fqdn("ptr."))).validNel AddChangeInput(v6Name, RecordType.PTR, None, ttl, PTRData(Fqdn("ptr."))).validNel
} }
val underTestPTRZonesList: ExistingZones = underTest.getZonesForRequest(ptrs).unsafeRunSync() val underTestPTRZonesList: ExistingZones = underTest.getZonesForRequest(ptrs).unsafeRunSync()
@ -1300,10 +1304,10 @@ class BatchChangeServiceSpec
"properly discover records in forward zones" in { "properly discover records in forward zones" in {
val apex = apexZone.name val apex = apexZone.name
val aApex = AddChangeInput(apex, RecordType.A, ttl, AData("1.2.3.4")) val aApex = AddChangeInput(apex, RecordType.A, None, ttl, AData("1.2.3.4"))
val aNormal = AddChangeInput(s"record.$apex", RecordType.A, ttl, AData("1.2.3.4")) val aNormal = AddChangeInput(s"record.$apex", RecordType.A, None, ttl, AData("1.2.3.4"))
val aDotted = val aDotted =
AddChangeInput(s"some.dotted.record.$apex", RecordType.A, ttl, AData("1.2.3.4")) AddChangeInput(s"some.dotted.record.$apex", RecordType.A, None, ttl, AData("1.2.3.4"))
val expected = List( val expected = List(
AddChangeForValidation(apexZone, apex, aApex, 7200L), AddChangeForValidation(apexZone, apex, aApex, 7200L),
@ -1322,10 +1326,10 @@ class BatchChangeServiceSpec
"properly discover TXT records" in { "properly discover TXT records" in {
val apex = apexZone.name val apex = apexZone.name
val txtApex = AddChangeInput(apex, RecordType.TXT, ttl, TXTData("test")) val txtApex = AddChangeInput(apex, RecordType.TXT, None, ttl, TXTData("test"))
val txtNormal = AddChangeInput(s"record.$apex", RecordType.TXT, ttl, TXTData("test")) val txtNormal = AddChangeInput(s"record.$apex", RecordType.TXT, None, ttl, TXTData("test"))
val txtDotted = val txtDotted =
AddChangeInput(s"some.dotted.record.$apex", RecordType.TXT, ttl, TXTData("test")) AddChangeInput(s"some.dotted.record.$apex", RecordType.TXT, None, ttl, TXTData("test"))
val expected = List( val expected = List(
AddChangeForValidation(apexZone, apex, txtApex, 7200L), AddChangeForValidation(apexZone, apex, txtApex, 7200L),
@ -1419,20 +1423,22 @@ class BatchChangeServiceSpec
val ptrv6ZoneBig = Zone("0.1.0.0.2.ip6.arpa.", "email", id = "ptrv6big") val ptrv6ZoneBig = Zone("0.1.0.0.2.ip6.arpa.", "email", id = "ptrv6big")
val smallZoneAdd = val smallZoneAdd =
AddChangeInput("2001:db8::ff00:42:8329", RecordType.PTR, ttl, PTRData(Fqdn("ptr"))) AddChangeInput("2001:db8::ff00:42:8329", RecordType.PTR, None, ttl, PTRData(Fqdn("ptr")))
val medZoneAdd = AddChangeInput( val medZoneAdd = AddChangeInput(
"2001:0db8:0111:0000:0000:ff00:0042:8329", "2001:0db8:0111:0000:0000:ff00:0042:8329",
RecordType.PTR, RecordType.PTR,
None,
ttl, ttl,
PTRData(Fqdn("ptr")) PTRData(Fqdn("ptr"))
) )
val bigZoneAdd = AddChangeInput( val bigZoneAdd = AddChangeInput(
"2001:0000:0000:0000:0000:ff00:0042:8329", "2001:0000:0000:0000:0000:ff00:0042:8329",
RecordType.PTR, RecordType.PTR,
None,
ttl, ttl,
PTRData(Fqdn("ptr")) PTRData(Fqdn("ptr"))
) )
val notFoundZoneAdd = AddChangeInput("::1", RecordType.PTR, ttl, PTRData(Fqdn("ptr"))) val notFoundZoneAdd = AddChangeInput("::1", RecordType.PTR, None, ttl, PTRData(Fqdn("ptr")))
val ptripv6Adds = List( val ptripv6Adds = List(
smallZoneAdd.validNel, smallZoneAdd.validNel,
@ -1678,7 +1684,7 @@ class BatchChangeServiceSpec
"return a BatchChange if all data inputs are valid/soft failures and manual review is enabled and owner group ID " + "return a BatchChange if all data inputs are valid/soft failures and manual review is enabled and owner group ID " +
"is provided" in { "is provided" in {
val delete = DeleteRRSetChangeInput("some.test.delete.", RecordType.TXT) val delete = DeleteRRSetChangeInput("some.test.delete.", RecordType.TXT, None)
val result = underTestManualEnabled val result = underTestManualEnabled
.buildResponse( .buildResponse(
BatchChangeInput(None, List(apexAddA, onlyBaseAddAAAA, delete), Some("owner-group-ID")), BatchChangeInput(None, List(apexAddA, onlyBaseAddAAAA, delete), Some("owner-group-ID")),
@ -1805,7 +1811,7 @@ class BatchChangeServiceSpec
} }
"return a BatchChangeErrorList if all data inputs are valid/soft failures and manual review is disabled" in { "return a BatchChangeErrorList if all data inputs are valid/soft failures and manual review is disabled" in {
val delete = DeleteRRSetChangeInput("some.test.delete.", RecordType.TXT) val delete = DeleteRRSetChangeInput("some.test.delete.", RecordType.TXT, None)
val result = underTest val result = underTest
.buildResponse( .buildResponse(
BatchChangeInput(None, List(apexAddA, onlyBaseAddAAAA, delete)), BatchChangeInput(None, List(apexAddA, onlyBaseAddAAAA, delete)),
@ -1825,7 +1831,7 @@ class BatchChangeServiceSpec
"return a BatchChangeErrorList if all data inputs are valid/soft failures, scheduled, " + "return a BatchChangeErrorList if all data inputs are valid/soft failures, scheduled, " +
"and manual review is disabled" in { "and manual review is disabled" in {
val delete = DeleteRRSetChangeInput("some.test.delete.", RecordType.TXT) val delete = DeleteRRSetChangeInput("some.test.delete.", RecordType.TXT, None)
val result = underTest val result = underTest
.buildResponse( .buildResponse(
BatchChangeInput( BatchChangeInput(
@ -1872,7 +1878,7 @@ class BatchChangeServiceSpec
"return a BatchChangeErrorList if all data inputs are valid/soft failures, manual review is enabled, " + "return a BatchChangeErrorList if all data inputs are valid/soft failures, manual review is enabled, " +
"but batch change allowManualReview attribute is false" in { "but batch change allowManualReview attribute is false" in {
val delete = DeleteRRSetChangeInput("some.test.delete.", RecordType.TXT) val delete = DeleteRRSetChangeInput("some.test.delete.", RecordType.TXT, None)
val result = underTestManualEnabled val result = underTestManualEnabled
.buildResponse( .buildResponse(
BatchChangeInput(None, List(apexAddA, onlyBaseAddAAAA, delete)), BatchChangeInput(None, List(apexAddA, onlyBaseAddAAAA, delete)),

View File

@ -1951,19 +1951,22 @@ class RecordSetServiceSpec
val completeRecordSetChanges: List[RecordSetChange] = val completeRecordSetChanges: List[RecordSetChange] =
List(pendingCreateAAAA, pendingCreateCNAME, completeCreateAAAA, completeCreateCNAME) List(pendingCreateAAAA, pendingCreateCNAME, completeCreateAAAA, completeCreateCNAME)
doReturn(IO.pure(Some(zoneActive)))
.when(mockZoneRepo)
.getZone(zoneActive.id)
doReturn(IO.pure(ListRecordSetChangesResults(completeRecordSetChanges))) doReturn(IO.pure(ListRecordSetChangesResults(completeRecordSetChanges)))
.when(mockRecordChangeRepo) .when(mockRecordChangeRepo)
.listRecordSetChanges(zoneId = Some(okZone.id), startFrom = None, maxItems = 100, fqdn = None, recordType = None) .listRecordSetChanges(zoneId = Some(zoneActive.id), startFrom = None, maxItems = 100, fqdn = None, recordType = None)
doReturn(IO.pure(ListUsersResults(Seq(okUser), None))) doReturn(IO.pure(ListUsersResults(Seq(okUser), None)))
.when(mockUserRepo) .when(mockUserRepo)
.getUsers(any[Set[String]], any[Option[String]], any[Option[Int]]) .getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
val result: ListRecordSetChangesResponse = val result: ListRecordSetChangesResponse =
underTest.listRecordSetChanges(Some(okZone.id), authPrincipal = okAuth).value.unsafeRunSync().toOption.get underTest.listRecordSetChanges(zoneActive.id, authPrincipal = okAuth).value.unsafeRunSync().toOption.get
val changesWithName = val changesWithName =
completeRecordSetChanges.map(change => RecordSetChangeInfo(change, Some("ok"))) completeRecordSetChanges.map(change => RecordSetChangeInfo(change, Some("ok")))
val expectedResults = ListRecordSetChangesResponse( val expectedResults = ListRecordSetChangesResponse(
zoneId = okZone.id, zoneId = zoneActive.id,
recordSetChanges = changesWithName, recordSetChanges = changesWithName,
nextId = None, nextId = None,
startFrom = None, startFrom = None,
@ -2010,7 +2013,7 @@ class RecordSetServiceSpec
.getUsers(any[Set[String]], any[Option[String]], any[Option[Int]]) .getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
val result: ListRecordSetChangesResponse = val result: ListRecordSetChangesResponse =
underTest.listRecordSetChanges(Some(okZone.id), authPrincipal = okAuth).value.unsafeRunSync().toOption.get underTest.listRecordSetChanges(okZone.id, authPrincipal = okAuth).value.unsafeRunSync().toOption.get
val expectedResults = ListRecordSetChangesResponse( val expectedResults = ListRecordSetChangesResponse(
zoneId = okZone.id, zoneId = okZone.id,
recordSetChanges = List(), recordSetChanges = List(),
@ -2076,7 +2079,7 @@ class RecordSetServiceSpec
"return a NotAuthorizedError" in { "return a NotAuthorizedError" in {
val error = val error =
underTest.listRecordSetChanges(Some(zoneNotAuthorized.id), authPrincipal = okAuth).value.unsafeRunSync().swap.toOption.get underTest.listRecordSetChanges(zoneNotAuthorized.id, authPrincipal = okAuth).value.unsafeRunSync().swap.toOption.get
error shouldBe a[NotAuthorizedError] error shouldBe a[NotAuthorizedError]
} }
@ -2093,7 +2096,7 @@ class RecordSetServiceSpec
.getUsers(any[Set[String]], any[Option[String]], any[Option[Int]]) .getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
val result: ListRecordSetChangesResponse = val result: ListRecordSetChangesResponse =
underTest.listRecordSetChanges(Some(okZone.id), authPrincipal = okAuth).value.unsafeRunSync().toOption.get underTest.listRecordSetChanges(okZone.id, authPrincipal = okAuth).value.unsafeRunSync().toOption.get
val changesWithName = val changesWithName =
List(RecordSetChangeInfo(rsChange2, Some("ok")), RecordSetChangeInfo(rsChange1, Some("ok"))) List(RecordSetChangeInfo(rsChange2, Some("ok")), RecordSetChangeInfo(rsChange1, Some("ok")))
val expectedResults = ListRecordSetChangesResponse( val expectedResults = ListRecordSetChangesResponse(

View File

@ -127,15 +127,15 @@ class BatchChangeJsonProtocolSpec
addCNAMEChangeInputJson addCNAMEChangeInputJson
) )
val addAChangeInput = AddChangeInput("foo.", A, Some(3600), AData("1.1.1.1")) val addAChangeInput = AddChangeInput("foo.", A, None, Some(3600), AData("1.1.1.1"))
val deleteAChangeInput = DeleteRRSetChangeInput("foo.", A) val deleteAChangeInput = DeleteRRSetChangeInput("foo.", A, None)
val addAAAAChangeInput = AddChangeInput("bar.", AAAA, Some(1200), AAAAData("1:2:3:4:5:6:7:8")) val addAAAAChangeInput = AddChangeInput("bar.", AAAA, None, Some(1200), AAAAData("1:2:3:4:5:6:7:8"))
val addCNAMEChangeInput = AddChangeInput("bizz.baz.", CNAME, Some(200), CNAMEData(Fqdn("buzz."))) val addCNAMEChangeInput = AddChangeInput("bizz.baz.", CNAME, None, Some(200), CNAMEData(Fqdn("buzz.")))
val addPTRChangeInput = AddChangeInput("4.5.6.7", PTR, Some(200), PTRData(Fqdn("test.com."))) val addPTRChangeInput = AddChangeInput("4.5.6.7", PTR, None, Some(200), PTRData(Fqdn("test.com.")))
val fooDiscoveryError = ZoneDiscoveryError("foo.") val fooDiscoveryError = ZoneDiscoveryError("foo.")
@ -211,7 +211,7 @@ class BatchChangeJsonProtocolSpec
) )
val result = ChangeInputSerializer.fromJson(json).value val result = ChangeInputSerializer.fromJson(json).value
result shouldBe AddChangeInput("foo.", A, None, AData("1.1.1.1")) result shouldBe AddChangeInput("foo.", A, None, None, AData("1.1.1.1"))
} }
"return an error if the record is not specified for add" in { "return an error if the record is not specified for add" in {
@ -250,7 +250,7 @@ class BatchChangeJsonProtocolSpec
"serializing ChangeInputSerializer to JSON" should { "serializing ChangeInputSerializer to JSON" should {
"successfully serialize valid data for delete" in { "successfully serialize valid data for delete" in {
val deleteChangeInput = DeleteRRSetChangeInput("foo.", A, Some(AData("1.1.1.1"))) val deleteChangeInput = DeleteRRSetChangeInput("foo.", A, None, Some(AData("1.1.1.1")))
val json: JObject = buildDeleteRRSetInputJson(Some("foo."), Some(A), Some(AData("1.1.1.1"))) val json: JObject = buildDeleteRRSetInputJson(Some("foo."), Some(A), Some(AData("1.1.1.1")))
val result = ChangeInputSerializer.toJson(deleteChangeInput) val result = ChangeInputSerializer.toJson(deleteChangeInput)

View File

@ -758,16 +758,14 @@ class RecordSetRoutingSpec
}.toResult }.toResult
def listRecordSetChanges( def listRecordSetChanges(
zoneId: Option[String], zoneId: String,
startFrom: Option[Int], startFrom: Option[Int],
maxItems: Int, maxItems: Int,
fqdn: Option[String],
recordType: Option[RecordType],
authPrincipal: AuthPrincipal authPrincipal: AuthPrincipal
): Result[ListRecordSetChangesResponse] = { ): Result[ListRecordSetChangesResponse] = {
zoneId match { zoneId match {
case Some(zoneNotFound.id) => Left(ZoneNotFoundError(s"$zoneId")) case zoneNotFound.id => Left(ZoneNotFoundError(s"$zoneId"))
case Some(notAuthorizedZone.id) => Left(NotAuthorizedError("no way")) case notAuthorizedZone.id => Left(NotAuthorizedError("no way"))
case _ => Right(listRecordSetChangesResponse) case _ => Right(listRecordSetChangesResponse)
} }
}.toResult }.toResult

View File

@ -352,6 +352,10 @@ akka.http {
# Set to `infinite` to disable. # Set to `infinite` to disable.
bind-timeout = 5s 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 # Show verbose error messages back to the client
verbose-error-messages = on verbose-error-messages = on
} }

View File

@ -7,7 +7,7 @@
</encoder> </encoder>
</appender> </appender>
<logger name="com.zaxxer.hikari" level="TRACE"> <logger name="com.zaxxer.hikari" level="ERROR">
<appender-ref ref="CONSOLE"/> <appender-ref ref="CONSOLE"/>
</logger> </logger>

View File

@ -46,11 +46,11 @@ sealed trait SingleChange {
delete.copy(status = SingleChangeStatus.Failed, systemMessage = Some(error)) delete.copy(status = SingleChangeStatus.Failed, systemMessage = Some(error))
} }
def withDoesNotExistMessage(error: String): SingleChange = this match { def withDoesNotExistMessage: SingleChange = this match {
case add: SingleAddChange => case add: SingleAddChange =>
add.copy(status = SingleChangeStatus.Failed, systemMessage = Some(error)) add.copy(status = SingleChangeStatus.Failed)
case delete: SingleDeleteRRSetChange => case delete: SingleDeleteRRSetChange =>
delete.copy(status = SingleChangeStatus.Complete, systemMessage = Some(error)) delete.copy(status = SingleChangeStatus.Complete)
} }
def withProcessingError(message: Option[String], failedRecordChangeId: String): SingleChange = def withProcessingError(message: Option[String], failedRecordChangeId: String): SingleChange =

View File

@ -990,7 +990,10 @@ dotted-hosts = {
# The time period within which the TCP binding process must be completed. # The time period within which the TCP binding process must be completed.
# Set to `infinite` to disable. # Set to `infinite` to disable.
bind-timeout = 5s 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 # Show verbose error messages back to the client
verbose-error-messages = on verbose-error-messages = on
} }

View File

@ -17,7 +17,7 @@ can read data from the Directory. Once you have that information, proceed to th
**Considerations** **Considerations**
You _should_ communicate to your Directory over LDAP using TLS. To do so, the SSL certs should be installed You _should_ communicate to your Directory over LDAP using TLS. To do so, the SSL certs should be installed
on the portal servers, or provided via a java trust store (key store). The portal provides an option to specific on the portal servers, or provided via a java trust store (key store). The portal provides an option to specific
a java key store when it starts up. a java key store when it starts up. For more information: [Using Java Key Store In VinylDNS](https://github.com/vinyldns/vinyldns/tree/master/modules/portal#building-locally)
## Configuring LDAP ## Configuring LDAP
Before you can configure LDAP, make note of the host, username, and password that you will be using. Before you can configure LDAP, make note of the host, username, and password that you will be using.

View File

@ -216,13 +216,11 @@ class MySqlRecordChangeRepositoryIntegrationSpec
} }
saveRecChange.attempt.unsafeRunSync() shouldBe right saveRecChange.attempt.unsafeRunSync() shouldBe right
val page1 = repo.listFailedRecordSetChanges(Some(okZone.id), 2, 0).unsafeRunSync() val page1 = repo.listFailedRecordSetChanges(Some(okZone.id), 2, 0).unsafeRunSync()
println(page1.items)
page1.nextId shouldBe 3 page1.nextId shouldBe 3
page1.maxItems shouldBe 2 page1.maxItems shouldBe 2
(page1.items should contain).theSameElementsInOrderAs(expectedOrder.take(2)) (page1.items should contain).theSameElementsInOrderAs(expectedOrder.take(2))
val page2 = repo.listFailedRecordSetChanges(Some(okZone.id), 2, page1.nextId).unsafeRunSync() val page2 = repo.listFailedRecordSetChanges(Some(okZone.id), 2, page1.nextId).unsafeRunSync()
println(page2.items)
page2.nextId shouldBe 6 page2.nextId shouldBe 6
page2.maxItems shouldBe 2 page2.maxItems shouldBe 2
(page2.items should contain).theSameElementsInOrderAs(expectedOrder.slice(3, 5)) (page2.items should contain).theSameElementsInOrderAs(expectedOrder.slice(3, 5))

View File

@ -17,9 +17,9 @@
package vinyldns.mysql.repository package vinyldns.mysql.repository
import java.sql.Timestamp import java.sql.Timestamp
import cats.data._ import cats.data._
import cats.effect._ import cats.effect._
import java.time.Instant import java.time.Instant
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import scalikejdbc._ import scalikejdbc._
@ -166,8 +166,8 @@ class MySqlBatchChangeRepository
} }
def getBatchFromSingleChangeId( def getBatchFromSingleChangeId(
singleChangeId: String singleChangeId: String
)(implicit s: DBSession): Option[BatchChange] = )(implicit s: DBSession): Option[BatchChange] =
GET_BATCH_CHANGE_METADATA_FROM_SINGLE_CHANGE GET_BATCH_CHANGE_METADATA_FROM_SINGLE_CHANGE
.bind(singleChangeId) .bind(singleChangeId)
.map(extractBatchChange(None)) .map(extractBatchChange(None))
@ -181,12 +181,9 @@ class MySqlBatchChangeRepository
.apply() .apply()
batchMeta.copy(changes = changes) batchMeta.copy(changes = changes)
} }
monitor("repo.BatchChangeJDBC.updateSingleChanges") { monitor("repo.BatchChangeJDBC.updateSingleChanges") {
IO { IO {
logger.info( logger.info(s"Updating single change status: ${singleChanges.map(ch => (ch.id, ch.status))}")
s"Updating single change statuses: ${singleChanges.map(ch => (ch.id, ch.status))}"
)
DB.localTx { implicit s => DB.localTx { implicit s =>
for { for {
headChange <- singleChanges.headOption headChange <- singleChanges.headOption
@ -194,8 +191,7 @@ class MySqlBatchChangeRepository
_ = UPDATE_SINGLE_CHANGE.batchByName(batchParams: _*).apply() _ = UPDATE_SINGLE_CHANGE.batchByName(batchParams: _*).apply()
batchChange <- getBatchFromSingleChangeId(headChange.id) batchChange <- getBatchFromSingleChangeId(headChange.id)
} yield batchChange } yield batchChange
} }}
}
} }
} }
@ -394,7 +390,6 @@ class MySqlBatchChangeRepository
case Left(e) => throw e case Left(e) => throw e
} }
} }
PUT_SINGLE_CHANGE.batchByName(singleChangesParams: _*).apply() PUT_SINGLE_CHANGE.batchByName(singleChangesParams: _*).apply()
batchChange batchChange
} }

View File

@ -23,7 +23,7 @@ import vinyldns.core.domain.record.RecordType.RecordType
import vinyldns.core.domain.record._ import vinyldns.core.domain.record._
import vinyldns.core.protobuf.ProtobufConversions import vinyldns.core.protobuf.ProtobufConversions
import vinyldns.core.route.Monitored import vinyldns.core.route.Monitored
import vinyldns.mysql.repository.MySqlRecordSetRepository.fromRecordType import vinyldns.mysql.repository.MySqlRecordSetRepository.{fromRecordType, toFQDN}
import vinyldns.proto.VinylDNSProto import vinyldns.proto.VinylDNSProto
class MySqlRecordChangeRepository class MySqlRecordChangeRepository
@ -103,7 +103,7 @@ class MySqlRecordChangeRepository
change.zoneId, change.zoneId,
change.created.toEpochMilli, change.created.toEpochMilli,
fromChangeType(change.changeType), fromChangeType(change.changeType),
if(change.recordSet.name == change.zone.name) change.zone.name else change.recordSet.name + "." + change.zone.name, toFQDN(change.zone.name, change.recordSet.name),
fromRecordType(change.recordSet.typ), fromRecordType(change.recordSet.typ),
toPB(change).toByteArray, toPB(change).toByteArray,
) )

View File

@ -326,7 +326,7 @@
<label class="batch-change-csv-label btn btn-default" for="batchChangeCsv" id="batchChangeCsvImportLabel"> <label class="batch-change-csv-label btn btn-default" for="batchChangeCsv" id="batchChangeCsvImportLabel">
<span><span class="glyphicon glyphicon-import"></span> Import CSV</span> <span><span class="glyphicon glyphicon-import"></span> Import CSV</span>
</label> </label>
<input type="file" id="batchChangeCsv" ng-model="csvInput" name="batchChangeCsv" class="batchChangeCsv" ng-change="uploadCSV(createBatchChangeForm.batchChangeCsv.$viewValue)" batch-change-file> <input type="file" id="batchChangeCsv" ng-model="csvInput" name="batchChangeCsv" class="batchChangeCsv" ng-change="uploadCSV(createBatchChangeForm.batchChangeCsv.$viewValue, batchChangeLimit )" batch-change-file>
<p><a href="https://www.vinyldns.io/portal/dns-changes#dns-change-csv-import" target="_blank" rel="noopener noreferrer">See documentation for sample CSV</a></p> <p><a href="https://www.vinyldns.io/portal/dns-changes#dns-change-csv-import" target="_blank" rel="noopener noreferrer">See documentation for sample CSV</a></p>
</div> </div>
<p ng-if="newBatch.changes.length >= batchChangeLimit">Limit reached. Cannot add more than {{batchChangeLimit}} records per DNS change.</p> <p ng-if="newBatch.changes.length >= batchChangeLimit">Limit reached. Cannot add more than {{batchChangeLimit}} records per DNS change.</p>
@ -336,6 +336,9 @@
<button type="button" id="create-batch-changes-button" class="btn btn-primary" ng-click="submitChange(manualReviewEnabled)">Submit</button> <button type="button" id="create-batch-changes-button" class="btn btn-primary" ng-click="submitChange(manualReviewEnabled)">Submit</button>
</div> </div>
<div ng-if="formStatus=='pendingConfirm'" class="pull-right"> <div ng-if="formStatus=='pendingConfirm'" class="pull-right">
<div class="modal fade" id="loader" tabindex="-1" role="dialog" >
<div class="spinner" ></div>
</div>
<span ng-if="!batchChangeErrors">{{ confirmationPrompt }}</span> <span ng-if="!batchChangeErrors">{{ confirmationPrompt }}</span>
<span ng-if="batchChangeErrors" class="batch-change-error-help">There were errors, please review the highlighted rows and then proceed.</span> <span ng-if="batchChangeErrors" class="batch-change-error-help">There were errors, please review the highlighted rows and then proceed.</span>
<button class="btn btn-default" ng-click="cancelSubmit()">Cancel</button> <button class="btn btn-default" ng-click="cancelSubmit()">Cancel</button>

View File

@ -347,7 +347,7 @@
name="name" name="name"
class="form-control" class="form-control"
ng-model="currentGroup.name" ng-model="currentGroup.name"
ng-model-options="{ updateOn: 'submit' }" ng-change="checkForChanges()"
type="text" type="text"
required> required>
</input> </input>
@ -360,7 +360,7 @@
name="email" name="email"
class="form-control" class="form-control"
ng-model="currentGroup.email" ng-model="currentGroup.email"
ng-model-options="{ updateOn: 'submit' }" ng-change="checkForChanges()"
type="text" type="text"
required> required>
</input> </input>
@ -389,7 +389,7 @@
name="description" name="description"
class="form-control" class="form-control"
ng-model="currentGroup.description" ng-model="currentGroup.description"
ng-model-options="{ updateOn: 'submit' }" ng-change="checkForChanges()"
type="text"> type="text">
</input> </input>
<span class="help-block"> <span class="help-block">
@ -399,7 +399,7 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button id="edit-group-button" class="btn btn-primary pull-right">Update</button> <button id="edit-group-button" class="btn btn-primary pull-right" ng-disabled="submitEditGroupForm.$invalid || !hasChanges">Update</button>
<button type="button" class="btn btn-default" data-dismiss="modal" ng-click="closeEditModal()">Close</button> <button type="button" class="btn btn-default" data-dismiss="modal" ng-click="closeEditModal()">Close</button>
</div> </div>
</div> </div>

View File

@ -67,6 +67,7 @@
<div class="col-md-2 pull-right"> <div class="col-md-2 pull-right">
<form class="input-group remove-bottom-margin" ng-submit="refreshRecords()"> <form class="input-group remove-bottom-margin" ng-submit="refreshRecords()">
<div class="input-group remove-bottom-margin"> <div class="input-group remove-bottom-margin">
<span class="input-group-btn"> <span class="input-group-btn">
<button id="record-search-button" type="submit" class="btn btn-primary"><span class="fa fa-search"></span></button> <button id="record-search-button" type="submit" class="btn btn-primary"><span class="fa fa-search"></span></button>
</span> </span>
@ -86,6 +87,9 @@
<div class="panel-body"> <div class="panel-body">
<div class="vinyldns-panel-top"> <div class="vinyldns-panel-top">
<div class="btn-group"> <div class="btn-group">
<span class="modal fade" id="loader" tabindex="-1" role="dialog" >
<span class="spinner" ></span>
</span>
<button id="refresh-records-button" class="btn btn-default" ng-click="refreshRecords()"><span class="fa fa-refresh"></span> Refresh</button> <button id="refresh-records-button" class="btn btn-default" ng-click="refreshRecords()"><span class="fa fa-refresh"></span> Refresh</button>
<button id="create-record-button" class="btn btn-default" ng-if="canReadZone && (zoneInfo.accessLevel == 'Delete' || canCreateRecordsViaAcl())" ng-click="createRecord(defaultTtl)"><span class="fa fa-plus"></span> Create Record Set</button> <button id="create-record-button" class="btn btn-default" ng-if="canReadZone && (zoneInfo.accessLevel == 'Delete' || canCreateRecordsViaAcl())" ng-click="createRecord(defaultTtl)"><span class="fa fa-plus"></span> Create Record Set</button>
<button id="zone-sync-button" class="btn btn-default mb-control" ng-if="zoneInfo.accessLevel=='Delete'" data-toggle="modal" data-target="#mb-sync"><span class="fa fa-exchange"></span> Sync Zone</button> <button id="zone-sync-button" class="btn btn-default mb-control" ng-if="zoneInfo.accessLevel=='Delete'" data-toggle="modal" data-target="#mb-sync"><span class="fa fa-exchange"></span> Sync Zone</button>

View File

@ -282,12 +282,10 @@
<div class="tab-pane" id="deletedZones"> <div class="tab-pane" id="deletedZones">
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<!-- SIMPLE DATATABLE --> <!-- SIMPLE DATATABLE -->
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<button id="deleted-zone-refresh-button" class="btn btn-default" ng-click="refreshZones()">
<button id="zone-refresh-button" class="btn btn-default" ng-click="refreshZones()">
<span class="fa fa-refresh"></span> Refresh <span class="fa fa-refresh"></span> Refresh
</button> </button>
@ -295,7 +293,7 @@
<div class="pull-right"> <div class="pull-right">
<form class="input-group" ng-submit="refreshZones()"> <form class="input-group" ng-submit="refreshZones()">
<div class="input-group"> <div class="input-group">
<span class="input-group-btn"> <span class="input-group-btn" >
<button id="my-deleted-zones-search-button" type="submit" class="btn btn-primary btn-left-round"> <button id="my-deleted-zones-search-button" type="submit" class="btn btn-primary btn-left-round">
<span class="fa fa-search"></span> <span class="fa fa-search"></span>
</button> </button>
@ -304,192 +302,184 @@
</div> </div>
</form> </form>
</div> </div>
<!-- END SEARCH BOX -->
<!-- DELETED ZONES TABS -->
<div class="panel panel-default panel-tabs">
<ul class="nav nav-tabs bar_tabs">
<li class="active"><a href="#myDeletedZones" data-toggle="tab" ng-click="myZonesAccess()" >My Zones</a></li>
<li><a id="tab2-button" href="#allDeletedZones" data-toggle="tab" ng-click="allZonesAccess()">All Zones</a></li>
</ul>
<div class="panel-body tab-content">
<div class="tab-pane active" id="myDeletedZones">
<div id="zone-list-table" class="panel-body">
<p ng-if="!myDeletedZonesLoaded">Loading my deleted zones...</p>
<p ng-if="myDeletedZonesLoaded && !myDeletedZones.length">No zones match the search criteria.</p>
<!-- PAGINATION -->
<div class="dataTables_paginate vinyldns_zones_paginate">
<span class="vinyldns_zones_page_number">{{ getZonesPageNumber("myDeletedZones") }}</span>
<ul class="pagination">
<li class="paginate_button previous">
<a ng-if="prevPageEnabled('myDeletedZones')" ng-click="prevPageMyDeletedZones()">Previous</a>
</li>
<li class="paginate_button next">
<a ng-if="nextPageEnabled('myDeletedZones')" ng-click="nextPageMyDeletedZones()">Next</a>
</li>
</ul>
</div>
<!-- END PAGINATION -->
<table class="table" ng-if="myDeletedZones.length">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Admin Group</th>
<th>Created</th>
<th>Abandoned</th>
<th>Status</th>
<th>Abandoned By</th>
@if(meta.sharedDisplayEnabled) {
<th>Access</th>
}
</tr>
</thead>
<tbody>
<tr ng-repeat="deletedZone in myDeletedZones">
<td class="wrap-long-text" ng-bind="deletedZone.zoneChange.zone.name">
</td>
<td class="wrap-long-text" ng-bind="deletedZone.zoneChange.zone.email">
</td>
<td>
<a ng-if="canAccessGroup(deletedZone.zoneChange.zone.adminGroupId)" ng-bind="deletedZone.adminGroupName"
href="/groups/{{deletedZone.zoneChange.zone.adminGroupId}}"></a>
<span ng-if="!canAccessGroup(deletedZone.zoneChange.zone.adminGroupId)" ng-bind="deletedZone.adminGroupName"
style="line-height: 0"></span>
</td>
<td>
{{deletedZone.zoneChange.zone.created}}
</td>
<td>
{{deletedZone.zoneChange.zone.updated}}
</td>
<td ng-bind="deletedZone.zoneChange.zone.status"></td>
<td>
{{deletedZone.userName}}
</td>
@if(meta.sharedDisplayEnabled) {
<td>{{zone.shared ? "Shared" : "Private"}}</td>
}
</tr>
</tbody>
</table>
<!-- PAGINATION -->
<div class="dataTables_paginate vinyldns_zones_paginate">
<span class="vinyldns_zones_page_number">{{ getZonesPageNumber("myDeletedZones") }}</span>
<ul class="pagination">
<li class="paginate_button previous">
<a ng-if="prevPageEnabled('myDeletedZones')" ng-click="prevPageMyDeletedZones()">Previous</a>
</li>
<li class="paginate_button next">
<a ng-if="nextPageEnabled('myDeletedZones')" ng-click="nextPageMyDeletedZones()">Next</a>
</li>
</ul>
</div>
<!-- END PAGINATION -->
</div>
</div>
<div class="tab-pane" id="allDeletedZones">
<div id="zone-list-table" class="panel-body">
<p ng-if="!allDeletedZonesLoaded">Loading all deleted zones...</p>
<p ng-if="allDeletedZonesLoaded && !allDeletedZones.length">No zones match the search criteria.</p>
<!-- PAGINATION -->
<div class="dataTables_paginate vinyldns_zones_paginate">
<span class="vinyldns_zones_page_number">{{ getZonesPageNumber("allDeletedZones") }}</span>
<ul class="pagination">
<li class="paginate_button previous">
<a ng-if="prevPageEnabled('allDeletedZones')" ng-click="prevPageAllDeletedZones()">Previous</a>
</li>
<li class="paginate_button next">
<a ng-if="nextPageEnabled('allDeletedZones')" ng-click="nextPageAllDeletedZones()">Next</a>
</li>
</ul>
</div>
<!-- END PAGINATION -->
<table class="table" ng-if="allDeletedZones.length">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Admin Group</th>
<th>Created</th>
<th>Abandoned</th>
<th>Status</th>
<th>Abandoned By</th>
@if(meta.sharedDisplayEnabled) {
<th>Access</th>
}
</tr>
</thead>
<tbody>
<tr ng-repeat="deletedZone in allDeletedZones">
<td class="wrap-long-text" ng-bind="deletedZone.zoneChange.zone.name">
</td>
<td class="wrap-long-text" ng-bind="deletedZone.zoneChange.zone.email">
</td>
<td>
<a ng-if="canAccessGroup(deletedZone.zoneChange.zone.adminGroupId)" ng-bind="deletedZone.adminGroupName"
href="/groups/{{deletedZone.zoneChange.zone.adminGroupId}}"></a>
<span ng-if="!canAccessGroup(deletedZone.zoneChange.zone.adminGroupId)" ng-bind="deletedZone.adminGroupName"
style="line-height: 0"></span>
</td>
<td>
{{deletedZone.zoneChange.zone.created}}
</td>
<td>
{{deletedZone.zoneChange.zone.updated}}
</td>
<td ng-bind="deletedZone.zoneChange.zone.status"></td>
<td>
{{deletedZone.userName}}
</td>
@if(meta.sharedDisplayEnabled) {
<td>{{zone.shared ? "Shared" : "Private"}}</td>
}
</tr>
</tbody>
</table>
<!-- PAGINATION -->
<div class="dataTables_paginate vinyldns_zones_paginate">
<span class="vinyldns_zones_page_number">{{ getZonesPageNumber("allDeletedZones") }}</span>
<ul class="pagination">
<li class="paginate_button previous">
<a ng-if="prevPageEnabled('allDeletedZones')" ng-click="prevPageAllDeletedZones()">Previous</a>
</li>
<li class="paginate_button next">
<a ng-if="nextPageEnabled('allDeletedZones')" ng-click="nextPageAllDeletedZones()">Next</a>
</li>
</ul>
</div>
<!-- END PAGINATION -->
</div>
</div>
</div>
</div>
<!-- END DELETED ZONES TABS -->
</div> </div>
<!-- END SEARCH BOX -->
<!-- DELETED ZONES TABS -->
<div class="panel-default panel-tabs">
<span class="container-fluid" >
<!-- MY DELETED ZONE PAGINATION -->
<div class="dataTables_paginate vinyldns_paginate" ng-show="tab == 'myDeletedZones'" >
<span class="vinyldns_page_number">{{ getZonesPageNumber("myDeletedZones") }}</span>
<ul class="pagination">
<li class="vinyldns_paginate_button previous">
<a ng-if="prevPageEnabled('myDeletedZones')" ng-click="prevPageMyDeletedZones()">Previous</a>
</li>
<li class="vinyldns_paginate_button next">
<a ng-if="nextPageEnabled('myDeletedZones')" ng-click="nextPageMyDeletedZones()">Next</a>
</li>
</ul>
</div>
<!-- MY DELETED ZONE END PAGINATION -->
<!-- ALL DELETED ZONE PAGINATION -->
<div class="dataTables_paginate vinyldns_paginate" ng-show="tab == 'allDeletedZones'" >
<span class="vinyldns_page_number">{{ getZonesPageNumber("allDeletedZones") }}</span>
<ul class="pagination ">
<li class="vinyldns_paginate_button previous">
<a ng-if="prevPageEnabled('allDeletedZones')" ng-click="prevPageAllDeletedZones()">Previous</a>
</li>
<li class="vinyldns_paginate_button next">
<a ng-if="nextPageEnabled('allDeletedZones')" ng-click="nextPageAllDeletedZones()">Next</a>
</li>
</ul>
</div>
<!--ALL DELETED ZONE END PAGINATION -->
<ul class="nav nav-tabs bar_tabs" ng-init = "tab='myDeletedZones'">
<li class = "active"><a ng-class="{active: tab == 'myDeletedZones'}" id = "myDeletedZone" href="#myDeletedZones" data-toggle="tab" ng-click="tab = 'myDeletedZones'; myZonesAccess()" >My Zones</a></li>
<li><a ng-class="{active: tab == 'allDeletedZones'}" href="#allDeletedZones" data-toggle="tab" ng-click="tab = 'allDeletedZones'; allZonesAccess()" >All Zones</a></li>
</ul>
<div class="tab-content">
<div class="tab-pane active" id="myDeletedZones">
<div id="my-deleted-zone-list-table" class="panel-body">
<p ng-if="!myDeletedZonesLoaded">Loading my deleted zones...</p>
<p ng-if="myDeletedZonesLoaded && !myDeletedZones.length">No zones match the search criteria.</p>
<table class="table" ng-if="myDeletedZones.length">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Admin Group</th>
<th>Created</th>
<th>Abandoned</th>
<th>Status</th>
<th>Abandoned By</th>
@if(meta.sharedDisplayEnabled) {
<th>Access</th>
}
</tr>
</thead>
<tbody>
<tr ng-repeat="deletedZone in myDeletedZones">
<td class="wrap-long-text" ng-bind="deletedZone.zoneChange.zone.name">
</td>
<td class="wrap-long-text" ng-bind="deletedZone.zoneChange.zone.email">
</td>
<td>
<a ng-if="canAccessGroup(deletedZone.zoneChange.zone.adminGroupId)" ng-bind="deletedZone.adminGroupName"
href="/groups/{{deletedZone.zoneChange.zone.adminGroupId}}"></a>
<span ng-if="!canAccessGroup(deletedZone.zoneChange.zone.adminGroupId)" ng-bind="deletedZone.adminGroupName"
style="line-height: 0"></span>
</td>
<td>
{{deletedZone.zoneChange.zone.created}}
</td>
<td>
{{deletedZone.zoneChange.zone.updated}}
</td>
<td ng-bind="deletedZone.zoneChange.zone.status"></td>
<td>
{{deletedZone.userName}}
</td>
@if(meta.sharedDisplayEnabled) {
<td>{{deletedZone.zoneChange.zone.shared ? "Shared" : "Private"}}</td>
}
</tr>
</tbody>
</table>
<!-- PAGINATION -->
<div class="dataTables_paginate vinyldns_paginate">
<span class="vinyldns_page_number">{{ getZonesPageNumber("myDeletedZones") }}</span>
<ul class="pagination">
<li class="paginate_button previous">
<a ng-if="prevPageEnabled('myDeletedZones')" ng-click="prevPageMyDeletedZones()">Previous</a>
</li>
<li class="paginate_button next">
<a ng-if="nextPageEnabled('myDeletedZones')" ng-click="nextPageMyDeletedZones()">Next</a>
</li>
</ul>
</div>
<!-- END PAGINATION -->
</div>
</div>
<div class="tab-pane" id="allDeletedZones">
<div id="all-deleted-zone-list-table" class="panel-body">
<p ng-if="!allDeletedZonesLoaded">Loading all deleted zones...</p>
<p ng-if="allDeletedZonesLoaded && !allDeletedZones.length">No zones match the search criteria.</p>
<table class="table" ng-if="allDeletedZones.length">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Admin Group</th>
<th>Created</th>
<th>Abandoned</th>
<th>Status</th>
<th>Abandoned By</th>
@if(meta.sharedDisplayEnabled) {
<th>Access</th>
}
</tr>
</thead>
<tbody>
<tr ng-repeat="deletedZone in allDeletedZones">
<td class="wrap-long-text" ng-bind="deletedZone.zoneChange.zone.name">
</td>
<td class="wrap-long-text" ng-bind="deletedZone.zoneChange.zone.email">
</td>
<td>
<a ng-if="canAccessGroup(deletedZone.zoneChange.zone.adminGroupId)" ng-bind="deletedZone.adminGroupName"
href="/groups/{{deletedZone.zoneChange.zone.adminGroupId}}"></a>
<span ng-if="!canAccessGroup(deletedZone.zoneChange.zone.adminGroupId)" ng-bind="deletedZone.adminGroupName"
style="line-height: 0"></span>
</td>
<td>
{{deletedZone.zoneChange.zone.created}}
</td>
<td>
{{deletedZone.zoneChange.zone.updated}}
</td>
<td ng-bind="deletedZone.zoneChange.zone.status"></td>
<td>
{{deletedZone.userName}}
</td>
@if(meta.sharedDisplayEnabled) {
<td>{{deletedZone.zoneChange.zone.shared ? "Shared" : "Private"}}</td>
}
</tr>
</tbody>
</table>
<!-- PAGINATION -->
<div class="dataTables_paginate vinyldns_paginate">
<span class="vinyldns_page_number">{{ getZonesPageNumber("allDeletedZones") }}</span>
<ul class="pagination">
<li class="paginate_button previous">
<a ng-if="prevPageEnabled('allDeletedZones')" ng-click="prevPageAllDeletedZones()">Previous</a>
</li>
<li class="paginate_button next">
<a ng-if="nextPageEnabled('allDeletedZones')" ng-click="nextPageAllDeletedZones()">Next</a>
</li>
</ul>
</div>
<!-- END PAGINATION -->
</div>
</div>
</div>
</span>
</div>
<!-- END DELETED ZONES TABS -->
<div class="panel-footer"></div> <div class="panel-footer"></div>
</div> </div>
<!-- END SIMPLE DATATABLE --> <!-- END SIMPLE DATATABLE -->
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- END VERTICAL TABS --> <!-- END VERTICAL TABS -->
</div> </div>
<!-- END PAGE CONTENT WRAPPER --> <!-- END PAGE CONTENT WRAPPER -->
</div> </div>
<!-- END PAGE CONTENT --> <!-- END PAGE CONTENT -->

View File

@ -88,6 +88,9 @@ api {
} }
} }
# Batch change limit
batch-change-limit = 1000
http.port = 9001 http.port = 9001
http.port = ${?PORTAL_PORT} http.port = ${?PORTAL_PORT}

View File

@ -15,7 +15,7 @@
<logger name="play" level="INFO" /> <logger name="play" level="INFO" />
<logger name="application" level="DEBUG" /> <logger name="application" level="DEBUG" />
<logger name="com.zaxxer.hikari" level="TRACE"> <logger name="com.zaxxer.hikari" level="ERROR">
<appender-ref ref="CONSOLE"/> <appender-ref ref="CONSOLE"/>
</logger> </logger>

View File

@ -462,6 +462,10 @@ input[type="file"] {
margin-right: 10px; margin-right: 10px;
} }
.vinyldns_paginate_button {
margin-right: 8px;
}
.no-top-margin { .no-top-margin {
margin-top: 0px; margin-top: 0px;
} }

View File

@ -210,8 +210,8 @@ angular.module('controller.groups', []).controller('GroupsController', function
handleError(error, 'groupsService::getGroups-failure'); handleError(error, 'groupsService::getGroups-failure');
}); });
} }
//Function for fetching list of valid domains
//Function for fetching list of valid domains
$scope.validDomains=function getValidEmailDomains() { $scope.validDomains=function getValidEmailDomains() {
function success(response) { function success(response) {
$log.debug('groupsService::listEmailDomains-success', response); $log.debug('groupsService::listEmailDomains-success', response);
@ -247,10 +247,25 @@ angular.module('controller.groups', []).controller('GroupsController', function
$scope.editGroup = function (groupInfo) { $scope.editGroup = function (groupInfo) {
$scope.currentGroup = groupInfo; $scope.currentGroup = groupInfo;
$scope.initialGroup = {
name: $scope.currentGroup.name,
email: $scope.currentGroup.email,
description: $scope.currentGroup.description
};
$scope.hasChanges = false;
$scope.validDomains(); $scope.validDomains();
$("#modal_edit_group").modal("show"); $("#modal_edit_group").modal("show");
}; };
// Function to check for changes
$scope.checkForChanges = function() {
$scope.hasChanges =
$scope.currentGroup.name !== $scope.initialGroup.name ||
$scope.currentGroup.email !== $scope.initialGroup.email ||
($scope.currentGroup.description !== $scope.initialGroup.description &&
!($scope.currentGroup.description === "" && $scope.initialGroup.description === undefined));
};
$scope.getGroupAndUpdate = function(groupId, name, email, description) { $scope.getGroupAndUpdate = function(groupId, name, email, description) {
function success(response) { function success(response) {
$log.debug('groupsService::getGroup-success'); $log.debug('groupsService::getGroup-success');
@ -281,6 +296,7 @@ angular.module('controller.groups', []).controller('GroupsController', function
return groupsService.updateGroup(groupId, payload) return groupsService.updateGroup(groupId, payload)
.then(success) .then(success)
.catch(function (error) { .catch(function (error) {
$scope.closeEditModal();
handleError(error, 'groupsService::updateGroup-failure'); handleError(error, 'groupsService::updateGroup-failure');
}); });
} }

View File

@ -44,6 +44,7 @@
$scope.manualReviewEnabled; $scope.manualReviewEnabled;
$scope.naptrFlags = ["U", "S", "A", "P"]; $scope.naptrFlags = ["U", "S", "A", "P"];
$scope.addSingleChange = function() { $scope.addSingleChange = function() {
$scope.newBatch.changes.push({changeType: "Add", type: "A+PTR"}); $scope.newBatch.changes.push({changeType: "Add", type: "A+PTR"});
var changesLength = $scope.newBatch.changes.length; var changesLength = $scope.newBatch.changes.length;
@ -116,7 +117,7 @@
$scope.alerts.push(alert); $scope.alerts.push(alert);
$timeout(function(){ $timeout(function(){
location.href = "/dnschanges/" + response.data.id; location.href = "/dnschanges/" + response.data.id;
}, 2000); }, 2000);
$scope.batch = response.data; $scope.batch = response.data;
} }
@ -161,14 +162,14 @@
$scope.alerts.push(alert); $scope.alerts.push(alert);
} }
$scope.uploadCSV = function(file) { $scope.uploadCSV = function(file, batchChangeLimit) {
parseFile(file).then(function(dataLength){ parseFile(file, batchChangeLimit).then(function(dataLength){
$scope.alerts.push({type: 'success', content: 'Successfully imported ' + dataLength + ' changes.' }); $scope.alerts.push({type: 'success', content: 'Successfully imported ' + dataLength + ' DNS changes.' });
}, function(error) { }, function(error) {
$scope.alerts.push({type: 'danger', content: error}); $scope.alerts.push({type: 'danger', content: error});
}); });
function parseFile(file) { function parseFile(file, batchChangeLimit) {
return $q(function(resolve, reject) { return $q(function(resolve, reject) {
if (!file.name.endsWith('.csv')) { if (!file.name.endsWith('.csv')) {
reject("Import failed. File should be of .csv type."); reject("Import failed. File should be of .csv type.");
@ -177,6 +178,9 @@
var reader = new FileReader(); var reader = new FileReader();
reader.onload = function(e) { reader.onload = function(e) {
var rows = e.target.result.split("\n"); var rows = e.target.result.split("\n");
if(rows.length - 1 > batchChangeLimit)
{reject("Import failed. Cannot add more than " + batchChangeLimit + " records per DNS change.");
} else {
if (rows[0].trim() == "Change Type,Record Type,Input Name,TTL,Record Data") { if (rows[0].trim() == "Change Type,Record Type,Input Name,TTL,Record Data") {
$scope.newBatch.changes = []; $scope.newBatch.changes = [];
for(var i = 1; i < rows.length; i++) { for(var i = 1; i < rows.length; i++) {
@ -186,10 +190,10 @@
} }
$scope.$apply() $scope.$apply()
resolve($scope.newBatch.changes.length); resolve($scope.newBatch.changes.length);
} else { } else {
reject("Import failed. CSV header must be: Change Type,Record Type,Input Name,TTL,Record Data"); reject("Import failed. CSV header must be: Change Type,Record Type,Input Name,TTL,Record Data");
} }
} }}
reader.readAsText(file); reader.readAsText(file);
} }
}); });

View File

@ -30,7 +30,24 @@
"allowManualReview": allowManualReview "allowManualReview": allowManualReview
} }
var url = utilityService.urlBuilder('/api/dnschanges', params); var url = utilityService.urlBuilder('/api/dnschanges', params);
return $http.post(url, data, {headers: utilityService.getCsrfHeader()}); let loader = $("#loader");
loader.modal({
backdrop: "static",
keyboard: false, //remove option to close with keyboard
show: true //Display loader!
})
let promis = $http.post(url, data, {headers: utilityService.getCsrfHeader()});
function hideLoader() {
loader.modal("hide");
// Manually remove the backdrop after the modal is hidden
$('.modal-backdrop').remove();
$('body').removeClass('modal-open'); // Remove the class that prevents scrolling
}
// Hide loader when api gets response
promis.then(hideLoader, hideLoader).catch(hideLoader).finally(hideLoader);
return promis
}; };
this.getBatchChanges = function (maxItems, startFrom, ignoreAccess, approvalStatus, userName, dateTimeRangeStart, dateTimeRangeEnd) { this.getBatchChanges = function (maxItems, startFrom, ignoreAccess, approvalStatus, userName, dateTimeRangeStart, dateTimeRangeEnd) {

View File

@ -279,7 +279,7 @@
$scope.changeHistoryPrevPage = function() { $scope.changeHistoryPrevPage = function() {
var startFrom = pagingService.getPrevStartFrom(changePaging); var startFrom = pagingService.getPrevStartFrom(changePaging);
return recordsService return recordsService
.listRecordSetChangeHistory(changePaging.maxItems, startFrom, $scope.recordFqdn, $scope.recordType) .listRecordSetChangeHistory($scope.zoneId, changePaging.maxItems, startFrom, $scope.recordFqdn, $scope.recordType)
.then(function(response) { .then(function(response) {
changePaging = pagingService.prevPageUpdate(response.data.nextId, changePaging); changePaging = pagingService.prevPageUpdate(response.data.nextId, changePaging);
updateChangeDisplay(response.data.recordSetChanges); updateChangeDisplay(response.data.recordSetChanges);
@ -291,7 +291,7 @@
$scope.changeHistoryNextPage = function() { $scope.changeHistoryNextPage = function() {
return recordsService return recordsService
.listRecordSetChangeHistory(changePaging.maxItems, changePaging.next, $scope.recordFqdn, $scope.recordType) .listRecordSetChangeHistory($scope.zoneId, changePaging.maxItems, changePaging.next, $scope.recordFqdn, $scope.recordType)
.then(function(response) { .then(function(response) {
var changes = response.data.recordSetChanges; var changes = response.data.recordSetChanges;
changePaging = pagingService.nextPageUpdate(changes, response.data.nextId, changePaging); changePaging = pagingService.nextPageUpdate(changes, response.data.nextId, changePaging);

View File

@ -77,7 +77,16 @@ angular.module('service.records', [])
"recordTypeSort": recordTypeSort "recordTypeSort": recordTypeSort
}; };
var url = utilityService.urlBuilder("/api/zones/"+id+"/recordsets", params); var url = utilityService.urlBuilder("/api/zones/"+id+"/recordsets", params);
return $http.get(url); let loader = $("#loader");
loader.modal({
backdrop: "static", //remove ability to close modal with click
keyboard: false, //remove option to close with keyboard
show: true //Display loader!
})
let promis = $http.get(url);
// Hide loader when api gets response
promis.then(()=>loader.modal("hide"), ()=>loader.modal("hide"))
return promis;
}; };
this.getRecordSet = function (rsid) { this.getRecordSet = function (rsid) {

View File

@ -1,5 +1,3 @@
version: "3.8"
services: services:
# LDAP container hosting example users # LDAP container hosting example users
@ -9,7 +7,7 @@ services:
ports: ports:
- "19004:19004" - "19004:19004"
# Integration image hosting r53, sns, sqs, bind, and mysql # Integration image hosting r53, sns, sqs, bind and mysql
integration: integration:
container_name: "vinyldns-api-integration" container_name: "vinyldns-api-integration"
hostname: "vinyldns-integration" hostname: "vinyldns-integration"

View File

@ -302,6 +302,10 @@ akka.http {
# Set to `infinite` to disable. # Set to `infinite` to disable.
bind-timeout = 5s 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 # Show verbose error messages back to the client
verbose-error-messages = on verbose-error-messages = on
} }

View File

@ -360,6 +360,10 @@ akka.http {
# Set to `infinite` to disable. # Set to `infinite` to disable.
bind-timeout = 5s 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 # Show verbose error messages back to the client
verbose-error-messages = on verbose-error-messages = on
} }

View File

@ -1 +1 @@
version in ThisBuild := "0.20.0" version in ThisBuild := "0.20.2"