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

Merge pull request #1348 from Jay07GIT/records_ownership_transfer

Ownership transfer for records
This commit is contained in:
Nicholas Spadaccino 2024-09-24 16:02:03 -04:00 committed by GitHub
commit a3e848a52c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 2394 additions and 112 deletions

View File

@ -19,6 +19,7 @@ package vinyldns.api.domain.record
import cats.effect._ import cats.effect._
import cats.implicits._ import cats.implicits._
import cats.scalatest.EitherMatchers import cats.scalatest.EitherMatchers
import org.mockito.Matchers.any
import java.time.Instant import java.time.Instant
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
import org.mockito.Mockito._ import org.mockito.Mockito._
@ -41,6 +42,9 @@ import vinyldns.core.domain.membership.{Group, GroupRepository, User, UserReposi
import vinyldns.core.domain.record.RecordType._ import vinyldns.core.domain.record.RecordType._
import vinyldns.core.domain.record._ import vinyldns.core.domain.record._
import vinyldns.core.domain.zone._ import vinyldns.core.domain.zone._
import vinyldns.core.notifier.{AllNotifiers, Notification, Notifier}
import scala.concurrent.ExecutionContext
class RecordSetServiceIntegrationSpec class RecordSetServiceIntegrationSpec
extends AnyWordSpec extends AnyWordSpec
@ -53,11 +57,16 @@ class RecordSetServiceIntegrationSpec
with BeforeAndAfterAll with BeforeAndAfterAll
with TransactionProvider { with TransactionProvider {
private implicit val contextShift: ContextShift[IO] = IO.contextShift(ExecutionContext.global)
private val vinyldnsConfig = VinylDNSConfig.load().unsafeRunSync() private val vinyldnsConfig = VinylDNSConfig.load().unsafeRunSync()
private val recordSetRepo = recordSetRepository private val recordSetRepo = recordSetRepository
private val recordSetCacheRepo = recordSetCacheRepository private val recordSetCacheRepo = recordSetCacheRepository
private val mockNotifier = mock[Notifier]
private val mockNotifiers = AllNotifiers(List(mockNotifier))
private val zoneRepo: ZoneRepository = zoneRepository private val zoneRepo: ZoneRepository = zoneRepository
private val groupRepo: GroupRepository = groupRepository private val groupRepo: GroupRepository = groupRepository
@ -232,6 +241,36 @@ class RecordSetServiceIntegrationSpec
ownerGroupId = Some("non-existent") ownerGroupId = Some("non-existent")
) )
private val sharedTestRecordPendingReviewOwnerShip = RecordSet(
sharedZone.id,
"shared-record-ownerShip-pendingReview",
A,
200,
RecordSetStatus.Active,
Instant.now.truncatedTo(ChronoUnit.MILLIS),
None,
List(AData("1.1.1.1")),
ownerGroupId = Some(sharedGroup.id),
recordSetGroupChange = Some(OwnerShipTransfer(
ownerShipTransferStatus = OwnerShipTransferStatus.PendingReview,
requestedOwnerGroupId = Some(group.id)))
)
private val sharedTestRecordCancelledOwnerShip = RecordSet(
sharedZone.id,
"shared-record-ownerShip-cancelled",
A,
200,
RecordSetStatus.Active,
Instant.now.truncatedTo(ChronoUnit.MILLIS),
None,
List(AData("1.1.1.1")),
ownerGroupId = Some(sharedGroup.id),
recordSetGroupChange = Some(OwnerShipTransfer(
ownerShipTransferStatus = OwnerShipTransferStatus.Cancelled,
requestedOwnerGroupId = Some(group.id)))
)
private val testOwnerGroupRecordInNormalZone = RecordSet( private val testOwnerGroupRecordInNormalZone = RecordSet(
zone.id, zone.id,
"user-in-owner-group-but-zone-not-shared", "user-in-owner-group-but-zone-not-shared",
@ -285,7 +324,10 @@ class RecordSetServiceIntegrationSpec
// Seeding records in DB // Seeding records in DB
val sharedRecords = List( val sharedRecords = List(
sharedTestRecord, sharedTestRecord,
sharedTestRecordBadOwnerGroup sharedTestRecordBadOwnerGroup,
sharedTestRecordPendingReviewOwnerShip,
sharedTestRecordCancelledOwnerShip
) )
val conflictRecords = List( val conflictRecords = List(
subTestRecordNameConflict, subTestRecordNameConflict,
@ -324,7 +366,8 @@ class RecordSetServiceIntegrationSpec
vinyldnsConfig.highValueDomainConfig, vinyldnsConfig.highValueDomainConfig,
vinyldnsConfig.dottedHostsConfig, vinyldnsConfig.dottedHostsConfig,
vinyldnsConfig.serverConfig.approvedNameServers, vinyldnsConfig.serverConfig.approvedNameServers,
useRecordSetCache = true useRecordSetCache = true,
mockNotifiers
) )
} }
@ -425,6 +468,227 @@ class RecordSetServiceIntegrationSpec
leftValue(result) shouldBe a[InvalidRequest] leftValue(result) shouldBe a[InvalidRequest]
} }
"auto-approve ownership transfer request, if user tried to update the ownership" in {
val newRecord = sharedTestRecord.copy(recordSetGroupChange =
Some(OwnerShipTransfer(ownerShipTransferStatus = OwnerShipTransferStatus.AutoApproved,
requestedOwnerGroupId = Some(group.id))))
val result = testRecordSetService
.updateRecordSet(newRecord, auth2)
.value
.unsafeRunSync()
val change = rightValue(result).asInstanceOf[RecordSetChange]
change.recordSet.name shouldBe "shared-record"
change.recordSet.ownerGroupId.get shouldBe group.id
change.recordSet.recordSetGroupChange.get.ownerShipTransferStatus shouldBe OwnerShipTransferStatus.AutoApproved
change.recordSet.recordSetGroupChange.get.requestedOwnerGroupId.get shouldBe group.id
}
"approve ownership transfer request, if user requested for ownership transfer" in {
val newRecord = sharedTestRecordPendingReviewOwnerShip.copy(recordSetGroupChange =
Some(OwnerShipTransfer(
ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyApproved)))
doReturn(IO.unit).when(mockNotifier).notify(any[Notification[_]])
val result = testRecordSetService
.updateRecordSet(newRecord, auth2)
.value
.unsafeRunSync()
val change = rightValue(result).asInstanceOf[RecordSetChange]
change.recordSet.name shouldBe "shared-record-ownerShip-pendingReview"
change.recordSet.ownerGroupId.get shouldBe group.id
change.recordSet.recordSetGroupChange.get.ownerShipTransferStatus shouldBe OwnerShipTransferStatus.ManuallyApproved
change.recordSet.recordSetGroupChange.get.requestedOwnerGroupId.get shouldBe group.id
}
"reject ownership transfer request, if user requested for ownership transfer" in {
val newRecord = sharedTestRecordPendingReviewOwnerShip.copy(recordSetGroupChange =
Some(OwnerShipTransfer(
ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyRejected)))
doReturn(IO.unit).when(mockNotifier).notify(any[Notification[_]])
val result = testRecordSetService
.updateRecordSet(newRecord, auth2)
.value
.unsafeRunSync()
val change = rightValue(result).asInstanceOf[RecordSetChange]
change.recordSet.name shouldBe "shared-record-ownerShip-pendingReview"
change.recordSet.ownerGroupId.get shouldBe sharedGroup.id
change.recordSet.recordSetGroupChange.get.ownerShipTransferStatus shouldBe OwnerShipTransferStatus.ManuallyRejected
change.recordSet.recordSetGroupChange.get.requestedOwnerGroupId.get shouldBe group.id
}
"request ownership transfer, if user not in the owner group and wants to own the record" in {
val newRecord = sharedTestRecord.copy(recordSetGroupChange =
Some(OwnerShipTransfer(
ownerShipTransferStatus = OwnerShipTransferStatus.Requested,
requestedOwnerGroupId = Some(dummyGroup.id))))
doReturn(IO.unit).when(mockNotifier).notify(any[Notification[_]])
val result = testRecordSetService
.updateRecordSet(newRecord, dummyAuth)
.value
.unsafeRunSync()
val change = rightValue(result).asInstanceOf[RecordSetChange]
change.recordSet.name shouldBe "shared-record"
change.recordSet.ownerGroupId.get shouldBe sharedGroup.id
change.recordSet.recordSetGroupChange.get.ownerShipTransferStatus shouldBe OwnerShipTransferStatus.PendingReview
change.recordSet.recordSetGroupChange.get.requestedOwnerGroupId.get shouldBe dummyGroup.id
}
"fail requesting ownership transfer if user is not in owner group and tried to update other fields in record set" in {
val newRecord = sharedTestRecord.copy(
ttl = 3000,
recordSetGroupChange =
Some(OwnerShipTransfer(
ownerShipTransferStatus = OwnerShipTransferStatus.Requested,
requestedOwnerGroupId = Some(dummyGroup.id))))
val result = testRecordSetService
.updateRecordSet(newRecord, dummyAuth)
.value
.unsafeRunSync()
leftValue(result) shouldBe a[InvalidRequest]
}
"fail updating if user is not in owner group for ownership transfer approval" in {
val newRecord = sharedTestRecordPendingReviewOwnerShip.copy(recordSetGroupChange =
Some(OwnerShipTransfer(
ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyApproved)))
val result = testRecordSetService
.updateRecordSet(newRecord, dummyAuth)
.value
.unsafeRunSync()
leftValue(result) shouldBe a[NotAuthorizedError]
}
"fail updating if user is not in owner group for ownership transfer reject" in {
val newRecord = sharedTestRecordPendingReviewOwnerShip.copy(recordSetGroupChange =
Some(OwnerShipTransfer(
ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyRejected)))
val result = testRecordSetService
.updateRecordSet(newRecord, dummyAuth)
.value
.unsafeRunSync()
leftValue(result) shouldBe a[NotAuthorizedError]
}
"cancel the ownership transfer request, if user not require ownership transfer further" in {
val newRecord = sharedTestRecordPendingReviewOwnerShip.copy(recordSetGroupChange =
Some(OwnerShipTransfer(
ownerShipTransferStatus = OwnerShipTransferStatus.Cancelled)))
doReturn(IO.unit).when(mockNotifier).notify(any[Notification[_]])
val result = testRecordSetService
.updateRecordSet(newRecord, auth)
.value
.unsafeRunSync()
val change = rightValue(result).asInstanceOf[RecordSetChange]
change.recordSet.name shouldBe "shared-record-ownerShip-pendingReview"
change.recordSet.ownerGroupId.get shouldBe sharedGroup.id
change.recordSet.recordSetGroupChange.get.ownerShipTransferStatus shouldBe OwnerShipTransferStatus.Cancelled
change.recordSet.recordSetGroupChange.get.requestedOwnerGroupId.get shouldBe group.id
}
"fail approving ownership transfer request, if user is cancelled" in {
val newRecord = sharedTestRecordCancelledOwnerShip.copy(recordSetGroupChange =
Some(OwnerShipTransfer(
ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyApproved)))
val result = testRecordSetService
.updateRecordSet(newRecord, auth)
.value
.unsafeRunSync()
leftValue(result) shouldBe a[InvalidRequest]
}
"fail rejecting ownership transfer request, if user is cancelled" in {
val newRecord = sharedTestRecordCancelledOwnerShip.copy(recordSetGroupChange =
Some(OwnerShipTransfer(
ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyRejected)))
val result = testRecordSetService
.updateRecordSet(newRecord, auth)
.value
.unsafeRunSync()
leftValue(result) shouldBe a[InvalidRequest]
}
"fail auto-approving ownership transfer request, if user is cancelled" in {
val newRecord = sharedTestRecordCancelledOwnerShip.copy(recordSetGroupChange =
Some(OwnerShipTransfer(
ownerShipTransferStatus = OwnerShipTransferStatus.AutoApproved
)))
doReturn(IO.unit).when(mockNotifier).notify(any[Notification[_]])
val result = testRecordSetService
.updateRecordSet(newRecord, auth)
.value
.unsafeRunSync()
leftValue(result) shouldBe a[InvalidRequest]
}
"fail auto-approving ownership transfer request, if zone is not shared" in {
val newRecord = dottedTestRecord.copy(recordSetGroupChange =
Some(OwnerShipTransfer(ownerShipTransferStatus = OwnerShipTransferStatus.AutoApproved,
requestedOwnerGroupId = Some(group.id))))
val result = testRecordSetService
.updateRecordSet(newRecord, auth2)
.value
.unsafeRunSync()
leftValue(result) shouldBe a[InvalidRequest]
}
"fail approving ownership transfer request, if zone is not shared" in {
val newRecord = dottedTestRecord.copy(recordSetGroupChange =
Some(OwnerShipTransfer(
ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyApproved
)))
val result = testRecordSetService
.updateRecordSet(newRecord, auth2)
.value
.unsafeRunSync()
leftValue(result) shouldBe a[InvalidRequest]
}
"fail requesting ownership transfer, if zone is not shared" in {
val newRecord = dottedTestRecord.copy(recordSetGroupChange =
Some(OwnerShipTransfer(
ownerShipTransferStatus = OwnerShipTransferStatus.Requested,
requestedOwnerGroupId = Some(dummyGroup.id)
)))
doReturn(IO.unit).when(mockNotifier).notify(any[Notification[_]])
val result = testRecordSetService
.updateRecordSet(newRecord, dummyAuth)
.value
.unsafeRunSync()
leftValue(result) shouldBe a[InvalidRequest]
}
"update dotted record succeeds if it satisfies all dotted hosts config" in { "update dotted record succeeds if it satisfies all dotted hosts config" in {
val newRecord = dottedTestRecord.copy(ttl = 37000) val newRecord = dottedTestRecord.copy(ttl = 37000)

View File

@ -24,7 +24,7 @@ import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpecLike import org.scalatest.wordspec.AnyWordSpecLike
import vinyldns.core.domain.batch._ import vinyldns.core.domain.batch._
import vinyldns.core.domain.record.RecordType import vinyldns.core.domain.record.RecordType
import vinyldns.core.domain.record.AData import vinyldns.core.domain.record.{AData, OwnerShipTransferStatus, RecordSetChange, RecordSetChangeStatus, RecordSetChangeType, RecordType}
import java.time.Instant import java.time.Instant
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
import vinyldns.core.TestMembershipData._ import vinyldns.core.TestMembershipData._
@ -35,6 +35,8 @@ import cats.effect.{IO, Resource}
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
import org.scalatest.BeforeAndAfterEach import org.scalatest.BeforeAndAfterEach
import cats.implicits._ import cats.implicits._
import vinyldns.core.TestRecordSetData.{ownerShipTransfer, rsOk}
import vinyldns.core.TestZoneData.okZone
class EmailNotifierIntegrationSpec class EmailNotifierIntegrationSpec
extends MySqlApiIntegrationSpec extends MySqlApiIntegrationSpec
@ -57,7 +59,7 @@ class EmailNotifierIntegrationSpec
"Email Notifier" should { "Email Notifier" should {
"send an email" taggedAs (SkipCI) in { "send an email for batch change" taggedAs (SkipCI) in {
val batchChange = BatchChange( val batchChange = BatchChange(
okUser.id, okUser.id,
okUser.userName, okUser.userName,
@ -84,7 +86,7 @@ class EmailNotifierIntegrationSpec
val program = for { val program = for {
_ <- userRepository.save(okUser) _ <- userRepository.save(okUser)
notifier <- new EmailNotifierProvider() notifier <- new EmailNotifierProvider()
.load(NotifierConfig("", emailConfig), userRepository) .load(NotifierConfig("", emailConfig), userRepository, groupRepository)
_ <- notifier.notify(Notification(batchChange)) _ <- notifier.notify(Notification(batchChange))
emailFiles <- retrieveEmailFiles(targetDirectory) emailFiles <- retrieveEmailFiles(targetDirectory)
} yield emailFiles } yield emailFiles
@ -94,7 +96,29 @@ class EmailNotifierIntegrationSpec
files.length should be(1) files.length should be(1)
} }
"send an email for recordSetChange ownerShip transfer" taggedAs (SkipCI) in {
val recordSetChange = RecordSetChange(
okZone,
rsOk.copy(ownerGroupId= Some(okGroup.id),recordSetGroupChange =
Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.PendingReview, requestedOwnerGroupId = Some(dummyGroup.id)))),
"system",
RecordSetChangeType.Create,
RecordSetChangeStatus.Complete
)
val program = for {
_ <- userRepository.save(okUser)
notifier <- new EmailNotifierProvider()
.load(NotifierConfig("", emailConfig), userRepository, groupRepository)
_ <- notifier.notify(Notification(recordSetChange))
emailFiles <- retrieveEmailFiles(targetDirectory)
} yield emailFiles
val files = program.unsafeRunSync()
files.length should be(1)
}
} }
def deleteEmailFiles(path: Path): IO[Unit] = def deleteEmailFiles(path: Path): IO[Unit] =

View File

@ -111,7 +111,7 @@ class SnsNotifierIntegrationSpec
sns.subscribe(topic, "sqs", queueUrl) sns.subscribe(topic, "sqs", queueUrl)
} }
notifier <- new SnsNotifierProvider() notifier <- new SnsNotifierProvider()
.load(NotifierConfig("", snsConfig), userRepository) .load(NotifierConfig("", snsConfig), userRepository, groupRepository)
_ <- notifier.notify(Notification(batchChange)) _ <- notifier.notify(Notification(batchChange))
_ <- IO.sleep(1.seconds) _ <- IO.sleep(1.seconds)
messages <- IO { messages <- IO {

View File

@ -96,7 +96,8 @@ object Boot extends App {
msgsPerPoll <- IO.fromEither(MessageCount(vinyldnsConfig.messageQueueConfig.messagesPerPoll)) msgsPerPoll <- IO.fromEither(MessageCount(vinyldnsConfig.messageQueueConfig.messagesPerPoll))
notifiers <- NotifierLoader.loadAll( notifiers <- NotifierLoader.loadAll(
vinyldnsConfig.notifierConfigs, vinyldnsConfig.notifierConfigs,
repositories.userRepository repositories.userRepository,
repositories.groupRepository
) )
_ <- APIMetrics.initialize(vinyldnsConfig.apiMetricSettings) _ <- APIMetrics.initialize(vinyldnsConfig.apiMetricSettings)
// Schedule the zone sync task to be executed every 5 seconds // Schedule the zone sync task to be executed every 5 seconds
@ -161,7 +162,8 @@ object Boot extends App {
vinyldnsConfig.highValueDomainConfig, vinyldnsConfig.highValueDomainConfig,
vinyldnsConfig.dottedHostsConfig, vinyldnsConfig.dottedHostsConfig,
vinyldnsConfig.serverConfig.approvedNameServers, vinyldnsConfig.serverConfig.approvedNameServers,
vinyldnsConfig.serverConfig.useRecordSetCache vinyldnsConfig.serverConfig.useRecordSetCache,
notifiers
) )
val zoneService = ZoneService( val zoneService = ZoneService(
repositories, repositories,

View File

@ -205,7 +205,8 @@ trait DnsConversions {
ttl = r.getTTL, ttl = r.getTTL,
status = RecordSetStatus.Active, status = RecordSetStatus.Active,
created = Instant.now.truncatedTo(ChronoUnit.MILLIS), created = Instant.now.truncatedTo(ChronoUnit.MILLIS),
records = f(r) records = f(r),
recordSetGroupChange = Some(OwnerShipTransfer(ownerShipTransferStatus = OwnerShipTransferStatus.None))
) )
// if we do not know the record type, then we cannot parse the records, but we should be able to get everything else // if we do not know the record type, then we cannot parse the records, but we should be able to get everything else
@ -217,7 +218,8 @@ trait DnsConversions {
ttl = r.getTTL, ttl = r.getTTL,
status = RecordSetStatus.Active, status = RecordSetStatus.Active,
created = Instant.now.truncatedTo(ChronoUnit.MILLIS), created = Instant.now.truncatedTo(ChronoUnit.MILLIS),
records = Nil records = Nil,
recordSetGroupChange = Some(OwnerShipTransfer(ownerShipTransferStatus = OwnerShipTransferStatus.None))
) )
def fromARecord(r: DNS.ARecord, zoneName: DNS.Name, zoneId: String): RecordSet = def fromARecord(r: DNS.ARecord, zoneName: DNS.Name, zoneId: String): RecordSet =

View File

@ -240,7 +240,8 @@ class BatchChangeConverter(batchChangeRepo: BatchChangeRepository, messageQueue:
Instant.now.truncatedTo(ChronoUnit.MILLIS), Instant.now.truncatedTo(ChronoUnit.MILLIS),
None, None,
proposedRecordData.toList, proposedRecordData.toList,
ownerGroupId = setOwnerGroupId ownerGroupId = setOwnerGroupId,
recordSetGroupChange = Some(OwnerShipTransfer(ownerShipTransferStatus = OwnerShipTransferStatus.None))
) )
} }
} }

View File

