2
0
mirror of https://github.com/VinylDNS/vinyldns synced 2025-08-22 02:02:14 +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:
- name: Checkout current branch
if: github.event.inputs.verify-first == 'true'
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
fetch-depth: 0
@ -53,7 +53,7 @@ jobs:
steps:
- name: Checkout current branch
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
fetch-depth: 0
@ -67,7 +67,7 @@ jobs:
- name: Create GitHub Release
id: create_release
uses: softprops/action-gh-release@1e07f4398721186383de40550babbdf2b84acfc5 # v0.1.14
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ steps.get-version.outputs.vinyldns_version }}
generate_release_notes: true
@ -85,13 +85,13 @@ jobs:
run: echo "::set-output name=vinyldns_version::$(curl -s https://api.github.com/repos/vinyldns/vinyldns/releases | jq -rc '.[0].tag_name')"
- name: Checkout current branch (full)
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
ref: ${{ steps.get-version.outputs.vinyldns_version }}
fetch-depth: 0
- name: Login to Docker Hub
uses: docker/login-action@v1
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_TOKEN }}
@ -120,13 +120,13 @@ jobs:
run: echo "::set-output name=vinyldns_version::$(curl -s https://api.github.com/repos/vinyldns/vinyldns/releases | jq -rc '.[0].tag_name')"
- name: Checkout current branch (full)
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
ref: ${{ steps.get-version.outputs.vinyldns_version }}
fetch-depth: 0
- name: Login to Docker Hub
uses: docker/login-action@v1
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_TOKEN }}

View File

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

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
- Jearvon Dharrie
- Andrew Dunn
- Josh Edwards
- Ryan Emerle
- David Grizzanti
- 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
- Timo Schmid
- Trent Schmidt
- Nick Spadaccino
- Arpit Shah
- Ghafar Shah
- Nick Spadaccino
- Rebecca Star
- Jess Stodola
- Juan Valencia

View File