@ -36,6 +36,7 @@ import vinyldns.core.domain.record.RecordType.RecordType
import vinyldns.core.domain.DomainHelpers.ensureTrailingDot import vinyldns.core.domain.DomainHelpers.ensureTrailingDot
import vinyldns.core.domain.backend.{Backend, BackendResolver} import vinyldns.core.domain.backend.{Backend, BackendResolver}
import vinyldns.core.domain.record.RecordTypeSort.RecordTypeSort import vinyldns.core.domain.record.RecordTypeSort.RecordTypeSort
import vinyldns.core.notifier.{AllNotifiers, Notification}
import scala.util.matching.Regex import scala.util.matching.Regex
@ -49,7 +50,8 @@ object RecordSetService {
highValueDomainConfig: HighValueDomainConfig, highValueDomainConfig: HighValueDomainConfig,
dottedHostsConfig: DottedHostsConfig, dottedHostsConfig: DottedHostsConfig,
approvedNameServers: List[Regex], approvedNameServers: List[Regex],
useRecordSetCache: Boolean useRecordSetCache: Boolean,
notifiers: AllNotifiers
): RecordSetService = ): RecordSetService =
new RecordSetService( new RecordSetService(
dataAccessor.zoneRepository, dataAccessor.zoneRepository,
@ -65,7 +67,9 @@ object RecordSetService {
highValueDomainConfig, highValueDomainConfig,
dottedHostsConfig, dottedHostsConfig,
approvedNameServers, approvedNameServers,
useRecordSetCache useRecordSetCache,
notifiers
) )
} }
@ -83,12 +87,16 @@ class RecordSetService(
highValueDomainConfig: HighValueDomainConfig, highValueDomainConfig: HighValueDomainConfig,
dottedHostsConfig: DottedHostsConfig, dottedHostsConfig: DottedHostsConfig,
approvedNameServers: List[Regex], approvedNameServers: List[Regex],
useRecordSetCache: Boolean useRecordSetCache: Boolean,
notifiers: AllNotifiers
) extends RecordSetServiceAlgebra { ) extends RecordSetServiceAlgebra {
import RecordSetValidations._ import RecordSetValidations._
import accessValidation._ import accessValidation._
val approverOwnerShipTransferStatus = List(OwnerShipTransferStatus.ManuallyApproved , OwnerShipTransferStatus.AutoApproved, OwnerShipTransferStatus.ManuallyRejected)
val requestorOwnerShipTransferStatus = List(OwnerShipTransferStatus.Cancelled , OwnerShipTransferStatus.Requested, OwnerShipTransferStatus.PendingReview)
def addRecordSet(recordSet: RecordSet, auth: AuthPrincipal): Result[ZoneCommandResult] = def addRecordSet(recordSet: RecordSet, auth: AuthPrincipal): Result[ZoneCommandResult] =
for { for {
zone <- getZone(recordSet.zoneId) zone <- getZone(recordSet.zoneId)
@ -143,14 +151,28 @@ class RecordSetService(
_ <- unchangedRecordName(existing, recordSet, zone).toResult _ <- unchangedRecordName(existing, recordSet, zone).toResult
_ <- unchangedRecordType(existing, recordSet).toResult _ <- unchangedRecordType(existing, recordSet).toResult
_ <- unchangedZoneId(existing, recordSet).toResult _ <- unchangedZoneId(existing, recordSet).toResult
_ <- if(requestorOwnerShipTransferStatus.contains(recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("<none>"))
&& !auth.isSuper && !auth.isGroupMember(existing.ownerGroupId.getOrElse("None")))
unchangedRecordSet(existing, recordSet).toResult else ().toResult
_ <- if(existing.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("<none>") == OwnerShipTransferStatus.Cancelled
&& !auth.isSuper)
recordSetOwnerShipApproveStatus(recordSet).toResult else ().toResult
recordSet <- updateRecordSetGroupChangeStatus(recordSet, existing, zone)
change <- RecordSetChangeGenerator.forUpdate(existing, recordSet, zone, Some(auth)).toResult change <- RecordSetChangeGenerator.forUpdate(existing, recordSet, zone, Some(auth)).toResult
// because changes happen to the RS in forUpdate itself, converting 1st and validating on that // because changes happen to the RS in forUpdate itself, converting 1st and validating on that
rsForValidations = change.recordSet rsForValidations = change.recordSet
superUserCanUpdateOwnerGroup = canSuperUserUpdateOwnerGroup(existing, recordSet, zone, auth) superUserCanUpdateOwnerGroup = canSuperUserUpdateOwnerGroup(existing, recordSet, zone, auth)
_ <- isNotHighValueDomain(recordSet, zone, highValueDomainConfig).toResult _ <- isNotHighValueDomain(recordSet, zone, highValueDomainConfig).toResult
_ <- canUpdateRecordSet(auth, existing.name, existing.typ, zone, existing.ownerGroupId, superUserCanUpdateOwnerGroup).toResult _ <- if(requestorOwnerShipTransferStatus.contains(recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("<none>"))
&& !auth.isSuper && !auth.isGroupMember(existing.ownerGroupId.getOrElse("None"))) ().toResult
else canUpdateRecordSet(auth, existing.name, existing.typ, zone, existing.ownerGroupId, superUserCanUpdateOwnerGroup).toResult
ownerGroup <- getGroupIfProvided(rsForValidations.ownerGroupId) ownerGroup <- getGroupIfProvided(rsForValidations.ownerGroupId)
_ <- canUseOwnerGroup(rsForValidations.ownerGroupId, ownerGroup, auth).toResult _ <- if(requestorOwnerShipTransferStatus.contains(recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("<none>"))
&& !auth.isSuper && !auth.isGroupMember(existing.ownerGroupId.getOrElse("None")))
canUseOwnerGroup(rsForValidations.recordSetGroupChange.map(_.requestedOwnerGroupId).get, ownerGroup, auth).toResult
else if(approverOwnerShipTransferStatus.contains(recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("<none>"))
&& !auth.isSuper) canUseOwnerGroup(existing.ownerGroupId, ownerGroup, auth).toResult
else canUseOwnerGroup(rsForValidations.ownerGroupId, ownerGroup, auth).toResult
_ <- notPending(existing).toResult _ <- notPending(existing).toResult
existingRecordsWithName <- recordSetRepository existingRecordsWithName <- recordSetRepository
.getRecordSetsByName(zone.id, rsForValidations.name) .getRecordSetsByName(zone.id, rsForValidations.name)
@ -185,6 +207,11 @@ class RecordSetService(
_ <- if(existing.name == rsForValidations.name) ().toResult else if(allowedZoneList.contains(zone.name)) checkAllowedDots(allowedDotsLimit, rsForValidations, zone).toResult else ().toResult _ <- if(existing.name == rsForValidations.name) ().toResult else if(allowedZoneList.contains(zone.name)) checkAllowedDots(allowedDotsLimit, rsForValidations, zone).toResult else ().toResult
_ <- if(allowedZoneList.contains(zone.name)) isNotApexEndsWithDot(rsForValidations, zone).toResult else ().toResult _ <- if(allowedZoneList.contains(zone.name)) isNotApexEndsWithDot(rsForValidations, zone).toResult else ().toResult
_ <- messageQueue.send(change).toResult[Unit] _ <- messageQueue.send(change).toResult[Unit]
_ <- if(recordSet.recordSetGroupChange != None &&
recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("<none>") != OwnerShipTransferStatus.None &&
recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("<none>") != OwnerShipTransferStatus.AutoApproved)
notifiers.notify(Notification(change)).toResult
else ().toResult
} yield change } yield change
def deleteRecordSet( def deleteRecordSet(
@ -203,6 +230,65 @@ class RecordSetService(
_ <- messageQueue.send(change).toResult[Unit] _ <- messageQueue.send(change).toResult[Unit]
} yield change } yield change
//update ownership transfer is zone is shared
def updateRecordSetGroupChangeStatus(recordSet: RecordSet, existing: RecordSet, zone: Zone): Result[RecordSet] = {
val existingOwnerShipTransfer = existing.recordSetGroupChange.getOrElse(OwnerShipTransfer.apply(OwnerShipTransferStatus.None, Some("none")))
val ownerShipTransfer = recordSet.recordSetGroupChange.getOrElse(OwnerShipTransfer.apply(OwnerShipTransferStatus.None, Some("none")))
if (recordSet.recordSetGroupChange != None &&
ownerShipTransfer.ownerShipTransferStatus != OwnerShipTransferStatus.None)
if (zone.shared){
if (approverOwnerShipTransferStatus.contains(ownerShipTransfer.ownerShipTransferStatus)) {
val recordSetOwnerApproval =
ownerShipTransfer.ownerShipTransferStatus match {
case OwnerShipTransferStatus.ManuallyApproved =>
recordSet.copy(ownerGroupId = existingOwnerShipTransfer.requestedOwnerGroupId,
recordSetGroupChange = Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyApproved,
requestedOwnerGroupId = existingOwnerShipTransfer.requestedOwnerGroupId)))
case OwnerShipTransferStatus.ManuallyRejected =>
recordSet.copy(
recordSetGroupChange = Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyRejected,
requestedOwnerGroupId = existingOwnerShipTransfer.requestedOwnerGroupId)))
case OwnerShipTransferStatus.AutoApproved =>
recordSet.copy(
ownerGroupId = ownerShipTransfer.requestedOwnerGroupId,
recordSetGroupChange = Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.AutoApproved,
requestedOwnerGroupId = ownerShipTransfer.requestedOwnerGroupId)))
case _ => recordSet.copy(
recordSetGroupChange = Some(ownerShipTransfer.copy(
ownerShipTransferStatus = OwnerShipTransferStatus.None,
requestedOwnerGroupId = Some("null"))))
}
for {
recordSet <- recordSetOwnerApproval.toResult
} yield recordSet
}
else {
val recordSetOwnerRequest =
ownerShipTransfer.ownerShipTransferStatus match {
case OwnerShipTransferStatus.Cancelled =>
recordSet.copy(recordSetGroupChange = Some(ownerShipTransfer.copy(
ownerShipTransferStatus = OwnerShipTransferStatus.Cancelled,
requestedOwnerGroupId = existingOwnerShipTransfer.requestedOwnerGroupId)))
case OwnerShipTransferStatus.Requested | OwnerShipTransferStatus.PendingReview => recordSet.copy(
recordSetGroupChange = Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.PendingReview)))
}
for {
recordSet <- recordSetOwnerRequest.toResult
} yield recordSet
}
} else for {
_ <- unchangedRecordSetOwnershipStatus(recordSet, existing).toResult
} yield recordSet.copy(
recordSetGroupChange = Some(ownerShipTransfer.copy(
ownerShipTransferStatus = OwnerShipTransferStatus.None,
requestedOwnerGroupId = Some("null"))))
else recordSet.copy(
recordSetGroupChange = Some(ownerShipTransfer.copy(
ownerShipTransferStatus = OwnerShipTransferStatus.None,
requestedOwnerGroupId = Some("null")))).toResult
}
// For dotted hosts. Check if a record that may conflict with dotted host exist or not // For dotted hosts. Check if a record that may conflict with dotted host exist or not
def recordFQDNDoesNotExist(newRecordSet: RecordSet, zone: Zone): IO[Boolean] = { def recordFQDNDoesNotExist(newRecordSet: RecordSet, zone: Zone): IO[Boolean] = {
// Use fqdn for searching through `recordset` mysql table to see if it already exist // Use fqdn for searching through `recordset` mysql table to see if it already exist

View File

@ -27,7 +27,7 @@ import vinyldns.core.domain.record.RecordType._
import vinyldns.api.domain.zone._ import vinyldns.api.domain.zone._
import vinyldns.core.domain.auth.AuthPrincipal import vinyldns.core.domain.auth.AuthPrincipal
import vinyldns.core.domain.membership.Group import vinyldns.core.domain.membership.Group
import vinyldns.core.domain.record.{RecordSet, RecordType} import vinyldns.core.domain.record.{OwnerShipTransferStatus, RecordSet, RecordType}
import vinyldns.core.domain.zone.Zone import vinyldns.core.domain.zone.Zone
import vinyldns.core.Messages._ import vinyldns.core.Messages._
@ -462,4 +462,41 @@ object RecordSetValidations {
val wildcardRegex = raw"^\s*[*%].*[*%]\s*$$".r val wildcardRegex = raw"^\s*[*%].*[*%]\s*$$".r
searchRegex.findFirstIn(recordNameFilter).isDefined && wildcardRegex.findFirstIn(recordNameFilter).isEmpty searchRegex.findFirstIn(recordNameFilter).isDefined && wildcardRegex.findFirstIn(recordNameFilter).isEmpty
} }
def unchangedRecordSet(
existing: RecordSet,
updates: RecordSet
): Either[Throwable, Unit] =
Either.cond(
updates.typ == existing.typ &&
updates.records == existing.records &&
updates.id == existing.id &&
updates.zoneId == existing.zoneId &&
updates.name == existing.name &&
updates.ownerGroupId == existing.ownerGroupId &&
updates.ttl == existing.ttl,
(),
InvalidRequest("Cannot update RecordSet's if user not a member of ownership group. User can only request for ownership transfer")
)
def recordSetOwnerShipApproveStatus(
updates: RecordSet,
): Either[Throwable, Unit] =
Either.cond(
updates.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("<none>") != OwnerShipTransferStatus.ManuallyApproved &&
updates.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("<none>") != OwnerShipTransferStatus.AutoApproved &&
updates.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("<none>") != OwnerShipTransferStatus.ManuallyRejected,
(),
InvalidRequest("Cannot update RecordSet OwnerShip Status when request is cancelled.")
)
def unchangedRecordSetOwnershipStatus(
updates: RecordSet,
existing: RecordSet
): Either[Throwable, Unit] =
Either.cond(
updates.recordSetGroupChange == existing.recordSetGroupChange,
(),
InvalidRequest("Cannot update RecordSet OwnerShip Status when zone is not shared.")
)
} }

View File

@ -21,7 +21,7 @@ import vinyldns.core.domain.record.RecordSetChangeStatus.RecordSetChangeStatus
import vinyldns.core.domain.record.RecordSetChangeType.RecordSetChangeType import vinyldns.core.domain.record.RecordSetChangeType.RecordSetChangeType
import vinyldns.core.domain.record.RecordSetStatus.RecordSetStatus import vinyldns.core.domain.record.RecordSetStatus.RecordSetStatus
import vinyldns.core.domain.record.RecordType.RecordType import vinyldns.core.domain.record.RecordType.RecordType
import vinyldns.core.domain.record.{RecordData, RecordSet, RecordSetChange} import vinyldns.core.domain.record.{RecordData, RecordSet, RecordSetChange, OwnerShipTransfer}
import vinyldns.core.domain.zone.{ACLRuleInfo, AccessLevel, Zone, ZoneACL, ZoneChange, ZoneConnection} import vinyldns.core.domain.zone.{ACLRuleInfo, AccessLevel, Zone, ZoneACL, ZoneChange, ZoneConnection}
import vinyldns.core.domain.zone.AccessLevel.AccessLevel import vinyldns.core.domain.zone.AccessLevel.AccessLevel
import vinyldns.core.domain.zone.ZoneStatus.ZoneStatus import vinyldns.core.domain.zone.ZoneStatus.ZoneStatus
@ -180,6 +180,7 @@ case class RecordSetListInfo(
accessLevel: AccessLevel, accessLevel: AccessLevel,
ownerGroupId: Option[String], ownerGroupId: Option[String],
ownerGroupName: Option[String], ownerGroupName: Option[String],
recordSetGroupChange: Option[OwnerShipTransfer],
fqdn: Option[String] fqdn: Option[String]
) )
@ -199,6 +200,7 @@ object RecordSetListInfo {
accessLevel = accessLevel, accessLevel = accessLevel,
ownerGroupId = recordSet.ownerGroupId, ownerGroupId = recordSet.ownerGroupId,
ownerGroupName = recordSet.ownerGroupName, ownerGroupName = recordSet.ownerGroupName,
recordSetGroupChange = recordSet.recordSetGroupChange,
fqdn = recordSet.fqdn fqdn = recordSet.fqdn
) )
} }
@ -216,6 +218,7 @@ case class RecordSetInfo(
account: String, account: String,
ownerGroupId: Option[String], ownerGroupId: Option[String],
ownerGroupName: Option[String], ownerGroupName: Option[String],
recordSetGroupChange: Option[OwnerShipTransfer],
fqdn: Option[String] fqdn: Option[String]
) )
@ -234,6 +237,7 @@ object RecordSetInfo {
account = recordSet.account, account = recordSet.account,
ownerGroupId = recordSet.ownerGroupId, ownerGroupId = recordSet.ownerGroupId,
ownerGroupName = groupName, ownerGroupName = groupName,
recordSetGroupChange = recordSet.recordSetGroupChange,
fqdn = recordSet.fqdn fqdn = recordSet.fqdn
) )
} }
@ -251,6 +255,7 @@ case class RecordSetGlobalInfo(
account: String, account: String,
ownerGroupId: Option[String], ownerGroupId: Option[String],
ownerGroupName: Option[String], ownerGroupName: Option[String],
recordSetGroupChange: Option[OwnerShipTransfer],
fqdn: Option[String], fqdn: Option[String],
zoneName: String, zoneName: String,
zoneShared: Boolean zoneShared: Boolean
@ -276,6 +281,7 @@ object RecordSetGlobalInfo {
account = recordSet.account, account = recordSet.account,
ownerGroupId = recordSet.ownerGroupId, ownerGroupId = recordSet.ownerGroupId,
ownerGroupName = groupName, ownerGroupName = groupName,
recordSetGroupChange = recordSet.recordSetGroupChange,
fqdn = recordSet.fqdn, fqdn = recordSet.fqdn,
zoneName = zoneName, zoneName = zoneName,
zoneShared = zoneShared zoneShared = zoneShared

View File

@ -18,33 +18,25 @@ package vinyldns.api.notifier.email
import vinyldns.core.notifier.{Notification, Notifier} import vinyldns.core.notifier.{Notification, Notifier}
import cats.effect.IO import cats.effect.IO
import vinyldns.core.domain.batch.{ import cats.implicits._
BatchChange, import cats.effect.IO
BatchChangeApprovalStatus, import vinyldns.core.domain.batch.{BatchChange, BatchChangeApprovalStatus, SingleAddChange, SingleChange, SingleDeleteRRSetChange}
SingleAddChange, import vinyldns.core.domain.membership.{GroupRepository, User, UserRepository}
SingleChange,
SingleDeleteRRSetChange
}
import vinyldns.core.domain.membership.UserRepository
import vinyldns.core.domain.membership.User
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import javax.mail.internet.{InternetAddress, MimeMessage} import javax.mail.internet.{InternetAddress, MimeMessage}
import javax.mail.{Address, Message, Session} import javax.mail.{Address, Message, Session}
import scala.util.Try import scala.util.Try
import vinyldns.core.domain.record.AData import vinyldns.core.domain.record.{AAAAData, AData, CNAMEData, MXData, OwnerShipTransferStatus, PTRData, RecordData, RecordSetChange, TXTData}
import vinyldns.core.domain.record.AAAAData import vinyldns.core.domain.record.OwnerShipTransferStatus.OwnerShipTransferStatus
import vinyldns.core.domain.record.CNAMEData
import vinyldns.core.domain.record.MXData
import vinyldns.core.domain.record.TXTData
import vinyldns.core.domain.record.PTRData
import vinyldns.core.domain.record.RecordData
import java.time.format.{DateTimeFormatter, FormatStyle} import java.time.format.{DateTimeFormatter, FormatStyle}
import vinyldns.core.domain.batch.BatchChangeStatus._ import vinyldns.core.domain.batch.BatchChangeStatus._
import vinyldns.core.domain.batch.BatchChangeApprovalStatus._ import vinyldns.core.domain.batch.BatchChangeApprovalStatus._
import java.time.ZoneId import java.time.ZoneId
class EmailNotifier(config: EmailNotifierConfig, session: Session, userRepository: UserRepository) class EmailNotifier(config: EmailNotifierConfig, session: Session, userRepository: UserRepository, groupRepository: GroupRepository)
extends Notifier { extends Notifier {
private val logger = LoggerFactory.getLogger(classOf[EmailNotifier]) private val logger = LoggerFactory.getLogger(classOf[EmailNotifier])
@ -52,12 +44,15 @@ class EmailNotifier(config: EmailNotifierConfig, session: Session, userRepositor
def notify(notification: Notification[_]): IO[Unit] = def notify(notification: Notification[_]): IO[Unit] =
notification.change match { notification.change match {
case bc: BatchChange => sendBatchChangeNotification(bc) case bc: BatchChange => sendBatchChangeNotification(bc)
case rsc: RecordSetChange => sendRecordSetOwnerTransferNotification(rsc)
case _ => IO.unit case _ => IO.unit
} }
def send(addresses: Address*)(buildMessage: Message => Message): IO[Unit] = IO {
def send(toAddresses: Address*)(ccAddresses: Address*)(buildMessage: Message => Message): IO[Unit] = IO {
val message = new MimeMessage(session) val message = new MimeMessage(session)
message.setRecipients(Message.RecipientType.TO, addresses.toArray) message.setRecipients(Message.RecipientType.TO, toAddresses.toArray)
message.setRecipients(Message.RecipientType.CC, ccAddresses.toArray)
message.setFrom(config.from) message.setFrom(config.from)
buildMessage(message) buildMessage(message)
message.saveChanges() message.saveChanges()
@ -67,10 +62,10 @@ class EmailNotifier(config: EmailNotifierConfig, session: Session, userRepositor
transport.close() transport.close()
} }
def sendBatchChangeNotification(bc: BatchChange): IO[Unit] = def sendBatchChangeNotification(bc: BatchChange): IO[Unit] = {
userRepository.getUser(bc.userId).flatMap { userRepository.getUser(bc.userId).flatMap {
case Some(UserWithEmail(email)) => case Some(UserWithEmail(email)) =>
send(email) { message => send(email)() { message =>
message.setSubject(s"VinylDNS Batch change ${bc.id} results") message.setSubject(s"VinylDNS Batch change ${bc.id} results")
message.setContent(formatBatchChange(bc), "text/html") message.setContent(formatBatchChange(bc), "text/html")
message message
@ -81,9 +76,58 @@ class EmailNotifier(config: EmailNotifierConfig, session: Session, userRepositor
s"Unable to properly parse email for ${user.id}: ${user.email.getOrElse("<none>")}" s"Unable to properly parse email for ${user.id}: ${user.email.getOrElse("<none>")}"
) )
} }
case None => IO { logger.warn(s"Unable to find user: ${bc.userId}") } case None => IO {
logger.warn(s"Unable to find user: ${bc.userId}")
}
case _ => IO.unit case _ => IO.unit
} }
}
def sendRecordSetOwnerTransferNotification(rsc: RecordSetChange): IO[Unit] = {
for {
toGroup <- groupRepository.getGroup(rsc.recordSet.ownerGroupId.getOrElse("<none>"))
ccGroup <- groupRepository.getGroup(rsc.recordSet.recordSetGroupChange.map(_.requestedOwnerGroupId.getOrElse("<none>")).getOrElse("<none>"))
_ <- toGroup match {
case Some(group) =>
group.memberIds.toList.traverse { id =>
userRepository.getUser(id).flatMap {
case Some(UserWithEmail(toEmail)) =>
ccGroup match {
case Some(ccg) =>
ccg.memberIds.toList.traverse { id =>
userRepository.getUser(id).flatMap {
case Some(ccUser) =>
val ccEmail = ccUser.email.getOrElse("<none>")
send(toEmail)(new InternetAddress(ccEmail)) { message =>
message.setSubject(s"VinylDNS RecordSet change ${rsc.id} results")
message.setContent(formatRecordSetChange(rsc), "text/html")
message
}
case None =>
IO.unit
}
}
case None => IO.unit
}
case Some(user: User) if user.email.isDefined =>
IO {
logger.warn(
s"Unable to properly parse email for ${user.id}: ${user.email.getOrElse("<none>")}"
)
}
case None =>
IO {
logger.warn(s"Unable to find user: ${rsc.userId}")
}
case _ =>
IO.unit
}
}
case None => IO.unit // Handle case where toGroup is None
}
} yield ()
}
def formatBatchChange(bc: BatchChange): String = { def formatBatchChange(bc: BatchChange): String = {
val sb = new StringBuilder val sb = new StringBuilder
@ -93,7 +137,7 @@ class EmailNotifier(config: EmailNotifierConfig, session: Session, userRepositor
| ${bc.comments.map(comments => s"<b>Description:</b> $comments</br>").getOrElse("")} | ${bc.comments.map(comments => s"<b>Description:</b> $comments</br>").getOrElse("")}
| <b>Created:</b> ${DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL).withZone(ZoneId.systemDefault()).format(bc.createdTimestamp)} <br/> | <b>Created:</b> ${DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL).withZone(ZoneId.systemDefault()).format(bc.createdTimestamp)} <br/>
| <b>Id:</b> ${bc.id}<br/> | <b>Id:</b> ${bc.id}<br/>
| <b>Status:</b> ${formatStatus(bc.approvalStatus, bc.status)}<br/>""".stripMargin) | <b>Status:</b> ${formatBatchStatus(bc.approvalStatus, bc.status)}<br/>""".stripMargin)
// For manually reviewed e-mails, add additional info; e-mails are not sent for pending batch changes // For manually reviewed e-mails, add additional info; e-mails are not sent for pending batch changes
if (bc.approvalStatus != AutoApproved) { if (bc.approvalStatus != AutoApproved) {
@ -125,7 +169,8 @@ class EmailNotifier(config: EmailNotifierConfig, session: Session, userRepositor
sb.toString sb.toString
} }
def formatStatus(approval: BatchChangeApprovalStatus, status: BatchChangeStatus): String =
def formatBatchStatus(approval: BatchChangeApprovalStatus, status: BatchChangeStatus): String =
(approval, status) match { (approval, status) match {
case (ManuallyRejected, _) => "Rejected" case (ManuallyRejected, _) => "Rejected"
case (BatchChangeApprovalStatus.PendingReview, _) => "Pending Review" case (BatchChangeApprovalStatus.PendingReview, _) => "Pending Review"
@ -133,6 +178,28 @@ class EmailNotifier(config: EmailNotifierConfig, session: Session, userRepositor
case (_, status) => status.toString case (_, status) => status.toString
} }
def formatRecordSetChange(rsc: RecordSetChange): String = {
val sb = new StringBuilder
sb.append(s"""<h1>RecordSet Ownership Transfer</h1>
| <b>Submitter:</b> ${ userRepository.getUser(rsc.userId).map(_.get.userName)}
| <b>Id:</b> ${rsc.id}<br/>
| <b>Submitted time:</b> ${DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL).withZone(ZoneId.systemDefault()).format(rsc.created)} <br/>
| <b>OwnerShip Current Group:</b> ${rsc.recordSet.ownerGroupId.getOrElse("none")} <br/>
| <b>OwnerShip Transfer Group:</b> ${rsc.recordSet.recordSetGroupChange.map(_.requestedOwnerGroupId.getOrElse("none")).getOrElse("none")} <br/>
| <b>OwnerShip Transfer Status:</b> ${formatOwnerShipStatus(rsc.recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).get)}<br/>
""".stripMargin)
sb.toString
}
def formatOwnerShipStatus(status: OwnerShipTransferStatus): String =
status match {
case OwnerShipTransferStatus.ManuallyRejected => "Rejected"
case OwnerShipTransferStatus.PendingReview => "Pending Review"
case OwnerShipTransferStatus.ManuallyApproved => "Approved"
case OwnerShipTransferStatus.Cancelled => "Cancelled"
}
def formatSingleChange(sc: SingleChange, index: Int): String = sc match { def formatSingleChange(sc: SingleChange, index: Int): String = sc match {
case SingleAddChange( case SingleAddChange(
_, _,

View File

@ -17,7 +17,7 @@
package vinyldns.api.notifier.email package vinyldns.api.notifier.email
import vinyldns.core.notifier.{Notifier, NotifierConfig, NotifierProvider} import vinyldns.core.notifier.{Notifier, NotifierConfig, NotifierProvider}
import vinyldns.core.domain.membership.UserRepository import vinyldns.core.domain.membership.{GroupRepository, UserRepository}
import pureconfig._ import pureconfig._
import pureconfig.generic.auto._ import pureconfig.generic.auto._
import pureconfig.module.catseffect.syntax._ import pureconfig.module.catseffect.syntax._
@ -30,13 +30,13 @@ class EmailNotifierProvider extends NotifierProvider {
private implicit val cs: ContextShift[IO] = private implicit val cs: ContextShift[IO] =
IO.contextShift(scala.concurrent.ExecutionContext.global) IO.contextShift(scala.concurrent.ExecutionContext.global)
def load(config: NotifierConfig, userRepository: UserRepository): IO[Notifier] = def load(config: NotifierConfig, userRepository: UserRepository, groupRepository: GroupRepository): IO[Notifier] =
for { for {
emailConfig <- Blocker[IO].use( emailConfig <- Blocker[IO].use(
ConfigSource.fromConfig(config.settings).loadF[IO, EmailNotifierConfig](_) ConfigSource.fromConfig(config.settings).loadF[IO, EmailNotifierConfig](_)
) )
session <- createSession(emailConfig) session <- createSession(emailConfig)
} yield new EmailNotifier(emailConfig, session, userRepository) } yield new EmailNotifier(emailConfig, session, userRepository, groupRepository)
def createSession(config: EmailNotifierConfig): IO[Session] = IO { def createSession(config: EmailNotifierConfig): IO[Session] = IO {
Session.getInstance(config.smtp) Session.getInstance(config.smtp)

View File

@ -17,7 +17,7 @@
package vinyldns.api.notifier.sns package vinyldns.api.notifier.sns
import vinyldns.core.notifier.{Notifier, NotifierConfig, NotifierProvider} import vinyldns.core.notifier.{Notifier, NotifierConfig, NotifierProvider}
import vinyldns.core.domain.membership.UserRepository import vinyldns.core.domain.membership.{GroupRepository, UserRepository}
import pureconfig._ import pureconfig._
import pureconfig.generic.auto._ import pureconfig.generic.auto._
import pureconfig.module.catseffect.syntax._ import pureconfig.module.catseffect.syntax._
@ -35,7 +35,7 @@ class SnsNotifierProvider extends NotifierProvider {
IO.contextShift(scala.concurrent.ExecutionContext.global) IO.contextShift(scala.concurrent.ExecutionContext.global)
private val logger = LoggerFactory.getLogger(classOf[SnsNotifierProvider]) private val logger = LoggerFactory.getLogger(classOf[SnsNotifierProvider])
def load(config: NotifierConfig, userRepository: UserRepository): IO[Notifier] = def load(config: NotifierConfig, userRepository: UserRepository, groupRepository: GroupRepository): IO[Notifier] =
for { for {
snsConfig <- Blocker[IO].use( snsConfig <- Blocker[IO].use(
ConfigSource.fromConfig(config.settings).loadF[IO, SnsNotifierConfig](_) ConfigSource.fromConfig(config.settings).loadF[IO, SnsNotifierConfig](_)

View File

@ -31,6 +31,8 @@ import vinyldns.core.domain.{EncryptFromJson, Encrypted, Fqdn}
import vinyldns.core.domain.record._ import vinyldns.core.domain.record._
import vinyldns.core.domain.zone._ import vinyldns.core.domain.zone._
import vinyldns.core.Messages._ import vinyldns.core.Messages._
import vinyldns.core.domain.record.OwnerShipTransferStatus
import vinyldns.core.domain.record.OwnerShipTransferStatus.OwnerShipTransferStatus
trait DnsJsonProtocol extends JsonValidation { trait DnsJsonProtocol extends JsonValidation {
import vinyldns.core.domain.record.RecordType._ import vinyldns.core.domain.record.RecordType._
@ -42,11 +44,13 @@ trait DnsJsonProtocol extends JsonValidation {
AlgorithmSerializer, AlgorithmSerializer,
EncryptedSerializer, EncryptedSerializer,
RecordSetSerializer, RecordSetSerializer,
ownerShipTransferSerializer,
RecordSetListInfoSerializer, RecordSetListInfoSerializer,
RecordSetGlobalInfoSerializer, RecordSetGlobalInfoSerializer,
RecordSetInfoSerializer, RecordSetInfoSerializer,
RecordSetChangeSerializer, RecordSetChangeSerializer,
JsonEnumV(ZoneStatus), JsonEnumV(ZoneStatus),
JsonEnumV(OwnerShipTransferStatus),
JsonEnumV(ZoneChangeStatus), JsonEnumV(ZoneChangeStatus),
JsonEnumV(RecordSetStatus), JsonEnumV(RecordSetStatus),
JsonEnumV(RecordSetChangeStatus), JsonEnumV(RecordSetChangeStatus),
@ -232,6 +236,7 @@ trait DnsJsonProtocol extends JsonValidation {
(js \ "id").default[String](UUID.randomUUID().toString), (js \ "id").default[String](UUID.randomUUID().toString),
(js \ "account").default[String]("system"), (js \ "account").default[String]("system"),
(js \ "ownerGroupId").optional[String], (js \ "ownerGroupId").optional[String],
(js \ "recordSetGroupChange").optional[OwnerShipTransfer],
(js \ "fqdn").optional[String] (js \ "fqdn").optional[String]
).mapN(RecordSet.apply) ).mapN(RecordSet.apply)
@ -256,9 +261,23 @@ trait DnsJsonProtocol extends JsonValidation {
("id" -> rs.id) ~ ("id" -> rs.id) ~
("account" -> rs.account) ~ ("account" -> rs.account) ~
("ownerGroupId" -> rs.ownerGroupId) ~ ("ownerGroupId" -> rs.ownerGroupId) ~
("recordSetGroupChange" -> Extraction.decompose(rs.recordSetGroupChange)) ~
("fqdn" -> rs.fqdn) ("fqdn" -> rs.fqdn)
} }
case object ownerShipTransferSerializer extends ValidationSerializer[OwnerShipTransfer] {
override def fromJson(js: JValue): ValidatedNel[String, OwnerShipTransfer] =
(
(js \ "ownerShipTransferStatus").required[OwnerShipTransferStatus]("Missing ownerShipTransfer.ownerShipTransferStatus"),
(js \ "requestedOwnerGroupId").optional[String],
).mapN(OwnerShipTransfer.apply)
override def toJson(rsa: OwnerShipTransfer): JValue =
("ownerShipTransferStatus" -> Extraction.decompose(rsa.ownerShipTransferStatus)) ~
("requestedOwnerGroupId" -> Extraction.decompose(rsa.requestedOwnerGroupId))
}
case object RecordSetListInfoSerializer extends ValidationSerializer[RecordSetListInfo] { case object RecordSetListInfoSerializer extends ValidationSerializer[RecordSetListInfo] {
override def fromJson(js: JValue): ValidatedNel[String, RecordSetListInfo] = override def fromJson(js: JValue): ValidatedNel[String, RecordSetListInfo] =
( (
@ -280,6 +299,7 @@ trait DnsJsonProtocol extends JsonValidation {
("accessLevel" -> rs.accessLevel.toString) ~ ("accessLevel" -> rs.accessLevel.toString) ~
("ownerGroupId" -> rs.ownerGroupId) ~ ("ownerGroupId" -> rs.ownerGroupId) ~
("ownerGroupName" -> rs.ownerGroupName) ~ ("ownerGroupName" -> rs.ownerGroupName) ~
("recordSetGroupChange" -> Extraction.decompose(rs.recordSetGroupChange)) ~
("fqdn" -> rs.fqdn) ("fqdn" -> rs.fqdn)
} }
@ -301,6 +321,7 @@ trait DnsJsonProtocol extends JsonValidation {
("account" -> rs.account) ~ ("account" -> rs.account) ~
("ownerGroupId" -> rs.ownerGroupId) ~ ("ownerGroupId" -> rs.ownerGroupId) ~
("ownerGroupName" -> rs.ownerGroupName) ~ ("ownerGroupName" -> rs.ownerGroupName) ~
("recordSetGroupChange" -> Extraction.decompose(rs.recordSetGroupChange)) ~
("fqdn" -> rs.fqdn) ("fqdn" -> rs.fqdn)
} }
@ -326,6 +347,7 @@ trait DnsJsonProtocol extends JsonValidation {
("account" -> rs.account) ~ ("account" -> rs.account) ~
("ownerGroupId" -> rs.ownerGroupId) ~ ("ownerGroupId" -> rs.ownerGroupId) ~
("ownerGroupName" -> rs.ownerGroupName) ~ ("ownerGroupName" -> rs.ownerGroupName) ~
("recordSetGroupChange" -> Extraction.decompose(rs.recordSetGroupChange)) ~
("fqdn" -> rs.fqdn) ~ ("fqdn" -> rs.fqdn) ~
("zoneName" -> rs.zoneName) ~ ("zoneName" -> rs.zoneName) ~
("zoneShared" -> rs.zoneShared) ("zoneShared" -> rs.zoneShared)

View File

@ -1927,6 +1927,561 @@ def test_update_from_acl_for_shared_zone_passes(shared_zone_test_context):
delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202) delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
shared_client.wait_until_recordset_change_status(delete_result, "Complete") shared_client.wait_until_recordset_change_status(delete_result, "Complete")
def test_update_owner_group_transfer_auto_approved(shared_zone_test_context):
"""
Test auto approve ownerShip transfer, for shared zones
"""
shared_client = shared_zone_test_context.shared_zone_vinyldns_client
zone = shared_zone_test_context.shared_zone
shared_group = shared_zone_test_context.shared_record_group
ok_group = shared_zone_test_context.ok_group
update_rs = None
try:
record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}])
record_json["ownerGroupId"] = shared_group["id"]
create_response = shared_client.create_recordset(record_json, status=202)
update = shared_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
assert_that(update["ownerGroupId"], is_(shared_group["id"]))
recordset_group_change_json = {"ownerShipTransferStatus": "AutoApproved",
"requestedOwnerGroupId": ok_group["id"]}
update["recordSetGroupChange"] = recordset_group_change_json
update_response = shared_client.update_recordset(update, status=202)
update_rs = shared_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"]
assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_json))
assert_that(update_rs["ownerGroupId"], is_(ok_group["id"]))
finally:
if update_rs:
delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
shared_client.wait_until_recordset_change_status(delete_result, "Complete")
def test_update_owner_group_transfer_request(shared_zone_test_context):
"""
Test requesting ownerShip transfer, for shared zones
"""
shared_client = shared_zone_test_context.shared_zone_vinyldns_client
dummy_client = shared_zone_test_context.dummy_vinyldns_client
zone = shared_zone_test_context.shared_zone
shared_group = shared_zone_test_context.shared_record_group
dummy_group = shared_zone_test_context.dummy_group
update_rs = None
try:
record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}])
record_json["ownerGroupId"] = dummy_group["id"]
create_response = dummy_client.create_recordset(record_json, status=202)
update = dummy_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
assert_that(update["ownerGroupId"], is_(dummy_group["id"]))
recordset_group_change_json = {"ownerShipTransferStatus": "Requested",
"requestedOwnerGroupId": shared_group["id"]}
recordset_group_change_pending_review_json = {"ownerShipTransferStatus": "PendingReview",
"requestedOwnerGroupId": shared_group["id"]}
update["recordSetGroupChange"] = recordset_group_change_json
update_response = shared_client.update_recordset(update, status=202)
update_rs = shared_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"]
assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_pending_review_json))
assert_that(update_rs["ownerGroupId"], is_(dummy_group["id"]))
finally:
if update_rs:
delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
shared_client.wait_until_recordset_change_status(delete_result, "Complete")
def test_update_request_owner_group_transfer_manually_approved(shared_zone_test_context):
"""
Test approving ownerShip transfer request, for shared zones
"""
shared_client = shared_zone_test_context.shared_zone_vinyldns_client
ok_client = shared_zone_test_context.ok_vinyldns_client
zone = shared_zone_test_context.shared_zone
shared_group = shared_zone_test_context.shared_record_group
ok_group = shared_zone_test_context.ok_group
update_rs = None
try:
record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}])
record_json["ownerGroupId"] = shared_group["id"]
create_response = shared_client.create_recordset(record_json, status=202)
update = shared_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
assert_that(update["ownerGroupId"], is_(shared_group["id"]))
recordset_group_change_json = {"ownerShipTransferStatus": "Requested",
"requestedOwnerGroupId": ok_group["id"]}
recordset_group_change_pending_review_json = {"ownerShipTransferStatus": "PendingReview",
"requestedOwnerGroupId": ok_group["id"]}
update["recordSetGroupChange"] = recordset_group_change_json
update_response = ok_client.update_recordset(update, status=202)
update_rs = ok_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"]
assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_pending_review_json))
assert_that(update_rs["ownerGroupId"], is_(shared_group["id"]))
recordset_group_change_json = {"ownerShipTransferStatus": "ManuallyApproved"}
recordset_group_change_manually_approved_json = {"ownerShipTransferStatus": "ManuallyApproved",
"requestedOwnerGroupId": ok_group["id"]}
update_rs["recordSetGroupChange"] = recordset_group_change_json
update_rs_response = shared_client.update_recordset(update_rs, status=202)
update_rs_ownership = shared_client.wait_until_recordset_change_status(update_rs_response, "Complete")[
"recordSet"]
assert_that(update_rs_ownership["recordSetGroupChange"], is_(recordset_group_change_manually_approved_json))
assert_that(update_rs_ownership["ownerGroupId"], is_(ok_group["id"]))
finally:
if update_rs:
delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
shared_client.wait_until_recordset_change_status(delete_result, "Complete")
def test_update_request_owner_group_transfer_manually_rejected(shared_zone_test_context):
"""
Test rejecting ownerShip transfer request, for shared zones
"""
shared_client = shared_zone_test_context.shared_zone_vinyldns_client
ok_client = shared_zone_test_context.ok_vinyldns_client
zone = shared_zone_test_context.shared_zone
shared_group = shared_zone_test_context.shared_record_group
ok_group = shared_zone_test_context.ok_group
update_rs = None
try:
record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}])
record_json["ownerGroupId"] = shared_group["id"]
create_response = shared_client.create_recordset(record_json, status=202)
update = shared_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
assert_that(update["ownerGroupId"], is_(shared_group["id"]))
recordset_group_change_json = {"ownerShipTransferStatus": "Requested",
"requestedOwnerGroupId": ok_group["id"]}
recordset_group_change_pending_review_json = {"ownerShipTransferStatus": "PendingReview",
"requestedOwnerGroupId": ok_group["id"]}
update["recordSetGroupChange"] = recordset_group_change_json
update_response = ok_client.update_recordset(update, status=202)
update_rs = ok_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"]
assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_pending_review_json))
assert_that(update_rs["ownerGroupId"], is_(shared_group["id"]))
recordset_group_change_json = {"ownerShipTransferStatus": "ManuallyRejected"}
recordset_group_change_manually_rejected_json = {"ownerShipTransferStatus": "ManuallyRejected",
"requestedOwnerGroupId": ok_group["id"]}
update_rs["recordSetGroupChange"] = recordset_group_change_json
update_rs_response = shared_client.update_recordset(update_rs, status=202)
update_rs_ownership = shared_client.wait_until_recordset_change_status(update_rs_response, "Complete")[
"recordSet"]
assert_that(update_rs_ownership["recordSetGroupChange"], is_(recordset_group_change_manually_rejected_json))
assert_that(update_rs_ownership["ownerGroupId"], is_(shared_group["id"]))
finally:
if update_rs:
delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
shared_client.wait_until_recordset_change_status(delete_result, "Complete")
def test_update_request_owner_group_transfer_cancelled(shared_zone_test_context):
"""
Test cancelling ownerShip transfer request
"""
shared_client = shared_zone_test_context.shared_zone_vinyldns_client
ok_client = shared_zone_test_context.ok_vinyldns_client
zone = shared_zone_test_context.shared_zone
shared_group = shared_zone_test_context.shared_record_group
ok_group = shared_zone_test_context.ok_group
update_rs = None
try:
record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}])
record_json["ownerGroupId"] = shared_group["id"]
create_response = shared_client.create_recordset(record_json, status=202)
update = shared_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
assert_that(update["ownerGroupId"], is_(shared_group["id"]))
recordset_group_change_json = {"ownerShipTransferStatus": "Requested",
"requestedOwnerGroupId": ok_group["id"]}
recordset_group_change_pending_review_json = {"ownerShipTransferStatus": "PendingReview",
"requestedOwnerGroupId": ok_group["id"]}
update["recordSetGroupChange"] = recordset_group_change_json
update_response = ok_client.update_recordset(update, status=202)
update_rs = ok_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"]
assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_pending_review_json))
assert_that(update_rs["ownerGroupId"], is_(shared_group["id"]))
recordset_group_change_json = {"ownerShipTransferStatus": "Cancelled"}
recordset_group_change_cancelled_json = {"ownerShipTransferStatus": "Cancelled",
"requestedOwnerGroupId": ok_group["id"]}
update_rs["recordSetGroupChange"] = recordset_group_change_json
update_rs_response = ok_client.update_recordset(update_rs, status=202)
update_rs_ownership = ok_client.wait_until_recordset_change_status(update_rs_response, "Complete")["recordSet"]
assert_that(update_rs_ownership["recordSetGroupChange"], is_(recordset_group_change_cancelled_json))
assert_that(update_rs_ownership["ownerGroupId"], is_(shared_group["id"]))
finally:
if update_rs:
delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
shared_client.wait_until_recordset_change_status(delete_result, "Complete")
def test_update_owner_group_transfer_approval_to_group_a_user_is_not_in_fails(shared_zone_test_context):
"""
Test approving ownerShip transfer request, for user not a member of owner group
"""
shared_client = shared_zone_test_context.shared_zone_vinyldns_client
dummy_client = shared_zone_test_context.dummy_vinyldns_client
zone = shared_zone_test_context.shared_zone
shared_group = shared_zone_test_context.shared_record_group
dummy_group = shared_zone_test_context.dummy_group
update_rs = None
try:
record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}])
record_json["ownerGroupId"] = dummy_group["id"]
create_response = dummy_client.create_recordset(record_json, status=202)
update = dummy_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
assert_that(update["ownerGroupId"], is_(dummy_group["id"]))
recordset_group_change_json = {"ownerShipTransferStatus": "Requested",
"requestedOwnerGroupId": shared_group["id"]}
recordset_group_change_pending_review_json = {"ownerShipTransferStatus": "PendingReview",
"requestedOwnerGroupId": shared_group["id"]}
update["recordSetGroupChange"] = recordset_group_change_json
update_response = shared_client.update_recordset(update, status=202)
update_rs = shared_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"]
assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_pending_review_json))
assert_that(update_rs["ownerGroupId"], is_(dummy_group["id"]))
recordset_group_change_approved_json = {"ownerShipTransferStatus": "ManuallyApproved"}
update_rs["recordSetGroupChange"] = recordset_group_change_approved_json
error = shared_client.update_recordset(update_rs, status=422)
assert_that(error, is_(f"User not in record owner group with id \"{dummy_group['id']}\""))
finally:
if update_rs:
delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
shared_client.wait_until_recordset_change_status(delete_result, "Complete")
def test_update_owner_group_transfer_reject_to_group_a_user_is_not_in_fails(shared_zone_test_context):
"""
Test rejecting ownerShip transfer request, for user not a member of owner group
"""
shared_client = shared_zone_test_context.shared_zone_vinyldns_client
dummy_client = shared_zone_test_context.dummy_vinyldns_client
zone = shared_zone_test_context.shared_zone
shared_group = shared_zone_test_context.shared_record_group
dummy_group = shared_zone_test_context.dummy_group
update_rs = None
try:
record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}])
record_json["ownerGroupId"] = dummy_group["id"]
create_response = dummy_client.create_recordset(record_json, status=202)
update = dummy_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
assert_that(update["ownerGroupId"], is_(dummy_group["id"]))
recordset_group_change_json = {"ownerShipTransferStatus": "Requested",
"requestedOwnerGroupId": shared_group["id"]}
recordset_group_change_pending_review_json = {"ownerShipTransferStatus": "PendingReview",
"requestedOwnerGroupId": shared_group["id"]}
update["recordSetGroupChange"] = recordset_group_change_json
update_response = shared_client.update_recordset(update, status=202)
update_rs = shared_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"]
assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_pending_review_json))
assert_that(update_rs["ownerGroupId"], is_(dummy_group["id"]))
recordset_group_change_approved_json = {"ownerShipTransferStatus": "ManuallyRejected"}
update_rs["recordSetGroupChange"] = recordset_group_change_approved_json
error = shared_client.update_recordset(update_rs, status=422)
assert_that(error, is_(f"User not in record owner group with id \"{dummy_group['id']}\""))
finally:
if update_rs:
delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
shared_client.wait_until_recordset_change_status(delete_result, "Complete")
def test_update_owner_group_transfer_auto_approved_to_group_a_user_is_not_in_fails(shared_zone_test_context):
"""
Test approving ownerShip transfer request, for user not a member of owner group
"""
shared_client = shared_zone_test_context.shared_zone_vinyldns_client
dummy_client = shared_zone_test_context.dummy_vinyldns_client
zone = shared_zone_test_context.shared_zone
shared_group = shared_zone_test_context.shared_record_group
dummy_group = shared_zone_test_context.dummy_group
update_rs = None
try:
record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}])
record_json["ownerGroupId"] = dummy_group["id"]
create_response = dummy_client.create_recordset(record_json, status=202)
update = dummy_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
assert_that(update["ownerGroupId"], is_(dummy_group["id"]))
recordset_group_change_json = {"ownerShipTransferStatus": "Requested",
"requestedOwnerGroupId": shared_group["id"]}
recordset_group_change_pending_review_json = {"ownerShipTransferStatus": "PendingReview",
"requestedOwnerGroupId": shared_group["id"]}
update["recordSetGroupChange"] = recordset_group_change_json
update_response = shared_client.update_recordset(update, status=202)
update_rs = shared_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"]
assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_pending_review_json))
assert_that(update_rs["ownerGroupId"], is_(dummy_group["id"]))
recordset_group_change_approved_json = {"ownerShipTransferStatus": "AutoApproved"}
update_rs["recordSetGroupChange"] = recordset_group_change_approved_json
error = shared_client.update_recordset(update_rs, status=422)
assert_that(error, is_(f"Record owner group with id \"{dummy_group['id']}\" not found"))
finally:
if update_rs:
delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
shared_client.wait_until_recordset_change_status(delete_result, "Complete")
def test_update_owner_group_transfer_approved_when_request_cancelled_in_fails(shared_zone_test_context):
"""
Test approving ownerShip transfer, for cancelled request
"""
shared_client = shared_zone_test_context.shared_zone_vinyldns_client
ok_client = shared_zone_test_context.ok_vinyldns_client
zone = shared_zone_test_context.shared_zone
shared_group = shared_zone_test_context.shared_record_group
ok_group = shared_zone_test_context.ok_group
update_rs = None
try:
record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}])
record_json["ownerGroupId"] = shared_group["id"]
create_response = shared_client.create_recordset(record_json, status=202)
update = shared_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
assert_that(update["ownerGroupId"], is_(shared_group["id"]))
recordset_group_change_json = {"ownerShipTransferStatus": "Requested",
"requestedOwnerGroupId": ok_group["id"]}
recordset_group_change_pending_review_json = {"ownerShipTransferStatus": "PendingReview",
"requestedOwnerGroupId": ok_group["id"]}
update["recordSetGroupChange"] = recordset_group_change_json
update_response = ok_client.update_recordset(update, status=202)
update_rs = ok_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"]
assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_pending_review_json))
assert_that(update_rs["ownerGroupId"], is_(shared_group["id"]))
recordset_group_change_json = {"ownerShipTransferStatus": "Cancelled"}
recordset_group_change_cancelled_json = {"ownerShipTransferStatus": "Cancelled",
"requestedOwnerGroupId": ok_group["id"]}
update_rs["recordSetGroupChange"] = recordset_group_change_json
update_rs_response = ok_client.update_recordset(update_rs, status=202)
update_rs_ownership = ok_client.wait_until_recordset_change_status(update_rs_response, "Complete")["recordSet"]
assert_that(update_rs_ownership["recordSetGroupChange"], is_(recordset_group_change_cancelled_json))
assert_that(update_rs_ownership["ownerGroupId"], is_(shared_group["id"]))
recordset_group_change_json = {"ownerShipTransferStatus": "ManuallyApproved"}
update_rs["recordSetGroupChange"] = recordset_group_change_json
error = ok_client.update_recordset(update_rs, status=422)
assert_that(error, is_("Cannot update RecordSet OwnerShip Status when request is cancelled."))
finally:
if update_rs:
delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
shared_client.wait_until_recordset_change_status(delete_result, "Complete")
def test_update_owner_group_transfer_rejected_when_request_cancelled_in_fails(shared_zone_test_context):
"""
Test rejecting ownerShip transfer, for cancelled request
"""
shared_client = shared_zone_test_context.shared_zone_vinyldns_client
ok_client = shared_zone_test_context.ok_vinyldns_client
zone = shared_zone_test_context.shared_zone
shared_group = shared_zone_test_context.shared_record_group
ok_group = shared_zone_test_context.ok_group
update_rs = None
try:
record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}])
record_json["ownerGroupId"] = shared_group["id"]
create_response = shared_client.create_recordset(record_json, status=202)
update = shared_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
assert_that(update["ownerGroupId"], is_(shared_group["id"]))
recordset_group_change_json = {"ownerShipTransferStatus": "Requested",
"requestedOwnerGroupId": ok_group["id"]}
recordset_group_change_pending_review_json = {"ownerShipTransferStatus": "PendingReview",
"requestedOwnerGroupId": ok_group["id"]}
update["recordSetGroupChange"] = recordset_group_change_json
update_response = ok_client.update_recordset(update, status=202)
update_rs = ok_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"]
assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_pending_review_json))
assert_that(update_rs["ownerGroupId"], is_(shared_group["id"]))
recordset_group_change_json = {"ownerShipTransferStatus": "Cancelled"}
recordset_group_change_cancelled_json = {"ownerShipTransferStatus": "Cancelled",
"requestedOwnerGroupId": ok_group["id"]}
update_rs["recordSetGroupChange"] = recordset_group_change_json
update_rs_response = ok_client.update_recordset(update_rs, status=202)
update_rs_ownership = ok_client.wait_until_recordset_change_status(update_rs_response, "Complete")["recordSet"]
assert_that(update_rs_ownership["recordSetGroupChange"], is_(recordset_group_change_cancelled_json))
assert_that(update_rs_ownership["ownerGroupId"], is_(shared_group["id"]))
recordset_group_change_json = {"ownerShipTransferStatus": "ManuallyRejected"}
update_rs["recordSetGroupChange"] = recordset_group_change_json
error = ok_client.update_recordset(update_rs, status=422)
assert_that(error, is_("Cannot update RecordSet OwnerShip Status when request is cancelled."))
finally:
if update_rs:
delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
shared_client.wait_until_recordset_change_status(delete_result, "Complete")
def test_update_owner_group_transfer_auto_approved_when_request_cancelled_in_fails(shared_zone_test_context):
"""
Test auto_approving ownerShip transfer, for cancelled request
"""
shared_client = shared_zone_test_context.shared_zone_vinyldns_client
ok_client = shared_zone_test_context.ok_vinyldns_client
zone = shared_zone_test_context.shared_zone
shared_group = shared_zone_test_context.shared_record_group
ok_group = shared_zone_test_context.ok_group
update_rs = None
try:
record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}])
record_json["ownerGroupId"] = shared_group["id"]
create_response = shared_client.create_recordset(record_json, status=202)
update = shared_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
assert_that(update["ownerGroupId"], is_(shared_group["id"]))
recordset_group_change_json = {"ownerShipTransferStatus": "Requested",
"requestedOwnerGroupId": ok_group["id"]}
recordset_group_change_pending_review_json = {"ownerShipTransferStatus": "PendingReview",
"requestedOwnerGroupId": ok_group["id"]}
update["recordSetGroupChange"] = recordset_group_change_json
update_response = ok_client.update_recordset(update, status=202)
update_rs = ok_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"]
assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_pending_review_json))
assert_that(update_rs["ownerGroupId"], is_(shared_group["id"]))
recordset_group_change_json = {"ownerShipTransferStatus": "Cancelled"}
recordset_group_change_cancelled_json = {"ownerShipTransferStatus": "Cancelled",
"requestedOwnerGroupId": ok_group["id"]}
update_rs["recordSetGroupChange"] = recordset_group_change_json
update_rs_response = ok_client.update_recordset(update_rs, status=202)
update_rs_ownership = ok_client.wait_until_recordset_change_status(update_rs_response, "Complete")["recordSet"]
assert_that(update_rs_ownership["recordSetGroupChange"], is_(recordset_group_change_cancelled_json))
assert_that(update_rs_ownership["ownerGroupId"], is_(shared_group["id"]))
recordset_group_change_json = {"ownerShipTransferStatus": "AutoApproved"}
update_rs["recordSetGroupChange"] = recordset_group_change_json
error = ok_client.update_recordset(update_rs, status=422)
assert_that(error, is_("Cannot update RecordSet OwnerShip Status when request is cancelled."))
finally:
if update_rs:
delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
shared_client.wait_until_recordset_change_status(delete_result, "Complete")
def test_update_owner_group_transfer_on_non_shared_zones_in_fails(shared_zone_test_context):
"""
Test that requesting ownerShip transfer for non shared zones
"""
shared_client = shared_zone_test_context.shared_zone_vinyldns_client
ok_client = shared_zone_test_context.ok_vinyldns_client
shared_group = shared_zone_test_context.shared_record_group
ok_zone = shared_zone_test_context.ok_zone
update_rs = None
try:
record_json = create_recordset(ok_zone, "test_update_success", "A", [{"address": "1.1.1.1"}])
create_response = ok_client.create_recordset(record_json, status=202)
update = ok_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
recordset_group_change_json = {"ownerShipTransferStatus": "Requested",
"requestedOwnerGroupId": shared_group["id"]}
update["recordSetGroupChange"] = recordset_group_change_json
error = shared_client.update_recordset(update, status=422)
assert_that(error, is_("Cannot update RecordSet OwnerShip Status when zone is not shared."))
finally:
if update_rs:
delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
shared_client.wait_until_recordset_change_status(delete_result, "Complete")
def test_update_owner_group_transfer_and_ttl_on_user_not_in_owner_group_in_fails(shared_zone_test_context):
"""
Test that updating record "i.e.ttl" with requesting ownerShip transfer, where user not in the member of the owner group
"""
shared_client = shared_zone_test_context.shared_zone_vinyldns_client
dummy_client = shared_zone_test_context.dummy_vinyldns_client
zone = shared_zone_test_context.shared_zone
shared_group = shared_zone_test_context.shared_record_group
dummy_group = shared_zone_test_context.dummy_group
update_rs = None
try:
record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}])
record_json["ownerGroupId"] = shared_group["id"]
create_response = shared_client.create_recordset(record_json, status=202)
update = shared_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
assert_that(update["ownerGroupId"], is_(shared_group["id"]))
recordset_group_change_json = {"ownerShipTransferStatus": "Requested",
"requestedOwnerGroupId": dummy_group["id"]}
update["recordSetGroupChange"] = recordset_group_change_json
update["ttl"] = update["ttl"] + 100
error = dummy_client.update_recordset(update, status=422)
assert_that(error, is_(f"Cannot update RecordSet's if user not a member of ownership group. User can only request for ownership transfer"))
finally:
if update_rs:
delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
shared_client.wait_until_recordset_change_status(delete_result, "Complete")
def test_update_to_no_group_owner_passes(shared_zone_test_context): def test_update_to_no_group_owner_passes(shared_zone_test_context):
""" """