@ -146,7 +146,8 @@ See the [Contributing Guide](CONTRIBUTING.md).
The current maintainers (people who can merge pull requests) are:
- Ryan Emerle ([@remerle](https://github.com/remerle))
- Sriram Ramakrishnan ([@sramakr](https://github.com/sramakr))
- Arpit Shah ([@arpit4ever](https://github.com/arpit4ever))
- Nick Spadaccino ([@nspadaccino](https://github.com/nspadaccino))
- Jim Wakemen ([@jwakemen](https://github.com/jwakemen))
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.
bind-timeout = 5s
# A default request timeout is applied globally to all routes and can be configured using the
# akka.http.server.request-timeout setting (which defaults to 20 seconds).
# request-timeout = 60s
# Show verbose error messages back to the client
verbose-error-messages = on
}

View File

@ -14,7 +14,7 @@
<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"/>
</logger>

View File

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

View File

@ -348,6 +348,10 @@ akka.http {
# Set to `infinite` to disable.
bind-timeout = 5s
# A default request timeout is applied globally to all routes and can be configured using the
# akka.http.server.request-timeout setting (which defaults to 20 seconds).
# request-timeout = 60s
# Show verbose error messages back to the client
verbose-error-messages = on
}

View File

@ -367,6 +367,10 @@ akka.http {
# Set to `infinite` to disable.
bind-timeout = 5s
# A default request timeout is applied globally to all routes and can be configured using the
# akka.http.server.request-timeout setting (which defaults to 20 seconds).
# request-timeout = 60s
# Show verbose error messages back to the client
verbose-error-messages = on
}

View File

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

View File

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

View File

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

View File

@ -16,7 +16,6 @@
package vinyldns.api.domain.batch
import java.net.InetAddress
import java.time.Instant
import java.time.temporal.ChronoUnit
import cats.data._
@ -315,16 +314,34 @@ class BatchChangeValidations(
else
().validNel
def matchRecordData(existingRecordSetData: List[RecordData], recordData: RecordData): Boolean =
existingRecordSetData.exists { rd =>
(rd, recordData) match {
case (AAAAData(rdAddress), AAAAData(proposedAddress)) =>
InetAddress.getByName(proposedAddress).getHostName == InetAddress
.getByName(rdAddress)
.getHostName
case _ => rd == recordData
}
def matchRecordData(existingRecordSetData: List[RecordData], recordData: RecordData): Boolean = {
existingRecordSetData.par.exists { rd =>
rd == recordData
}
}
def ensureRecordExists(
change: ChangeForValidation,
groupedChanges: ChangeForValidationMap
): 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(
change: ChangeForValidation,
@ -333,26 +350,37 @@ class BatchChangeValidations(
isApproved: Boolean
): 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 {
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 isSameRecordUpdateInBatch = if(recordData.nonEmpty){
if(addInBatch.contains(RecordData.fromString(recordData, change.inputChange.typ).get)) true else false
} else false
val isSameRecordUpdateInBatch = recordData.nonEmpty && addInBatch.contains(RecordData.fromString(recordData, change.inputChange.typ).get)
val validations =
groupedChanges.getExistingRecordSet(change.recordKey) match {
case Some(rs) =>
userCanDeleteRecordSet(change, auth, rs.ownerGroupId, rs.records) |+|
zoneDoesNotRequireManualReview(change, isApproved)
case None =>
if(isSameRecordUpdateInBatch) InvalidUpdateRequest(change.inputChange.inputName).invalidNel else ().validNel
}
validations.map(_ => change)
// Perform the system message update based on the condition
val updatedChange = if (groupedChanges.getExistingRecordSet(change.recordKey).isEmpty && !isSameRecordUpdateInBatch) {
val updatedChangeInput = updateSystemMessage(change.inputChange, nonExistentRecordDeleteMessage)
change.withUpdatedInputChange(updatedChangeInput)
} else if (!ensureRecordExists(change, groupedChanges)) {
val updatedChangeInput = updateSystemMessage(change.inputChange, nonExistentRecordDataDeleteMessage)
change.withUpdatedInputChange(updatedChangeInput)
} else {
change
}
val validations = groupedChanges.getExistingRecordSet(updatedChange.recordKey) match {
case Some(rs) =>
userCanDeleteRecordSet(updatedChange, auth, rs.ownerGroupId, rs.records) |+|
zoneDoesNotRequireManualReview(updatedChange, isApproved)
case None =>
if (isSameRecordUpdateInBatch) InvalidUpdateRequest(updatedChange.inputChange.inputName).invalidNel else ().validNel
}
validations.map(_ => updatedChange)
}
def validateAddUpdateWithContext(
@ -397,28 +425,40 @@ class BatchChangeValidations(
isApproved: Boolean
): 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
val recordData = change match {
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 isSameRecordUpdateInBatch = if(recordData.nonEmpty){
if(addInBatch.contains(RecordData.fromString(recordData, change.inputChange.typ).get)) true else false
} else false
val isSameRecordUpdateInBatch = recordData.nonEmpty && addInBatch.contains(RecordData.fromString(recordData, change.inputChange.typ).get)
// Perform the system message update based on the condition
val updatedChange = if (groupedChanges.getExistingRecordSet(change.recordKey).isEmpty && !isSameRecordUpdateInBatch) {
val updatedChangeInput = updateSystemMessage(change.inputChange, nonExistentRecordDeleteMessage)
change.withUpdatedInputChange(updatedChangeInput)
} else if (!ensureRecordExists(change, groupedChanges)) {
val updatedChangeInput = updateSystemMessage(change.inputChange, nonExistentRecordDataDeleteMessage)
change.withUpdatedInputChange(updatedChangeInput)
} else {
change
}
val validations =
groupedChanges.getExistingRecordSet(change.recordKey) match {
groupedChanges.getExistingRecordSet(updatedChange.recordKey) match {
case Some(rs) =>
val adds = groupedChanges.getProposedAdds(change.recordKey).toList
userCanUpdateRecordSet(change, auth, rs.ownerGroupId, adds) |+|
zoneDoesNotRequireManualReview(change, isApproved)
val adds = groupedChanges.getProposedAdds(updatedChange.recordKey).toList
userCanUpdateRecordSet(updatedChange, auth, rs.ownerGroupId, adds) |+|
zoneDoesNotRequireManualReview(updatedChange, isApproved)
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(

View File

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

View File

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

View File

@ -101,11 +101,9 @@ trait RecordSetServiceAlgebra {
): Result[RecordSetChange]
def listRecordSetChanges(
zoneId: Option[String],
zoneId: String,
startFrom: Option[Int],
maxItems: Int,
fqdn: Option[String],
recordType: Option[RecordType],
authPrincipal: AuthPrincipal
): 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 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 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."
final case class Requeue(change: RecordSetChange) extends Throwable
@ -392,12 +393,18 @@ object RecordSetChangeHandler extends TransactionProvider {
case AlreadyApplied(_) => Completed(change.successful)
case ReadyToApply(_) => Validated(change)
case Failure(_, message) =>
if(message == outOfSyncFailureMessage || message == incompatibleRecordFailureMessage){
if(message == outOfSyncFailureMessage){
Completed(
change.failed(
syncZoneMessage
)
)
} else if (message == incompatibleRecordFailureMessage) {
Completed(
change.failed(
wrongRecordDataMessage
)
)
} else if (message == "referral") {
Completed(
change.failed(

View File

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

View File

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

View File

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

View File

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

View File

@ -39,8 +39,6 @@ import vinyldns.core.domain.zone.Zone
class BatchChangeConverterSpec extends AnyWordSpec with Matchers {
private val nonExistentRecordDeleteMessage: String = "This record does not exist. " +
"No further action is required."
private val nonExistentRecordDataDeleteMessage: String = "Record data entered does not exist. " +
"No further action is required."
private def makeSingleAddChange(
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}"
SingleDeleteRRSetChange(
Some(zone.id),
@ -74,7 +72,7 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers {
typ,
None,
SingleChangeStatus.Pending,
None,
systemMessage,
None,
None
)
@ -88,18 +86,19 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers {
AddChangeForValidation(
okZone,
s"$recordName",
AddChangeInput(s"$recordName.ok.", typ, Some(123), recordData),
AddChangeInput(s"$recordName.ok.", typ, None, Some(123), recordData),
7200L
)
private def makeDeleteRRSetChangeForValidation(
recordName: String,
typ: RecordType = RecordType.A
typ: RecordType = RecordType.A,
systemMessage: Option[String] = None
): DeleteRRSetChangeForValidation =
DeleteRRSetChangeForValidation(
okZone,
s"$recordName",
DeleteRRSetChangeInput(s"$recordName.ok.", typ)
DeleteRRSetChangeInput(s"$recordName.ok.", typ, systemMessage, None)
)
private val addSingleChangesGood = List(
@ -165,19 +164,11 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers {
)
private val singleChangesOneDelete = List(
makeSingleDeleteRRSetChange("DoesNotExistToDelete", A)
)
private val singleChangesOneDeleteGood = List(
makeSingleDeleteRRSetChange("aToDelete", A).copy(recordData = Some(AData("2.3.4.6"))),
makeSingleDeleteRRSetChange("DoesNotExistToDelete", A, okZone, Some(nonExistentRecordDeleteMessage))
)
private val changeForValidationOneDelete = List(
makeDeleteRRSetChangeForValidation("DoesNotExistToDelete", A)
)
private val changeForValidationOneDeleteGood = List(
makeDeleteRRSetChangeForValidation("aToDelete", A)
makeDeleteRRSetChangeForValidation("DoesNotExistToDelete", A, Some(nonExistentRecordDeleteMessage))
)
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 {
val singleAddChange = makeSingleAddChange("shared-rs", AData("1.2.3.4"), A, sharedZone)
val ownerGroupId = Some("some-owner-group-id")

View File

@ -31,16 +31,16 @@ class BatchChangeInputSpec extends AnyWordSpec with Matchers {
"BatchChangeInput" should {
"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 =
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 =
AddChangeInput("cname.test.com", CNAME, Some(100), CNAMEData(Fqdn("testing.test.com")))
val changeADotted = AddChangeInput("adot.test.com.", A, Some(100), AData("1.1.1.1"))
AddChangeInput("cname.test.com", CNAME, None, Some(100), CNAMEData(Fqdn("testing.test.com")))
val changeADotted = AddChangeInput("adot.test.com.", A, None, Some(100), AData("1.1.1.1"))
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 =
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(
None,
@ -58,7 +58,7 @@ class BatchChangeInputSpec extends AnyWordSpec with Matchers {
}
"asNewStoredChange" should {
"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(
NonEmptyList.of(ZoneDiscoveryError("test")),
VinylDNSTestHelpers.defaultTtl
@ -80,7 +80,7 @@ class BatchChangeInputSpec extends AnyWordSpec with Matchers {
asAdd.recordSetId shouldBe None
}
"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(
NonEmptyList.of(ZoneDiscoveryError("test")),
VinylDNSTestHelpers.defaultTtl
@ -111,14 +111,14 @@ class BatchChangeInputSpec extends AnyWordSpec with Matchers {
1234,
AData("1.2.3.4"),
SingleChangeStatus.NeedsReview,
Some("msg"),
None,
None,
None,
List(SingleChangeError(DomainValidationErrorType.ZoneDiscoveryError, "test err"))
)
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(
Some("testZoneId"),
@ -128,14 +128,14 @@ class BatchChangeInputSpec extends AnyWordSpec with Matchers {
A,
None,
SingleChangeStatus.NeedsReview,
Some("msg"),
None,
None,
None,
List(SingleChangeError(DomainValidationErrorType.ZoneDiscoveryError, "test err"))
)
val expectedDelChange =
DeleteRRSetChangeInput("testRname.testZoneName.", A)
DeleteRRSetChangeInput("testRname.testZoneName.", A, None)
val change = BatchChange(
"userId",

View File

@ -81,34 +81,36 @@ class BatchChangeServiceSpec
)
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 =
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 =
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 =
AddChangeInput("have.only.base", RecordType.AAAA, 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"))
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, None, ttl, AData("1.1.1.1"))
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 =
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 =
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(
"cname.55.144.10.in-addr.arpa",
RecordType.CNAME,
None,
ttl,
CNAMEData(Fqdn("testing.cname.com."))
)
private val ptrAdd = AddChangeInput("10.144.55.11", RecordType.PTR, ttl, PTRData(Fqdn("ptr")))
private val ptrAdd2 = AddChangeInput("10.144.55.255", 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, None, ttl, PTRData(Fqdn("ptr")))
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 =
AddChangeInput(
"2001:0000:0000:0000:0000:ff00:0042:8329",
RecordType.PTR,
None,
ttl,
PTRData(Fqdn("ptr"))
)
@ -489,6 +491,7 @@ class BatchChangeServiceSpec
val ptr = AddChangeInput(
"2001:0000:0000:0001:0000:ff00:0042:8329",
RecordType.PTR,
None,
ttl,
PTRData(Fqdn("ptr"))
)
@ -520,6 +523,7 @@ class BatchChangeServiceSpec
val ptr = AddChangeInput(
"2001:0000:0000:0001:0000:ff00:0042:8329",
RecordType.PTR,
None,
ttl,
PTRData(Fqdn("ptr"))
)
@ -577,12 +581,12 @@ class BatchChangeServiceSpec
}
"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 =
AddChangeInput("with-ttl-add-2.test.com", RecordType.A, Some(900), AData("1.1.1.1"))
val noTtlDel = DeleteRRSetChangeInput("non-apex.test.com.", RecordType.TXT)
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, None)
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 result = underTest.applyBatchChange(input, auth, true).value.unsafeRunSync().toOption.get
@ -1172,7 +1176,7 @@ class BatchChangeServiceSpec
"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 zoneNames = underTestPTRZonesList.zones.map(_.name)
@ -1198,7 +1202,7 @@ class BatchChangeServiceSpec
)
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 zoneNames = underTestPTRZonesList.zones.map(_.name)
@ -1245,7 +1249,7 @@ class BatchChangeServiceSpec
val ips = ip1 :: ip2s
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()
@ -1300,10 +1304,10 @@ class BatchChangeServiceSpec
"properly discover records in forward zones" in {
val apex = apexZone.name
val aApex = AddChangeInput(apex, RecordType.A, ttl, AData("1.2.3.4"))
val aNormal = AddChangeInput(s"record.$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, None, ttl, AData("1.2.3.4"))
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(
AddChangeForValidation(apexZone, apex, aApex, 7200L),
@ -1322,10 +1326,10 @@ class BatchChangeServiceSpec
"properly discover TXT records" in {
val apex = apexZone.name
val txtApex = AddChangeInput(apex, RecordType.TXT, ttl, TXTData("test"))
val txtNormal = AddChangeInput(s"record.$apex", RecordType.TXT, ttl, TXTData("test"))
val txtApex = AddChangeInput(apex, RecordType.TXT, None, ttl, TXTData("test"))
val txtNormal = AddChangeInput(s"record.$apex", RecordType.TXT, None, ttl, TXTData("test"))
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(
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 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(
"2001:0db8:0111:0000:0000:ff00:0042:8329",
RecordType.PTR,
None,
ttl,
PTRData(Fqdn("ptr"))
)
val bigZoneAdd = AddChangeInput(
"2001:0000:0000:0000:0000:ff00:0042:8329",
RecordType.PTR,
None,
ttl,
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(
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 " +
"is provided" in {
val delete = DeleteRRSetChangeInput("some.test.delete.", RecordType.TXT)
val delete = DeleteRRSetChangeInput("some.test.delete.", RecordType.TXT, None)
val result = underTestManualEnabled
.buildResponse(
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 {
val delete = DeleteRRSetChangeInput("some.test.delete.", RecordType.TXT)
val delete = DeleteRRSetChangeInput("some.test.delete.", RecordType.TXT, None)
val result = underTest
.buildResponse(
BatchChangeInput(None, List(apexAddA, onlyBaseAddAAAA, delete)),
@ -1825,7 +1831,7 @@ class BatchChangeServiceSpec
"return a BatchChangeErrorList if all data inputs are valid/soft failures, scheduled, " +
"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
.buildResponse(
BatchChangeInput(
@ -1872,7 +1878,7 @@ class BatchChangeServiceSpec
"return a BatchChangeErrorList if all data inputs are valid/soft failures, manual review is enabled, " +
"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
.buildResponse(
BatchChangeInput(None, List(apexAddA, onlyBaseAddAAAA, delete)),

View File

@ -1951,19 +1951,22 @@ class RecordSetServiceSpec
val completeRecordSetChanges: List[RecordSetChange] =
List(pendingCreateAAAA, pendingCreateCNAME, completeCreateAAAA, completeCreateCNAME)
doReturn(IO.pure(Some(zoneActive)))
.when(mockZoneRepo)
.getZone(zoneActive.id)
doReturn(IO.pure(ListRecordSetChangesResults(completeRecordSetChanges)))
.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)))
.when(mockUserRepo)
.getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
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 =
completeRecordSetChanges.map(change => RecordSetChangeInfo(change, Some("ok")))
val expectedResults = ListRecordSetChangesResponse(
zoneId = okZone.id,
zoneId = zoneActive.id,
recordSetChanges = changesWithName,
nextId = None,
startFrom = None,
@ -2010,7 +2013,7 @@ class RecordSetServiceSpec
.getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
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(
zoneId = okZone.id,
recordSetChanges = List(),
@ -2076,7 +2079,7 @@ class RecordSetServiceSpec
"return a NotAuthorizedError" in {
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]
}
@ -2093,7 +2096,7 @@ class RecordSetServiceSpec
.getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
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 =
List(RecordSetChangeInfo(rsChange2, Some("ok")), RecordSetChangeInfo(rsChange1, Some("ok")))
val expectedResults = ListRecordSetChangesResponse(

View File

@ -127,15 +127,15 @@ class BatchChangeJsonProtocolSpec
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.")
@ -211,7 +211,7 @@ class BatchChangeJsonProtocolSpec
)
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 {
@ -250,7 +250,7 @@ class BatchChangeJsonProtocolSpec
"serializing ChangeInputSerializer to JSON" should {
"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 result = ChangeInputSerializer.toJson(deleteChangeInput)

View File

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

View File

@ -352,6 +352,10 @@ akka.http {
# Set to `infinite` to disable.
bind-timeout = 5s
# A default request timeout is applied globally to all routes and can be configured using the
# akka.http.server.request-timeout setting (which defaults to 20 seconds).
# request-timeout = 60s
# Show verbose error messages back to the client
verbose-error-messages = on
}

View File

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

View File

@ -46,11 +46,11 @@ sealed trait SingleChange {
delete.copy(status = SingleChangeStatus.Failed, systemMessage = Some(error))
}
def withDoesNotExistMessage(error: String): SingleChange = this match {
def withDoesNotExistMessage: SingleChange = this match {
case add: SingleAddChange =>
add.copy(status = SingleChangeStatus.Failed, systemMessage = Some(error))
add.copy(status = SingleChangeStatus.Failed)
case delete: SingleDeleteRRSetChange =>
delete.copy(status = SingleChangeStatus.Complete, systemMessage = Some(error))
delete.copy(status = SingleChangeStatus.Complete)
}
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.
# Set to `infinite` to disable.
bind-timeout = 5s
# A default request timeout is applied globally to all routes and can be configured using the
# akka.http.server.request-timeout setting (which defaults to 20 seconds).
# request-timeout = 60s
# Show verbose error messages back to the client
verbose-error-messages = on
}

View File

@ -17,7 +17,7 @@ can read data from the Directory. Once you have that information, proceed to th
**Considerations**
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
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
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
val page1 = repo.listFailedRecordSetChanges(Some(okZone.id), 2, 0).unsafeRunSync()
println(page1.items)
page1.nextId shouldBe 3
page1.maxItems shouldBe 2
(page1.items should contain).theSameElementsInOrderAs(expectedOrder.take(2))
val page2 = repo.listFailedRecordSetChanges(Some(okZone.id), 2, page1.nextId).unsafeRunSync()
println(page2.items)
page2.nextId shouldBe 6
page2.maxItems shouldBe 2
(page2.items should contain).theSameElementsInOrderAs(expectedOrder.slice(3, 5))

View File

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

View File

@ -23,7 +23,7 @@ import vinyldns.core.domain.record.RecordType.RecordType
import vinyldns.core.domain.record._
import vinyldns.core.protobuf.ProtobufConversions
import vinyldns.core.route.Monitored
import vinyldns.mysql.repository.MySqlRecordSetRepository.fromRecordType
import vinyldns.mysql.repository.MySqlRecordSetRepository.{fromRecordType, toFQDN}
import vinyldns.proto.VinylDNSProto
class MySqlRecordChangeRepository
@ -103,7 +103,7 @@ class MySqlRecordChangeRepository
change.zoneId,
change.created.toEpochMilli,
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),
toPB(change).toByteArray,
)

View File

@ -326,7 +326,7 @@
<label class="batch-change-csv-label btn btn-default" for="batchChangeCsv" id="batchChangeCsvImportLabel">
<span><span class="glyphicon glyphicon-import"></span> Import CSV</span>
</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>
</div>
<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>
</div>
<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" 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>

View File

@ -347,7 +347,7 @@
name="name"
class="form-control"
ng-model="currentGroup.name"
ng-model-options="{ updateOn: 'submit' }"
ng-change="checkForChanges()"
type="text"
required>
</input>
@ -360,7 +360,7 @@
name="email"
class="form-control"
ng-model="currentGroup.email"
ng-model-options="{ updateOn: 'submit' }"
ng-change="checkForChanges()"
type="text"
required>
</input>
@ -389,7 +389,7 @@
name="description"
class="form-control"
ng-model="currentGroup.description"
ng-model-options="{ updateOn: 'submit' }"
ng-change="checkForChanges()"
type="text">
</input>
<span class="help-block">
@ -399,7 +399,7 @@
</div>
</div>
<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>
</div>
</div>

View File

@ -67,6 +67,7 @@
<div class="col-md-2 pull-right">
<form class="input-group remove-bottom-margin" ng-submit="refreshRecords()">
<div class="input-group remove-bottom-margin">
<span class="input-group-btn">
<button id="record-search-button" type="submit" class="btn btn-primary"><span class="fa fa-search"></span></button>
</span>
@ -86,6 +87,9 @@
<div class="panel-body">
<div class="vinyldns-panel-top">
<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="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>

View File

@ -282,12 +282,10 @@
<div class="tab-pane" id="deletedZones">
<div class="row">
<div class="col-md-12">
<!-- SIMPLE DATATABLE -->
<div class="panel panel-default">
<div class="panel-heading">
<button id="zone-refresh-button" class="btn btn-default" ng-click="refreshZones()">
<button id="deleted-zone-refresh-button" class="btn btn-default" ng-click="refreshZones()">
<span class="fa fa-refresh"></span> Refresh
</button>
@ -295,7 +293,7 @@
<div class="pull-right">
<form class="input-group" ng-submit="refreshZones()">
<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">
<span class="fa fa-search"></span>
</button>
@ -304,192 +302,184 @@
</div>
</form>
</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>
<!-- 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>
<!-- END SIMPLE DATATABLE -->
</div>
</div>
</div>
</div>
</div>
<!-- END VERTICAL TABS -->
</div>
<!-- END PAGE CONTENT WRAPPER -->
</div>
<!-- END PAGE CONTENT -->

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -30,7 +30,24 @@
"allowManualReview": allowManualReview
}
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) {

View File

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

View File

@ -77,7 +77,16 @@ angular.module('service.records', [])
"recordTypeSort": recordTypeSort
};
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) {

View File

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

View File

@ -302,6 +302,10 @@ akka.http {
# Set to `infinite` to disable.
bind-timeout = 5s
# A default request timeout is applied globally to all routes and can be configured using the
# akka.http.server.request-timeout setting (which defaults to 20 seconds).
# request-timeout = 60s
# Show verbose error messages back to the client
verbose-error-messages = on
}

View File

@ -360,6 +360,10 @@ akka.http {
# Set to `infinite` to disable.
bind-timeout = 5s
# A default request timeout is applied globally to all routes and can be configured using the
# akka.http.server.request-timeout setting (which defaults to 20 seconds).
# request-timeout = 60s
# Show verbose error messages back to the client
verbose-error-messages = on
}

View File

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