View File

@ -40,6 +40,8 @@ import vinyldns.core.domain.membership.{GroupRepository, ListUsersResults, UserR
import vinyldns.core.domain.record._ import vinyldns.core.domain.record._
import vinyldns.core.domain.zone._ import vinyldns.core.domain.zone._
import vinyldns.core.queue.MessageQueue import vinyldns.core.queue.MessageQueue
import vinyldns.core.notifier.{AllNotifiers, Notification, Notifier}
import scala.concurrent.ExecutionContext
class RecordSetServiceSpec class RecordSetServiceSpec
extends AnyWordSpec extends AnyWordSpec
@ -47,6 +49,7 @@ class RecordSetServiceSpec
with Matchers with Matchers
with MockitoSugar with MockitoSugar
with BeforeAndAfterEach { with BeforeAndAfterEach {
private implicit val contextShift: ContextShift[IO] = IO.contextShift(ExecutionContext.global)
private val mockZoneRepo = mock[ZoneRepository] private val mockZoneRepo = mock[ZoneRepository]
private val mockGroupRepo = mock[GroupRepository] private val mockGroupRepo = mock[GroupRepository]
@ -58,6 +61,8 @@ class RecordSetServiceSpec
private val mockBackend = private val mockBackend =
mock[Backend] mock[Backend]
private val mockBackendResolver = mock[BackendResolver] private val mockBackendResolver = mock[BackendResolver]
private val mockNotifier = mock[Notifier]
private val mockNotifiers = AllNotifiers(List(mockNotifier))
doReturn(IO.pure(Some(okZone))).when(mockZoneRepo).getZone(okZone.id) doReturn(IO.pure(Some(okZone))).when(mockZoneRepo).getZone(okZone.id)
doReturn(IO.pure(Some(zoneNotAuthorized))) doReturn(IO.pure(Some(zoneNotAuthorized)))
@ -85,7 +90,8 @@ class RecordSetServiceSpec
VinylDNSTestHelpers.highValueDomainConfig, VinylDNSTestHelpers.highValueDomainConfig,
VinylDNSTestHelpers.dottedHostsConfig, VinylDNSTestHelpers.dottedHostsConfig,
VinylDNSTestHelpers.approvedNameServers, VinylDNSTestHelpers.approvedNameServers,
true true,
mockNotifiers
) )
val underTestWithDnsBackendValidations = new RecordSetService( val underTestWithDnsBackendValidations = new RecordSetService(
@ -104,7 +110,8 @@ class RecordSetServiceSpec
VinylDNSTestHelpers.highValueDomainConfig, VinylDNSTestHelpers.highValueDomainConfig,
VinylDNSTestHelpers.dottedHostsConfig, VinylDNSTestHelpers.dottedHostsConfig,
VinylDNSTestHelpers.approvedNameServers, VinylDNSTestHelpers.approvedNameServers,
true true,
mockNotifiers
) )
val underTestWithEmptyDottedHostsConfig = new RecordSetService( val underTestWithEmptyDottedHostsConfig = new RecordSetService(
@ -123,7 +130,8 @@ class RecordSetServiceSpec
VinylDNSTestHelpers.highValueDomainConfig, VinylDNSTestHelpers.highValueDomainConfig,
VinylDNSTestHelpers.emptyDottedHostsConfig, VinylDNSTestHelpers.emptyDottedHostsConfig,
VinylDNSTestHelpers.approvedNameServers, VinylDNSTestHelpers.approvedNameServers,
true true,
mockNotifiers
) )
def getDottedHostsConfigGroupsAllowed(zone: Zone, config: DottedHostsConfig): List[String] = { def getDottedHostsConfigGroupsAllowed(zone: Zone, config: DottedHostsConfig): List[String] = {
@ -1177,6 +1185,9 @@ class RecordSetServiceSpec
doReturn(IO.pure(Some(oldRecord))) doReturn(IO.pure(Some(oldRecord)))
.when(mockRecordRepo) .when(mockRecordRepo)
.getRecordSet(oldRecord.id) .getRecordSet(oldRecord.id)
doReturn(IO.pure(Some(newRecord)))
.when(mockRecordRepo)
.getRecordSet(newRecord.id)
doReturn(IO.pure(List(oldRecord))) doReturn(IO.pure(List(oldRecord)))
.when(mockRecordRepo) .when(mockRecordRepo)
.getRecordSetsByName(zone.id, oldRecord.name) .getRecordSetsByName(zone.id, oldRecord.name)
@ -1185,7 +1196,7 @@ class RecordSetServiceSpec
.getGroup(okGroup.id) .getGroup(okGroup.id)
val result = underTest.updateRecordSet(newRecord, auth).value.unsafeRunSync().swap.toOption.get val result = underTest.updateRecordSet(newRecord, auth).value.unsafeRunSync().swap.toOption.get
result shouldBe an[InvalidRequest] result shouldBe an[NotAuthorizedError]
} }
"succeed if user is in owner group and zone is shared" in { "succeed if user is in owner group and zone is shared" in {
val zone = okZone.copy(shared = true, id = "test-owner-group") val zone = okZone.copy(shared = true, id = "test-owner-group")
@ -1314,6 +1325,9 @@ class RecordSetServiceSpec
doReturn(IO.pure(Some(oldRecord))) doReturn(IO.pure(Some(oldRecord)))
.when(mockRecordRepo) .when(mockRecordRepo)
.getRecordSet(newRecord.id) .getRecordSet(newRecord.id)
doReturn(IO.pure(Some(newRecord)))
.when(mockRecordRepo)
.getRecordSet(newRecord.id)
doReturn(IO.pure(List(oldRecord))) doReturn(IO.pure(List(oldRecord)))
.when(mockRecordRepo) .when(mockRecordRepo)
.getRecordSetsByName(zone.id, newRecord.name) .getRecordSetsByName(zone.id, newRecord.name)
@ -2200,4 +2214,514 @@ class RecordSetServiceSpec
"thing.com." "thing.com."
} }
} }
"ownerShipTransfer" should {
"success if user request AutoApproved for the owner group is null" in {
val zone = okZone.copy(shared = true, id = "test-owner-group")
val auth = AuthPrincipal(listOfDummyUsers.head, Seq(oneUserDummyGroup.id))
val oldRecord = aaaa.copy(
name = "test-owner-group-success",
zoneId = zone.id,
status = RecordSetStatus.Active,
ownerGroupId = None
)
val newRecord = oldRecord.copy(recordSetGroupChange =
Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.AutoApproved,requestedOwnerGroupId = Some(okGroup.id))))
doReturn(IO.pure(Some(zone)))
.when(mockZoneRepo)
.getZone(zone.id)
doReturn(IO.pure(Some(oldRecord)))
.when(mockRecordRepo)
.getRecordSet(newRecord.id)
doReturn(IO.pure(List(oldRecord)))
.when(mockRecordRepo)
.getRecordSetsByName(zone.id, newRecord.name)
doReturn(IO.pure(Some(oneUserDummyGroup)))
.when(mockGroupRepo)
.getGroup(oneUserDummyGroup.id)
doReturn(IO.pure(Some(okGroup)))
.when(mockGroupRepo)
.getGroup(okGroup.id)
doReturn(IO.pure(Set(dottedZone, abcZone, xyzZone, dotZone)))
.when(mockZoneRepo)
.getZonesByNames(dottedHostsConfigZonesAllowed.toSet)
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(Set.empty)
doReturn(IO.pure(None))
.when(mockZoneRepo)
.getZoneByName(newRecord.name + "." + okZone.name)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByFQDNs(Set(newRecord.name + "." + okZone.name))
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(Set.empty)
doReturn(IO.pure(Set()))
.when(mockGroupRepo)
.getGroupsByName(dottedHostsConfigGroupsAllowed.toSet)
doReturn(IO.pure(ListUsersResults(Seq(), None)))
.when(mockUserRepo)
.getUsers(Set.empty, None, None)
val result =
underTest.updateRecordSet(newRecord, auth).map(_.asInstanceOf[RecordSetChange]).value.unsafeRunSync().toOption.get
result.recordSet.ownerGroupId shouldBe Some(okGroup.id)
result.recordSet.recordSetGroupChange.map(_.requestedOwnerGroupId.get) shouldBe Some(okGroup.id)
result.recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus) shouldBe Some(OwnerShipTransferStatus.AutoApproved)
}
"fail if user request ownership transfer for non shared zone" in {
val zone = okZone.copy(id = "test-owner-group")
val oldRecord = aaaa.copy(
name = "test-owner-group-failure",
zoneId = zone.id,
status = RecordSetStatus.Active,
ownerGroupId = Some(oneUserDummyGroup.id)
)
val newRecord = oldRecord.copy(recordSetGroupChange =
Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.Requested, requestedOwnerGroupId = Some(okGroup.id))))
doReturn(IO.pure(Some(zone)))
.when(mockZoneRepo)
.getZone(zone.id)
doReturn(IO.pure(Some(oldRecord)))
.when(mockRecordRepo)
.getRecordSet(newRecord.id)
doReturn(IO.pure(List(oldRecord)))
.when(mockRecordRepo)
.getRecordSetsByName(zone.id, oldRecord.name)
val result = underTest.updateRecordSet(newRecord, okAuth).value.unsafeRunSync().swap.toOption.get
result shouldBe an[InvalidRequest]
}
"fail if user not a member of owner group and tried to Approve ownership transfer request" in {
val zone = okZone.copy(shared = true, id = "test-owner-group")
val oldRecord = aaaa.copy(
name = "test-owner-group-failure",
zoneId = zone.id,
status = RecordSetStatus.Active,
ownerGroupId = Some(oneUserDummyGroup.id),
recordSetGroupChange =
Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.PendingReview,requestedOwnerGroupId = Some(okGroup.id)))
)
val newRecord = oldRecord.copy(recordSetGroupChange =
Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyApproved)))
doReturn(IO.pure(Some(zone)))
.when(mockZoneRepo)
.getZone(zone.id)
doReturn(IO.pure(Some(oldRecord)))
.when(mockRecordRepo)
.getRecordSet(newRecord.id)
doReturn(IO.pure(List(oldRecord)))
.when(mockRecordRepo)
.getRecordSetsByName(zone.id, oldRecord.name)
doReturn(IO.pure(Some(okGroup)))
.when(mockGroupRepo)
.getGroup(okGroup.id)
val result = underTest.updateRecordSet(newRecord, okAuth).value.unsafeRunSync().swap.toOption.get
result shouldBe an[InvalidRequest]
}
"fail if user not a member of owner group and tried to Reject ownership transfer request" in {
val zone = okZone.copy(shared = true, id = "test-owner-group")
val oldRecord = aaaa.copy(
name = "test-owner-group-failure",
zoneId = zone.id,
status = RecordSetStatus.Active,
ownerGroupId = Some(oneUserDummyGroup.id),
recordSetGroupChange =
Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.PendingReview,requestedOwnerGroupId = Some(okGroup.id)))
)
val newRecord = oldRecord.copy(recordSetGroupChange =
Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyRejected)))
doReturn(IO.pure(Some(zone)))
.when(mockZoneRepo)
.getZone(zone.id)
doReturn(IO.pure(Some(oldRecord)))
.when(mockRecordRepo)
.getRecordSet(newRecord.id)
doReturn(IO.pure(List(oldRecord)))
.when(mockRecordRepo)
.getRecordSetsByName(zone.id, oldRecord.name)
doReturn(IO.pure(Some(okGroup)))
.when(mockGroupRepo)
.getGroup(okGroup.id)
val result = underTest.updateRecordSet(newRecord, okAuth).value.unsafeRunSync().swap.toOption.get
result shouldBe an[InvalidRequest]
}
"success if user not a member of owner group and tried to Request ownership transfer request" in {
val zone = okZone.copy(shared = true, id = "test-owner-group")
val oldRecord = aaaa.copy(
name = "test-owner-group-success",
zoneId = zone.id,
status = RecordSetStatus.Active,
ownerGroupId = Some(oneUserDummyGroup.id)
)
val newRecord = oldRecord.copy(recordSetGroupChange =
Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.Requested,requestedOwnerGroupId = Some(okGroup.id))))
doReturn(IO.pure(Some(zone)))
.when(mockZoneRepo)
.getZone(zone.id)
doReturn(IO.pure(Some(oldRecord)))
.when(mockRecordRepo)
.getRecordSet(newRecord.id)
doReturn(IO.pure(List(oldRecord)))
.when(mockRecordRepo)
.getRecordSetsByName(zone.id, newRecord.name)
doReturn(IO.pure(Some(oneUserDummyGroup)))
.when(mockGroupRepo)
.getGroup(oneUserDummyGroup.id)
doReturn(IO.pure(Some(okGroup)))
.when(mockGroupRepo)
.getGroup(okGroup.id)
doReturn(IO.pure(Set(dottedZone, abcZone, xyzZone, dotZone)))
.when(mockZoneRepo)
.getZonesByNames(dottedHostsConfigZonesAllowed.toSet)
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(Set.empty)
doReturn(IO.pure(None))
.when(mockZoneRepo)
.getZoneByName(newRecord.name + "." + okZone.name)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByFQDNs(Set(newRecord.name + "." + okZone.name))
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(Set.empty)
doReturn(IO.pure(Set()))
.when(mockGroupRepo)
.getGroupsByName(dottedHostsConfigGroupsAllowed.toSet)
doReturn(IO.pure(ListUsersResults(Seq(), None)))
.when(mockUserRepo)
.getUsers(Set.empty, None, None)
doReturn(IO.unit).when(mockNotifier).notify(any[Notification[_]])
val result =
underTest.updateRecordSet(newRecord, okAuth).map(_.asInstanceOf[RecordSetChange]).value.unsafeRunSync().toOption.get
result.recordSet.ownerGroupId shouldBe Some(oneUserDummyGroup.id)
result.recordSet.recordSetGroupChange.map(_.requestedOwnerGroupId.get) shouldBe Some(okGroup.id)
result.recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus) shouldBe Some(OwnerShipTransferStatus.PendingReview)
}
"success if user not a member of owner group and tried to Cancel ownership transfer request" in {
val zone = okZone.copy(shared = true, id = "test-owner-group")
val oldRecord = aaaa.copy(
name = "test-owner-group-success",
zoneId = zone.id,
status = RecordSetStatus.Active,
ownerGroupId = Some(oneUserDummyGroup.id),
recordSetGroupChange =
Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.PendingReview, requestedOwnerGroupId = Some(okGroup.id)))
)
val newRecord = oldRecord.copy(recordSetGroupChange =
Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.Cancelled)))
doReturn(IO.pure(Some(zone)))
.when(mockZoneRepo)
.getZone(zone.id)
doReturn(IO.pure(Some(oldRecord)))
.when(mockRecordRepo)
.getRecordSet(newRecord.id)
doReturn(IO.pure(List(oldRecord)))
.when(mockRecordRepo)
.getRecordSetsByName(zone.id, newRecord.name)
doReturn(IO.pure(Some(oneUserDummyGroup)))
.when(mockGroupRepo)
.getGroup(oneUserDummyGroup.id)
doReturn(IO.pure(Some(okGroup)))
.when(mockGroupRepo)
.getGroup(okGroup.id)
doReturn(IO.pure(Set(dottedZone, abcZone, xyzZone, dotZone)))
.when(mockZoneRepo)
.getZonesByNames(dottedHostsConfigZonesAllowed.toSet)
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(Set.empty)
doReturn(IO.pure(None))
.when(mockZoneRepo)
.getZoneByName(newRecord.name + "." + okZone.name)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByFQDNs(Set(newRecord.name + "." + okZone.name))
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(Set.empty)
doReturn(IO.pure(Set()))
.when(mockGroupRepo)
.getGroupsByName(dottedHostsConfigGroupsAllowed.toSet)
doReturn(IO.pure(ListUsersResults(Seq(), None)))
.when(mockUserRepo)
.getUsers(Set.empty, None, None)
doReturn(IO.unit).when(mockNotifier).notify(any[Notification[_]])
val result =
underTest.updateRecordSet(newRecord, okAuth).map(_.asInstanceOf[RecordSetChange]).value.unsafeRunSync().toOption.get
result.recordSet.ownerGroupId shouldBe Some(oneUserDummyGroup.id)
result.recordSet.recordSetGroupChange.map(_.requestedOwnerGroupId.get) shouldBe Some(okGroup.id)
result.recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus) shouldBe Some(OwnerShipTransferStatus.Cancelled)
}
"fail if user not a member of owner group and tried to update ttl while requesting ownership transfer" in {
val zone = okZone.copy(shared = true, id = "test-owner-group")
val oldRecord =
aaaa.copy(zoneId = zone.id, status = RecordSetStatus.Active, ownerGroupId = Some(oneUserDummyGroup.id))
val newRecord = oldRecord.copy(
ttl = oldRecord.ttl + 1000,
recordSetGroupChange =
Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.Requested,requestedOwnerGroupId = Some(okGroup.id))))
doReturn(IO.pure(Some(okZone)))
.when(mockZoneRepo)
.getZone(newRecord.zoneId)
doReturn(IO.pure(Some(oldRecord)))
.when(mockRecordRepo)
.getRecordSet(newRecord.id)
val result = underTest.updateRecordSet(newRecord, okAuth).value.unsafeRunSync().swap.toOption.get
result shouldBe a[InvalidRequest]
}
"fail if user not a member of owner group and tried to update ttl while cancel ownership transfer" in {
val zone = okZone.copy(shared = true, id = "test-owner-group")
val oldRecord =
aaaa.copy(zoneId = zone.id, status = RecordSetStatus.Active, ownerGroupId = Some(oneUserDummyGroup.id),recordSetGroupChange =
Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.PendingReview, requestedOwnerGroupId = Some(okGroup.id)))
)
val newRecord = oldRecord.copy(
records = List(AAAAData("1:2:3:4:5:6:7:9")),
recordSetGroupChange =
Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.Cancelled)))
doReturn(IO.pure(Some(okZone)))
.when(mockZoneRepo)
.getZone(newRecord.zoneId)
doReturn(IO.pure(Some(oldRecord)))
.when(mockRecordRepo)
.getRecordSet(newRecord.id)
val result = underTest.updateRecordSet(newRecord, okAuth).value.unsafeRunSync().swap.toOption.get
result shouldBe a[InvalidRequest]
}
"fail if user cancelled the ownership request and group member tried to approve the request" in {
val zone = okZone.copy(shared = true, id = "test-owner-group")
val auth = AuthPrincipal(listOfDummyUsers.head, Seq(oneUserDummyGroup.id))
val oldRecord = aaaa.copy(
name = "test-owner-group-failure",
zoneId = zone.id,
status = RecordSetStatus.Active,
ownerGroupId = Some(oneUserDummyGroup.id),
recordSetGroupChange =
Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.Cancelled,requestedOwnerGroupId = Some(okGroup.id)))
)
val newRecord = oldRecord.copy(recordSetGroupChange =
Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyApproved)))
doReturn(IO.pure(Some(okZone)))
.when(mockZoneRepo)
.getZone(newRecord.zoneId)
doReturn(IO.pure(Some(oldRecord)))
.when(mockRecordRepo)
.getRecordSet(newRecord.id)
val result = underTest.updateRecordSet(newRecord, auth).value.unsafeRunSync().swap.toOption.get
result shouldBe a[InvalidRequest]
}
"fail if user cancelled the ownership request and group member tried to reject the request" in {
val zone = okZone.copy(shared = true, id = "test-owner-group")
val auth = AuthPrincipal(listOfDummyUsers.head, Seq(oneUserDummyGroup.id))
val oldRecord = aaaa.copy(
name = "test-owner-group-failure",
zoneId = zone.id,
status = RecordSetStatus.Active,
ownerGroupId = Some(oneUserDummyGroup.id),
recordSetGroupChange =
Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.Cancelled,requestedOwnerGroupId = Some(okGroup.id)))
)
val newRecord = oldRecord.copy(recordSetGroupChange =
Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyRejected)))
doReturn(IO.pure(Some(okZone)))
.when(mockZoneRepo)
.getZone(newRecord.zoneId)
doReturn(IO.pure(Some(oldRecord)))
.when(mockRecordRepo)
.getRecordSet(newRecord.id)
val result = underTest.updateRecordSet(newRecord, auth).value.unsafeRunSync().swap.toOption.get
result shouldBe a[InvalidRequest]
}
"fail if user cancelled the ownership request and group member tried to reject the auto-approve" in {
val zone = okZone.copy(shared = true, id = "test-owner-group")
val auth = AuthPrincipal(listOfDummyUsers.head, Seq(oneUserDummyGroup.id))
val oldRecord = aaaa.copy(
name = "test-owner-group-failure",
zoneId = zone.id,
status = RecordSetStatus.Active,
ownerGroupId = Some(oneUserDummyGroup.id),
recordSetGroupChange =
Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.Cancelled,requestedOwnerGroupId = Some(okGroup.id)))
)
val newRecord = oldRecord.copy(recordSetGroupChange =
Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.AutoApproved)))
doReturn(IO.pure(Some(okZone)))
.when(mockZoneRepo)
.getZone(newRecord.zoneId)
doReturn(IO.pure(Some(oldRecord)))
.when(mockRecordRepo)
.getRecordSet(newRecord.id)
val result = underTest.updateRecordSet(newRecord, auth).value.unsafeRunSync().swap.toOption.get
result shouldBe a[InvalidRequest]
}
"success if user Approve a ownership transfer request Manually" in {
val zone = okZone.copy(shared = true, id = "test-owner-group")
val auth = AuthPrincipal(listOfDummyUsers.head, Seq(oneUserDummyGroup.id))
val oldRecord = aaaa.copy(
name = "test-owner-group-success",
zoneId = zone.id,
status = RecordSetStatus.Active,
ownerGroupId = Some(oneUserDummyGroup.id),
recordSetGroupChange =
Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.PendingReview,requestedOwnerGroupId = Some(okGroup.id)))
)
val newRecord = oldRecord.copy(recordSetGroupChange =
Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyApproved)))
doReturn(IO.pure(Some(zone)))
.when(mockZoneRepo)
.getZone(zone.id)
doReturn(IO.pure(Some(oldRecord)))
.when(mockRecordRepo)
.getRecordSet(newRecord.id)
doReturn(IO.pure(List(oldRecord)))
.when(mockRecordRepo)
.getRecordSetsByName(zone.id, newRecord.name)
doReturn(IO.pure(Some(oneUserDummyGroup)))
.when(mockGroupRepo)
.getGroup(oneUserDummyGroup.id)
doReturn(IO.pure(Some(okGroup)))
.when(mockGroupRepo)
.getGroup(okGroup.id)
doReturn(IO.pure(Set(dottedZone, abcZone, xyzZone, dotZone)))
.when(mockZoneRepo)
.getZonesByNames(dottedHostsConfigZonesAllowed.toSet)
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(Set.empty)
doReturn(IO.pure(None))
.when(mockZoneRepo)
.getZoneByName(newRecord.name + "." + okZone.name)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByFQDNs(Set(newRecord.name + "." + okZone.name))
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(Set.empty)
doReturn(IO.pure(Set()))
.when(mockGroupRepo)
.getGroupsByName(dottedHostsConfigGroupsAllowed.toSet)
doReturn(IO.pure(ListUsersResults(Seq(), None)))
.when(mockUserRepo)
.getUsers(Set.empty, None, None)
doReturn(IO.unit).when(mockNotifier).notify(any[Notification[_]])
val result = {
underTest.updateRecordSet(newRecord, auth).map(_.asInstanceOf[RecordSetChange]).value.unsafeRunSync().toOption.get
}
result.recordSet.ownerGroupId shouldBe Some(okGroup.id)
result.recordSet.recordSetGroupChange.map(_.requestedOwnerGroupId.get) shouldBe Some(okGroup.id)
result.recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus) shouldBe Some(OwnerShipTransferStatus.ManuallyApproved)
}
"success if user Reject a ownership transfer request Manually" in {
val zone = okZone.copy(shared = true, id = "test-owner-group")
val auth = AuthPrincipal(listOfDummyUsers.head, Seq(oneUserDummyGroup.id))
val oldRecord = aaaa.copy(
name = "test-owner-group-success",
zoneId = zone.id,
status = RecordSetStatus.Active,
ownerGroupId = Some(oneUserDummyGroup.id),
recordSetGroupChange =
Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.PendingReview,requestedOwnerGroupId = Some(okGroup.id)))
)
val newRecord = oldRecord.copy(recordSetGroupChange =
Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyRejected)))
doReturn(IO.pure(Some(zone)))
.when(mockZoneRepo)
.getZone(zone.id)
doReturn(IO.pure(Some(oldRecord)))
.when(mockRecordRepo)
.getRecordSet(newRecord.id)
doReturn(IO.pure(List(oldRecord)))
.when(mockRecordRepo)
.getRecordSetsByName(zone.id, newRecord.name)
doReturn(IO.pure(Some(oneUserDummyGroup)))
.when(mockGroupRepo)
.getGroup(oneUserDummyGroup.id)
doReturn(IO.pure(Some(okGroup)))
.when(mockGroupRepo)
.getGroup(okGroup.id)
doReturn(IO.pure(Set(dottedZone, abcZone, xyzZone, dotZone)))
.when(mockZoneRepo)
.getZonesByNames(dottedHostsConfigZonesAllowed.toSet)
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(Set.empty)
doReturn(IO.pure(None))
.when(mockZoneRepo)
.getZoneByName(newRecord.name + "." + okZone.name)
doReturn(IO.pure(List()))
.when(mockRecordRepo)
.getRecordSetsByFQDNs(Set(newRecord.name + "." + okZone.name))
doReturn(IO.pure(Set()))
.when(mockZoneRepo)
.getZonesByFilters(Set.empty)
doReturn(IO.pure(Set()))
.when(mockGroupRepo)
.getGroupsByName(dottedHostsConfigGroupsAllowed.toSet)
doReturn(IO.pure(ListUsersResults(Seq(), None)))
.when(mockUserRepo)
.getUsers(Set.empty, None, None)
doReturn(IO.unit).when(mockNotifier).notify(any[Notification[_]])
val result = {
underTest.updateRecordSet(newRecord, auth).map(_.asInstanceOf[RecordSetChange]).value.unsafeRunSync().toOption.get
}
result.recordSet.ownerGroupId shouldBe Some(oneUserDummyGroup.id)
result.recordSet.recordSetGroupChange.map(_.requestedOwnerGroupId.get) shouldBe Some(okGroup.id)
result.recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus) shouldBe Some(OwnerShipTransferStatus.ManuallyRejected)
}
}
} }

View File

@ -722,5 +722,85 @@ class RecordSetValidationsSpec
canSuperUserUpdateOwnerGroup(existing, rs, zone, superUserAuth) should be(false) canSuperUserUpdateOwnerGroup(existing, rs, zone, superUserAuth) should be(false)
} }
} }
"unchangedRecordSet" should {
"return invalid request when given zone ID does not match existing recordset zone ID" in {
val existing = rsOk
val rs = rsOk.copy(zoneId = "not-real")
val error = leftValue(unchangedRecordSet(existing, rs))
error shouldBe an[InvalidRequest]
error.getMessage() shouldBe "Cannot update RecordSet's if user not a member of ownership group. User can only request for ownership transfer"
}
"return invalid request when given record type does not match existing recordset record type" in {
val existing = rsOk
val rs = rsOk.copy(typ = RecordType.AAAA)
val error = leftValue(unchangedRecordSet(existing, rs))
error shouldBe an[InvalidRequest]
error.getMessage() shouldBe "Cannot update RecordSet's if user not a member of ownership group. User can only request for ownership transfer"
}
"return invalid request when given records does not match existing recordset records" in {
val existing = rsOk
val rs = rsOk.copy(records = List(AData("10.1.1.0")))
val error = leftValue(unchangedRecordSet(existing, rs))
error shouldBe an[InvalidRequest]
error.getMessage() shouldBe "Cannot update RecordSet's if user not a member of ownership group. User can only request for ownership transfer"
}
"return invalid request when given recordset id does not match existing recordset ID" in {
val existing = rsOk
val rs = rsOk.copy(id = abcRecord.id)
val error = leftValue(unchangedRecordSet(existing, rs))
error shouldBe an[InvalidRequest]
error.getMessage() shouldBe "Cannot update RecordSet's if user not a member of ownership group. User can only request for ownership transfer"
}
"return invalid request when given recordset name does not match existing recordset name" in {
val existing = rsOk
val rs = rsOk.copy(name = "abc")
val error = leftValue(unchangedRecordSet(existing, rs))
error shouldBe an[InvalidRequest]
error.getMessage() shouldBe "Cannot update RecordSet's if user not a member of ownership group. User can only request for ownership transfer"
}
"return invalid request when given owner group ID does not match existing recordset owner group ID" in {
val existing = rsOk
val rs = rsOk.copy(ownerGroupId = Some(abcGroup.id))
val error = leftValue(unchangedRecordSet(existing, rs))
error shouldBe an[InvalidRequest]
error.getMessage() shouldBe "Cannot update RecordSet's if user not a member of ownership group. User can only request for ownership transfer"
}
"return invalid request when given ttl does not match existing recordset ttl" in {
val existing = rsOk
val rs = rsOk.copy(ttl = 3000)
val error = leftValue(unchangedRecordSet(existing, rs))
error shouldBe an[InvalidRequest]
error.getMessage() shouldBe "Cannot update RecordSet's if user not a member of ownership group. User can only request for ownership transfer"
}
}
"recordSetOwnerShipApproveStatus" should {
"return invalid request when given ownership transfer does not match OwnerShipTransferStatus as ManuallyRejected" in {
val rs = rsOk.copy(recordSetGroupChange = Some(ownerShipTransfer.copy(OwnerShipTransferStatus.ManuallyRejected)))
val error = leftValue(recordSetOwnerShipApproveStatus(rs))
error shouldBe an[InvalidRequest]
error.getMessage() shouldBe "Cannot update RecordSet OwnerShip Status when request is cancelled."
}
"return invalid request when given ownership transfer does not match OwnerShipTransferStatus as ManuallyApproved" in {
val rs = rsOk.copy(recordSetGroupChange = Some(ownerShipTransfer.copy(OwnerShipTransferStatus.ManuallyApproved)))
val error = leftValue(recordSetOwnerShipApproveStatus(rs))
error shouldBe an[InvalidRequest]
error.getMessage() shouldBe "Cannot update RecordSet OwnerShip Status when request is cancelled."
}
"return invalid request when given ownership transfer does not match OwnerShipTransferStatus as AutoApproved" in {
val rs = rsOk.copy(recordSetGroupChange = Some(ownerShipTransfer.copy(OwnerShipTransferStatus.AutoApproved)))
val error = leftValue(recordSetOwnerShipApproveStatus(rs))
error shouldBe an[InvalidRequest]
error.getMessage() shouldBe "Cannot update RecordSet OwnerShip Status when request is cancelled."
}
}
"unchangedRecordSetOwnershipStatus" should {
"return invalid request when given ownership transfer status does not match existing recordset ownership transfer status for non shared zones" in {
val existing = rsOk
val rs = rsOk.copy(recordSetGroupChange = Some(ownerShipTransfer.copy(OwnerShipTransferStatus.AutoApproved)))
val error = leftValue(unchangedRecordSetOwnershipStatus(existing, rs))
error shouldBe an[InvalidRequest]
error.getMessage() shouldBe "Cannot update RecordSet OwnerShip Status when zone is not shared."
}
}
} }
} }

View File

@ -20,9 +20,10 @@ import org.scalatest.wordspec.AnyWordSpec
import org.scalatest.BeforeAndAfterEach import org.scalatest.BeforeAndAfterEach
import org.scalatestplus.mockito.MockitoSugar import org.scalatestplus.mockito.MockitoSugar
import vinyldns.api.CatsHelpers import vinyldns.api.CatsHelpers
import javax.mail.{Provider, Session, Transport, URLName} import javax.mail.{Provider, Session, Transport, URLName}
import java.util.Properties import java.util.Properties
import vinyldns.core.domain.membership.{User, UserRepository} import vinyldns.core.domain.membership.{GroupRepository, User, UserRepository}
import vinyldns.core.notifier.Notification import vinyldns.core.notifier.Notification
import javax.mail.internet.InternetAddress import javax.mail.internet.InternetAddress
@ -30,18 +31,22 @@ import org.mockito.Matchers.{eq => eqArg, _}
import org.mockito.Mockito._ import org.mockito.Mockito._
import org.mockito.ArgumentCaptor import org.mockito.ArgumentCaptor
import cats.effect.IO import cats.effect.IO
import javax.mail.{Address, Message} import javax.mail.{Address, Message}
import _root_.vinyldns.core.domain.batch._ import _root_.vinyldns.core.domain.batch._
import java.time.Instant import java.time.Instant
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
import vinyldns.core.domain.record.RecordType import vinyldns.core.domain.record.{AData, OwnerShipTransferStatus, RecordSetChange, RecordSetChangeStatus, RecordSetChangeType, RecordType}
import vinyldns.core.domain.record.AData
import com.typesafe.config.Config import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigFactory
import vinyldns.core.domain.Encrypted import vinyldns.core.domain.Encrypted
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
import vinyldns.core.notifier.NotifierConfig import vinyldns.core.notifier.NotifierConfig
import vinyldns.core.TestMembershipData.{dummyGroup, dummyUser, okGroup, okUser}
import vinyldns.core.TestRecordSetData.{ownerShipTransfer, rsOk}
import vinyldns.core.TestZoneData.okZone
object MockTransport extends MockitoSugar { object MockTransport extends MockitoSugar {
val mockTransport: Transport = mock[Transport] val mockTransport: Transport = mock[Transport]
@ -68,6 +73,7 @@ class EmailNotifierSpec
import MockTransport._ import MockTransport._
val mockUserRepository: UserRepository = mock[UserRepository] val mockUserRepository: UserRepository = mock[UserRepository]
val mockGroupRepository: GroupRepository = mock[GroupRepository]
val session: Session = Session.getInstance(new Properties()) val session: Session = Session.getInstance(new Properties())
session.setProvider( session.setProvider(
new Provider( new Provider(
@ -100,6 +106,17 @@ class EmailNotifierSpec
"testBatch" "testBatch"
) )
def reccordSetChange: RecordSetChange =
RecordSetChange(
okZone,
rsOk.copy(ownerGroupId= Some(okGroup.id),recordSetGroupChange =
Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.PendingReview, requestedOwnerGroupId = Some(dummyGroup.id)))),
"system",
RecordSetChangeType.Create,
RecordSetChangeStatus.Complete
)
"Email Notifier" should { "Email Notifier" should {
"do nothing for unsupported Notifications" in { "do nothing for unsupported Notifications" in {
val emailConfig: Config = ConfigFactory.parseMap( val emailConfig: Config = ConfigFactory.parseMap(
@ -110,7 +127,7 @@ class EmailNotifierSpec
).asJava ).asJava
) )
val notifier = new EmailNotifierProvider() val notifier = new EmailNotifierProvider()
.load(NotifierConfig("", emailConfig), mockUserRepository) .load(NotifierConfig("", emailConfig), mockUserRepository, mockGroupRepository)
.unsafeRunSync() .unsafeRunSync()
notifier.notify(new Notification("this won't be supported ever")) should be(IO.unit) notifier.notify(new Notification("this won't be supported ever")) should be(IO.unit)
@ -120,7 +137,8 @@ class EmailNotifierSpec
val notifier = new EmailNotifier( val notifier = new EmailNotifier(
EmailNotifierConfig(new InternetAddress("test@test.com"), new Properties()), EmailNotifierConfig(new InternetAddress("test@test.com"), new Properties()),
session, session,
mockUserRepository mockUserRepository,
mockGroupRepository
) )
doReturn(IO.pure(Some(User("testUser", "access", Encrypted("secret"))))) doReturn(IO.pure(Some(User("testUser", "access", Encrypted("secret")))))
@ -132,11 +150,60 @@ class EmailNotifierSpec
verify(mockUserRepository).getUser("test") verify(mockUserRepository).getUser("test")
} }
"send an email to a user for recordSet ownership transfer" in {
val fromAddress = new InternetAddress("test@test.com")
val notifier = new EmailNotifier(
EmailNotifierConfig(fromAddress, new Properties()),
session,
mockUserRepository,
mockGroupRepository
)
val expectedAddresses = Array[Address](new InternetAddress("test@test.com"),new InternetAddress("test@test.com"))
val messageArgument = ArgumentCaptor.forClass(classOf[Message])
val dummyGrp = dummyGroup.copy(memberIds = Set(dummyUser.id))
val dummyUsr = dummyUser.copy(id=dummyUser.id,email = Some("test@test.com"))
doNothing().when(mockTransport).connect()
doNothing()
.when(mockTransport)
.sendMessage(messageArgument.capture(), eqArg(expectedAddresses))
doNothing().when(mockTransport).close()
doReturn(IO.pure(Some(okGroup)))
.when(mockGroupRepository)
.getGroup(okGroup.id)
doReturn(IO.pure(Some(dummyGrp)))
.when(mockGroupRepository)
.getGroup(dummyGrp.id)
doReturn(IO.pure(Some(okUser)))
.when(mockUserRepository)
.getUser(okUser.id)
doReturn(IO.pure(Some(dummyUsr)))
.when(mockUserRepository)
.getUser(dummyGrp.memberIds.head)
val rsc = reccordSetChange.copy(userId = okUser.id)
notifier.notify(Notification(rsc)).unsafeRunSync()
val message = messageArgument.getValue
message.getFrom should be(Array(fromAddress))
message.getContentType should be("text/html; charset=us-ascii")
message.getAllRecipients should be(expectedAddresses)
message.getSubject should be(s"VinylDNS RecordSet change ${rsc.id} results")
val content = message.getContent.asInstanceOf[String]
content.contains(rsc.id) should be(true)
content.contains(rsc.recordSet.ownerGroupId.get) should be(true)
content.contains(rsc.recordSet.recordSetGroupChange.map(_.requestedOwnerGroupId.get).get) should be(true)
}
"do nothing when user not found" in { "do nothing when user not found" in {
val notifier = new EmailNotifier( val notifier = new EmailNotifier(
EmailNotifierConfig(new InternetAddress("test@test.com"), new Properties()), EmailNotifierConfig(new InternetAddress("test@test.com"), new Properties()),
session, session,
mockUserRepository mockUserRepository,
mockGroupRepository
) )
doReturn(IO.pure(None)) doReturn(IO.pure(None))
@ -148,12 +215,13 @@ class EmailNotifierSpec
verify(mockUserRepository).getUser("test") verify(mockUserRepository).getUser("test")
} }
"send an email to a user" in { "send an email to a user for batch change" in {
val fromAddress = new InternetAddress("test@test.com") val fromAddress = new InternetAddress("test@test.com")
val notifier = new EmailNotifier( val notifier = new EmailNotifier(
EmailNotifierConfig(fromAddress, new Properties()), EmailNotifierConfig(fromAddress, new Properties()),
session, session,
mockUserRepository mockUserRepository,
mockGroupRepository
) )
doReturn( doReturn(

View File

@ -20,6 +20,7 @@ import org.scalatest.wordspec.AnyWordSpec
import org.scalatest.BeforeAndAfterEach import org.scalatest.BeforeAndAfterEach
import org.scalatestplus.mockito.MockitoSugar import org.scalatestplus.mockito.MockitoSugar
import vinyldns.api.CatsHelpers import vinyldns.api.CatsHelpers
import vinyldns.core.domain.membership.{GroupRepository, UserRepository}
import vinyldns.core.domain.membership.UserRepository import vinyldns.core.domain.membership.UserRepository
import vinyldns.core.notifier.Notification import vinyldns.core.notifier.Notification
import org.mockito.Matchers._ import org.mockito.Matchers._
@ -51,6 +52,7 @@ class SnsNotifierSpec
with CatsHelpers { with CatsHelpers {
val mockUserRepository = mock[UserRepository] val mockUserRepository = mock[UserRepository]
val mockGroupRepository = mock[GroupRepository]
val mockSns = mock[AmazonSNS] val mockSns = mock[AmazonSNS]
override protected def beforeEach(): Unit = override protected def beforeEach(): Unit =
@ -86,7 +88,7 @@ class SnsNotifierSpec
).asJava ).asJava
) )
val notifier = new SnsNotifierProvider() val notifier = new SnsNotifierProvider()
.load(NotifierConfig("", snsConfig), mockUserRepository) .load(NotifierConfig("", snsConfig), mockUserRepository, mockGroupRepository)
.unsafeRunSync() .unsafeRunSync()
notifier.notify(Notification("this won't be supported ever")) should be(IO.unit) notifier.notify(Notification("this won't be supported ever")) should be(IO.unit)

View File

@ -680,6 +680,206 @@ class VinylDNSJsonProtocolSpec
val thrown = the[MappingException] thrownBy recordSetJValue.extract[RecordSet] val thrown = the[MappingException] thrownBy recordSetJValue.extract[RecordSet]
thrown.msg should include("Digest Type 0 is not a supported DS record digest type") thrown.msg should include("Digest Type 0 is not a supported DS record digest type")
} }
"auto-approve a owner ship transfer request" in {
val recordSetJValue: JValue =
("zoneId" -> "1") ~~
("name" -> "TestRecordName") ~~
("type" -> "CNAME") ~~
("ttl" -> 1000) ~~
("status" -> "Pending") ~~
("records" -> List("cname" -> "cname.data ")) ~~
("recordSetGroupChange" -> Some("ownerShipTransferStatus" -> "AutoApproved"))
val expected = RecordSet(
"1",
"TestRecordName",
RecordType.CNAME,
1000,
RecordSetStatus.Pending,
LocalDateTime.of(2010, Month.JANUARY, 1, 0, 0).toInstant(ZoneOffset.UTC),
records = List(CNAMEData(Fqdn("cname.data."))),
recordSetGroupChange = Some(OwnerShipTransfer(ownerShipTransferStatus = OwnerShipTransferStatus.AutoApproved))
)
val actual = recordSetJValue.extract[RecordSet]
anonymize(actual) shouldBe anonymize(expected)
anonymize(actual).recordSetGroupChange.get.ownerShipTransferStatus shouldBe OwnerShipTransferStatus.AutoApproved
}
"manually-approve a owner ship transfer request" in {
val recordSetJValue: JValue =
("zoneId" -> "1") ~~
("name" -> "TestRecordName") ~~
("type" -> "CNAME") ~~
("ttl" -> 1000) ~~
("status" -> "Pending") ~~
("records" -> List("cname" -> "cname.data ")) ~~
("ownerGroupId" -> "updated-admin-group-id") ~~
("recordSetGroupChange" -> Some(("ownerShipTransferStatus" -> "ManuallyApproved")~~
("requestedOwnerGroupId" -> "updated-admin-group-id")))
val expected = RecordSet(
"1",
"TestRecordName",
RecordType.CNAME,
1000,
RecordSetStatus.Pending,
LocalDateTime.of(2010, Month.JANUARY, 1, 0, 0).toInstant(ZoneOffset.UTC),
records = List(CNAMEData(Fqdn("cname.data."))),
ownerGroupId = Some("updated-admin-group-id"),
recordSetGroupChange = Some(OwnerShipTransfer(ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyApproved,requestedOwnerGroupId = Some("updated-admin-group-id")))
)
val actual = recordSetJValue.extract[RecordSet]
anonymize(actual) shouldBe anonymize(expected)
anonymize(actual).recordSetGroupChange.get.ownerShipTransferStatus shouldBe OwnerShipTransferStatus.ManuallyApproved
anonymize(actual).recordSetGroupChange.get.requestedOwnerGroupId shouldBe Some("updated-admin-group-id")
anonymize(actual).ownerGroupId shouldBe Some("updated-admin-group-id")
}
"request a owner ship transfer" in {
val recordSetJValue: JValue =
("zoneId" -> "1") ~~
("name" -> "TestRecordName") ~~
("type" -> "CNAME") ~~
("ttl" -> 1000) ~~
("status" -> "Pending") ~~
("records" -> List("cname" -> "cname.data ")) ~~
("ownerGroupId" -> "updated-ok-group-id") ~~
("recordSetGroupChange" -> Some(("ownerShipTransferStatus" -> "Requested")~~
("requestedOwnerGroupId" -> "updated-admin-group-id")))
val expected = RecordSet(
"1",
"TestRecordName",
RecordType.CNAME,
1000,
RecordSetStatus.Pending,
LocalDateTime.of(2010, Month.JANUARY, 1, 0, 0).toInstant(ZoneOffset.UTC),
records = List(CNAMEData(Fqdn("cname.data."))),
ownerGroupId = Some("updated-ok-group-id"),
recordSetGroupChange = Some(OwnerShipTransfer(
ownerShipTransferStatus = OwnerShipTransferStatus.Requested,
requestedOwnerGroupId = Some("updated-admin-group-id")))
)
val actual = recordSetJValue.extract[RecordSet]
anonymize(actual) shouldBe anonymize(expected)
anonymize(actual).recordSetGroupChange.get.ownerShipTransferStatus shouldBe OwnerShipTransferStatus.Requested
anonymize(actual).recordSetGroupChange.get.requestedOwnerGroupId shouldBe Some("updated-admin-group-id")
anonymize(actual).ownerGroupId shouldBe Some("updated-ok-group-id")
}
"request a owner ship transfer which will change to pending review" in {
val recordSetJValue: JValue =
("zoneId" -> "1") ~~
("name" -> "TestRecordName") ~~
("type" -> "CNAME") ~~
("ttl" -> 1000) ~~
("status" -> "Pending") ~~
("records" -> List("cname" -> "cname.data ")) ~~
("ownerGroupId" -> "updated-ok-group-id") ~~
("recordSetGroupChange" -> Some(("ownerShipTransferStatus" -> "PendingReview")~~
("requestedOwnerGroupId" -> "updated-admin-group-id")))
val expected = RecordSet(
"1",
"TestRecordName",
RecordType.CNAME,
1000,
RecordSetStatus.Pending,
LocalDateTime.of(2010, Month.JANUARY, 1, 0, 0).toInstant(ZoneOffset.UTC),
records = List(CNAMEData(Fqdn("cname.data."))),
ownerGroupId = Some("updated-ok-group-id"),
recordSetGroupChange = Some(OwnerShipTransfer(
ownerShipTransferStatus = OwnerShipTransferStatus.PendingReview,
requestedOwnerGroupId = Some("updated-admin-group-id")))
)
val actual = recordSetJValue.extract[RecordSet]
anonymize(actual) shouldBe anonymize(expected)
anonymize(actual).recordSetGroupChange.get.ownerShipTransferStatus shouldBe OwnerShipTransferStatus.PendingReview
anonymize(actual).recordSetGroupChange.get.requestedOwnerGroupId shouldBe Some("updated-admin-group-id")
anonymize(actual).ownerGroupId shouldBe Some("updated-ok-group-id")
}
"cancel a owner ship transfer request" in {
val recordSetJValue: JValue =
("zoneId" -> "1") ~~
("name" -> "TestRecordName") ~~
("type" -> "CNAME") ~~
("ttl" -> 1000) ~~
("status" -> "Pending") ~~
("records" -> List("cname" -> "cname.data ")) ~~
("ownerGroupId" -> "updated-ok-group-id") ~~
("recordSetGroupChange" -> Some(("ownerShipTransferStatus" -> "Cancelled")~~
("requestedOwnerGroupId" -> "updated-admin-group-id")))
val expected = RecordSet(
"1",
"TestRecordName",
RecordType.CNAME,
1000,
RecordSetStatus.Pending,
LocalDateTime.of(2010, Month.JANUARY, 1, 0, 0).toInstant(ZoneOffset.UTC),
records = List(CNAMEData(Fqdn("cname.data."))),
ownerGroupId = Some("updated-ok-group-id"),
recordSetGroupChange = Some(OwnerShipTransfer(
ownerShipTransferStatus = OwnerShipTransferStatus.Cancelled,
requestedOwnerGroupId = Some("updated-admin-group-id")))
)
val actual = recordSetJValue.extract[RecordSet]
anonymize(actual) shouldBe anonymize(expected)
anonymize(actual).recordSetGroupChange.get.ownerShipTransferStatus shouldBe OwnerShipTransferStatus.Cancelled
anonymize(actual).recordSetGroupChange.get.requestedOwnerGroupId shouldBe Some("updated-admin-group-id")
anonymize(actual).ownerGroupId shouldBe Some("updated-ok-group-id")
}
"manually-reject a owner ship transfer request" in {
val recordSetJValue: JValue =
("zoneId" -> "1") ~~
("name" -> "TestRecordName") ~~
("type" -> "CNAME") ~~
("ttl" -> 1000) ~~
("status" -> "Pending") ~~
("records" -> List("cname" -> "cname.data ")) ~~
("ownerGroupId" -> "updated-ok-group-id") ~~
("recordSetGroupChange" -> Some(("ownerShipTransferStatus" -> "ManuallyRejected")~~
("requestedOwnerGroupId" -> "updated-admin-group-id")))
val expected = RecordSet(
"1",
"TestRecordName",
RecordType.CNAME,
1000,
RecordSetStatus.Pending,
LocalDateTime.of(2010, Month.JANUARY, 1, 0, 0).toInstant(ZoneOffset.UTC),
records = List(CNAMEData(Fqdn("cname.data."))),
ownerGroupId = Some("updated-ok-group-id"),
recordSetGroupChange = Some(OwnerShipTransfer(
ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyRejected,
requestedOwnerGroupId = Some("updated-admin-group-id")))
)
val actual = recordSetJValue.extract[RecordSet]
anonymize(actual) shouldBe anonymize(expected)
anonymize(actual).recordSetGroupChange.get.ownerShipTransferStatus shouldBe OwnerShipTransferStatus.ManuallyRejected
anonymize(actual).recordSetGroupChange.get.requestedOwnerGroupId shouldBe Some("updated-admin-group-id")
anonymize(actual).ownerGroupId shouldBe Some("updated-ok-group-id")
}
} }
} }

View File

@ -141,7 +141,13 @@ message RecordSet {
repeated RecordData record = 9; repeated RecordData record = 9;
required string account = 10; required string account = 10;
optional string ownerGroupId = 11; optional string ownerGroupId = 11;
optional string fqdn = 12; optional ownerShipTransfer recordSetGroupChange = 12;
optional string fqdn = 13;
}
message ownerShipTransfer {
optional string requestedOwnerGroupId = 1;
optional string ownerShipTransferStatus = 2;
} }
message RecordSetChange { message RecordSetChange {

View File

@ -16,6 +16,8 @@
package vinyldns.core.domain.record package vinyldns.core.domain.record
import vinyldns.core.domain.record.OwnerShipTransferStatus.OwnerShipTransferStatus
import java.util.UUID import java.util.UUID
import java.time.Instant import java.time.Instant
@ -33,6 +35,11 @@ object RecordSetStatus extends Enumeration {
val Active, Inactive, Pending, PendingUpdate, PendingDelete = Value val Active, Inactive, Pending, PendingUpdate, PendingDelete = Value
} }
object OwnerShipTransferStatus extends Enumeration {
type OwnerShipTransferStatus = Value
val AutoApproved, Cancelled, ManuallyApproved, ManuallyRejected, Requested, PendingReview, None = Value
}
import RecordSetStatus._ import RecordSetStatus._
import RecordType._ import RecordType._
@ -48,6 +55,7 @@ case class RecordSet(
id: String = UUID.randomUUID().toString, id: String = UUID.randomUUID().toString,
account: String = "system", account: String = "system",
ownerGroupId: Option[String] = None, ownerGroupId: Option[String] = None,
recordSetGroupChange: Option[OwnerShipTransfer] = None,
fqdn: Option[String] = None fqdn: Option[String] = None
) { ) {
@ -69,10 +77,25 @@ case class RecordSet(
sb.append("account=\"").append(account).append("\"; ") sb.append("account=\"").append(account).append("\"; ")
sb.append("status=\"").append(status.toString).append("\"; ") sb.append("status=\"").append(status.toString).append("\"; ")
sb.append("records=\"").append(records.toString).append("\"; ") sb.append("records=\"").append(records.toString).append("\"; ")
sb.append("ownerGroupId=\"").append(ownerGroupId).append("\"") sb.append("ownerGroupId=\"").append(ownerGroupId).append("\"; ")
sb.append("recordSetGroupChange=\"").append(recordSetGroupChange.toString).append("\"; ")
sb.append("fqdn=\"").append(fqdn).append("\"") sb.append("fqdn=\"").append(fqdn).append("\"")
sb.append("]") sb.append("]")
sb.toString sb.toString
} }
} }
case class OwnerShipTransfer(
ownerShipTransferStatus: OwnerShipTransferStatus,
requestedOwnerGroupId: Option[String] = None
){
override def toString: String = {
val sb = new StringBuilder
sb.append("OwnerShipTransfer: [")
sb.append("ownerShipTransferStatus=\"").append(ownerShipTransferStatus.toString).append("\"; ")
sb.append("requestedOwnerGroupId=\"").append(requestedOwnerGroupId.toString).append("\"")
sb.append("]")
sb.toString
}}

View File

@ -15,21 +15,21 @@
*/ */
package vinyldns.core.notifier package vinyldns.core.notifier
import vinyldns.core.domain.membership.UserRepository import vinyldns.core.domain.membership.{GroupRepository, UserRepository}
import cats.effect.IO import cats.effect.IO
import cats.implicits._ import cats.implicits._
import cats.effect.ContextShift import cats.effect.ContextShift
object NotifierLoader { object NotifierLoader {
def loadAll(configs: List[NotifierConfig], userRepository: UserRepository)( def loadAll(configs: List[NotifierConfig], userRepository: UserRepository, groupRepository: GroupRepository)(
implicit cs: ContextShift[IO] implicit cs: ContextShift[IO]
): IO[AllNotifiers] = ): IO[AllNotifiers] =
for { for {
notifiers <- configs.parTraverse(load(_, userRepository)) notifiers <- configs.parTraverse(load(_, userRepository, groupRepository))
} yield AllNotifiers(notifiers) } yield AllNotifiers(notifiers)
def load(config: NotifierConfig, userRepository: UserRepository): IO[Notifier] = def load(config: NotifierConfig, userRepository: UserRepository, groupRepository: GroupRepository): IO[Notifier] =
for { for {
provider <- IO( provider <- IO(
Class Class
@ -38,7 +38,7 @@ object NotifierLoader {
.newInstance() .newInstance()
.asInstanceOf[NotifierProvider] .asInstanceOf[NotifierProvider]
) )
notifier <- provider.load(config, userRepository) notifier <- provider.load(config, userRepository, groupRepository)
} yield notifier } yield notifier
} }

View File

@ -15,9 +15,9 @@
*/ */
package vinyldns.core.notifier package vinyldns.core.notifier
import vinyldns.core.domain.membership.UserRepository import vinyldns.core.domain.membership.{GroupRepository, UserRepository}
import cats.effect.IO import cats.effect.IO
trait NotifierProvider { trait NotifierProvider {
def load(config: NotifierConfig, userRepository: UserRepository): IO[Notifier] def load(config: NotifierConfig, userRepository: UserRepository, groupRepository: GroupRepository): IO[Notifier]
} }

View File

@ -100,9 +100,23 @@ trait ProtobufConversions {
records = records =
rs.getRecordList.asScala.map(rd => fromPB(rd, RecordType.withName(rs.getTyp))).toList, rs.getRecordList.asScala.map(rd => fromPB(rd, RecordType.withName(rs.getTyp))).toList,
account = rs.getAccount, account = rs.getAccount,
ownerGroupId = if (rs.hasOwnerGroupId) Some(rs.getOwnerGroupId) else None ownerGroupId = if (rs.hasOwnerGroupId) Some(rs.getOwnerGroupId) else None ,
recordSetGroupChange = if (rs.hasRecordSetGroupChange) Some(fromPB(rs.getRecordSetGroupChange)) else None,
) )
def fromPB(rsa: VinylDNSProto.ownerShipTransfer): OwnerShipTransfer =
record.OwnerShipTransfer(
ownerShipTransferStatus = OwnerShipTransferStatus.withName(rsa.getOwnerShipTransferStatus),
requestedOwnerGroupId = if (rsa.hasRequestedOwnerGroupId) Some(rsa.getRequestedOwnerGroupId) else None)
def toPB(rsa: OwnerShipTransfer): VinylDNSProto.ownerShipTransfer = {
val builder = VinylDNSProto.ownerShipTransfer
.newBuilder()
.setOwnerShipTransferStatus(rsa.ownerShipTransferStatus.toString)
rsa.requestedOwnerGroupId.foreach(id => builder.setRequestedOwnerGroupId(id))
builder.build()
}
def fromPB(zn: VinylDNSProto.Zone): Zone = { def fromPB(zn: VinylDNSProto.Zone): Zone = {
val pbStatus = zn.getStatus val pbStatus = zn.getStatus
val status = val status =
@ -377,6 +391,7 @@ trait ProtobufConversions {
rs.updated.foreach(dt => builder.setUpdated(dt.toEpochMilli)) rs.updated.foreach(dt => builder.setUpdated(dt.toEpochMilli))
rs.ownerGroupId.foreach(id => builder.setOwnerGroupId(id)) rs.ownerGroupId.foreach(id => builder.setOwnerGroupId(id))
rs.recordSetGroupChange.foreach(rsg => builder.setRecordSetGroupChange(toPB(rsg)))
// Map the records, first map to bytes, and then map the bytes to a record data instance // Map the records, first map to bytes, and then map the bytes to a record data instance
rs.records.map(toRecordData).foreach(rd => builder.addRecord(rd)) rs.records.map(toRecordData).foreach(rd => builder.addRecord(rd))

View File

@ -37,7 +37,8 @@ object TestRecordSetData {
RecordSetStatus.Active, RecordSetStatus.Active,
Instant.now.truncatedTo(ChronoUnit.MILLIS), Instant.now.truncatedTo(ChronoUnit.MILLIS),
None, None,
List(AData("10.1.1.1")) List(AData("10.1.1.1")),
recordSetGroupChange = None
) )
val abcRecord: RecordSet = RecordSet( val abcRecord: RecordSet = RecordSet(
@ -48,7 +49,8 @@ object TestRecordSetData {
RecordSetStatus.Pending, RecordSetStatus.Pending,
Instant.now.truncatedTo(ChronoUnit.MILLIS), Instant.now.truncatedTo(ChronoUnit.MILLIS),
None, None,
List(AAAAData("1:2:3:4:5:6:7:8")) List(AAAAData("1:2:3:4:5:6:7:8")),
recordSetGroupChange= None
) )
val aaaa: RecordSet = RecordSet( val aaaa: RecordSet = RecordSet(
@ -59,7 +61,8 @@ object TestRecordSetData {
RecordSetStatus.Pending, RecordSetStatus.Pending,
Instant.now.truncatedTo(ChronoUnit.MILLIS), Instant.now.truncatedTo(ChronoUnit.MILLIS),
None, None,
List(AAAAData("1:2:3:4:5:6:7:8")) List(AAAAData("1:2:3:4:5:6:7:8")),
recordSetGroupChange= None
) )
val aaaaOrigin: RecordSet = RecordSet( val aaaaOrigin: RecordSet = RecordSet(
@ -70,7 +73,8 @@ object TestRecordSetData {
RecordSetStatus.Pending, RecordSetStatus.Pending,
Instant.now.truncatedTo(ChronoUnit.MILLIS), Instant.now.truncatedTo(ChronoUnit.MILLIS),
None, None,
List(AAAAData("1:2:3:4:5:6:7:8")) List(AAAAData("1:2:3:4:5:6:7:8")),
recordSetGroupChange= None
) )
val cname: RecordSet = RecordSet( val cname: RecordSet = RecordSet(
@ -81,7 +85,8 @@ object TestRecordSetData {
RecordSetStatus.Pending, RecordSetStatus.Pending,
Instant.now.truncatedTo(ChronoUnit.MILLIS), Instant.now.truncatedTo(ChronoUnit.MILLIS),
None, None,
List(CNAMEData(Fqdn("cname"))) List(CNAMEData(Fqdn("cname"))),
recordSetGroupChange= None
) )
val ptrIp4: RecordSet = RecordSet( val ptrIp4: RecordSet = RecordSet(
@ -92,7 +97,8 @@ object TestRecordSetData {
RecordSetStatus.Active, RecordSetStatus.Active,
Instant.now.truncatedTo(ChronoUnit.MILLIS), Instant.now.truncatedTo(ChronoUnit.MILLIS),
None, None,
List(PTRData(Fqdn("ptr"))) List(PTRData(Fqdn("ptr"))),
recordSetGroupChange= None
) )
val ptrIp6: RecordSet = RecordSet( val ptrIp6: RecordSet = RecordSet(
@ -103,7 +109,8 @@ object TestRecordSetData {
RecordSetStatus.Active, RecordSetStatus.Active,
Instant.now.truncatedTo(ChronoUnit.MILLIS), Instant.now.truncatedTo(ChronoUnit.MILLIS),
None, None,
List(PTRData(Fqdn("ptr"))) List(PTRData(Fqdn("ptr"))),
recordSetGroupChange= None
) )
val srv: RecordSet = RecordSet( val srv: RecordSet = RecordSet(
@ -114,7 +121,8 @@ object TestRecordSetData {
RecordSetStatus.Active, RecordSetStatus.Active,
Instant.now.truncatedTo(ChronoUnit.MILLIS), Instant.now.truncatedTo(ChronoUnit.MILLIS),
None, None,
List(SRVData(1, 2, 3, Fqdn("target"))) List(SRVData(1, 2, 3, Fqdn("target"))),
recordSetGroupChange= None
) )
val naptr: RecordSet = RecordSet( val naptr: RecordSet = RecordSet(
@ -125,7 +133,8 @@ object TestRecordSetData {
RecordSetStatus.Active, RecordSetStatus.Active,
Instant.now.truncatedTo(ChronoUnit.MILLIS), Instant.now.truncatedTo(ChronoUnit.MILLIS),
None, None,
List(NAPTRData(1, 2, "S", "E2U+sip", "", Fqdn("target"))) List(NAPTRData(1, 2, "S", "E2U+sip", "", Fqdn("target"))),
recordSetGroupChange= None
) )
val mx: RecordSet = RecordSet( val mx: RecordSet = RecordSet(
@ -136,7 +145,8 @@ object TestRecordSetData {
RecordSetStatus.Pending, RecordSetStatus.Pending,
Instant.now.truncatedTo(ChronoUnit.MILLIS), Instant.now.truncatedTo(ChronoUnit.MILLIS),
None, None,
List(MXData(3, Fqdn("mx"))) List(MXData(3, Fqdn("mx"))),
recordSetGroupChange= None
) )
val ns: RecordSet = RecordSet( val ns: RecordSet = RecordSet(
@ -147,7 +157,8 @@ object TestRecordSetData {
RecordSetStatus.Active, RecordSetStatus.Active,
Instant.now.truncatedTo(ChronoUnit.MILLIS), Instant.now.truncatedTo(ChronoUnit.MILLIS),
None, None,
records = List(NSData(Fqdn("ns1.test.com")), NSData(Fqdn("ns2.test.com"))) records = List(NSData(Fqdn("ns1.test.com")), NSData(Fqdn("ns2.test.com"))),
recordSetGroupChange= None
) )
val txt: RecordSet = RecordSet( val txt: RecordSet = RecordSet(
@ -158,7 +169,8 @@ object TestRecordSetData {
RecordSetStatus.Pending, RecordSetStatus.Pending,
Instant.now.truncatedTo(ChronoUnit.MILLIS), Instant.now.truncatedTo(ChronoUnit.MILLIS),
None, None,
List(TXTData("txt")) List(TXTData("txt")),
recordSetGroupChange= None
) )
// example at https://tools.ietf.org/html/rfc4034#page-18 // example at https://tools.ietf.org/html/rfc4034#page-18
@ -198,7 +210,12 @@ object TestRecordSetData {
Instant.now.truncatedTo(ChronoUnit.MILLIS), Instant.now.truncatedTo(ChronoUnit.MILLIS),
None, None,
List(AAAAData("1:2:3:4:5:6:7:8")), List(AAAAData("1:2:3:4:5:6:7:8")),
ownerGroupId = Some(okGroup.id) ownerGroupId = Some(okGroup.id),
recordSetGroupChange= None
)
val ownerShipTransfer: OwnerShipTransfer = OwnerShipTransfer(
OwnerShipTransferStatus.None
) )
val sharedZoneRecordNoOwnerGroup: RecordSet = val sharedZoneRecordNoOwnerGroup: RecordSet =

View File

@ -19,7 +19,7 @@ package vinyldns.core.notifier
import cats.scalatest.{EitherMatchers, EitherValues, ValidatedMatchers} import cats.scalatest.{EitherMatchers, EitherValues, ValidatedMatchers}
import com.typesafe.config.{Config, ConfigFactory} import com.typesafe.config.{Config, ConfigFactory}
import org.scalatestplus.mockito.MockitoSugar import org.scalatestplus.mockito.MockitoSugar
import vinyldns.core.domain.membership.UserRepository import vinyldns.core.domain.membership.{GroupRepository, UserRepository}
import cats.effect.IO import cats.effect.IO
import org.mockito.Mockito._ import org.mockito.Mockito._
@ -37,13 +37,13 @@ object MockNotifierProvider extends MockitoSugar {
class MockNotifierProvider extends NotifierProvider { class MockNotifierProvider extends NotifierProvider {
def load(config: NotifierConfig, userRepo: UserRepository): IO[Notifier] = def load(config: NotifierConfig, userRepo: UserRepository, groupRepo: GroupRepository): IO[Notifier] =
IO.pure(MockNotifierProvider.mockNotifier) IO.pure(MockNotifierProvider.mockNotifier)
} }
class FailingProvider extends NotifierProvider { class FailingProvider extends NotifierProvider {
def load(config: NotifierConfig, userRepo: UserRepository): IO[Notifier] = def load(config: NotifierConfig, userRepo: UserRepository, groupRepo: GroupRepository): IO[Notifier] =
IO.raiseError(new IllegalStateException("always failing")) IO.raiseError(new IllegalStateException("always failing"))
} }
@ -63,6 +63,7 @@ class NotifierLoaderSpec
val goodConfig = NotifierConfig("vinyldns.core.notifier.MockNotifierProvider", placeholderConfig) val goodConfig = NotifierConfig("vinyldns.core.notifier.MockNotifierProvider", placeholderConfig)
val mockUserRepository: UserRepository = mock[UserRepository] val mockUserRepository: UserRepository = mock[UserRepository]
val mockGroupRepository: GroupRepository = mock[GroupRepository]
import MockNotifierProvider._ import MockNotifierProvider._
@ -73,14 +74,13 @@ class NotifierLoaderSpec
"return some notifier with no configs" in { "return some notifier with no configs" in {
val notifier = NotifierLoader.loadAll(List.empty, mockUserRepository).unsafeRunSync() val notifier = NotifierLoader.loadAll(List.empty, mockUserRepository, mockGroupRepository).unsafeRunSync()
notifier shouldBe a[AllNotifiers] notifier shouldBe a[AllNotifiers]
notifier.notify(Notification(3)).unsafeRunSync() shouldBe (()) notifier.notify(Notification(3)).unsafeRunSync() shouldBe (())
} }
"return a notifier for valid config of one notifier" in { "return a notifier for valid config of one notifier" in {
val notifier = NotifierLoader.loadAll(List(goodConfig), mockUserRepository).unsafeRunSync() val notifier = NotifierLoader.loadAll(List(goodConfig), mockUserRepository, mockGroupRepository).unsafeRunSync()
notifier shouldNot be(null) notifier shouldNot be(null)
@ -95,7 +95,7 @@ class NotifierLoaderSpec
"return a notifier for valid config of multiple notifiers" in { "return a notifier for valid config of multiple notifiers" in {
val notifier = val notifier =
NotifierLoader.loadAll(List(goodConfig, goodConfig), mockUserRepository).unsafeRunSync() NotifierLoader.loadAll(List(goodConfig, goodConfig), mockUserRepository, mockGroupRepository).unsafeRunSync()
notifier shouldNot be(null) notifier shouldNot be(null)
@ -113,7 +113,7 @@ class NotifierLoaderSpec
val badProvider = val badProvider =
NotifierConfig("vinyldns.core.notifier.NotFoundNotifierProvider", placeholderConfig) NotifierConfig("vinyldns.core.notifier.NotFoundNotifierProvider", placeholderConfig)
val load = NotifierLoader.loadAll(List(goodConfig, badProvider), mockUserRepository) val load = NotifierLoader.loadAll(List(goodConfig, badProvider), mockUserRepository, mockGroupRepository)
a[ClassNotFoundException] shouldBe thrownBy(load.unsafeRunSync()) a[ClassNotFoundException] shouldBe thrownBy(load.unsafeRunSync())
} }
@ -123,7 +123,7 @@ class NotifierLoaderSpec
val exceptionProvider = val exceptionProvider =
NotifierConfig("vinyldns.core.notifier.FailingProvider", placeholderConfig) NotifierConfig("vinyldns.core.notifier.FailingProvider", placeholderConfig)
val load = NotifierLoader.loadAll(List(goodConfig, exceptionProvider), mockUserRepository) val load = NotifierLoader.loadAll(List(goodConfig, exceptionProvider), mockUserRepository, mockGroupRepository)
a[IllegalStateException] shouldBe thrownBy(load.unsafeRunSync()) a[IllegalStateException] shouldBe thrownBy(load.unsafeRunSync())
} }

View File

@ -133,6 +133,7 @@
<th class="col-md-4">Record Data</th> <th class="col-md-4">Record Data</th>
@if(meta.sharedDisplayEnabled) { @if(meta.sharedDisplayEnabled) {
<th ng-if="zoneInfo.shared">Owner Group Name</th> <th ng-if="zoneInfo.shared">Owner Group Name</th>
<th ng-if="zoneInfo.shared">OwnerShip Transfer Status</th>
} }
<th>Actions</th> <th>Actions</th>
</tr> </tr>
@ -148,8 +149,8 @@
{{record.name}} {{record.name}}
</div> </div>
</td> </td>
<td class="wrap-long-text">{{record.type}}</td> <td>{{record.type}}</td>
<td class="wrap-long-text">{{record.ttl}}</td> <td>{{record.ttl}}</td>
<td class="record-data"> <td class="record-data">
<span ng-if="record.accessLevel != 'NoAccess'"> <span ng-if="record.accessLevel != 'NoAccess'">
<span ng-if="record.type == 'A'"> <span ng-if="record.type == 'A'">
@ -336,19 +337,42 @@
@if(meta.sharedDisplayEnabled) { @if(meta.sharedDisplayEnabled) {
<td ng-if="zoneInfo.shared"> <td ng-if="zoneInfo.shared">
<a ng-if="record.ownerGroupName && canAccessGroup(record.ownerGroupId)" ng-bind="record.ownerGroupName" href="/groups/{{record.ownerGroupId}}"></a> <a ng-if="record.ownerGroupName && canAccessGroup(record.ownerGroupId)" ng-bind="record.ownerGroupName" href="/groups/{{record.ownerGroupId}}"></a>
<span ng-if="record.ownerGroupName && !canAccessGroup(record.ownerGroupId)" ng-bind="record.ownerGroupName"></span> <span ng-if="record.ownerGroupName && !canAccessGroup(record.ownerGroupId)" ng-bind="record.ownerGroupName" ></span> <span ng-if="!record.ownerGroupId">Unowned</span>
<span ng-if="!record.ownerGroupId">Unowned</span>
<span ng-if="record.ownerGroupId && !record.ownerGroupName" class="text-danger" data-toggle="tooltip" data-placement="top" <span ng-if="record.ownerGroupId && !record.ownerGroupName" class="text-danger" data-toggle="tooltip" data-placement="top"
title="Group with ID {{record.ownerGroupId}} no longer exists."><span class="fa fa-warning"></span> Group deleted</span> title="Group with ID {{record.ownerGroupId}} no longer exists."><span class="fa fa-warning"></span> Group deleted</span>
</td> </td>
<td ng-if="zoneInfo.shared">
<span class="label label-success"
ng-if= "record.recordSetGroupChange.ownerShipTransferStatus == 'ManuallyApproved' ||
record.recordSetGroupChange.ownerShipTransferStatus == 'AutoApproved'">Approved</span>
<span class="label label-danger"
ng-if= "record.recordSetGroupChange.ownerShipTransferStatus == 'ManuallyRejected'">Rejected</span>
<span ng-if= "record.recordSetGroupChange.ownerShipTransferStatus == 'None'"></span>
<span class="label label-default"
ng-if= "record.recordSetGroupChange.ownerShipTransferStatus == 'Cancelled'">Cancelled</span>
<span class="label label-info"
ng-if= "record.recordSetGroupChange.ownerShipTransferStatus == 'PendingReview'">Pending Action</span>
</td>
} }
<td> <td>
<span ng-if="(record.canBeEdited && record.accessLevel != 'NoAccess' && record.accessLevel != 'Read')"> <span ng-if="(record.canBeEdited && record.accessLevel != 'NoAccess' && record.accessLevel != 'Read')">
<div class="table-form-group"> <div class="table-form-group">
<span><button class="btn btn-info btn-sm" ng-click="editRecord(record)">Update</button></span> <span><button class="btn btn-info btn-sm" ng-click="editRecord(record)">Update</button></span>
<span ng-if="record.accessLevel == 'Delete'"><button id="delete-record-{{record.name}}-button" class="btn btn-danger btn-sm btn-rounded" ng-click="deleteRecord(record)">Delete</button></span> <span ng-if="record.accessLevel == 'Delete'"><button id="delete-record-{{record.name}}-button" class="btn btn-danger btn-sm btn-rounded" ng-click="deleteRecord(record)">Delete</button></span>
<span ng-if="zoneInfo.shared && record.ownerGroupId == undefined && record.canBeEdited ">
<button class="btn btn-info btn-sm" ng-click="requestOwnerShip(record)">Request</button>
</span>
</div> </div>
</span> </span>
<span ng-if="zoneInfo.shared && record.ownerGroupId != undefined && record.canBeEdited"
ng-switch="record.isCurrentRecordSetOwner">
<span ng-switch-when="false">
<button class="btn btn-info btn-sm" ng-click="requestOwnerShipTransfer(record, true)">Request</button>
</span>
<span ng-switch-when="true" ng-if= record.recordSetGroupChange.ownerShipTransferStatus=="PendingReview">
<button class="btn btn-info btn-sm" ng-click="requestOwnerShipTransfer(record, false)">Close Request</button>
</span>
</span>
</td> </td>
</tr> </tr>
</tbody> </tbody>

View File

@ -31,6 +31,7 @@ angular.module('controller.records', [])
$scope.alerts = []; $scope.alerts = [];
$scope.recordTypes = ['A', 'AAAA', 'CNAME', 'DS', 'MX', 'NS', 'PTR', 'SRV', 'NAPTR', 'SSHFP', 'TXT']; $scope.recordTypes = ['A', 'AAAA', 'CNAME', 'DS', 'MX', 'NS', 'PTR', 'SRV', 'NAPTR', 'SSHFP', 'TXT'];
$scope.ownerShipTransferStatus = ['AutoApproved', 'Cancelled', 'ManuallyApproved', 'ManuallyRejected', 'Requested', 'PendingReview'];
$scope.readRecordTypes = ['A', 'AAAA', 'CNAME', 'DS', 'MX', 'NS', 'PTR', "SOA", 'SRV', 'NAPTR', 'SSHFP', 'TXT']; $scope.readRecordTypes = ['A', 'AAAA', 'CNAME', 'DS', 'MX', 'NS', 'PTR', "SOA", 'SRV', 'NAPTR', 'SSHFP', 'TXT'];
$scope.selectedRecordTypes = []; $scope.selectedRecordTypes = [];
$scope.naptrFlags = ["U", "S", "A", "P"]; $scope.naptrFlags = ["U", "S", "A", "P"];
@ -46,6 +47,7 @@ angular.module('controller.records', [])
{name: '(254) PRIVATEOID', number: 254}] {name: '(254) PRIVATEOID', number: 254}]
$scope.dsDigestTypes = [{name: '(1) SHA1', number: 1}, {name: '(2) SHA256', number: 2}, {name: '(3) GOSTR341194', number: 3}, {name: '(4) SHA384', number: 4}] $scope.dsDigestTypes = [{name: '(1) SHA1', number: 1}, {name: '(2) SHA256', number: 2}, {name: '(3) GOSTR341194', number: 3}, {name: '(4) SHA384', number: 4}]
$scope.records = {}; $scope.records = {};
$scope.isOwnerShipRequest = true;
$scope.recordsetChangesPreview = {}; $scope.recordsetChangesPreview = {};
$scope.recordsetChanges = {}; $scope.recordsetChanges = {};
$scope.currentRecord = {}; $scope.currentRecord = {};
@ -57,6 +59,12 @@ angular.module('controller.records', [])
var loadZonesPromise; var loadZonesPromise;
var loadRecordsPromise; var loadRecordsPromise;
$scope.ownerShipTransferApproverStatus = [{value: 'ManuallyApproved' , label: 'Approve'},
{value: 'ManuallyRejected', label: 'Reject'}];
$scope.ownerShipTransferRequestorStatus = [{value: 'Requested', label: 'Request'},
{value: 'Cancelled', label: 'Cancel'}];
$scope.recordModalState = { $scope.recordModalState = {
CREATE: 0, CREATE: 0,
UPDATE: 1, UPDATE: 1,
@ -83,6 +91,7 @@ angular.module('controller.records', [])
$scope.isZoneAdmin = false; $scope.isZoneAdmin = false;
$scope.canReadZone = false; $scope.canReadZone = false;
$scope.canCreateRecords = false; $scope.canCreateRecords = false;
$scope.isCurrentRecordSetOwner = false;
// paging status for recordsets // paging status for recordsets
var recordsPaging = pagingService.getNewPagingParams(100); var recordsPaging = pagingService.getNewPagingParams(100);
@ -94,6 +103,43 @@ angular.module('controller.records', [])
* Modal control functions * Modal control functions
*/ */
$scope.recordSetGroupOwnerShipStatus = function recordSetGroupOwnerShipStatus(groupId, profileId, record) {
function success(response) {
var ownerShipTransferStatus;
if (response.data.members.some(x => x.id === profileId)){
ownerShipTransferStatus = $scope.ownerShipTransferApproverStatus;
$scope.currentOwnerShipTransferApprover= true;
record.isCurrentRecordSetOwner = true;}
else{ownerShipTransferStatus = $scope.ownerShipTransferRequestorStatus;
$scope.currentOwnerShipTransferApprover= false;
record.isCurrentRecordSetOwner= false;}
$scope.ownerShipTransferStatus = ownerShipTransferStatus
}
return groupsService
.getGroupMemberList(groupId)
.then(success)
.catch(function (error) {
handleError(error, 'groupsService::getGroupMemberList-failure');
});
};
function getGroup(groupId) {
if (groupId != undefined && groupId != "null"){
$log.log('groupsService::getGroup-success');
function success(response) {
$scope.recordSetRequestedOwnerShipName = response.data.name;
}
return groupsService
.getGroup(groupId)
.then(success)
.catch(function (error) {
handleError(error, 'groupsService::getGroup-failure');
});
}
else {$scope.recordSetRequestedOwnerShipName = "None";}
};
$scope.deleteRecord = function(record) { $scope.deleteRecord = function(record) {
$scope.currentRecord = angular.copy(record); $scope.currentRecord = angular.copy(record);
$scope.recordModal = { $scope.recordModal = {
@ -132,6 +178,8 @@ angular.module('controller.records', [])
$scope.editRecord = function(record) { $scope.editRecord = function(record) {
$scope.currentRecord = angular.copy(record); $scope.currentRecord = angular.copy(record);
$scope.currentRecord.recordSetGroupChange = angular.copy(record.recordSetGroupChange);
getGroup($scope.currentRecord.recordSetGroupChange.requestedOwnerGroupId);
$scope.recordModal = { $scope.recordModal = {
previous: angular.copy(record), previous: angular.copy(record),
action: $scope.recordModalState.UPDATE, action: $scope.recordModalState.UPDATE,
@ -145,6 +193,81 @@ angular.module('controller.records', [])
$("#record_modal").modal("show"); $("#record_modal").modal("show");
}; };
$scope.requestOwnerShip = function(record) {
$scope.currentRecord = angular.copy(record);
$scope.currentRecord.recordSetGroupChange.ownerShipTransferStatus = angular.copy("AutoApproved")
$scope.recordModal = {
action: $scope.recordModalState.UPDATE,
title: "Request OwnerShip transfer",
basics: $scope.recordModalParams.readOnly,
details: $scope.recordModalParams.editable,
sharedZone: $scope.zoneInfo.shared,
sharedDisplayEnabled: $scope.sharedDisplayEnabled,
isCurrentRecordOwnerGroup : false
};
$scope.recordOwnerShipForm.$setPristine();
$("#record_modal_ownership").modal("show");
};
$scope.requestOwnerShipTransfer = function(record, isOwnerShipRequest) {
$scope.currentRecord = angular.copy(record);
$scope.currentRecord.recordSetGroupChange = angular.copy(record.recordSetGroupChange);
$scope.recordModal = {
previous: angular.copy(record),
action: $scope.recordModalState.UPDATE,
title: "Request OwnerShip transfer",
basics: $scope.recordModalParams.readOnly,
details: $scope.recordModalParams.editable,
sharedZone: $scope.zoneInfo.shared,
sharedDisplayEnabled: $scope.sharedDisplayEnabled,
isCurrentRecordOwnerGroup : false
};
var currentRecordOwnerGroupId = $scope.currentRecord.ownerGroupId;
if (isOwnerShipRequest && $scope.currentRecord.recordSetGroupChange.ownerShipTransferStatus != "PendingReview") {
$scope.currentRecord.recordSetGroupChange.requestedOwnerGroupId = angular.copy(null);
$scope.currentRecord.recordSetGroupChange.ownerShipTransferStatus = angular.copy(null);
}else ($scope.currentRecord.recordSetGroupChange.ownerShipTransferStatus = angular.copy(null))
getGroup($scope.currentRecord.recordSetGroupChange.requestedOwnerGroupId);
$scope.ownerShipTransferApprover = false;
$scope.ownerShipTransferRequestor = false;
if (currentRecordOwnerGroupId != undefined){$scope.recordModal.isCurrentRecordOwnerGroup = true
}else{ $scope.recordModal.isCurrentRecordOwnerGroup = false }
if ($scope.zoneInfo.shared == true && $scope.recordModal.isCurrentRecordOwnerGroup){
$scope.recordSetGroupOwnerShipStatus(currentRecordOwnerGroupId, $scope.profile.id, record);
$scope.ownerShipTransferApproverStatus.forEach(function(ownerShipTransferApproverStatus, index) {
if (ownerShipTransferApproverStatus.value.indexOf($scope.currentRecord.recordSetGroupChange.ownerShipTransferStatus) > -1)
{$scope.ownerShipTransferApprover = true}else{$scope.ownerShipTransferRequestor = true}})
}
$scope.recordOwnerShipForm.$setPristine();
$("#record_modal_ownership_transfer").modal("show");
};
$scope.requestedOwnerShip = function() {
var record = angular.copy($scope.currentRecord);
record['onlyFour'] = true;
if ($scope.recordOwnerShipForm.$valid) {
updateRecordSet(record);
$scope.recordOwnerShipForm.$setPristine();
$("#record_modal_ownership").modal('hide');
}
};
$scope.submitRequestedOwnerShipTransfer = function () {
var record = angular.copy($scope.currentRecord);
record['onlyFour'] = true;
var invalidRecordOwnerShipForm = $scope.recordOwnerShipForm.ownerGroupStatus.$viewValue != null &&
$scope.recordOwnerShipForm.ownerGroupStatus.$viewValue
if ($scope.recordOwnerShipForm.$valid && invalidRecordOwnerShipForm) {
updateRecordSet(record);
$scope.recordOwnerShipForm.$setPristine();
$("#record_modal_ownership_transfer").modal('hide');
}
};
$scope.confirmUpdate = function() { $scope.confirmUpdate = function() {
$scope.recordModal.action = $scope.recordModalState.CONFIRM_UPDATE; $scope.recordModal.action = $scope.recordModalState.CONFIRM_UPDATE;
$scope.recordModal.details = $scope.recordModalParams.readOnly; $scope.recordModal.details = $scope.recordModalParams.readOnly;
@ -190,11 +313,15 @@ angular.module('controller.records', [])
$scope.submitUpdateRecord = function () { $scope.submitUpdateRecord = function () {
var record = angular.copy($scope.currentRecord); var record = angular.copy($scope.currentRecord);
if(record.recordSetGroupChange.requestedOwnerGroupId != undefined){
if (record.ownerGroupId != $scope.recordModal.previous.ownerGroupId && $scope.isZoneAdmin){
record.recordSetGroupChange.requestedOwnerGroupId = angular.copy(record.ownerGroupId);
record.recordSetGroupChange.ownerShipTransferStatus = angular.copy("ManuallyApproved");
}
}
record['onlyFour'] = true; record['onlyFour'] = true;
if ($scope.addRecordForm.$valid) { if ($scope.addRecordForm.$valid) {
updateRecordSet(record); updateRecordSet(record);
$scope.addRecordForm.$setPristine(); $scope.addRecordForm.$setPristine();
$("#record_modal").modal('hide'); $("#record_modal").modal('hide');
} }
@ -496,6 +623,11 @@ angular.module('controller.records', [])
angular.forEach(records, function(record) { angular.forEach(records, function(record) {
newRecords.push(recordsService.toDisplayRecord(record, $scope.zoneInfo.name)); newRecords.push(recordsService.toDisplayRecord(record, $scope.zoneInfo.name));
}); });
angular.forEach(newRecords, function(record) {
if(record.ownerGroupId != undefined) {
$scope.recordSetGroupOwnerShipStatus(record.ownerGroupId, $scope.profile.id, record);
}else {record.isCurrentRecordSetOwner= null;}
});
$scope.records = newRecords; $scope.records = newRecords;
$scope.getRecordSetCount(); $scope.getRecordSetCount();
if($scope.records.length > 0) { if($scope.records.length > 0) {

View File

@ -280,7 +280,10 @@ angular.module('service.records', [])
"id": record.id, "id": record.id,
"name": record.name, "name": record.name,
"type": record.type, "type": record.type,
"ttl": Number(record.ttl) "ttl": Number(record.ttl),
"isCurrentRecordSetOwner": record.isCurrentRecordSetOwner,
"recordSetGroupChange": record.recordSetGroupChange
}; };
switch (record.type) { switch (record.type) {
case 'A': case 'A':

View File

@ -111,7 +111,9 @@ describe('Service: recordsService', function () {
"type": 'SSHFP', "type": 'SSHFP',
"ttl": '300', "ttl": '300',
"sshfpItems": [{algorithm: '1', type: '1', fingerprint: '123456789ABCDEF67890123456789ABCDEF67890'}, "sshfpItems": [{algorithm: '1', type: '1', fingerprint: '123456789ABCDEF67890123456789ABCDEF67890'},
{algorithm: '2', type: '1', fingerprint: '123456789ABCDEF67890123456789ABCDEF67890'}] {algorithm: '2', type: '1', fingerprint: '123456789ABCDEF67890123456789ABCDEF67890'}],
"recordSetGroupChange": 'None',
"isCurrentRecordSetOwner": 'null'
}; };
expectedRecord = { expectedRecord = {
"id": 'recordId', "id": 'recordId',
@ -119,7 +121,9 @@ describe('Service: recordsService', function () {
"type": 'SSHFP', "type": 'SSHFP',
"ttl": 300, "ttl": 300,
"records": [{algorithm: 1, type: 1, fingerprint: '123456789ABCDEF67890123456789ABCDEF67890'}, "records": [{algorithm: 1, type: 1, fingerprint: '123456789ABCDEF67890123456789ABCDEF67890'},
{algorithm: 2, type: 1, fingerprint: '123456789ABCDEF67890123456789ABCDEF67890'}] {algorithm: 2, type: 1, fingerprint: '123456789ABCDEF67890123456789ABCDEF67890'}],
"recordSetGroupChange": 'None',
"isCurrentRecordSetOwner": 'null'
}; };
var actualRecord = this.recordsService.toVinylRecord(sentRecord); var actualRecord = this.recordsService.toVinylRecord(sentRecord);
@ -133,7 +137,8 @@ describe('Service: recordsService', function () {
"type": 'SSHFP', "type": 'SSHFP',
"ttl": 300, "ttl": 300,
"records": [{algorithm: 1, type: 1, fingerprint: '123456789ABCDEF67890123456789ABCDEF67890'}, "records": [{algorithm: 1, type: 1, fingerprint: '123456789ABCDEF67890123456789ABCDEF67890'},
{algorithm: 2, type: 1, fingerprint: 'F23456789ABCDEF67890123456789ABCDEF67890'}] {algorithm: 2, type: 1, fingerprint: 'F23456789ABCDEF67890123456789ABCDEF67890'}],
"recordSetGroupChange": 'None'
}; };
displayRecord = { displayRecord = {
@ -144,6 +149,7 @@ describe('Service: recordsService', function () {
"records": undefined, "records": undefined,
"sshfpItems": [{algorithm: 1, type: 1, fingerprint: '123456789ABCDEF67890123456789ABCDEF67890'}, "sshfpItems": [{algorithm: 1, type: 1, fingerprint: '123456789ABCDEF67890123456789ABCDEF67890'},
{algorithm: 2, type: 1, fingerprint: 'F23456789ABCDEF67890123456789ABCDEF67890'}], {algorithm: 2, type: 1, fingerprint: 'F23456789ABCDEF67890123456789ABCDEF67890'}],
"recordSetGroupChange": 'None',
"onlyFour": true, "onlyFour": true,
"isDotted": false, "isDotted": false,
"canBeEdited": true "canBeEdited": true
@ -160,7 +166,8 @@ describe('Service: recordsService', function () {
"type": 'SSHFP', "type": 'SSHFP',
"ttl": 300, "ttl": 300,
"records": [{algorithm: 1, type: 1, fingerprint: '123456789ABCDEF67890123456789ABCDEF67890'}, "records": [{algorithm: 1, type: 1, fingerprint: '123456789ABCDEF67890123456789ABCDEF67890'},
{algorithm: 2, type: 1, fingerprint: 'F23456789ABCDEF67890123456789ABCDEF67890'}] {algorithm: 2, type: 1, fingerprint: 'F23456789ABCDEF67890123456789ABCDEF67890'}],
"recordSetGroupChange": 'None'
}; };
displayRecord = { displayRecord = {
@ -171,6 +178,7 @@ describe('Service: recordsService', function () {
"records": undefined, "records": undefined,
"sshfpItems": [{algorithm: 1, type: 1, fingerprint: '123456789ABCDEF67890123456789ABCDEF67890'}, "sshfpItems": [{algorithm: 1, type: 1, fingerprint: '123456789ABCDEF67890123456789ABCDEF67890'},
{algorithm: 2, type: 1, fingerprint: 'F23456789ABCDEF67890123456789ABCDEF67890'}], {algorithm: 2, type: 1, fingerprint: 'F23456789ABCDEF67890123456789ABCDEF67890'}],
"recordSetGroupChange": 'None',
"onlyFour": true, "onlyFour": true,
"isDotted": true, "isDotted": true,
"canBeEdited": true "canBeEdited": true
@ -187,7 +195,8 @@ describe('Service: recordsService', function () {
"type": 'SSHFP', "type": 'SSHFP',
"ttl": 300, "ttl": 300,
"records": [{algorithm: 1, type: 1, fingerprint: '123456789ABCDEF67890123456789ABCDEF67890'}, "records": [{algorithm: 1, type: 1, fingerprint: '123456789ABCDEF67890123456789ABCDEF67890'},
{algorithm: 2, type: 1, fingerprint: 'F23456789ABCDEF67890123456789ABCDEF67890'}] {algorithm: 2, type: 1, fingerprint: 'F23456789ABCDEF67890123456789ABCDEF67890'}],
"recordSetGroupChange": 'None'
}; };
displayRecord = { displayRecord = {
@ -198,6 +207,7 @@ describe('Service: recordsService', function () {
"records": undefined, "records": undefined,
"sshfpItems": [{algorithm: 1, type: 1, fingerprint: '123456789ABCDEF67890123456789ABCDEF67890'}, "sshfpItems": [{algorithm: 1, type: 1, fingerprint: '123456789ABCDEF67890123456789ABCDEF67890'},
{algorithm: 2, type: 1, fingerprint: 'F23456789ABCDEF67890123456789ABCDEF67890'}], {algorithm: 2, type: 1, fingerprint: 'F23456789ABCDEF67890123456789ABCDEF67890'}],
"recordSetGroupChange": 'None',
"onlyFour": true, "onlyFour": true,
"isDotted": false, "isDotted": false,
"canBeEdited": true "canBeEdited": true
@ -213,7 +223,8 @@ describe('Service: recordsService', function () {
"name": 'apex.with.dot.', "name": 'apex.with.dot.',
"type": 'NS', "type": 'NS',
"ttl": 300, "ttl": 300,
"records": [{nsdname: "ns1.com."}, {nsdname: "ns2.com."}] "records": [{nsdname: "ns1.com."}, {nsdname: "ns2.com."}],
"recordSetGroupChange": 'None'
}; };
displayRecord = { displayRecord = {
@ -223,6 +234,7 @@ describe('Service: recordsService', function () {
"ttl": 300, "ttl": 300,
"records": undefined, "records": undefined,
"nsRecordData": ["ns1.com.", "ns2.com."], "nsRecordData": ["ns1.com.", "ns2.com."],
"recordSetGroupChange": 'None',
"onlyFour": true, "onlyFour": true,
"isDotted": false, "isDotted": false,
"canBeEdited": false "canBeEdited": false
@ -239,7 +251,8 @@ describe('Service: recordsService', function () {
"type": 'SSHFP', "type": 'SSHFP',
"ttl": 300, "ttl": 300,
"records": [{algorithm: 1, type: 1, fingerprint: '123456789ABCDEF67890123456789ABCDEF67890'}, "records": [{algorithm: 1, type: 1, fingerprint: '123456789ABCDEF67890123456789ABCDEF67890'},
{algorithm: 2, type: 1, fingerprint: 'F23456789ABCDEF67890123456789ABCDEF67890'}] {algorithm: 2, type: 1, fingerprint: 'F23456789ABCDEF67890123456789ABCDEF67890'}],
"recordSetGroupChange": 'None'
}; };
displayRecord = { displayRecord = {
@ -250,6 +263,7 @@ describe('Service: recordsService', function () {
"records": undefined, "records": undefined,
"sshfpItems": [{algorithm: 1, type: 1, fingerprint: '123456789ABCDEF67890123456789ABCDEF67890'}, "sshfpItems": [{algorithm: 1, type: 1, fingerprint: '123456789ABCDEF67890123456789ABCDEF67890'},
{algorithm: 2, type: 1, fingerprint: 'F23456789ABCDEF67890123456789ABCDEF67890'}], {algorithm: 2, type: 1, fingerprint: 'F23456789ABCDEF67890123456789ABCDEF67890'}],
"recordSetGroupChange": 'None',
"onlyFour": true, "onlyFour": true,
"isDotted": false, "isDotted": false,
"canBeEdited": true "canBeEdited": true

View File

@ -1,3 +1,5 @@
<div>
<form name="addRecordForm" role="form" class="form-horizontal" novalidate> <form name="addRecordForm" role="form" class="form-horizontal" novalidate>
<modal modal-id="record_modal" modal-title="{{ recordModal.title }}"> <modal modal-id="record_modal" modal-title="{{ recordModal.title }}">
<modal-body> <modal-body>
@ -468,10 +470,19 @@
<option value="" selected="selected">Please choose a record owner group</option> <option value="" selected="selected">Please choose a record owner group</option>
</select> </select>
<modal-invalid> <modal-invalid>
Record Owner Group is required for records in shared zones Owner Group transfer status is required for records in shared zones
</modal-invalid> </modal-invalid>
</modal-element> </modal-element>
<modal-element label="Requested Record Owner Group"
invalid-when="addRecordForm.$submitted && addRecordForm.recordSetGroupChange.requestedOwnerGroupId.$invalid"
ng-if="recordModal.sharedDisplayEnabled && recordModal.sharedZone && recordModal.previous.recordSetGroupChange.ownerShipTransferStatus=='PendingReview'">
<input name="requestedOwnerGroupName"
class="form-control"
ng-model="recordSetRequestedOwnerShipName"
ng-class="recordModal.details.class"
ng-readonly="recordModal.basics.readOnly"
placeholder="">
</modal-element>
</modal-body> </modal-body>
<modal-footer> <modal-footer>
<span ng-if="recordModal.action == recordModalState.CREATE"> <span ng-if="recordModal.action == recordModalState.CREATE">
@ -506,3 +517,97 @@
</modal-footer> </modal-footer>
</modal> </modal>
</form> </form>
<form name="recordOwnerShipForm" role="form" class="form-horizontal" novalidate>
<!--Request ownership for unowned records in shared zones-->
<modal modal-id="record_modal_ownership" modal-title="Request OwnerShip">
<modal-body>
<modal-element label="Record Owner Group Request"
invalid-when="recordOwnerShipForm.$submitted && recordOwnerShipForm.requestOwnerGroupId.$invalid"
ng-if = "recordModal.sharedDisplayEnabled && recordModal.sharedZone && !recordModal.isCurrentRecordOwnerGroup && currentRecord.ownerGroupId == undefined">
<select name="requestOwnerGroupId"
class="form-control"
ng-model="currentRecord.recordSetGroupChange.requestedOwnerGroupId"
ng-disabled="recordModal.details.readOnly"
ng-class="recordModal.details.class"
ng-options="group.id as group.name for group in myGroups | orderBy: 'name'"
required>
<option value="" selected="selected">Please choose a record owner group</option>
</select>
<modal-invalid>
Requesting Record Owner Group is required for records in shared zones
</modal-invalid>
</modal-element>
</modal-body>
<modal-footer>
<span>
<button type="button" class="btn btn-default" data-dismiss="modal" ng-click="closeRecordModal()">Close</button>
<button class="btn btn-primary pull-right" ng-click="requestedOwnerShip()" id="requested_ownership">Request</button>
</span>
</modal-footer>
</modal>
<!--Request and approve ownership for owned records in shared zones-->
<modal modal-id="record_modal_ownership_transfer" modal-title="OwnerShip Transfer">
<modal-body>
<modal-element label="Record Owner Group Request"
invalid-when="recordOwnerShipForm.$submitted && recordOwnerShipForm.requestedOwnerGroupId1.$invalid"
ng-if="recordModal.sharedDisplayEnabled && recordModal.sharedZone"
ng-switch="currentOwnerShipTransferApprover || recordModal.previous.recordSetGroupChange.ownerShipTransferStatus == 'PendingReview'">
<select name="requestedOwnerGroupId1"
class="form-control"
ng-model="currentRecord.recordSetGroupChange.requestedOwnerGroupId"
ng-switch-when="false"
ng-disabled="recordModal.details.readOnly"
ng-class="recordModal.details.class"
ng-options="group.id as group.name for group in myGroups | orderBy: 'name'"
required>
<option value="" selected="selected">Please choose a record owner group</option>
</select>
<modal-invalid>
Record Owner Group is required for records in shared zones
</modal-invalid>
<input name="requestedOwnerGroupId2"
class="form-control"
ng-switch-default
ng-model="recordSetRequestedOwnerShipName"
ng-class="recordModal.details.class"
ng-readonly="recordModal.basics.readOnly"
>
</modal-element>
<modal-element label="Record Owner Group Status"
invalid-when="recordOwnerShipForm.$submitted && (recordOwnerShipForm.ownerGroupStatus.$invalid
|| recordOwnerShipForm.ownerGroupStatus.$viewValue == null || !recordOwnerShipForm.ownerGroupStatus.$viewValue)"
ng-if="recordModal.sharedDisplayEnabled && recordModal.sharedZone "
ng-switch="!currentOwnerShipTransferApprover">
<select name="ownerGroupStatus"
class="form-control"
ng-switch-when="false"
ng-model="currentRecord.recordSetGroupChange.ownerShipTransferStatus"
ng-class="recordModal.details.class"
ng-options="ownerShipTransferApproverStatus.value as ownerShipTransferApproverStatus.label for ownerShipTransferApproverStatus in ownerShipTransferStatus" required>
<option value= "" selected="selected">Please choose a record owner group status</option>
</select>
<select name="ownerGroupStatus"
class="form-control"
ng-switch-default
ng-model="currentRecord.recordSetGroupChange.ownerShipTransferStatus"
ng-class="recordModal.details.class">
<option value= "" selected="selected">Please choose a record owner group status</option>
<option ng-if="recordModal.previous.recordSetGroupChange.ownerShipTransferStatus == 'PendingReview'" value="Cancelled" required>Cancel</option>
<option ng-if="recordModal.previous.recordSetGroupChange.ownerShipTransferStatus != 'PendingReview'" value="Requested" required>Request</option>
</select>
<modal-invalid>
Owner Group transfer status is required for records in shared zones
</modal-invalid>
</modal-element>
</modal-body>
<modal-footer>
<span>
<button type="button" class="btn btn-default" data-dismiss="modal" ng-click="closeRecordModal()">Close</button>
<button class="btn btn-primary pull-right" ng-click="submitRequestedOwnerShipTransfer()" id="update_recordset_ownership_request">Submit</button>
</span>
</modal-footer>
</modal>
</form>
</div>

View File

@ -296,6 +296,7 @@ trait TestApplicationData { this: Mockito =>
| "ttl": "200", | "ttl": "200",
| "status": "${RecordSetStatus.Active}", | "status": "${RecordSetStatus.Active}",
| "records": [ { "address": "10.1.1.1" } ], | "records": [ { "address": "10.1.1.1" } ],
| "recordSetGroupChange": "None",
| "id": "$hobbitRecordSetId" | "id": "$hobbitRecordSetId"
| } | }
""".stripMargin) """.stripMargin)

View File

@ -25,7 +25,7 @@ import com.amazonaws.services.route53.model.{
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
import java.time.Instant import java.time.Instant
import vinyldns.core.domain.Fqdn import vinyldns.core.domain.Fqdn
import vinyldns.core.domain.record.{NSData, RecordData, RecordSet, RecordSetStatus, RecordType} import vinyldns.core.domain.record.{NSData, OwnerShipTransfer, OwnerShipTransferStatus, RecordData, RecordSet, RecordSetStatus, RecordType}
import vinyldns.core.domain.record.RecordType.RecordType import vinyldns.core.domain.record.RecordType.RecordType
import vinyldns.core.domain.record.RecordType._ import vinyldns.core.domain.record.RecordType._
import vinyldns.core.domain.zone.Zone import vinyldns.core.domain.zone.Zone
@ -82,6 +82,7 @@ trait Route53Conversions {
Instant.now.truncatedTo(ChronoUnit.MILLIS), Instant.now.truncatedTo(ChronoUnit.MILLIS),
Some(Instant.now.truncatedTo(ChronoUnit.MILLIS)), Some(Instant.now.truncatedTo(ChronoUnit.MILLIS)),
r53RecordSet.getResourceRecords.asScala.toList.flatMap(toVinyl(typ, _)), r53RecordSet.getResourceRecords.asScala.toList.flatMap(toVinyl(typ, _)),
recordSetGroupChange=Some(OwnerShipTransfer(ownerShipTransferStatus = OwnerShipTransferStatus.AutoApproved)),
fqdn = Some(r53RecordSet.getName) fqdn = Some(r53RecordSet.getName)
) )
} }
@ -110,6 +111,7 @@ trait Route53Conversions {
Instant.now.truncatedTo(ChronoUnit.MILLIS), Instant.now.truncatedTo(ChronoUnit.MILLIS),
Some(Instant.now.truncatedTo(ChronoUnit.MILLIS)), Some(Instant.now.truncatedTo(ChronoUnit.MILLIS)),
nsData, nsData,
recordSetGroupChange = Some(OwnerShipTransfer(ownerShipTransferStatus = OwnerShipTransferStatus.AutoApproved)),
fqdn = Some(Fqdn(zoneName).fqdn) fqdn = Some(Fqdn(zoneName).fqdn)
) )
} }