From 184fcf01b45bcd9c485fad9e82cc7d88eaad4ef3 Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Fri, 1 Mar 2024 23:50:00 +0530 Subject: [PATCH 01/25] ownership transfer for records --- .../RecordSetServiceIntegrationSpec.scala | 263 ++++++++- .../email/EmailNotifierIntegrationSpec.scala | 30 +- .../sns/SnsNotifierIntegrationSpec.scala | 2 +- modules/api/src/main/resources/reference.conf | 2 +- .../src/main/scala/vinyldns/api/Boot.scala | 6 +- .../api/backend/dns/DnsConversions.scala | 6 +- .../domain/batch/BatchChangeConverter.scala | 3 +- .../api/domain/record/RecordSetService.scala | 90 ++- .../domain/record/RecordSetValidations.scala | 39 +- .../api/domain/zone/ZoneProtocol.scala | 8 +- .../api/notifier/email/EmailNotifier.scala | 97 ++- .../email/EmailNotifierProvider.scala | 6 +- .../notifier/sns/SnsNotifierProvider.scala | 4 +- .../vinyldns/api/route/DnsJsonProtocol.scala | 22 + .../tests/recordsets/update_recordset_test.py | 556 ++++++++++++++++++ .../domain/record/RecordSetServiceSpec.scala | 532 ++++++++++++++++- .../record/RecordSetValidationsSpec.scala | 80 +++ .../notifier/email/EmailNotifierSpec.scala | 86 ++- .../api/notifier/sns/SnsNotifierSpec.scala | 4 +- .../api/route/VinylDNSJsonProtocolSpec.scala | 200 +++++++ .../src/main/protobuf/VinylDNSProto.proto | 8 +- .../core/domain/record/RecordSet.scala | 25 +- .../core/notifier/NotifierLoader.scala | 12 +- .../core/notifier/NotifierProvider.scala | 4 +- .../core/protobuf/ProtobufConversions.scala | 17 +- .../vinyldns/core/TestRecordSetData.scala | 43 +- .../core/notifier/NotifierLoaderSpec.scala | 18 +- .../zones/zoneTabs/manageRecords.scala.html | 29 +- .../lib/controllers/controller.records.js | 123 ++++ .../lib/services/records/service.records.js | 5 +- .../services/records/service.records.spec.js | 28 +- .../portal/public/templates/record-modal.html | 105 +++- .../controllers/TestApplicationData.scala | 1 + .../route53/backend/Route53Conversions.scala | 4 +- 34 files changed, 2360 insertions(+), 98 deletions(-) diff --git a/modules/api/src/it/scala/vinyldns/api/domain/record/RecordSetServiceIntegrationSpec.scala b/modules/api/src/it/scala/vinyldns/api/domain/record/RecordSetServiceIntegrationSpec.scala index 95c1f8704..9d1762e27 100644 --- a/modules/api/src/it/scala/vinyldns/api/domain/record/RecordSetServiceIntegrationSpec.scala +++ b/modules/api/src/it/scala/vinyldns/api/domain/record/RecordSetServiceIntegrationSpec.scala @@ -19,6 +19,7 @@ package vinyldns.api.domain.record import cats.effect._ import cats.implicits._ import cats.scalatest.EitherMatchers +import org.mockito.Matchers.any import java.time.Instant import java.time.temporal.ChronoUnit import org.mockito.Mockito._ @@ -41,6 +42,9 @@ import vinyldns.core.domain.membership.{Group, GroupRepository, User, UserReposi import vinyldns.core.domain.record.RecordType._ import vinyldns.core.domain.record._ import vinyldns.core.domain.zone._ +import vinyldns.core.notifier.{AllNotifiers, Notification, Notifier} + +import scala.concurrent.ExecutionContext class RecordSetServiceIntegrationSpec extends AnyWordSpec @@ -232,6 +236,36 @@ class RecordSetServiceIntegrationSpec ownerGroupId = Some("non-existent") ) + private val sharedTestRecordPendingReviewOwnerShip = RecordSet( + sharedZone.id, + "shared-record-ownerShip-pendingReview", + A, + 200, + RecordSetStatus.Active, + Instant.now.truncatedTo(ChronoUnit.MILLIS), + None, + List(AData("1.1.1.1")), + ownerGroupId = Some(sharedGroup.id), + recordSetGroupChange = Some(OwnerShipTransfer( + ownerShipTransferStatus = OwnerShipTransferStatus.PendingReview, + requestedOwnerGroupId = Some(group.id))) + ) + + private val sharedTestRecordCancelledOwnerShip = RecordSet( + sharedZone.id, + "shared-record-ownerShip-cancelled", + A, + 200, + RecordSetStatus.Active, + Instant.now.truncatedTo(ChronoUnit.MILLIS), + None, + List(AData("1.1.1.1")), + ownerGroupId = Some(sharedGroup.id), + recordSetGroupChange = Some(OwnerShipTransfer( + ownerShipTransferStatus = OwnerShipTransferStatus.Cancelled, + requestedOwnerGroupId = Some(group.id))) + ) + private val testOwnerGroupRecordInNormalZone = RecordSet( zone.id, "user-in-owner-group-but-zone-not-shared", @@ -285,7 +319,10 @@ class RecordSetServiceIntegrationSpec // Seeding records in DB val sharedRecords = List( sharedTestRecord, - sharedTestRecordBadOwnerGroup + sharedTestRecordBadOwnerGroup, + sharedTestRecordPendingReviewOwnerShip, + sharedTestRecordCancelledOwnerShip + ) val conflictRecords = List( subTestRecordNameConflict, @@ -324,7 +361,8 @@ class RecordSetServiceIntegrationSpec vinyldnsConfig.highValueDomainConfig, vinyldnsConfig.dottedHostsConfig, vinyldnsConfig.serverConfig.approvedNameServers, - useRecordSetCache = true + useRecordSetCache = true, + mockNotifiers ) } @@ -425,6 +463,227 @@ class RecordSetServiceIntegrationSpec leftValue(result) shouldBe a[InvalidRequest] } + "auto-approve ownership transfer request, if user tried to update the ownership" in { + val newRecord = sharedTestRecord.copy(recordSetGroupChange = + Some(OwnerShipTransfer(ownerShipTransferStatus = OwnerShipTransferStatus.AutoApproved, + requestedOwnerGroupId = Some(group.id)))) + + val result = testRecordSetService + .updateRecordSet(newRecord, auth2) + .value + .unsafeRunSync() + + val change = rightValue(result).asInstanceOf[RecordSetChange] + change.recordSet.name shouldBe "shared-record" + change.recordSet.ownerGroupId.get shouldBe group.id + change.recordSet.recordSetGroupChange.get.ownerShipTransferStatus shouldBe OwnerShipTransferStatus.AutoApproved + change.recordSet.recordSetGroupChange.get.requestedOwnerGroupId.get shouldBe group.id + } + + "approve ownership transfer request, if user requested for ownership transfer" in { + val newRecord = sharedTestRecordPendingReviewOwnerShip.copy(recordSetGroupChange = + Some(OwnerShipTransfer( + ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyApproved))) + + doReturn(IO.unit).when(mockNotifier).notify(any[Notification[_]]) + + val result = testRecordSetService + .updateRecordSet(newRecord, auth2) + .value + .unsafeRunSync() + + val change = rightValue(result).asInstanceOf[RecordSetChange] + change.recordSet.name shouldBe "shared-record-ownerShip-pendingReview" + change.recordSet.ownerGroupId.get shouldBe group.id + change.recordSet.recordSetGroupChange.get.ownerShipTransferStatus shouldBe OwnerShipTransferStatus.ManuallyApproved + change.recordSet.recordSetGroupChange.get.requestedOwnerGroupId.get shouldBe group.id + } + + "reject ownership transfer request, if user requested for ownership transfer" in { + val newRecord = sharedTestRecordPendingReviewOwnerShip.copy(recordSetGroupChange = + Some(OwnerShipTransfer( + ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyRejected))) + + doReturn(IO.unit).when(mockNotifier).notify(any[Notification[_]]) + + val result = testRecordSetService + .updateRecordSet(newRecord, auth2) + .value + .unsafeRunSync() + + val change = rightValue(result).asInstanceOf[RecordSetChange] + change.recordSet.name shouldBe "shared-record-ownerShip-pendingReview" + change.recordSet.ownerGroupId.get shouldBe sharedGroup.id + change.recordSet.recordSetGroupChange.get.ownerShipTransferStatus shouldBe OwnerShipTransferStatus.ManuallyRejected + change.recordSet.recordSetGroupChange.get.requestedOwnerGroupId.get shouldBe group.id + } + + "request ownership transfer, if user not in the owner group and wants to own the record" in { + val newRecord = sharedTestRecord.copy(recordSetGroupChange = + Some(OwnerShipTransfer( + ownerShipTransferStatus = OwnerShipTransferStatus.Requested, + requestedOwnerGroupId = Some(dummyGroup.id)))) + + doReturn(IO.unit).when(mockNotifier).notify(any[Notification[_]]) + + val result = testRecordSetService + .updateRecordSet(newRecord, dummyAuth) + .value + .unsafeRunSync() + + val change = rightValue(result).asInstanceOf[RecordSetChange] + change.recordSet.name shouldBe "shared-record" + change.recordSet.ownerGroupId.get shouldBe sharedGroup.id + change.recordSet.recordSetGroupChange.get.ownerShipTransferStatus shouldBe OwnerShipTransferStatus.PendingReview + change.recordSet.recordSetGroupChange.get.requestedOwnerGroupId.get shouldBe dummyGroup.id + } + + "fail requesting ownership transfer if user is not in owner group and tried to update other fields in record set" in { + val newRecord = sharedTestRecord.copy( + ttl = 3000, + recordSetGroupChange = + Some(OwnerShipTransfer( + ownerShipTransferStatus = OwnerShipTransferStatus.Requested, + requestedOwnerGroupId = Some(dummyGroup.id)))) + + val result = testRecordSetService + .updateRecordSet(newRecord, dummyAuth) + .value + .unsafeRunSync() + + leftValue(result) shouldBe a[InvalidRequest] + } + + "fail updating if user is not in owner group for ownership transfer approval" in { + val newRecord = sharedTestRecordPendingReviewOwnerShip.copy(recordSetGroupChange = + Some(OwnerShipTransfer( + ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyApproved))) + + val result = testRecordSetService + .updateRecordSet(newRecord, dummyAuth) + .value + .unsafeRunSync() + + leftValue(result) shouldBe a[NotAuthorizedError] + } + + "fail updating if user is not in owner group for ownership transfer reject" in { + val newRecord = sharedTestRecordPendingReviewOwnerShip.copy(recordSetGroupChange = + Some(OwnerShipTransfer( + ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyRejected))) + + val result = testRecordSetService + .updateRecordSet(newRecord, dummyAuth) + .value + .unsafeRunSync() + + leftValue(result) shouldBe a[NotAuthorizedError] + } + + "cancel the ownership transfer request, if user not require ownership transfer further" in { + val newRecord = sharedTestRecordPendingReviewOwnerShip.copy(recordSetGroupChange = + Some(OwnerShipTransfer( + ownerShipTransferStatus = OwnerShipTransferStatus.Cancelled))) + + doReturn(IO.unit).when(mockNotifier).notify(any[Notification[_]]) + + val result = testRecordSetService + .updateRecordSet(newRecord, auth) + .value + .unsafeRunSync() + + val change = rightValue(result).asInstanceOf[RecordSetChange] + change.recordSet.name shouldBe "shared-record-ownerShip-pendingReview" + change.recordSet.ownerGroupId.get shouldBe sharedGroup.id + change.recordSet.recordSetGroupChange.get.ownerShipTransferStatus shouldBe OwnerShipTransferStatus.Cancelled + change.recordSet.recordSetGroupChange.get.requestedOwnerGroupId.get shouldBe group.id + } + + "fail approving ownership transfer request, if user is cancelled" in { + val newRecord = sharedTestRecordCancelledOwnerShip.copy(recordSetGroupChange = + Some(OwnerShipTransfer( + ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyApproved))) + val result = testRecordSetService + .updateRecordSet(newRecord, auth) + .value + .unsafeRunSync() + + leftValue(result) shouldBe a[InvalidRequest] + } + + "fail rejecting ownership transfer request, if user is cancelled" in { + val newRecord = sharedTestRecordCancelledOwnerShip.copy(recordSetGroupChange = + Some(OwnerShipTransfer( + ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyRejected))) + + val result = testRecordSetService + .updateRecordSet(newRecord, auth) + .value + .unsafeRunSync() + + leftValue(result) shouldBe a[InvalidRequest] + } + + "fail auto-approving ownership transfer request, if user is cancelled" in { + val newRecord = sharedTestRecordCancelledOwnerShip.copy(recordSetGroupChange = + Some(OwnerShipTransfer( + ownerShipTransferStatus = OwnerShipTransferStatus.AutoApproved + ))) + + doReturn(IO.unit).when(mockNotifier).notify(any[Notification[_]]) + + val result = testRecordSetService + .updateRecordSet(newRecord, auth) + .value + .unsafeRunSync() + + leftValue(result) shouldBe a[InvalidRequest] + } + + "fail auto-approving ownership transfer request, if zone is not shared" in { + val newRecord = dottedTestRecord.copy(recordSetGroupChange = + Some(OwnerShipTransfer(ownerShipTransferStatus = OwnerShipTransferStatus.AutoApproved, + requestedOwnerGroupId = Some(group.id)))) + + val result = testRecordSetService + .updateRecordSet(newRecord, auth2) + .value + .unsafeRunSync() + + leftValue(result) shouldBe a[InvalidRequest] + } + + "fail approving ownership transfer request, if zone is not shared" in { + val newRecord = dottedTestRecord.copy(recordSetGroupChange = + Some(OwnerShipTransfer( + ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyApproved + ))) + + val result = testRecordSetService + .updateRecordSet(newRecord, auth2) + .value + .unsafeRunSync() + + leftValue(result) shouldBe a[InvalidRequest] + } + + "fail requesting ownership transfer, if zone is not shared" in { + val newRecord = dottedTestRecord.copy(recordSetGroupChange = + Some(OwnerShipTransfer( + ownerShipTransferStatus = OwnerShipTransferStatus.Requested, + requestedOwnerGroupId = Some(dummyGroup.id) + ))) + + doReturn(IO.unit).when(mockNotifier).notify(any[Notification[_]]) + + val result = testRecordSetService + .updateRecordSet(newRecord, dummyAuth) + .value + .unsafeRunSync() + + leftValue(result) shouldBe a[InvalidRequest] + } + "update dotted record succeeds if it satisfies all dotted hosts config" in { val newRecord = dottedTestRecord.copy(ttl = 37000) diff --git a/modules/api/src/it/scala/vinyldns/api/notifier/email/EmailNotifierIntegrationSpec.scala b/modules/api/src/it/scala/vinyldns/api/notifier/email/EmailNotifierIntegrationSpec.scala index f28168966..b1bb4116c 100644 --- a/modules/api/src/it/scala/vinyldns/api/notifier/email/EmailNotifierIntegrationSpec.scala +++ b/modules/api/src/it/scala/vinyldns/api/notifier/email/EmailNotifierIntegrationSpec.scala @@ -24,7 +24,7 @@ import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpecLike import vinyldns.core.domain.batch._ import vinyldns.core.domain.record.RecordType -import vinyldns.core.domain.record.AData +import vinyldns.core.domain.record.{AData, OwnerShipTransferStatus, RecordSetChange, RecordSetChangeStatus, RecordSetChangeType, RecordType} import java.time.Instant import java.time.temporal.ChronoUnit import vinyldns.core.TestMembershipData._ @@ -35,6 +35,8 @@ import cats.effect.{IO, Resource} import scala.collection.JavaConverters._ import org.scalatest.BeforeAndAfterEach import cats.implicits._ +import vinyldns.core.TestRecordSetData.{ownerShipTransfer, rsOk} +import vinyldns.core.TestZoneData.okZone class EmailNotifierIntegrationSpec extends MySqlApiIntegrationSpec @@ -57,7 +59,7 @@ class EmailNotifierIntegrationSpec "Email Notifier" should { - "send an email" taggedAs (SkipCI) in { + "send an email for batch change" taggedAs (SkipCI) in { val batchChange = BatchChange( okUser.id, okUser.userName, @@ -84,7 +86,7 @@ class EmailNotifierIntegrationSpec val program = for { _ <- userRepository.save(okUser) notifier <- new EmailNotifierProvider() - .load(NotifierConfig("", emailConfig), userRepository) + .load(NotifierConfig("", emailConfig), userRepository, groupRepository) _ <- notifier.notify(Notification(batchChange)) emailFiles <- retrieveEmailFiles(targetDirectory) } yield emailFiles @@ -94,7 +96,29 @@ class EmailNotifierIntegrationSpec files.length should be(1) } + "send an email for recordSetChange ownerShip transfer" taggedAs (SkipCI) in { + val recordSetChange = RecordSetChange( + okZone, + rsOk.copy(ownerGroupId= Some(okGroup.id),recordSetGroupChange = + Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.PendingReview, requestedOwnerGroupId = Some(dummyGroup.id)))), + "system", + RecordSetChangeType.Create, + RecordSetChangeStatus.Complete + ) + val program = for { + _ <- userRepository.save(okUser) + notifier <- new EmailNotifierProvider() + .load(NotifierConfig("", emailConfig), userRepository, groupRepository) + _ <- notifier.notify(Notification(recordSetChange)) + emailFiles <- retrieveEmailFiles(targetDirectory) + } yield emailFiles + + val files = program.unsafeRunSync() + + files.length should be(1) + + } } def deleteEmailFiles(path: Path): IO[Unit] = diff --git a/modules/api/src/it/scala/vinyldns/api/notifier/sns/SnsNotifierIntegrationSpec.scala b/modules/api/src/it/scala/vinyldns/api/notifier/sns/SnsNotifierIntegrationSpec.scala index a10a558a2..5fb7b41e0 100644 --- a/modules/api/src/it/scala/vinyldns/api/notifier/sns/SnsNotifierIntegrationSpec.scala +++ b/modules/api/src/it/scala/vinyldns/api/notifier/sns/SnsNotifierIntegrationSpec.scala @@ -111,7 +111,7 @@ class SnsNotifierIntegrationSpec sns.subscribe(topic, "sqs", queueUrl) } notifier <- new SnsNotifierProvider() - .load(NotifierConfig("", snsConfig), userRepository) + .load(NotifierConfig("", snsConfig), userRepository, groupRepository) _ <- notifier.notify(Notification(batchChange)) _ <- IO.sleep(1.seconds) messages <- IO { diff --git a/modules/api/src/main/resources/reference.conf b/modules/api/src/main/resources/reference.conf index 817c668e1..776fd6da9 100644 --- a/modules/api/src/main/resources/reference.conf +++ b/modules/api/src/main/resources/reference.conf @@ -161,7 +161,7 @@ vinyldns { } } - notifiers = [] + notifiers = ["email"] email = { class-name = "vinyldns.api.notifier.email.EmailNotifierProvider" diff --git a/modules/api/src/main/scala/vinyldns/api/Boot.scala b/modules/api/src/main/scala/vinyldns/api/Boot.scala index 6cd59d894..7319c1289 100644 --- a/modules/api/src/main/scala/vinyldns/api/Boot.scala +++ b/modules/api/src/main/scala/vinyldns/api/Boot.scala @@ -96,7 +96,8 @@ object Boot extends App { msgsPerPoll <- IO.fromEither(MessageCount(vinyldnsConfig.messageQueueConfig.messagesPerPoll)) notifiers <- NotifierLoader.loadAll( vinyldnsConfig.notifierConfigs, - repositories.userRepository + repositories.userRepository, + repositories.groupRepository ) _ <- APIMetrics.initialize(vinyldnsConfig.apiMetricSettings) // Schedule the zone sync task to be executed every 5 seconds @@ -161,7 +162,8 @@ object Boot extends App { vinyldnsConfig.highValueDomainConfig, vinyldnsConfig.dottedHostsConfig, vinyldnsConfig.serverConfig.approvedNameServers, - vinyldnsConfig.serverConfig.useRecordSetCache + vinyldnsConfig.serverConfig.useRecordSetCache, + notifiers ) val zoneService = ZoneService( repositories, diff --git a/modules/api/src/main/scala/vinyldns/api/backend/dns/DnsConversions.scala b/modules/api/src/main/scala/vinyldns/api/backend/dns/DnsConversions.scala index b89d00f04..3c9d3949e 100644 --- a/modules/api/src/main/scala/vinyldns/api/backend/dns/DnsConversions.scala +++ b/modules/api/src/main/scala/vinyldns/api/backend/dns/DnsConversions.scala @@ -205,7 +205,8 @@ trait DnsConversions { ttl = r.getTTL, status = RecordSetStatus.Active, created = Instant.now.truncatedTo(ChronoUnit.MILLIS), - records = f(r) + records = f(r), + recordSetGroupChange = Some(OwnerShipTransfer(ownerShipTransferStatus = OwnerShipTransferStatus.None)) ) // if we do not know the record type, then we cannot parse the records, but we should be able to get everything else @@ -217,7 +218,8 @@ trait DnsConversions { ttl = r.getTTL, status = RecordSetStatus.Active, created = Instant.now.truncatedTo(ChronoUnit.MILLIS), - records = Nil + records = Nil, + recordSetGroupChange = Some(OwnerShipTransfer(ownerShipTransferStatus = OwnerShipTransferStatus.None)) ) def fromARecord(r: DNS.ARecord, zoneName: DNS.Name, zoneId: String): RecordSet = diff --git a/modules/api/src/main/scala/vinyldns/api/domain/batch/BatchChangeConverter.scala b/modules/api/src/main/scala/vinyldns/api/domain/batch/BatchChangeConverter.scala index d561bc121..29dc8477c 100644 --- a/modules/api/src/main/scala/vinyldns/api/domain/batch/BatchChangeConverter.scala +++ b/modules/api/src/main/scala/vinyldns/api/domain/batch/BatchChangeConverter.scala @@ -265,7 +265,8 @@ class BatchChangeConverter(batchChangeRepo: BatchChangeRepository, messageQueue: Instant.now.truncatedTo(ChronoUnit.MILLIS), None, proposedRecordData.toList, - ownerGroupId = setOwnerGroupId + ownerGroupId = setOwnerGroupId, + recordSetGroupChange = Some(OwnerShipTransfer(ownerShipTransferStatus = OwnerShipTransferStatus.None)) ) } } diff --git a/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetService.scala b/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetService.scala index cf6191eb4..1c2751e6c 100644 --- a/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetService.scala +++ b/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetService.scala @@ -36,6 +36,7 @@ import vinyldns.core.domain.record.RecordType.RecordType import vinyldns.core.domain.DomainHelpers.ensureTrailingDot import vinyldns.core.domain.backend.{Backend, BackendResolver} import vinyldns.core.domain.record.RecordTypeSort.RecordTypeSort +import vinyldns.core.notifier.{AllNotifiers, Notification} import scala.util.matching.Regex @@ -49,7 +50,8 @@ object RecordSetService { highValueDomainConfig: HighValueDomainConfig, dottedHostsConfig: DottedHostsConfig, approvedNameServers: List[Regex], - useRecordSetCache: Boolean + useRecordSetCache: Boolean, + notifiers: AllNotifiers ): RecordSetService = new RecordSetService( dataAccessor.zoneRepository, @@ -65,7 +67,9 @@ object RecordSetService { highValueDomainConfig, dottedHostsConfig, approvedNameServers, - useRecordSetCache + useRecordSetCache, + notifiers + ) } @@ -83,12 +87,16 @@ class RecordSetService( highValueDomainConfig: HighValueDomainConfig, dottedHostsConfig: DottedHostsConfig, approvedNameServers: List[Regex], - useRecordSetCache: Boolean + useRecordSetCache: Boolean, + notifiers: AllNotifiers ) extends RecordSetServiceAlgebra { import RecordSetValidations._ 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] = for { zone <- getZone(recordSet.zoneId) @@ -143,13 +151,25 @@ class RecordSetService( _ <- unchangedRecordName(existing, recordSet, zone).toResult _ <- unchangedRecordType(existing, recordSet).toResult _ <- unchangedZoneId(existing, recordSet).toResult + _ <- if(requestorOwnerShipTransferStatus.contains(recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("")) + && !auth.isSuper) unchangedRecordSet(existing, recordSet).toResult else ().toResult + _ <- if(existing.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("") == OwnerShipTransferStatus.Cancelled) + recordSetOwnerShipApproveStatus(recordSet).toResult else ().toResult + recordSet <- updateRecordSetGroupChangeStatus(recordSet, existing, zone) change <- RecordSetChangeGenerator.forUpdate(existing, recordSet, zone, Some(auth)).toResult // because changes happen to the RS in forUpdate itself, converting 1st and validating on that rsForValidations = change.recordSet superUserCanUpdateOwnerGroup = canSuperUserUpdateOwnerGroup(existing, recordSet, zone, auth) _ <- isNotHighValueDomain(recordSet, zone, highValueDomainConfig).toResult + _ <- if(requestorOwnerShipTransferStatus.contains(recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("") )) + ().toResult else canUpdateRecordSet(auth, existing.name, existing.typ, zone, existing.ownerGroupId, superUserCanUpdateOwnerGroup).toResult _ <- canUpdateRecordSet(auth, existing.name, existing.typ, zone, existing.ownerGroupId, superUserCanUpdateOwnerGroup).toResult ownerGroup <- getGroupIfProvided(rsForValidations.ownerGroupId) + _ <- if(requestorOwnerShipTransferStatus.contains(recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse(""))) + canUseOwnerGroup(rsForValidations.recordSetGroupChange.map(_.requestedOwnerGroupId).get, ownerGroup, auth).toResult + else if(approverOwnerShipTransferStatus.contains(recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse(""))) + canUseOwnerGroup(existing.ownerGroupId, ownerGroup, auth).toResult + else canUseOwnerGroup(rsForValidations.ownerGroupId, ownerGroup, auth).toResult _ <- canUseOwnerGroup(rsForValidations.ownerGroupId, ownerGroup, auth).toResult _ <- notPending(existing).toResult existingRecordsWithName <- recordSetRepository @@ -185,6 +205,11 @@ class RecordSetService( _ <- if(existing.name == rsForValidations.name) ().toResult else if(allowedZoneList.contains(zone.name)) checkAllowedDots(allowedDotsLimit, rsForValidations, zone).toResult else ().toResult _ <- if(allowedZoneList.contains(zone.name)) isNotApexEndsWithDot(rsForValidations, zone).toResult else ().toResult _ <- messageQueue.send(change).toResult[Unit] + _ <- if(recordSet.recordSetGroupChange != None && + recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("") != OwnerShipTransferStatus.None && + recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("") != OwnerShipTransferStatus.AutoApproved) + notifiers.notify(Notification(change)).toResult + else ().toResult } yield change def deleteRecordSet( @@ -203,6 +228,65 @@ class RecordSetService( _ <- messageQueue.send(change).toResult[Unit] } yield change + //update ownership transfer is zone is shared + def updateRecordSetGroupChangeStatus(recordSet: RecordSet, existing: RecordSet, zone: Zone): Result[RecordSet] = { + val existingOwnerShipTransfer = existing.recordSetGroupChange.getOrElse(OwnerShipTransfer.apply(OwnerShipTransferStatus.None, Some("none"))) + val ownerShipTransfer = recordSet.recordSetGroupChange.getOrElse(OwnerShipTransfer.apply(OwnerShipTransferStatus.None, Some("none"))) + if (recordSet.recordSetGroupChange != None && + ownerShipTransfer.ownerShipTransferStatus != OwnerShipTransferStatus.None) + if (zone.shared){ + if (approverOwnerShipTransferStatus.contains(ownerShipTransfer.ownerShipTransferStatus)) { + val recordSetOwnerApproval = + ownerShipTransfer.ownerShipTransferStatus match { + case OwnerShipTransferStatus.ManuallyApproved => + recordSet.copy(ownerGroupId = existingOwnerShipTransfer.requestedOwnerGroupId, + recordSetGroupChange = Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyApproved, + requestedOwnerGroupId = existingOwnerShipTransfer.requestedOwnerGroupId))) + case OwnerShipTransferStatus.ManuallyRejected => + recordSet.copy( + recordSetGroupChange = Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyRejected, + requestedOwnerGroupId = existingOwnerShipTransfer.requestedOwnerGroupId))) + case OwnerShipTransferStatus.AutoApproved => + recordSet.copy( + ownerGroupId = ownerShipTransfer.requestedOwnerGroupId, + recordSetGroupChange = Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.AutoApproved, + requestedOwnerGroupId = ownerShipTransfer.requestedOwnerGroupId))) + + case _ => recordSet.copy( + recordSetGroupChange = Some(ownerShipTransfer.copy( + ownerShipTransferStatus = OwnerShipTransferStatus.None, + requestedOwnerGroupId = Some("null")))) + } + for { + recordSet <- recordSetOwnerApproval.toResult + } yield recordSet + } + else { + val recordSetOwnerRequest = + ownerShipTransfer.ownerShipTransferStatus match { + case OwnerShipTransferStatus.Cancelled => + recordSet.copy(recordSetGroupChange = Some(ownerShipTransfer.copy( + ownerShipTransferStatus = OwnerShipTransferStatus.Cancelled, + requestedOwnerGroupId = existingOwnerShipTransfer.requestedOwnerGroupId))) + case OwnerShipTransferStatus.Requested => recordSet.copy( + recordSetGroupChange = Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.PendingReview))) + } + for { + recordSet <- recordSetOwnerRequest.toResult + } yield recordSet + } + } else for { + _ <- unchangedRecordSetOwnershipStatus(recordSet, existing).toResult + } yield recordSet.copy( + recordSetGroupChange = Some(ownerShipTransfer.copy( + ownerShipTransferStatus = OwnerShipTransferStatus.None, + requestedOwnerGroupId = Some("null")))) + else recordSet.copy( + recordSetGroupChange = Some(ownerShipTransfer.copy( + ownerShipTransferStatus = OwnerShipTransferStatus.None, + requestedOwnerGroupId = Some("null")))).toResult + } + // For dotted hosts. Check if a record that may conflict with dotted host exist or not def recordFQDNDoesNotExist(newRecordSet: RecordSet, zone: Zone): IO[Boolean] = { // Use fqdn for searching through `recordset` mysql table to see if it already exist diff --git a/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetValidations.scala b/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetValidations.scala index 44b7377b9..81b7c484e 100644 --- a/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetValidations.scala +++ b/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetValidations.scala @@ -27,7 +27,7 @@ import vinyldns.core.domain.record.RecordType._ import vinyldns.api.domain.zone._ import vinyldns.core.domain.auth.AuthPrincipal import vinyldns.core.domain.membership.Group -import vinyldns.core.domain.record.{RecordSet, RecordType} +import vinyldns.core.domain.record.{OwnerShipTransferStatus, RecordSet, RecordType} import vinyldns.core.domain.zone.Zone import vinyldns.core.Messages._ @@ -462,4 +462,41 @@ object RecordSetValidations { val wildcardRegex = raw"^\s*[*%].*[*%]\s*$$".r searchRegex.findFirstIn(recordNameFilter).isDefined && wildcardRegex.findFirstIn(recordNameFilter).isEmpty } + + def unchangedRecordSet( + existing: RecordSet, + updates: RecordSet + ): Either[Throwable, Unit] = + Either.cond( + updates.typ == existing.typ && + updates.records == existing.records && + updates.id == existing.id && + updates.zoneId == existing.zoneId && + updates.name == existing.name && + updates.ownerGroupId == existing.ownerGroupId && + updates.ttl == existing.ttl, + (), + InvalidRequest("Cannot update RecordSet's if user not a member of ownership group. User can only request for ownership transfer") + ) + + def recordSetOwnerShipApproveStatus( + updates: RecordSet, + ): Either[Throwable, Unit] = + Either.cond( + updates.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("") != OwnerShipTransferStatus.ManuallyApproved && + updates.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("") != OwnerShipTransferStatus.AutoApproved && + updates.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("") != 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.") + ) } diff --git a/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneProtocol.scala b/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneProtocol.scala index 2d1ce0dd2..94b663cf2 100644 --- a/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneProtocol.scala +++ b/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneProtocol.scala @@ -21,7 +21,7 @@ import vinyldns.core.domain.record.RecordSetChangeStatus.RecordSetChangeStatus import vinyldns.core.domain.record.RecordSetChangeType.RecordSetChangeType import vinyldns.core.domain.record.RecordSetStatus.RecordSetStatus import vinyldns.core.domain.record.RecordType.RecordType -import vinyldns.core.domain.record.{RecordData, RecordSet, RecordSetChange} +import vinyldns.core.domain.record.{RecordData, RecordSet, RecordSetChange, OwnerShipTransfer} import vinyldns.core.domain.zone.{ACLRuleInfo, AccessLevel, Zone, ZoneACL, ZoneChange, ZoneConnection} import vinyldns.core.domain.zone.AccessLevel.AccessLevel import vinyldns.core.domain.zone.ZoneStatus.ZoneStatus @@ -180,6 +180,7 @@ case class RecordSetListInfo( accessLevel: AccessLevel, ownerGroupId: Option[String], ownerGroupName: Option[String], + recordSetGroupChange: Option[OwnerShipTransfer], fqdn: Option[String] ) @@ -199,6 +200,7 @@ object RecordSetListInfo { accessLevel = accessLevel, ownerGroupId = recordSet.ownerGroupId, ownerGroupName = recordSet.ownerGroupName, + recordSetGroupChange = recordSet.recordSetGroupChange, fqdn = recordSet.fqdn ) } @@ -216,6 +218,7 @@ case class RecordSetInfo( account: String, ownerGroupId: Option[String], ownerGroupName: Option[String], + recordSetGroupChange: Option[OwnerShipTransfer], fqdn: Option[String] ) @@ -234,6 +237,7 @@ object RecordSetInfo { account = recordSet.account, ownerGroupId = recordSet.ownerGroupId, ownerGroupName = groupName, + recordSetGroupChange = recordSet.recordSetGroupChange, fqdn = recordSet.fqdn ) } @@ -251,6 +255,7 @@ case class RecordSetGlobalInfo( account: String, ownerGroupId: Option[String], ownerGroupName: Option[String], + recordSetGroupChange: Option[OwnerShipTransfer], fqdn: Option[String], zoneName: String, zoneShared: Boolean @@ -276,6 +281,7 @@ object RecordSetGlobalInfo { account = recordSet.account, ownerGroupId = recordSet.ownerGroupId, ownerGroupName = groupName, + recordSetGroupChange = recordSet.recordSetGroupChange, fqdn = recordSet.fqdn, zoneName = zoneName, zoneShared = zoneShared diff --git a/modules/api/src/main/scala/vinyldns/api/notifier/email/EmailNotifier.scala b/modules/api/src/main/scala/vinyldns/api/notifier/email/EmailNotifier.scala index dd0318a75..3eb7c5c84 100644 --- a/modules/api/src/main/scala/vinyldns/api/notifier/email/EmailNotifier.scala +++ b/modules/api/src/main/scala/vinyldns/api/notifier/email/EmailNotifier.scala @@ -25,26 +25,20 @@ import vinyldns.core.domain.batch.{ SingleChange, SingleDeleteRRSetChange } -import vinyldns.core.domain.membership.UserRepository -import vinyldns.core.domain.membership.User +import vinyldns.core.domain.membership.{GroupRepository, User, UserRepository} import org.slf4j.LoggerFactory import javax.mail.internet.{InternetAddress, MimeMessage} import javax.mail.{Address, Message, Session} import scala.util.Try -import vinyldns.core.domain.record.AData -import vinyldns.core.domain.record.AAAAData -import vinyldns.core.domain.record.CNAMEData -import vinyldns.core.domain.record.MXData -import vinyldns.core.domain.record.TXTData -import vinyldns.core.domain.record.PTRData -import vinyldns.core.domain.record.RecordData +import vinyldns.core.domain.record.{AAAAData, AData, CNAMEData, MXData, PTRData, RecordData, RecordSetChange, TXTData, OwnerShipTransferStatus} +import vinyldns.core.domain.record.OwnerShipTransferStatus.OwnerShipTransferStatus import java.time.format.{DateTimeFormatter, FormatStyle} import vinyldns.core.domain.batch.BatchChangeStatus._ import vinyldns.core.domain.batch.BatchChangeApprovalStatus._ import java.time.ZoneId -class EmailNotifier(config: EmailNotifierConfig, session: Session, userRepository: UserRepository) +class EmailNotifier(config: EmailNotifierConfig, session: Session, userRepository: UserRepository, groupRepository: GroupRepository) extends Notifier { private val logger = LoggerFactory.getLogger(classOf[EmailNotifier]) @@ -55,9 +49,11 @@ class EmailNotifier(config: EmailNotifierConfig, session: Session, userRepositor case _ => IO.unit } - def send(addresses: Address*)(buildMessage: Message => Message): IO[Unit] = IO { + + def send(toAddresses: Address*)(ccAddresses: Address*)(buildMessage: Message => Message): IO[Unit] = IO { val message = new MimeMessage(session) - message.setRecipients(Message.RecipientType.TO, addresses.toArray) + message.setRecipients(Message.RecipientType.TO, toAddresses.toArray) + message.setRecipients(Message.RecipientType.CC, ccAddresses.toArray) message.setFrom(config.from) buildMessage(message) message.saveChanges() @@ -67,10 +63,10 @@ class EmailNotifier(config: EmailNotifierConfig, session: Session, userRepositor transport.close() } - def sendBatchChangeNotification(bc: BatchChange): IO[Unit] = + def sendBatchChangeNotification(bc: BatchChange): IO[Unit] = { userRepository.getUser(bc.userId).flatMap { - case Some(UserWithEmail(email)) => - send(email) { message => + case Some(UserWithEmail(email)) => + send(email)() { message => message.setSubject(s"VinylDNS Batch change ${bc.id} results") message.setContent(formatBatchChange(bc), "text/html") message @@ -81,10 +77,52 @@ class EmailNotifier(config: EmailNotifierConfig, session: Session, userRepositor s"Unable to properly parse email for ${user.id}: ${user.email.getOrElse("")}" ) } - case None => IO { logger.warn(s"Unable to find user: ${bc.userId}") } + case None => IO { + logger.warn(s"Unable to find user: ${bc.userId}") + } case _ => IO.unit } + } + def sendRecordSetOwnerTransferNotification(rsc: RecordSetChange): IO[Unit] = { + val toUser = + for{ + group <- groupRepository.getGroup(rsc.recordSet.ownerGroupId.getOrElse("")) + member <- userRepository.getUser(group.map(_.memberIds.head).getOrElse("")) + user <- userRepository.getUser(member.get.id)} + yield user + + val cCUser = + for{ + group <- groupRepository.getGroup(rsc.recordSet.recordSetGroupChange.map(_.requestedOwnerGroupId.getOrElse("")).getOrElse("")) + member <- userRepository.getUser(group.map(_.memberIds.head).getOrElse("")) + user <- userRepository.getUser(member.get.id)} + yield user + + toUser.flatMap { + case Some(UserWithEmail(email)) => + cCUser.flatMap { ccEmail => + send(email)(new InternetAddress(ccEmail.get.email.get)) { message => + message.setSubject(s"VinylDNS RecordSet change ${rsc.id} results") + message.setContent(formatRecordSetChange(rsc), "text/html") + message + } + } + case Some(user: User) if user.email.isDefined => + IO { + logger.warn( + s"Unable to properly parse email for ${user.id}: ${user.email.getOrElse("")}" + ) + } + case None => + IO { + logger.warn(s"Unable to find user: ${rsc.userId}") + + } + case _ => + IO.unit + } + } def formatBatchChange(bc: BatchChange): String = { val sb = new StringBuilder // Batch change info @@ -93,7 +131,7 @@ class EmailNotifier(config: EmailNotifierConfig, session: Session, userRepositor | ${bc.comments.map(comments => s"Description: $comments
").getOrElse("")} | Created: ${DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL).withZone(ZoneId.systemDefault()).format(bc.createdTimestamp)}
| Id: ${bc.id}
- | Status: ${formatStatus(bc.approvalStatus, bc.status)}
""".stripMargin) + | Status: ${formatBatchStatus(bc.approvalStatus, bc.status)}
""".stripMargin) // For manually reviewed e-mails, add additional info; e-mails are not sent for pending batch changes if (bc.approvalStatus != AutoApproved) { @@ -125,7 +163,8 @@ class EmailNotifier(config: EmailNotifierConfig, session: Session, userRepositor sb.toString } - def formatStatus(approval: BatchChangeApprovalStatus, status: BatchChangeStatus): String = + + def formatBatchStatus(approval: BatchChangeApprovalStatus, status: BatchChangeStatus): String = (approval, status) match { case (ManuallyRejected, _) => "Rejected" case (BatchChangeApprovalStatus.PendingReview, _) => "Pending Review" @@ -133,6 +172,28 @@ class EmailNotifier(config: EmailNotifierConfig, session: Session, userRepositor case (_, status) => status.toString } + def formatRecordSetChange(rsc: RecordSetChange): String = { + + val sb = new StringBuilder + sb.append(s"""

RecordSet Ownership Transfer

+ | Submitter: ${ userRepository.getUser(rsc.userId).map(_.get.userName)} + | Id: ${rsc.id}
+ | Submitted time: ${DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL).withZone(ZoneId.systemDefault()).format(rsc.created)}
+ | OwnerShip Current Group: ${rsc.recordSet.ownerGroupId.getOrElse("none")}
+ | OwnerShip Transfer Group: ${rsc.recordSet.recordSetGroupChange.map(_.requestedOwnerGroupId.getOrElse("none")).getOrElse("none")}
+ | OwnerShip Transfer Status: ${formatOwnerShipStatus(rsc.recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).get)}
+ """.stripMargin) + sb.toString + } + + def formatOwnerShipStatus(status: OwnerShipTransferStatus): String = + status match { + case OwnerShipTransferStatus.ManuallyRejected => "Rejected" + case OwnerShipTransferStatus.PendingReview => "Pending Review" + case OwnerShipTransferStatus.ManuallyApproved => "Approved" + case OwnerShipTransferStatus.Cancelled => "Cancelled" + } + def formatSingleChange(sc: SingleChange, index: Int): String = sc match { case SingleAddChange( _, diff --git a/modules/api/src/main/scala/vinyldns/api/notifier/email/EmailNotifierProvider.scala b/modules/api/src/main/scala/vinyldns/api/notifier/email/EmailNotifierProvider.scala index e10768e17..bf82bce92 100644 --- a/modules/api/src/main/scala/vinyldns/api/notifier/email/EmailNotifierProvider.scala +++ b/modules/api/src/main/scala/vinyldns/api/notifier/email/EmailNotifierProvider.scala @@ -17,7 +17,7 @@ package vinyldns.api.notifier.email import vinyldns.core.notifier.{Notifier, NotifierConfig, NotifierProvider} -import vinyldns.core.domain.membership.UserRepository +import vinyldns.core.domain.membership.{GroupRepository, UserRepository} import pureconfig._ import pureconfig.generic.auto._ import pureconfig.module.catseffect.syntax._ @@ -30,13 +30,13 @@ class EmailNotifierProvider extends NotifierProvider { private implicit val cs: ContextShift[IO] = IO.contextShift(scala.concurrent.ExecutionContext.global) - def load(config: NotifierConfig, userRepository: UserRepository): IO[Notifier] = + def load(config: NotifierConfig, userRepository: UserRepository, groupRepository: GroupRepository): IO[Notifier] = for { emailConfig <- Blocker[IO].use( ConfigSource.fromConfig(config.settings).loadF[IO, EmailNotifierConfig](_) ) session <- createSession(emailConfig) - } yield new EmailNotifier(emailConfig, session, userRepository) + } yield new EmailNotifier(emailConfig, session, userRepository, groupRepository) def createSession(config: EmailNotifierConfig): IO[Session] = IO { Session.getInstance(config.smtp) diff --git a/modules/api/src/main/scala/vinyldns/api/notifier/sns/SnsNotifierProvider.scala b/modules/api/src/main/scala/vinyldns/api/notifier/sns/SnsNotifierProvider.scala index dccd3c56d..bd72c5650 100644 --- a/modules/api/src/main/scala/vinyldns/api/notifier/sns/SnsNotifierProvider.scala +++ b/modules/api/src/main/scala/vinyldns/api/notifier/sns/SnsNotifierProvider.scala @@ -17,7 +17,7 @@ package vinyldns.api.notifier.sns import vinyldns.core.notifier.{Notifier, NotifierConfig, NotifierProvider} -import vinyldns.core.domain.membership.UserRepository +import vinyldns.core.domain.membership.{GroupRepository, UserRepository} import pureconfig._ import pureconfig.generic.auto._ import pureconfig.module.catseffect.syntax._ @@ -35,7 +35,7 @@ class SnsNotifierProvider extends NotifierProvider { IO.contextShift(scala.concurrent.ExecutionContext.global) private val logger = LoggerFactory.getLogger(classOf[SnsNotifierProvider]) - def load(config: NotifierConfig, userRepository: UserRepository): IO[Notifier] = + def load(config: NotifierConfig, userRepository: UserRepository, groupRepository: GroupRepository): IO[Notifier] = for { snsConfig <- Blocker[IO].use( ConfigSource.fromConfig(config.settings).loadF[IO, SnsNotifierConfig](_) diff --git a/modules/api/src/main/scala/vinyldns/api/route/DnsJsonProtocol.scala b/modules/api/src/main/scala/vinyldns/api/route/DnsJsonProtocol.scala index 7c0961326..98669c4c2 100644 --- a/modules/api/src/main/scala/vinyldns/api/route/DnsJsonProtocol.scala +++ b/modules/api/src/main/scala/vinyldns/api/route/DnsJsonProtocol.scala @@ -31,6 +31,8 @@ import vinyldns.core.domain.{EncryptFromJson, Encrypted, Fqdn} import vinyldns.core.domain.record._ import vinyldns.core.domain.zone._ import vinyldns.core.Messages._ +import vinyldns.core.domain.record.OwnerShipTransferStatus +import vinyldns.core.domain.record.OwnerShipTransferStatus.OwnerShipTransferStatus trait DnsJsonProtocol extends JsonValidation { import vinyldns.core.domain.record.RecordType._ @@ -42,11 +44,13 @@ trait DnsJsonProtocol extends JsonValidation { AlgorithmSerializer, EncryptedSerializer, RecordSetSerializer, + ownerShipTransferSerializer, RecordSetListInfoSerializer, RecordSetGlobalInfoSerializer, RecordSetInfoSerializer, RecordSetChangeSerializer, JsonEnumV(ZoneStatus), + JsonEnumV(OwnerShipTransferStatus), JsonEnumV(ZoneChangeStatus), JsonEnumV(RecordSetStatus), JsonEnumV(RecordSetChangeStatus), @@ -232,6 +236,7 @@ trait DnsJsonProtocol extends JsonValidation { (js \ "id").default[String](UUID.randomUUID().toString), (js \ "account").default[String]("system"), (js \ "ownerGroupId").optional[String], + (js \ "recordSetGroupChange").optional[OwnerShipTransfer], (js \ "fqdn").optional[String] ).mapN(RecordSet.apply) @@ -256,9 +261,23 @@ trait DnsJsonProtocol extends JsonValidation { ("id" -> rs.id) ~ ("account" -> rs.account) ~ ("ownerGroupId" -> rs.ownerGroupId) ~ + ("recordSetGroupChange" -> Extraction.decompose(rs.recordSetGroupChange)) ~ ("fqdn" -> rs.fqdn) } + + case object ownerShipTransferSerializer extends ValidationSerializer[OwnerShipTransfer] { + override def fromJson(js: JValue): ValidatedNel[String, OwnerShipTransfer] = + ( + (js \ "ownerShipTransferStatus").required[OwnerShipTransferStatus]("Missing ownerShipTransfer.ownerShipTransferStatus"), + (js \ "requestedOwnerGroupId").optional[String], + ).mapN(OwnerShipTransfer.apply) + + override def toJson(rsa: OwnerShipTransfer): JValue = + ("ownerShipTransferStatus" -> Extraction.decompose(rsa.ownerShipTransferStatus)) ~ + ("requestedOwnerGroupId" -> Extraction.decompose(rsa.requestedOwnerGroupId)) + } + case object RecordSetListInfoSerializer extends ValidationSerializer[RecordSetListInfo] { override def fromJson(js: JValue): ValidatedNel[String, RecordSetListInfo] = ( @@ -280,6 +299,7 @@ trait DnsJsonProtocol extends JsonValidation { ("accessLevel" -> rs.accessLevel.toString) ~ ("ownerGroupId" -> rs.ownerGroupId) ~ ("ownerGroupName" -> rs.ownerGroupName) ~ + ("recordSetGroupChange" -> Extraction.decompose(rs.recordSetGroupChange)) ~ ("fqdn" -> rs.fqdn) } @@ -301,6 +321,7 @@ trait DnsJsonProtocol extends JsonValidation { ("account" -> rs.account) ~ ("ownerGroupId" -> rs.ownerGroupId) ~ ("ownerGroupName" -> rs.ownerGroupName) ~ + ("recordSetGroupChange" -> Extraction.decompose(rs.recordSetGroupChange)) ~ ("fqdn" -> rs.fqdn) } @@ -326,6 +347,7 @@ trait DnsJsonProtocol extends JsonValidation { ("account" -> rs.account) ~ ("ownerGroupId" -> rs.ownerGroupId) ~ ("ownerGroupName" -> rs.ownerGroupName) ~ + ("recordSetGroupChange" -> Extraction.decompose(rs.recordSetGroupChange)) ~ ("fqdn" -> rs.fqdn) ~ ("zoneName" -> rs.zoneName) ~ ("zoneShared" -> rs.zoneShared) diff --git a/modules/api/src/test/functional/tests/recordsets/update_recordset_test.py b/modules/api/src/test/functional/tests/recordsets/update_recordset_test.py index 6a4ef58cf..7fe4db8a3 100644 --- a/modules/api/src/test/functional/tests/recordsets/update_recordset_test.py +++ b/modules/api/src/test/functional/tests/recordsets/update_recordset_test.py @@ -1928,6 +1928,562 @@ def test_update_from_acl_for_shared_zone_passes(shared_zone_test_context): delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202) shared_client.wait_until_recordset_change_status(delete_result, "Complete") +def test_update_owner_group_transfer_auto_approved(shared_zone_test_context): + """ + Test auto approve ownerShip transfer, for shared zones + """ + shared_client = shared_zone_test_context.shared_zone_vinyldns_client + zone = shared_zone_test_context.shared_zone + shared_group = shared_zone_test_context.shared_record_group + ok_group = shared_zone_test_context.ok_group + + update_rs = None + + try: + record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}]) + record_json["ownerGroupId"] = shared_group["id"] + create_response = shared_client.create_recordset(record_json, status=202) + update = shared_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"] + assert_that(update["ownerGroupId"], is_(shared_group["id"])) + + recordset_group_change_json = {"ownerShipTransferStatus": "AutoApproved", + "requestedOwnerGroupId": ok_group["id"]} + + update["recordSetGroupChange"] = recordset_group_change_json + update_response = shared_client.update_recordset(update, status=202) + update_rs = shared_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"] + assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_json)) + assert_that(update_rs["ownerGroupId"], is_(ok_group["id"])) + finally: + if update_rs: + delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202) + shared_client.wait_until_recordset_change_status(delete_result, "Complete") + + +def test_update_owner_group_transfer_request(shared_zone_test_context): + """ + Test requesting ownerShip transfer, for shared zones + """ + shared_client = shared_zone_test_context.shared_zone_vinyldns_client + dummy_client = shared_zone_test_context.dummy_vinyldns_client + zone = shared_zone_test_context.shared_zone + shared_group = shared_zone_test_context.shared_record_group + dummy_group = shared_zone_test_context.dummy_group + + update_rs = None + + try: + record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}]) + record_json["ownerGroupId"] = dummy_group["id"] + + create_response = dummy_client.create_recordset(record_json, status=202) + update = dummy_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"] + assert_that(update["ownerGroupId"], is_(dummy_group["id"])) + + recordset_group_change_json = {"ownerShipTransferStatus": "Requested", + "requestedOwnerGroupId": shared_group["id"]} + recordset_group_change_pending_review_json = {"ownerShipTransferStatus": "PendingReview", + "requestedOwnerGroupId": shared_group["id"]} + update["recordSetGroupChange"] = recordset_group_change_json + + update_response = shared_client.update_recordset(update, status=202) + update_rs = shared_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"] + assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_pending_review_json)) + assert_that(update_rs["ownerGroupId"], is_(dummy_group["id"])) + + finally: + if update_rs: + delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202) + shared_client.wait_until_recordset_change_status(delete_result, "Complete") + + +def test_update_request_owner_group_transfer_manually_approved(shared_zone_test_context): + """ + Test approving ownerShip transfer request, for shared zones + """ + shared_client = shared_zone_test_context.shared_zone_vinyldns_client + ok_client = shared_zone_test_context.ok_vinyldns_client + zone = shared_zone_test_context.shared_zone + shared_group = shared_zone_test_context.shared_record_group + ok_group = shared_zone_test_context.ok_group + + update_rs = None + + try: + record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}]) + record_json["ownerGroupId"] = shared_group["id"] + + create_response = shared_client.create_recordset(record_json, status=202) + update = shared_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"] + assert_that(update["ownerGroupId"], is_(shared_group["id"])) + + recordset_group_change_json = {"ownerShipTransferStatus": "Requested", + "requestedOwnerGroupId": ok_group["id"]} + recordset_group_change_pending_review_json = {"ownerShipTransferStatus": "PendingReview", + "requestedOwnerGroupId": ok_group["id"]} + update["recordSetGroupChange"] = recordset_group_change_json + + update_response = ok_client.update_recordset(update, status=202) + update_rs = ok_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"] + assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_pending_review_json)) + assert_that(update_rs["ownerGroupId"], is_(shared_group["id"])) + + recordset_group_change_json = {"ownerShipTransferStatus": "ManuallyApproved"} + recordset_group_change_manually_approved_json = {"ownerShipTransferStatus": "ManuallyApproved", + "requestedOwnerGroupId": ok_group["id"]} + update_rs["recordSetGroupChange"] = recordset_group_change_json + update_rs_response = shared_client.update_recordset(update_rs, status=202) + update_rs_ownership = shared_client.wait_until_recordset_change_status(update_rs_response, "Complete")[ + "recordSet"] + assert_that(update_rs_ownership["recordSetGroupChange"], is_(recordset_group_change_manually_approved_json)) + assert_that(update_rs_ownership["ownerGroupId"], is_(ok_group["id"])) + + finally: + if update_rs: + delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202) + shared_client.wait_until_recordset_change_status(delete_result, "Complete") + + +def test_update_request_owner_group_transfer_manually_rejected(shared_zone_test_context): + """ + Test rejecting ownerShip transfer request, for shared zones + """ + shared_client = shared_zone_test_context.shared_zone_vinyldns_client + ok_client = shared_zone_test_context.ok_vinyldns_client + zone = shared_zone_test_context.shared_zone + shared_group = shared_zone_test_context.shared_record_group + ok_group = shared_zone_test_context.ok_group + + update_rs = None + + try: + record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}]) + record_json["ownerGroupId"] = shared_group["id"] + + create_response = shared_client.create_recordset(record_json, status=202) + update = shared_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"] + assert_that(update["ownerGroupId"], is_(shared_group["id"])) + + recordset_group_change_json = {"ownerShipTransferStatus": "Requested", + "requestedOwnerGroupId": ok_group["id"]} + recordset_group_change_pending_review_json = {"ownerShipTransferStatus": "PendingReview", + "requestedOwnerGroupId": ok_group["id"]} + update["recordSetGroupChange"] = recordset_group_change_json + + update_response = ok_client.update_recordset(update, status=202) + update_rs = ok_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"] + assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_pending_review_json)) + assert_that(update_rs["ownerGroupId"], is_(shared_group["id"])) + + recordset_group_change_json = {"ownerShipTransferStatus": "ManuallyRejected"} + recordset_group_change_manually_rejected_json = {"ownerShipTransferStatus": "ManuallyRejected", + "requestedOwnerGroupId": ok_group["id"]} + update_rs["recordSetGroupChange"] = recordset_group_change_json + update_rs_response = shared_client.update_recordset(update_rs, status=202) + update_rs_ownership = shared_client.wait_until_recordset_change_status(update_rs_response, "Complete")[ + "recordSet"] + assert_that(update_rs_ownership["recordSetGroupChange"], is_(recordset_group_change_manually_rejected_json)) + assert_that(update_rs_ownership["ownerGroupId"], is_(shared_group["id"])) + + finally: + if update_rs: + delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202) + shared_client.wait_until_recordset_change_status(delete_result, "Complete") + + +def test_update_request_owner_group_transfer_cancelled(shared_zone_test_context): + """ + Test cancelling ownerShip transfer request + """ + shared_client = shared_zone_test_context.shared_zone_vinyldns_client + ok_client = shared_zone_test_context.ok_vinyldns_client + zone = shared_zone_test_context.shared_zone + shared_group = shared_zone_test_context.shared_record_group + ok_group = shared_zone_test_context.ok_group + + update_rs = None + + try: + record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}]) + record_json["ownerGroupId"] = shared_group["id"] + + create_response = shared_client.create_recordset(record_json, status=202) + update = shared_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"] + assert_that(update["ownerGroupId"], is_(shared_group["id"])) + + recordset_group_change_json = {"ownerShipTransferStatus": "Requested", + "requestedOwnerGroupId": ok_group["id"]} + recordset_group_change_pending_review_json = {"ownerShipTransferStatus": "PendingReview", + "requestedOwnerGroupId": ok_group["id"]} + update["recordSetGroupChange"] = recordset_group_change_json + + update_response = ok_client.update_recordset(update, status=202) + update_rs = ok_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"] + assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_pending_review_json)) + assert_that(update_rs["ownerGroupId"], is_(shared_group["id"])) + + recordset_group_change_json = {"ownerShipTransferStatus": "Cancelled"} + recordset_group_change_cancelled_json = {"ownerShipTransferStatus": "Cancelled", + "requestedOwnerGroupId": ok_group["id"]} + update_rs["recordSetGroupChange"] = recordset_group_change_json + update_rs_response = ok_client.update_recordset(update_rs, status=202) + update_rs_ownership = ok_client.wait_until_recordset_change_status(update_rs_response, "Complete")["recordSet"] + assert_that(update_rs_ownership["recordSetGroupChange"], is_(recordset_group_change_cancelled_json)) + assert_that(update_rs_ownership["ownerGroupId"], is_(shared_group["id"])) + + finally: + if update_rs: + delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202) + shared_client.wait_until_recordset_change_status(delete_result, "Complete") + + +def test_update_owner_group_transfer_approval_to_group_a_user_is_not_in_fails(shared_zone_test_context): + """ + Test approving ownerShip transfer request, for user not a member of owner group + """ + shared_client = shared_zone_test_context.shared_zone_vinyldns_client + dummy_client = shared_zone_test_context.dummy_vinyldns_client + zone = shared_zone_test_context.shared_zone + shared_group = shared_zone_test_context.shared_record_group + dummy_group = shared_zone_test_context.dummy_group + + update_rs = None + + try: + record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}]) + record_json["ownerGroupId"] = dummy_group["id"] + + create_response = dummy_client.create_recordset(record_json, status=202) + update = dummy_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"] + assert_that(update["ownerGroupId"], is_(dummy_group["id"])) + + recordset_group_change_json = {"ownerShipTransferStatus": "Requested", + "requestedOwnerGroupId": shared_group["id"]} + recordset_group_change_pending_review_json = {"ownerShipTransferStatus": "PendingReview", + "requestedOwnerGroupId": shared_group["id"]} + update["recordSetGroupChange"] = recordset_group_change_json + + update_response = shared_client.update_recordset(update, status=202) + update_rs = shared_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"] + assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_pending_review_json)) + assert_that(update_rs["ownerGroupId"], is_(dummy_group["id"])) + + recordset_group_change_approved_json = {"ownerShipTransferStatus": "ManuallyApproved"} + + update_rs["recordSetGroupChange"] = recordset_group_change_approved_json + error = shared_client.update_recordset(update_rs, status=422) + assert_that(error, is_(f"User not in record owner group with id \"{dummy_group['id']}\"")) + + finally: + if update_rs: + delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202) + shared_client.wait_until_recordset_change_status(delete_result, "Complete") + + +def test_update_owner_group_transfer_reject_to_group_a_user_is_not_in_fails(shared_zone_test_context): + """ + Test rejecting ownerShip transfer request, for user not a member of owner group + """ + shared_client = shared_zone_test_context.shared_zone_vinyldns_client + dummy_client = shared_zone_test_context.dummy_vinyldns_client + zone = shared_zone_test_context.shared_zone + shared_group = shared_zone_test_context.shared_record_group + dummy_group = shared_zone_test_context.dummy_group + + update_rs = None + + try: + record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}]) + record_json["ownerGroupId"] = dummy_group["id"] + + create_response = dummy_client.create_recordset(record_json, status=202) + update = dummy_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"] + assert_that(update["ownerGroupId"], is_(dummy_group["id"])) + + recordset_group_change_json = {"ownerShipTransferStatus": "Requested", + "requestedOwnerGroupId": shared_group["id"]} + recordset_group_change_pending_review_json = {"ownerShipTransferStatus": "PendingReview", + "requestedOwnerGroupId": shared_group["id"]} + update["recordSetGroupChange"] = recordset_group_change_json + + update_response = shared_client.update_recordset(update, status=202) + update_rs = shared_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"] + assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_pending_review_json)) + assert_that(update_rs["ownerGroupId"], is_(dummy_group["id"])) + + recordset_group_change_approved_json = {"ownerShipTransferStatus": "ManuallyRejected"} + update_rs["recordSetGroupChange"] = recordset_group_change_approved_json + error = shared_client.update_recordset(update_rs, status=422) + assert_that(error, is_(f"User not in record owner group with id \"{dummy_group['id']}\"")) + + finally: + if update_rs: + delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202) + shared_client.wait_until_recordset_change_status(delete_result, "Complete") + + +def test_update_owner_group_transfer_auto_approved_to_group_a_user_is_not_in_fails(shared_zone_test_context): + """ + Test approving ownerShip transfer request, for user not a member of owner group + """ + shared_client = shared_zone_test_context.shared_zone_vinyldns_client + dummy_client = shared_zone_test_context.dummy_vinyldns_client + zone = shared_zone_test_context.shared_zone + shared_group = shared_zone_test_context.shared_record_group + dummy_group = shared_zone_test_context.dummy_group + + update_rs = None + + try: + record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}]) + record_json["ownerGroupId"] = dummy_group["id"] + + create_response = dummy_client.create_recordset(record_json, status=202) + update = dummy_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"] + assert_that(update["ownerGroupId"], is_(dummy_group["id"])) + + recordset_group_change_json = {"ownerShipTransferStatus": "Requested", + "requestedOwnerGroupId": shared_group["id"]} + recordset_group_change_pending_review_json = {"ownerShipTransferStatus": "PendingReview", + "requestedOwnerGroupId": shared_group["id"]} + update["recordSetGroupChange"] = recordset_group_change_json + + update_response = shared_client.update_recordset(update, status=202) + update_rs = shared_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"] + assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_pending_review_json)) + assert_that(update_rs["ownerGroupId"], is_(dummy_group["id"])) + + recordset_group_change_approved_json = {"ownerShipTransferStatus": "AutoApproved"} + + update_rs["recordSetGroupChange"] = recordset_group_change_approved_json + error = shared_client.update_recordset(update_rs, status=422) + assert_that(error, is_(f"Record owner group with id \"{dummy_group['id']}\" not found")) + + finally: + if update_rs: + delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202) + shared_client.wait_until_recordset_change_status(delete_result, "Complete") + + +def test_update_owner_group_transfer_approved_when_request_cancelled_in_fails(shared_zone_test_context): + """ + Test approving ownerShip transfer, for cancelled request + """ + shared_client = shared_zone_test_context.shared_zone_vinyldns_client + ok_client = shared_zone_test_context.ok_vinyldns_client + zone = shared_zone_test_context.shared_zone + shared_group = shared_zone_test_context.shared_record_group + ok_group = shared_zone_test_context.ok_group + + update_rs = None + + try: + record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}]) + record_json["ownerGroupId"] = shared_group["id"] + + create_response = shared_client.create_recordset(record_json, status=202) + update = shared_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"] + assert_that(update["ownerGroupId"], is_(shared_group["id"])) + + recordset_group_change_json = {"ownerShipTransferStatus": "Requested", + "requestedOwnerGroupId": ok_group["id"]} + recordset_group_change_pending_review_json = {"ownerShipTransferStatus": "PendingReview", + "requestedOwnerGroupId": ok_group["id"]} + update["recordSetGroupChange"] = recordset_group_change_json + + update_response = ok_client.update_recordset(update, status=202) + update_rs = ok_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"] + assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_pending_review_json)) + assert_that(update_rs["ownerGroupId"], is_(shared_group["id"])) + + recordset_group_change_json = {"ownerShipTransferStatus": "Cancelled"} + recordset_group_change_cancelled_json = {"ownerShipTransferStatus": "Cancelled", + "requestedOwnerGroupId": ok_group["id"]} + update_rs["recordSetGroupChange"] = recordset_group_change_json + update_rs_response = ok_client.update_recordset(update_rs, status=202) + update_rs_ownership = ok_client.wait_until_recordset_change_status(update_rs_response, "Complete")["recordSet"] + assert_that(update_rs_ownership["recordSetGroupChange"], is_(recordset_group_change_cancelled_json)) + assert_that(update_rs_ownership["ownerGroupId"], is_(shared_group["id"])) + + recordset_group_change_json = {"ownerShipTransferStatus": "ManuallyApproved"} + + update_rs["recordSetGroupChange"] = recordset_group_change_json + error = ok_client.update_recordset(update_rs, status=422) + assert_that(error, is_("Cannot update RecordSet OwnerShip Status when request is cancelled.")) + + finally: + if update_rs: + delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202) + shared_client.wait_until_recordset_change_status(delete_result, "Complete") + + +def test_update_owner_group_transfer_rejected_when_request_cancelled_in_fails(shared_zone_test_context): + """ + Test rejecting ownerShip transfer, for cancelled request + """ + shared_client = shared_zone_test_context.shared_zone_vinyldns_client + ok_client = shared_zone_test_context.ok_vinyldns_client + zone = shared_zone_test_context.shared_zone + shared_group = shared_zone_test_context.shared_record_group + ok_group = shared_zone_test_context.ok_group + + update_rs = None + + try: + record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}]) + record_json["ownerGroupId"] = shared_group["id"] + + create_response = shared_client.create_recordset(record_json, status=202) + update = shared_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"] + assert_that(update["ownerGroupId"], is_(shared_group["id"])) + + recordset_group_change_json = {"ownerShipTransferStatus": "Requested", + "requestedOwnerGroupId": ok_group["id"]} + recordset_group_change_pending_review_json = {"ownerShipTransferStatus": "PendingReview", + "requestedOwnerGroupId": ok_group["id"]} + update["recordSetGroupChange"] = recordset_group_change_json + + update_response = ok_client.update_recordset(update, status=202) + update_rs = ok_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"] + assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_pending_review_json)) + assert_that(update_rs["ownerGroupId"], is_(shared_group["id"])) + + recordset_group_change_json = {"ownerShipTransferStatus": "Cancelled"} + recordset_group_change_cancelled_json = {"ownerShipTransferStatus": "Cancelled", + "requestedOwnerGroupId": ok_group["id"]} + update_rs["recordSetGroupChange"] = recordset_group_change_json + update_rs_response = ok_client.update_recordset(update_rs, status=202) + update_rs_ownership = ok_client.wait_until_recordset_change_status(update_rs_response, "Complete")["recordSet"] + assert_that(update_rs_ownership["recordSetGroupChange"], is_(recordset_group_change_cancelled_json)) + assert_that(update_rs_ownership["ownerGroupId"], is_(shared_group["id"])) + + recordset_group_change_json = {"ownerShipTransferStatus": "ManuallyRejected"} + + update_rs["recordSetGroupChange"] = recordset_group_change_json + error = ok_client.update_recordset(update_rs, status=422) + assert_that(error, is_("Cannot update RecordSet OwnerShip Status when request is cancelled.")) + + finally: + if update_rs: + delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202) + shared_client.wait_until_recordset_change_status(delete_result, "Complete") + + +def test_update_owner_group_transfer_auto_approved_when_request_cancelled_in_fails(shared_zone_test_context): + """ + Test auto_approving ownerShip transfer, for cancelled request + """ + shared_client = shared_zone_test_context.shared_zone_vinyldns_client + ok_client = shared_zone_test_context.ok_vinyldns_client + zone = shared_zone_test_context.shared_zone + shared_group = shared_zone_test_context.shared_record_group + ok_group = shared_zone_test_context.ok_group + + update_rs = None + + try: + record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}]) + record_json["ownerGroupId"] = shared_group["id"] + + create_response = shared_client.create_recordset(record_json, status=202) + update = shared_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"] + assert_that(update["ownerGroupId"], is_(shared_group["id"])) + + recordset_group_change_json = {"ownerShipTransferStatus": "Requested", + "requestedOwnerGroupId": ok_group["id"]} + recordset_group_change_pending_review_json = {"ownerShipTransferStatus": "PendingReview", + "requestedOwnerGroupId": ok_group["id"]} + update["recordSetGroupChange"] = recordset_group_change_json + + update_response = ok_client.update_recordset(update, status=202) + update_rs = ok_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"] + assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_pending_review_json)) + assert_that(update_rs["ownerGroupId"], is_(shared_group["id"])) + + recordset_group_change_json = {"ownerShipTransferStatus": "Cancelled"} + recordset_group_change_cancelled_json = {"ownerShipTransferStatus": "Cancelled", + "requestedOwnerGroupId": ok_group["id"]} + update_rs["recordSetGroupChange"] = recordset_group_change_json + update_rs_response = ok_client.update_recordset(update_rs, status=202) + update_rs_ownership = ok_client.wait_until_recordset_change_status(update_rs_response, "Complete")["recordSet"] + assert_that(update_rs_ownership["recordSetGroupChange"], is_(recordset_group_change_cancelled_json)) + assert_that(update_rs_ownership["ownerGroupId"], is_(shared_group["id"])) + + recordset_group_change_json = {"ownerShipTransferStatus": "AutoApproved"} + + update_rs["recordSetGroupChange"] = recordset_group_change_json + error = ok_client.update_recordset(update_rs, status=422) + assert_that(error, is_("Cannot update RecordSet OwnerShip Status when request is cancelled.")) + + finally: + if update_rs: + delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202) + shared_client.wait_until_recordset_change_status(delete_result, "Complete") + + +def test_update_owner_group_transfer_on_non_shared_zones_in_fails(shared_zone_test_context): + """ + Test that requesting ownerShip transfer for non shared zones + """ + shared_client = shared_zone_test_context.shared_zone_vinyldns_client + ok_client = shared_zone_test_context.ok_vinyldns_client + shared_group = shared_zone_test_context.shared_record_group + ok_zone = shared_zone_test_context.ok_zone + + update_rs = None + + try: + record_json = create_recordset(ok_zone, "test_update_success", "A", [{"address": "1.1.1.1"}]) + + 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 + 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"]} + update["recordSetGroupChange"] = recordset_group_change_json + update["ttl"] = update["ttl"] + 100 + + error = ok_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): """ diff --git a/modules/api/src/test/scala/vinyldns/api/domain/record/RecordSetServiceSpec.scala b/modules/api/src/test/scala/vinyldns/api/domain/record/RecordSetServiceSpec.scala index 137579884..3334d8412 100644 --- a/modules/api/src/test/scala/vinyldns/api/domain/record/RecordSetServiceSpec.scala +++ b/modules/api/src/test/scala/vinyldns/api/domain/record/RecordSetServiceSpec.scala @@ -40,6 +40,8 @@ import vinyldns.core.domain.membership.{GroupRepository, ListUsersResults, UserR import vinyldns.core.domain.record._ import vinyldns.core.domain.zone._ import vinyldns.core.queue.MessageQueue +import vinyldns.core.notifier.{AllNotifiers, Notification, Notifier} +import scala.concurrent.ExecutionContext class RecordSetServiceSpec extends AnyWordSpec @@ -47,6 +49,7 @@ class RecordSetServiceSpec with Matchers with MockitoSugar with BeforeAndAfterEach { + private implicit val contextShift: ContextShift[IO] = IO.contextShift(ExecutionContext.global) private val mockZoneRepo = mock[ZoneRepository] private val mockGroupRepo = mock[GroupRepository] @@ -58,6 +61,8 @@ class RecordSetServiceSpec private val mockBackend = mock[Backend] 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(zoneNotAuthorized))) @@ -85,7 +90,8 @@ class RecordSetServiceSpec VinylDNSTestHelpers.highValueDomainConfig, VinylDNSTestHelpers.dottedHostsConfig, VinylDNSTestHelpers.approvedNameServers, - true + true, + mockNotifiers ) val underTestWithDnsBackendValidations = new RecordSetService( @@ -104,7 +110,8 @@ class RecordSetServiceSpec VinylDNSTestHelpers.highValueDomainConfig, VinylDNSTestHelpers.dottedHostsConfig, VinylDNSTestHelpers.approvedNameServers, - true + true, + mockNotifiers ) val underTestWithEmptyDottedHostsConfig = new RecordSetService( @@ -123,7 +130,8 @@ class RecordSetServiceSpec VinylDNSTestHelpers.highValueDomainConfig, VinylDNSTestHelpers.emptyDottedHostsConfig, VinylDNSTestHelpers.approvedNameServers, - true + true, + mockNotifiers ) def getDottedHostsConfigGroupsAllowed(zone: Zone, config: DottedHostsConfig): List[String] = { @@ -1177,6 +1185,9 @@ class RecordSetServiceSpec doReturn(IO.pure(Some(oldRecord))) .when(mockRecordRepo) .getRecordSet(oldRecord.id) + doReturn(IO.pure(Some(newRecord))) + .when(mockRecordRepo) + .getRecordSet(newRecord.id) doReturn(IO.pure(List(oldRecord))) .when(mockRecordRepo) .getRecordSetsByName(zone.id, oldRecord.name) @@ -1185,7 +1196,7 @@ class RecordSetServiceSpec .getGroup(okGroup.id) 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 { val zone = okZone.copy(shared = true, id = "test-owner-group") @@ -1317,6 +1328,9 @@ class RecordSetServiceSpec doReturn(IO.pure(List(oldRecord))) .when(mockRecordRepo) .getRecordSetsByName(zone.id, newRecord.name) + doReturn(IO.pure(Some(newRecord))) + .when(mockRecordRepo) + .getRecordSet(newRecord.id) doReturn(IO.pure(Set(dottedZone, abcZone, xyzZone))) .when(mockZoneRepo) .getZonesByNames(dottedHostsConfigZonesAllowed.toSet) @@ -2197,4 +2211,514 @@ class RecordSetServiceSpec "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) + } + } } + diff --git a/modules/api/src/test/scala/vinyldns/api/domain/record/RecordSetValidationsSpec.scala b/modules/api/src/test/scala/vinyldns/api/domain/record/RecordSetValidationsSpec.scala index 16fa80833..b104f0c3f 100644 --- a/modules/api/src/test/scala/vinyldns/api/domain/record/RecordSetValidationsSpec.scala +++ b/modules/api/src/test/scala/vinyldns/api/domain/record/RecordSetValidationsSpec.scala @@ -722,5 +722,85 @@ class RecordSetValidationsSpec 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." + } + } } } diff --git a/modules/api/src/test/scala/vinyldns/api/notifier/email/EmailNotifierSpec.scala b/modules/api/src/test/scala/vinyldns/api/notifier/email/EmailNotifierSpec.scala index 3a2c10d2b..2c959048a 100644 --- a/modules/api/src/test/scala/vinyldns/api/notifier/email/EmailNotifierSpec.scala +++ b/modules/api/src/test/scala/vinyldns/api/notifier/email/EmailNotifierSpec.scala @@ -22,7 +22,7 @@ import org.scalatestplus.mockito.MockitoSugar import vinyldns.api.CatsHelpers import javax.mail.{Provider, Session, Transport, URLName} 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 javax.mail.internet.InternetAddress @@ -34,14 +34,16 @@ import javax.mail.{Address, Message} import _root_.vinyldns.core.domain.batch._ import java.time.Instant import java.time.temporal.ChronoUnit -import vinyldns.core.domain.record.RecordType -import vinyldns.core.domain.record.AData +import vinyldns.core.domain.record.{AData, OwnerShipTransferStatus, RecordSetChange, RecordSetChangeStatus, RecordSetChangeType, RecordType} import com.typesafe.config.Config import com.typesafe.config.ConfigFactory import vinyldns.core.domain.Encrypted import scala.collection.JavaConverters._ 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 { val mockTransport: Transport = mock[Transport] @@ -68,6 +70,7 @@ class EmailNotifierSpec import MockTransport._ val mockUserRepository: UserRepository = mock[UserRepository] + val mockGroupRepository: GroupRepository = mock[GroupRepository] val session: Session = Session.getInstance(new Properties()) session.setProvider( new Provider( @@ -100,6 +103,17 @@ class EmailNotifierSpec "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 { "do nothing for unsupported Notifications" in { val emailConfig: Config = ConfigFactory.parseMap( @@ -110,7 +124,7 @@ class EmailNotifierSpec ).asJava ) val notifier = new EmailNotifierProvider() - .load(NotifierConfig("", emailConfig), mockUserRepository) + .load(NotifierConfig("", emailConfig), mockUserRepository, mockGroupRepository) .unsafeRunSync() notifier.notify(new Notification("this won't be supported ever")) should be(IO.unit) @@ -120,7 +134,8 @@ class EmailNotifierSpec val notifier = new EmailNotifier( EmailNotifierConfig(new InternetAddress("test@test.com"), new Properties()), session, - mockUserRepository + mockUserRepository, + mockGroupRepository ) doReturn(IO.pure(Some(User("testUser", "access", Encrypted("secret"))))) @@ -132,11 +147,65 @@ class EmailNotifierSpec 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]) + 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(okUser))) + .when(mockUserRepository) + .getUser(okGroup.memberIds.head) + doReturn(IO.pure(Some(okUser))) + .when(mockUserRepository) + .getUser(okUser.id) + doReturn(IO.pure(Some(dummyGroup))) + .when(mockGroupRepository) + .getGroup(dummyGroup.id) + doReturn(IO.pure(Some(dummyUser.copy(email = Some("test@test.com"))))) + .when(mockUserRepository) + .getUser(dummyGroup.memberIds.head) + doReturn(IO.pure(Some(dummyUser.copy(email = Some("test@test.com"))))) + .when(mockUserRepository) + .getUser(dummyUser.id) + + 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 { val notifier = new EmailNotifier( EmailNotifierConfig(new InternetAddress("test@test.com"), new Properties()), session, - mockUserRepository + mockUserRepository, + mockGroupRepository ) doReturn(IO.pure(None)) @@ -148,12 +217,13 @@ class EmailNotifierSpec 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 notifier = new EmailNotifier( EmailNotifierConfig(fromAddress, new Properties()), session, - mockUserRepository + mockUserRepository, + mockGroupRepository ) doReturn( diff --git a/modules/api/src/test/scala/vinyldns/api/notifier/sns/SnsNotifierSpec.scala b/modules/api/src/test/scala/vinyldns/api/notifier/sns/SnsNotifierSpec.scala index 2adc41e7c..8f8277417 100644 --- a/modules/api/src/test/scala/vinyldns/api/notifier/sns/SnsNotifierSpec.scala +++ b/modules/api/src/test/scala/vinyldns/api/notifier/sns/SnsNotifierSpec.scala @@ -20,6 +20,7 @@ import org.scalatest.wordspec.AnyWordSpec import org.scalatest.BeforeAndAfterEach import org.scalatestplus.mockito.MockitoSugar import vinyldns.api.CatsHelpers +import vinyldns.core.domain.membership.{GroupRepository, UserRepository} import vinyldns.core.domain.membership.UserRepository import vinyldns.core.notifier.Notification import org.mockito.Matchers._ @@ -51,6 +52,7 @@ class SnsNotifierSpec with CatsHelpers { val mockUserRepository = mock[UserRepository] + val mockGroupRepository = mock[GroupRepository] val mockSns = mock[AmazonSNS] override protected def beforeEach(): Unit = @@ -86,7 +88,7 @@ class SnsNotifierSpec ).asJava ) val notifier = new SnsNotifierProvider() - .load(NotifierConfig("", snsConfig), mockUserRepository) + .load(NotifierConfig("", snsConfig), mockUserRepository, mockGroupRepository) .unsafeRunSync() notifier.notify(Notification("this won't be supported ever")) should be(IO.unit) diff --git a/modules/api/src/test/scala/vinyldns/api/route/VinylDNSJsonProtocolSpec.scala b/modules/api/src/test/scala/vinyldns/api/route/VinylDNSJsonProtocolSpec.scala index 3d9576b56..dd0ce0259 100644 --- a/modules/api/src/test/scala/vinyldns/api/route/VinylDNSJsonProtocolSpec.scala +++ b/modules/api/src/test/scala/vinyldns/api/route/VinylDNSJsonProtocolSpec.scala @@ -680,6 +680,206 @@ class VinylDNSJsonProtocolSpec val thrown = the[MappingException] thrownBy recordSetJValue.extract[RecordSet] 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") + } } } diff --git a/modules/core/src/main/protobuf/VinylDNSProto.proto b/modules/core/src/main/protobuf/VinylDNSProto.proto index 94883d599..4d710aa24 100644 --- a/modules/core/src/main/protobuf/VinylDNSProto.proto +++ b/modules/core/src/main/protobuf/VinylDNSProto.proto @@ -141,7 +141,13 @@ message RecordSet { repeated RecordData record = 9; required string account = 10; 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 { diff --git a/modules/core/src/main/scala/vinyldns/core/domain/record/RecordSet.scala b/modules/core/src/main/scala/vinyldns/core/domain/record/RecordSet.scala index da227dbca..6cdc2482c 100644 --- a/modules/core/src/main/scala/vinyldns/core/domain/record/RecordSet.scala +++ b/modules/core/src/main/scala/vinyldns/core/domain/record/RecordSet.scala @@ -16,6 +16,8 @@ package vinyldns.core.domain.record +import vinyldns.core.domain.record.OwnerShipTransferStatus.OwnerShipTransferStatus + import java.util.UUID import java.time.Instant @@ -33,6 +35,11 @@ object RecordSetStatus extends Enumeration { 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 RecordType._ @@ -48,6 +55,7 @@ case class RecordSet( id: String = UUID.randomUUID().toString, account: String = "system", ownerGroupId: Option[String] = None, + recordSetGroupChange: Option[OwnerShipTransfer] = None, fqdn: Option[String] = None ) { @@ -69,10 +77,25 @@ case class RecordSet( sb.append("account=\"").append(account).append("\"; ") sb.append("status=\"").append(status.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("]") 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 + }} + diff --git a/modules/core/src/main/scala/vinyldns/core/notifier/NotifierLoader.scala b/modules/core/src/main/scala/vinyldns/core/notifier/NotifierLoader.scala index 78a65cc4a..49f69a321 100644 --- a/modules/core/src/main/scala/vinyldns/core/notifier/NotifierLoader.scala +++ b/modules/core/src/main/scala/vinyldns/core/notifier/NotifierLoader.scala @@ -15,21 +15,21 @@ */ package vinyldns.core.notifier -import vinyldns.core.domain.membership.UserRepository +import vinyldns.core.domain.membership.{GroupRepository, UserRepository} import cats.effect.IO import cats.implicits._ import cats.effect.ContextShift object NotifierLoader { - def loadAll(configs: List[NotifierConfig], userRepository: UserRepository)( - implicit cs: ContextShift[IO] + def loadAll(configs: List[NotifierConfig], userRepository: UserRepository, groupRepository: GroupRepository)( + implicit cs: ContextShift[IO] ): IO[AllNotifiers] = for { - notifiers <- configs.parTraverse(load(_, userRepository)) + notifiers <- configs.parTraverse(load(_, userRepository, groupRepository)) } yield AllNotifiers(notifiers) - def load(config: NotifierConfig, userRepository: UserRepository): IO[Notifier] = + def load(config: NotifierConfig, userRepository: UserRepository, groupRepository: GroupRepository): IO[Notifier] = for { provider <- IO( Class @@ -38,7 +38,7 @@ object NotifierLoader { .newInstance() .asInstanceOf[NotifierProvider] ) - notifier <- provider.load(config, userRepository) + notifier <- provider.load(config, userRepository, groupRepository) } yield notifier } diff --git a/modules/core/src/main/scala/vinyldns/core/notifier/NotifierProvider.scala b/modules/core/src/main/scala/vinyldns/core/notifier/NotifierProvider.scala index c5fe76a98..71d2ba0e0 100644 --- a/modules/core/src/main/scala/vinyldns/core/notifier/NotifierProvider.scala +++ b/modules/core/src/main/scala/vinyldns/core/notifier/NotifierProvider.scala @@ -15,9 +15,9 @@ */ package vinyldns.core.notifier -import vinyldns.core.domain.membership.UserRepository +import vinyldns.core.domain.membership.{GroupRepository, UserRepository} import cats.effect.IO trait NotifierProvider { - def load(config: NotifierConfig, userRepository: UserRepository): IO[Notifier] + def load(config: NotifierConfig, userRepository: UserRepository, groupRepository: GroupRepository): IO[Notifier] } diff --git a/modules/core/src/main/scala/vinyldns/core/protobuf/ProtobufConversions.scala b/modules/core/src/main/scala/vinyldns/core/protobuf/ProtobufConversions.scala index 3ff0f76ec..2e0a852c9 100644 --- a/modules/core/src/main/scala/vinyldns/core/protobuf/ProtobufConversions.scala +++ b/modules/core/src/main/scala/vinyldns/core/protobuf/ProtobufConversions.scala @@ -100,9 +100,23 @@ trait ProtobufConversions { records = rs.getRecordList.asScala.map(rd => fromPB(rd, RecordType.withName(rs.getTyp))).toList, 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 = { val pbStatus = zn.getStatus val status = @@ -377,6 +391,7 @@ trait ProtobufConversions { rs.updated.foreach(dt => builder.setUpdated(dt.toEpochMilli)) 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 rs.records.map(toRecordData).foreach(rd => builder.addRecord(rd)) diff --git a/modules/core/src/test/scala/vinyldns/core/TestRecordSetData.scala b/modules/core/src/test/scala/vinyldns/core/TestRecordSetData.scala index 1c74f0b86..2b2d4f2b0 100644 --- a/modules/core/src/test/scala/vinyldns/core/TestRecordSetData.scala +++ b/modules/core/src/test/scala/vinyldns/core/TestRecordSetData.scala @@ -37,7 +37,8 @@ object TestRecordSetData { RecordSetStatus.Active, Instant.now.truncatedTo(ChronoUnit.MILLIS), None, - List(AData("10.1.1.1")) + List(AData("10.1.1.1")), + recordSetGroupChange = None ) val abcRecord: RecordSet = RecordSet( @@ -48,7 +49,8 @@ object TestRecordSetData { RecordSetStatus.Pending, Instant.now.truncatedTo(ChronoUnit.MILLIS), 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( @@ -59,7 +61,8 @@ object TestRecordSetData { RecordSetStatus.Pending, Instant.now.truncatedTo(ChronoUnit.MILLIS), 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( @@ -70,7 +73,8 @@ object TestRecordSetData { RecordSetStatus.Pending, Instant.now.truncatedTo(ChronoUnit.MILLIS), 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( @@ -81,7 +85,8 @@ object TestRecordSetData { RecordSetStatus.Pending, Instant.now.truncatedTo(ChronoUnit.MILLIS), None, - List(CNAMEData(Fqdn("cname"))) + List(CNAMEData(Fqdn("cname"))), + recordSetGroupChange= None ) val ptrIp4: RecordSet = RecordSet( @@ -92,7 +97,8 @@ object TestRecordSetData { RecordSetStatus.Active, Instant.now.truncatedTo(ChronoUnit.MILLIS), None, - List(PTRData(Fqdn("ptr"))) + List(PTRData(Fqdn("ptr"))), + recordSetGroupChange= None ) val ptrIp6: RecordSet = RecordSet( @@ -103,7 +109,8 @@ object TestRecordSetData { RecordSetStatus.Active, Instant.now.truncatedTo(ChronoUnit.MILLIS), None, - List(PTRData(Fqdn("ptr"))) + List(PTRData(Fqdn("ptr"))), + recordSetGroupChange= None ) val srv: RecordSet = RecordSet( @@ -114,7 +121,8 @@ object TestRecordSetData { RecordSetStatus.Active, Instant.now.truncatedTo(ChronoUnit.MILLIS), None, - List(SRVData(1, 2, 3, Fqdn("target"))) + List(SRVData(1, 2, 3, Fqdn("target"))), + recordSetGroupChange= None ) val naptr: RecordSet = RecordSet( @@ -125,7 +133,8 @@ object TestRecordSetData { RecordSetStatus.Active, Instant.now.truncatedTo(ChronoUnit.MILLIS), 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( @@ -136,7 +145,8 @@ object TestRecordSetData { RecordSetStatus.Pending, Instant.now.truncatedTo(ChronoUnit.MILLIS), None, - List(MXData(3, Fqdn("mx"))) + List(MXData(3, Fqdn("mx"))), + recordSetGroupChange= None ) val ns: RecordSet = RecordSet( @@ -147,7 +157,8 @@ object TestRecordSetData { RecordSetStatus.Active, Instant.now.truncatedTo(ChronoUnit.MILLIS), 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( @@ -158,7 +169,8 @@ object TestRecordSetData { RecordSetStatus.Pending, Instant.now.truncatedTo(ChronoUnit.MILLIS), None, - List(TXTData("txt")) + List(TXTData("txt")), + recordSetGroupChange= None ) // example at https://tools.ietf.org/html/rfc4034#page-18 @@ -198,7 +210,12 @@ object TestRecordSetData { Instant.now.truncatedTo(ChronoUnit.MILLIS), None, 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 = diff --git a/modules/core/src/test/scala/vinyldns/core/notifier/NotifierLoaderSpec.scala b/modules/core/src/test/scala/vinyldns/core/notifier/NotifierLoaderSpec.scala index 849d46948..daa151044 100644 --- a/modules/core/src/test/scala/vinyldns/core/notifier/NotifierLoaderSpec.scala +++ b/modules/core/src/test/scala/vinyldns/core/notifier/NotifierLoaderSpec.scala @@ -19,7 +19,7 @@ package vinyldns.core.notifier import cats.scalatest.{EitherMatchers, EitherValues, ValidatedMatchers} import com.typesafe.config.{Config, ConfigFactory} import org.scalatestplus.mockito.MockitoSugar -import vinyldns.core.domain.membership.UserRepository +import vinyldns.core.domain.membership.{GroupRepository, UserRepository} import cats.effect.IO import org.mockito.Mockito._ @@ -37,13 +37,13 @@ object MockNotifierProvider extends MockitoSugar { 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) } 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")) } @@ -63,6 +63,7 @@ class NotifierLoaderSpec val goodConfig = NotifierConfig("vinyldns.core.notifier.MockNotifierProvider", placeholderConfig) val mockUserRepository: UserRepository = mock[UserRepository] + val mockGroupRepository: GroupRepository = mock[GroupRepository] import MockNotifierProvider._ @@ -73,14 +74,13 @@ class NotifierLoaderSpec "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.notify(Notification(3)).unsafeRunSync() shouldBe (()) } "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) @@ -95,7 +95,7 @@ class NotifierLoaderSpec "return a notifier for valid config of multiple notifiers" in { val notifier = - NotifierLoader.loadAll(List(goodConfig, goodConfig), mockUserRepository).unsafeRunSync() + NotifierLoader.loadAll(List(goodConfig, goodConfig), mockUserRepository, mockGroupRepository).unsafeRunSync() notifier shouldNot be(null) @@ -113,7 +113,7 @@ class NotifierLoaderSpec val badProvider = 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()) } @@ -123,7 +123,7 @@ class NotifierLoaderSpec val exceptionProvider = 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()) } diff --git a/modules/portal/app/views/zones/zoneTabs/manageRecords.scala.html b/modules/portal/app/views/zones/zoneTabs/manageRecords.scala.html index 450fd4ee9..f5fe8b8e9 100644 --- a/modules/portal/app/views/zones/zoneTabs/manageRecords.scala.html +++ b/modules/portal/app/views/zones/zoneTabs/manageRecords.scala.html @@ -129,6 +129,7 @@ Record Data @if(meta.sharedDisplayEnabled) { Owner Group Name + OwnerShip Transfer Status } Actions @@ -332,11 +333,22 @@ @if(meta.sharedDisplayEnabled) { - - Unowned + Unowned Group deleted + + Approved + Rejected + + Cancelled + Pending Action + } @@ -345,6 +357,19 @@ + + + + + + + + + + + diff --git a/modules/portal/public/lib/controllers/controller.records.js b/modules/portal/public/lib/controllers/controller.records.js index ccaa4b66f..10aeff23c 100644 --- a/modules/portal/public/lib/controllers/controller.records.js +++ b/modules/portal/public/lib/controllers/controller.records.js @@ -31,6 +31,7 @@ angular.module('controller.records', []) $scope.alerts = []; $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.selectedRecordTypes = []; $scope.naptrFlags = ["U", "S", "A", "P"]; @@ -56,6 +57,12 @@ angular.module('controller.records', []) var loadZonesPromise; var loadRecordsPromise; + $scope.ownerShipTransferApproverStatus = [{value: 'ManuallyApproved' , label: 'Approve'}, + {value: 'ManuallyRejected', label: 'Reject'}]; + + $scope.ownerShipTransferRequestorStatus = [{value: 'Requested', label: 'Request'}, + {value: 'Cancelled', label: 'Cancel'}]; + $scope.recordModalState = { CREATE: 0, UPDATE: 1, @@ -82,6 +89,7 @@ angular.module('controller.records', []) $scope.isZoneAdmin = false; $scope.canReadZone = false; $scope.canCreateRecords = false; + $scope.isCurrentRecordSetOwner = false; // paging status for recordsets var recordsPaging = pagingService.getNewPagingParams(100); @@ -93,6 +101,42 @@ angular.module('controller.records', []) * 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.currentRecord = angular.copy(record); $scope.recordModal = { @@ -131,6 +175,8 @@ angular.module('controller.records', []) $scope.editRecord = function(record) { $scope.currentRecord = angular.copy(record); + $scope.currentRecord.recordSetGroupChange = angular.copy(record.recordSetGroupChange); + getGroup($scope.currentRecord.recordSetGroupChange.requestedOwnerGroupId); $scope.recordModal = { previous: angular.copy(record), action: $scope.recordModalState.UPDATE, @@ -144,6 +190,72 @@ angular.module('controller.records', []) $("#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.addRecordForm.$setPristine(); + $("#record_modal_ownership").modal("show"); + }; + + $scope.requestOwnerShipTransfer = function(record) { + $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; + 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.addRecordForm.$setPristine(); + $("#record_modal_ownership_transfer").modal("show"); + }; + + $scope.requestedOwnerShip = function() { + var record = angular.copy($scope.currentRecord); + record['onlyFour'] = true; + updateRecordSet(record); + $scope.addRecordForm.$setPristine(); + $("#record_modal_ownership").modal('hide'); + }; + + $scope.submitRequestedOwnerShipTransfer = function () { + var record = angular.copy($scope.currentRecord); + record['onlyFour'] = true; + if ($scope.addRecordForm.$valid) { + updateRecordSet(record); + $scope.addRecordForm.$setPristine(); + $("#record_modal_ownership_transfer").modal('hide'); + } + }; + $scope.confirmUpdate = function() { $scope.recordModal.action = $scope.recordModalState.CONFIRM_UPDATE; $scope.recordModal.details = $scope.recordModalParams.readOnly; @@ -189,6 +301,12 @@ angular.module('controller.records', []) $scope.submitUpdateRecord = function () { 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; if ($scope.addRecordForm.$valid) { @@ -494,6 +612,11 @@ angular.module('controller.records', []) angular.forEach(records, function(record) { 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.getRecordSetCount(); if($scope.records.length > 0) { diff --git a/modules/portal/public/lib/services/records/service.records.js b/modules/portal/public/lib/services/records/service.records.js index 42e1d1f2c..f8505e1e1 100644 --- a/modules/portal/public/lib/services/records/service.records.js +++ b/modules/portal/public/lib/services/records/service.records.js @@ -271,7 +271,10 @@ angular.module('service.records', []) "id": record.id, "name": record.name, "type": record.type, - "ttl": Number(record.ttl) + "ttl": Number(record.ttl), + "isCurrentRecordSetOwner": record.isCurrentRecordSetOwner, + "recordSetGroupChange": record.recordSetGroupChange + }; switch (record.type) { case 'A': diff --git a/modules/portal/public/lib/services/records/service.records.spec.js b/modules/portal/public/lib/services/records/service.records.spec.js index 01744f757..36c9e95cc 100644 --- a/modules/portal/public/lib/services/records/service.records.spec.js +++ b/modules/portal/public/lib/services/records/service.records.spec.js @@ -111,7 +111,9 @@ describe('Service: recordsService', function () { "type": 'SSHFP', "ttl": '300', "sshfpItems": [{algorithm: '1', type: '1', fingerprint: '123456789ABCDEF67890123456789ABCDEF67890'}, - {algorithm: '2', type: '1', fingerprint: '123456789ABCDEF67890123456789ABCDEF67890'}] + {algorithm: '2', type: '1', fingerprint: '123456789ABCDEF67890123456789ABCDEF67890'}], + "recordSetGroupChange": 'None', + "isCurrentRecordSetOwner": 'null' }; expectedRecord = { "id": 'recordId', @@ -119,7 +121,9 @@ describe('Service: recordsService', function () { "type": 'SSHFP', "ttl": 300, "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); @@ -133,7 +137,8 @@ describe('Service: recordsService', function () { "type": 'SSHFP', "ttl": 300, "records": [{algorithm: 1, type: 1, fingerprint: '123456789ABCDEF67890123456789ABCDEF67890'}, - {algorithm: 2, type: 1, fingerprint: 'F23456789ABCDEF67890123456789ABCDEF67890'}] + {algorithm: 2, type: 1, fingerprint: 'F23456789ABCDEF67890123456789ABCDEF67890'}], + "recordSetGroupChange": 'None' }; displayRecord = { @@ -144,6 +149,7 @@ describe('Service: recordsService', function () { "records": undefined, "sshfpItems": [{algorithm: 1, type: 1, fingerprint: '123456789ABCDEF67890123456789ABCDEF67890'}, {algorithm: 2, type: 1, fingerprint: 'F23456789ABCDEF67890123456789ABCDEF67890'}], + "recordSetGroupChange": 'None', "onlyFour": true, "isDotted": false, "canBeEdited": true @@ -160,7 +166,8 @@ describe('Service: recordsService', function () { "type": 'SSHFP', "ttl": 300, "records": [{algorithm: 1, type: 1, fingerprint: '123456789ABCDEF67890123456789ABCDEF67890'}, - {algorithm: 2, type: 1, fingerprint: 'F23456789ABCDEF67890123456789ABCDEF67890'}] + {algorithm: 2, type: 1, fingerprint: 'F23456789ABCDEF67890123456789ABCDEF67890'}], + "recordSetGroupChange": 'None' }; displayRecord = { @@ -171,6 +178,7 @@ describe('Service: recordsService', function () { "records": undefined, "sshfpItems": [{algorithm: 1, type: 1, fingerprint: '123456789ABCDEF67890123456789ABCDEF67890'}, {algorithm: 2, type: 1, fingerprint: 'F23456789ABCDEF67890123456789ABCDEF67890'}], + "recordSetGroupChange": 'None', "onlyFour": true, "isDotted": true, "canBeEdited": true @@ -187,7 +195,8 @@ describe('Service: recordsService', function () { "type": 'SSHFP', "ttl": 300, "records": [{algorithm: 1, type: 1, fingerprint: '123456789ABCDEF67890123456789ABCDEF67890'}, - {algorithm: 2, type: 1, fingerprint: 'F23456789ABCDEF67890123456789ABCDEF67890'}] + {algorithm: 2, type: 1, fingerprint: 'F23456789ABCDEF67890123456789ABCDEF67890'}], + "recordSetGroupChange": 'None' }; displayRecord = { @@ -198,6 +207,7 @@ describe('Service: recordsService', function () { "records": undefined, "sshfpItems": [{algorithm: 1, type: 1, fingerprint: '123456789ABCDEF67890123456789ABCDEF67890'}, {algorithm: 2, type: 1, fingerprint: 'F23456789ABCDEF67890123456789ABCDEF67890'}], + "recordSetGroupChange": 'None', "onlyFour": true, "isDotted": false, "canBeEdited": true @@ -213,7 +223,8 @@ describe('Service: recordsService', function () { "name": 'apex.with.dot.', "type": 'NS', "ttl": 300, - "records": [{nsdname: "ns1.com."}, {nsdname: "ns2.com."}] + "records": [{nsdname: "ns1.com."}, {nsdname: "ns2.com."}], + "recordSetGroupChange": 'None' }; displayRecord = { @@ -223,6 +234,7 @@ describe('Service: recordsService', function () { "ttl": 300, "records": undefined, "nsRecordData": ["ns1.com.", "ns2.com."], + "recordSetGroupChange": 'None', "onlyFour": true, "isDotted": false, "canBeEdited": false @@ -239,7 +251,8 @@ describe('Service: recordsService', function () { "type": 'SSHFP', "ttl": 300, "records": [{algorithm: 1, type: 1, fingerprint: '123456789ABCDEF67890123456789ABCDEF67890'}, - {algorithm: 2, type: 1, fingerprint: 'F23456789ABCDEF67890123456789ABCDEF67890'}] + {algorithm: 2, type: 1, fingerprint: 'F23456789ABCDEF67890123456789ABCDEF67890'}], + "recordSetGroupChange": 'None' }; displayRecord = { @@ -250,6 +263,7 @@ describe('Service: recordsService', function () { "records": undefined, "sshfpItems": [{algorithm: 1, type: 1, fingerprint: '123456789ABCDEF67890123456789ABCDEF67890'}, {algorithm: 2, type: 1, fingerprint: 'F23456789ABCDEF67890123456789ABCDEF67890'}], + "recordSetGroupChange": 'None', "onlyFour": true, "isDotted": false, "canBeEdited": true diff --git a/modules/portal/public/templates/record-modal.html b/modules/portal/public/templates/record-modal.html index 4a636690c..0674b13e4 100644 --- a/modules/portal/public/templates/record-modal.html +++ b/modules/portal/public/templates/record-modal.html @@ -471,7 +471,16 @@ Record Owner Group is required for records in shared zones - + + + @@ -505,4 +514,98 @@ + + + + + + + + Requesting Record Owner Group is required for records in shared zones + + + + + + + + + + + + + + + + + + + + + + Record Owner Group is required for records in shared zones + + + + + + + + Record Owner Group is required for records in shared zones + + + + + + + + + + + + diff --git a/modules/portal/test/controllers/TestApplicationData.scala b/modules/portal/test/controllers/TestApplicationData.scala index fee7bd4eb..53a497010 100644 --- a/modules/portal/test/controllers/TestApplicationData.scala +++ b/modules/portal/test/controllers/TestApplicationData.scala @@ -296,6 +296,7 @@ trait TestApplicationData { this: Mockito => | "ttl": "200", | "status": "${RecordSetStatus.Active}", | "records": [ { "address": "10.1.1.1" } ], + | "recordSetGroupChange": "None", | "id": "$hobbitRecordSetId" | } """.stripMargin) diff --git a/modules/r53/src/main/scala/vinyldns/route53/backend/Route53Conversions.scala b/modules/r53/src/main/scala/vinyldns/route53/backend/Route53Conversions.scala index 87f02d0f5..79ec1403b 100644 --- a/modules/r53/src/main/scala/vinyldns/route53/backend/Route53Conversions.scala +++ b/modules/r53/src/main/scala/vinyldns/route53/backend/Route53Conversions.scala @@ -25,7 +25,7 @@ import com.amazonaws.services.route53.model.{ import java.time.temporal.ChronoUnit import java.time.Instant 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._ import vinyldns.core.domain.zone.Zone @@ -82,6 +82,7 @@ trait Route53Conversions { Instant.now.truncatedTo(ChronoUnit.MILLIS), Some(Instant.now.truncatedTo(ChronoUnit.MILLIS)), r53RecordSet.getResourceRecords.asScala.toList.flatMap(toVinyl(typ, _)), + recordSetGroupChange=Some(OwnerShipTransfer(ownerShipTransferStatus = OwnerShipTransferStatus.AutoApproved)), fqdn = Some(r53RecordSet.getName) ) } @@ -110,6 +111,7 @@ trait Route53Conversions { Instant.now.truncatedTo(ChronoUnit.MILLIS), Some(Instant.now.truncatedTo(ChronoUnit.MILLIS)), nsData, + recordSetGroupChange = Some(OwnerShipTransfer(ownerShipTransferStatus = OwnerShipTransferStatus.AutoApproved)), fqdn = Some(Fqdn(zoneName).fqdn) ) } From 45262c02c4147ea5312877ba85b34da9948f492b Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Sat, 2 Mar 2024 00:20:29 +0530 Subject: [PATCH 02/25] update --- .../api/domain/record/RecordSetServiceIntegrationSpec.scala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/api/src/it/scala/vinyldns/api/domain/record/RecordSetServiceIntegrationSpec.scala b/modules/api/src/it/scala/vinyldns/api/domain/record/RecordSetServiceIntegrationSpec.scala index 9d1762e27..815084a88 100644 --- a/modules/api/src/it/scala/vinyldns/api/domain/record/RecordSetServiceIntegrationSpec.scala +++ b/modules/api/src/it/scala/vinyldns/api/domain/record/RecordSetServiceIntegrationSpec.scala @@ -62,6 +62,9 @@ class RecordSetServiceIntegrationSpec private val recordSetRepo = recordSetRepository private val recordSetCacheRepo = recordSetCacheRepository + private val mockNotifier = mock[Notifier] + private val mockNotifiers = AllNotifiers(List(mockNotifier)) + private val zoneRepo: ZoneRepository = zoneRepository private val groupRepo: GroupRepository = groupRepository From e684587bd432a63003ff7eb03ce0c8995f3d6392 Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Sat, 2 Mar 2024 00:27:17 +0530 Subject: [PATCH 03/25] update --- .../api/domain/record/RecordSetServiceIntegrationSpec.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/api/src/it/scala/vinyldns/api/domain/record/RecordSetServiceIntegrationSpec.scala b/modules/api/src/it/scala/vinyldns/api/domain/record/RecordSetServiceIntegrationSpec.scala index 815084a88..74bc18c07 100644 --- a/modules/api/src/it/scala/vinyldns/api/domain/record/RecordSetServiceIntegrationSpec.scala +++ b/modules/api/src/it/scala/vinyldns/api/domain/record/RecordSetServiceIntegrationSpec.scala @@ -57,6 +57,8 @@ class RecordSetServiceIntegrationSpec with BeforeAndAfterAll with TransactionProvider { + private implicit val contextShift: ContextShift[IO] = IO.contextShift(ExecutionContext.global) + private val vinyldnsConfig = VinylDNSConfig.load().unsafeRunSync() private val recordSetRepo = recordSetRepository From 2565a9fbaed2922773416026a08cfcad9722e592 Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Sat, 2 Mar 2024 00:53:47 +0530 Subject: [PATCH 04/25] update --- .../scala/vinyldns/api/domain/record/RecordSetService.scala | 2 -- .../scala/vinyldns/api/notifier/email/EmailNotifier.scala | 1 + .../vinyldns/api/domain/record/RecordSetServiceSpec.scala | 6 +++--- .../vinyldns/api/notifier/email/EmailNotifierSpec.scala | 1 + 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetService.scala b/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetService.scala index 1c2751e6c..e9b446b28 100644 --- a/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetService.scala +++ b/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetService.scala @@ -163,14 +163,12 @@ class RecordSetService( _ <- isNotHighValueDomain(recordSet, zone, highValueDomainConfig).toResult _ <- if(requestorOwnerShipTransferStatus.contains(recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("") )) ().toResult else canUpdateRecordSet(auth, existing.name, existing.typ, zone, existing.ownerGroupId, superUserCanUpdateOwnerGroup).toResult - _ <- canUpdateRecordSet(auth, existing.name, existing.typ, zone, existing.ownerGroupId, superUserCanUpdateOwnerGroup).toResult ownerGroup <- getGroupIfProvided(rsForValidations.ownerGroupId) _ <- if(requestorOwnerShipTransferStatus.contains(recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse(""))) canUseOwnerGroup(rsForValidations.recordSetGroupChange.map(_.requestedOwnerGroupId).get, ownerGroup, auth).toResult else if(approverOwnerShipTransferStatus.contains(recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse(""))) canUseOwnerGroup(existing.ownerGroupId, ownerGroup, auth).toResult else canUseOwnerGroup(rsForValidations.ownerGroupId, ownerGroup, auth).toResult - _ <- canUseOwnerGroup(rsForValidations.ownerGroupId, ownerGroup, auth).toResult _ <- notPending(existing).toResult existingRecordsWithName <- recordSetRepository .getRecordSetsByName(zone.id, rsForValidations.name) diff --git a/modules/api/src/main/scala/vinyldns/api/notifier/email/EmailNotifier.scala b/modules/api/src/main/scala/vinyldns/api/notifier/email/EmailNotifier.scala index 3eb7c5c84..3744d0f2f 100644 --- a/modules/api/src/main/scala/vinyldns/api/notifier/email/EmailNotifier.scala +++ b/modules/api/src/main/scala/vinyldns/api/notifier/email/EmailNotifier.scala @@ -46,6 +46,7 @@ class EmailNotifier(config: EmailNotifierConfig, session: Session, userRepositor def notify(notification: Notification[_]): IO[Unit] = notification.change match { case bc: BatchChange => sendBatchChangeNotification(bc) + case rsc: RecordSetChange => sendRecordSetOwnerTransferNotification(rsc) case _ => IO.unit } diff --git a/modules/api/src/test/scala/vinyldns/api/domain/record/RecordSetServiceSpec.scala b/modules/api/src/test/scala/vinyldns/api/domain/record/RecordSetServiceSpec.scala index 3334d8412..2b713b5e2 100644 --- a/modules/api/src/test/scala/vinyldns/api/domain/record/RecordSetServiceSpec.scala +++ b/modules/api/src/test/scala/vinyldns/api/domain/record/RecordSetServiceSpec.scala @@ -1325,12 +1325,12 @@ class RecordSetServiceSpec 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(newRecord))) .when(mockRecordRepo) .getRecordSet(newRecord.id) + doReturn(IO.pure(List(oldRecord))) + .when(mockRecordRepo) + .getRecordSetsByName(zone.id, newRecord.name) doReturn(IO.pure(Set(dottedZone, abcZone, xyzZone))) .when(mockZoneRepo) .getZonesByNames(dottedHostsConfigZonesAllowed.toSet) diff --git a/modules/api/src/test/scala/vinyldns/api/notifier/email/EmailNotifierSpec.scala b/modules/api/src/test/scala/vinyldns/api/notifier/email/EmailNotifierSpec.scala index 2c959048a..62a1f4c46 100644 --- a/modules/api/src/test/scala/vinyldns/api/notifier/email/EmailNotifierSpec.scala +++ b/modules/api/src/test/scala/vinyldns/api/notifier/email/EmailNotifierSpec.scala @@ -187,6 +187,7 @@ class EmailNotifierSpec notifier.notify(Notification(rsc)).unsafeRunSync() val message = messageArgument.getValue + println(message) message.getFrom should be(Array(fromAddress)) message.getContentType should be("text/html; charset=us-ascii") From f103985306bad40575d237b24041b29b2d4ff0df Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Thu, 14 Mar 2024 21:03:48 +0530 Subject: [PATCH 05/25] resolved minor bugs --- .../api/domain/record/RecordSetService.scala | 16 ++++++++-------- .../public/lib/controllers/controller.records.js | 2 -- .../portal/public/templates/record-modal.html | 3 ++- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetService.scala b/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetService.scala index e9b446b28..59fab1603 100644 --- a/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetService.scala +++ b/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetService.scala @@ -152,21 +152,21 @@ class RecordSetService( _ <- unchangedRecordType(existing, recordSet).toResult _ <- unchangedZoneId(existing, recordSet).toResult _ <- if(requestorOwnerShipTransferStatus.contains(recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("")) - && !auth.isSuper) unchangedRecordSet(existing, recordSet).toResult else ().toResult - _ <- if(existing.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("") == OwnerShipTransferStatus.Cancelled) + && !auth.isSuper && !auth.isGroupMember(existing.ownerGroupId.get)) unchangedRecordSet(existing, recordSet).toResult else ().toResult + _ <- if(existing.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("") == OwnerShipTransferStatus.Cancelled && !auth.isSuper) recordSetOwnerShipApproveStatus(recordSet).toResult else ().toResult - recordSet <- updateRecordSetGroupChangeStatus(recordSet, existing, zone) + recordSet <- if (auth.isSuper) recordSet.toResult else updateRecordSetGroupChangeStatus(recordSet, existing, zone) change <- RecordSetChangeGenerator.forUpdate(existing, recordSet, zone, Some(auth)).toResult // because changes happen to the RS in forUpdate itself, converting 1st and validating on that rsForValidations = change.recordSet superUserCanUpdateOwnerGroup = canSuperUserUpdateOwnerGroup(existing, recordSet, zone, auth) _ <- isNotHighValueDomain(recordSet, zone, highValueDomainConfig).toResult - _ <- if(requestorOwnerShipTransferStatus.contains(recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("") )) - ().toResult else canUpdateRecordSet(auth, existing.name, existing.typ, zone, existing.ownerGroupId, superUserCanUpdateOwnerGroup).toResult + _ <- if(requestorOwnerShipTransferStatus.contains(recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("")) && !auth.isSuper && !auth.isGroupMember(existing.ownerGroupId.get)) + ().toResult else canUpdateRecordSet(auth, existing.name, existing.typ, zone, existing.ownerGroupId, superUserCanUpdateOwnerGroup).toResult ownerGroup <- getGroupIfProvided(rsForValidations.ownerGroupId) - _ <- if(requestorOwnerShipTransferStatus.contains(recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse(""))) + _ <- if(requestorOwnerShipTransferStatus.contains(recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("")) && !auth.isSuper && !auth.isGroupMember(existing.ownerGroupId.get)) canUseOwnerGroup(rsForValidations.recordSetGroupChange.map(_.requestedOwnerGroupId).get, ownerGroup, auth).toResult - else if(approverOwnerShipTransferStatus.contains(recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse(""))) + else if(approverOwnerShipTransferStatus.contains(recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("")) && !auth.isSuper) canUseOwnerGroup(existing.ownerGroupId, ownerGroup, auth).toResult else canUseOwnerGroup(rsForValidations.ownerGroupId, ownerGroup, auth).toResult _ <- notPending(existing).toResult @@ -266,7 +266,7 @@ class RecordSetService( recordSet.copy(recordSetGroupChange = Some(ownerShipTransfer.copy( ownerShipTransferStatus = OwnerShipTransferStatus.Cancelled, requestedOwnerGroupId = existingOwnerShipTransfer.requestedOwnerGroupId))) - case OwnerShipTransferStatus.Requested => recordSet.copy( + case OwnerShipTransferStatus.Requested | OwnerShipTransferStatus.PendingReview => recordSet.copy( recordSetGroupChange = Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.PendingReview))) } for { diff --git a/modules/portal/public/lib/controllers/controller.records.js b/modules/portal/public/lib/controllers/controller.records.js index 10aeff23c..5dfa2496d 100644 --- a/modules/portal/public/lib/controllers/controller.records.js +++ b/modules/portal/public/lib/controllers/controller.records.js @@ -308,10 +308,8 @@ angular.module('controller.records', []) } } record['onlyFour'] = true; - if ($scope.addRecordForm.$valid) { updateRecordSet(record); - $scope.addRecordForm.$setPristine(); $("#record_modal").modal('hide'); } diff --git a/modules/portal/public/templates/record-modal.html b/modules/portal/public/templates/record-modal.html index 0674b13e4..e148b9b26 100644 --- a/modules/portal/public/templates/record-modal.html +++ b/modules/portal/public/templates/record-modal.html @@ -558,7 +558,6 @@ 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> @@ -585,12 +584,14 @@ ng-model="currentRecord.recordSetGroupChange.ownerShipTransferStatus" ng-class="recordModal.details.class" ng-options="ownerShipTransferApproverStatus.value as ownerShipTransferApproverStatus.label for ownerShipTransferApproverStatus in ownerShipTransferStatus"> + From 8f9e13e5c13a87ead158dab23f5ab60735d3d670 Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Thu, 14 Mar 2024 22:09:08 +0530 Subject: [PATCH 06/25] update --- .../scala/vinyldns/api/domain/record/RecordSetService.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetService.scala b/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetService.scala index 59fab1603..9d377165b 100644 --- a/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetService.scala +++ b/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetService.scala @@ -152,7 +152,7 @@ class RecordSetService( _ <- unchangedRecordType(existing, recordSet).toResult _ <- unchangedZoneId(existing, recordSet).toResult _ <- if(requestorOwnerShipTransferStatus.contains(recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("")) - && !auth.isSuper && !auth.isGroupMember(existing.ownerGroupId.get)) unchangedRecordSet(existing, recordSet).toResult else ().toResult + && !auth.isSuper && !auth.isGroupMember(existing.ownerGroupId.getOrElse("None"))) unchangedRecordSet(existing, recordSet).toResult else ().toResult _ <- if(existing.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("") == OwnerShipTransferStatus.Cancelled && !auth.isSuper) recordSetOwnerShipApproveStatus(recordSet).toResult else ().toResult recordSet <- if (auth.isSuper) recordSet.toResult else updateRecordSetGroupChangeStatus(recordSet, existing, zone) @@ -161,10 +161,10 @@ class RecordSetService( rsForValidations = change.recordSet superUserCanUpdateOwnerGroup = canSuperUserUpdateOwnerGroup(existing, recordSet, zone, auth) _ <- isNotHighValueDomain(recordSet, zone, highValueDomainConfig).toResult - _ <- if(requestorOwnerShipTransferStatus.contains(recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("")) && !auth.isSuper && !auth.isGroupMember(existing.ownerGroupId.get)) + _ <- if(requestorOwnerShipTransferStatus.contains(recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("")) && !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) - _ <- if(requestorOwnerShipTransferStatus.contains(recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("")) && !auth.isSuper && !auth.isGroupMember(existing.ownerGroupId.get)) + _ <- if(requestorOwnerShipTransferStatus.contains(recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("")) && !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("")) && !auth.isSuper) canUseOwnerGroup(existing.ownerGroupId, ownerGroup, auth).toResult From ab6c42bc1041bfefcd0591aa7b5754b4fdbe2c67 Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Thu, 14 Mar 2024 22:35:08 +0530 Subject: [PATCH 07/25] update --- .../scala/vinyldns/api/domain/record/RecordSetService.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetService.scala b/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetService.scala index 9d377165b..86bfc15e5 100644 --- a/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetService.scala +++ b/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetService.scala @@ -152,7 +152,7 @@ class RecordSetService( _ <- unchangedRecordType(existing, recordSet).toResult _ <- unchangedZoneId(existing, recordSet).toResult _ <- if(requestorOwnerShipTransferStatus.contains(recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("")) - && !auth.isSuper && !auth.isGroupMember(existing.ownerGroupId.getOrElse("None"))) unchangedRecordSet(existing, recordSet).toResult else ().toResult + && !auth.isSuper) unchangedRecordSet(existing, recordSet).toResult else ().toResult _ <- if(existing.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("") == OwnerShipTransferStatus.Cancelled && !auth.isSuper) recordSetOwnerShipApproveStatus(recordSet).toResult else ().toResult recordSet <- if (auth.isSuper) recordSet.toResult else updateRecordSetGroupChangeStatus(recordSet, existing, zone) From 123f2fa4777f3163285319a8152d48dded06bd0a Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Fri, 15 Mar 2024 22:57:48 +0530 Subject: [PATCH 08/25] update --- .../lib/controllers/controller.records.js | 13 +- .../portal/public/templates/record-modal.html | 125 +++++++++--------- 2 files changed, 69 insertions(+), 69 deletions(-) diff --git a/modules/portal/public/lib/controllers/controller.records.js b/modules/portal/public/lib/controllers/controller.records.js index 3af7b741e..b7178284b 100644 --- a/modules/portal/public/lib/controllers/controller.records.js +++ b/modules/portal/public/lib/controllers/controller.records.js @@ -203,7 +203,7 @@ angular.module('controller.records', []) sharedDisplayEnabled: $scope.sharedDisplayEnabled, isCurrentRecordOwnerGroup : false }; - $scope.addRecordForm.$setPristine(); + $scope.recordOwnerShipForm.$setPristine(); $("#record_modal_ownership").modal("show"); }; @@ -235,24 +235,27 @@ angular.module('controller.records', []) if (ownerShipTransferApproverStatus.value.indexOf($scope.currentRecord.recordSetGroupChange.ownerShipTransferStatus) > -1) {$scope.ownerShipTransferApprover = true}else{$scope.ownerShipTransferRequestor = true}}) } - $scope.addRecordForm.$setPristine(); + $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.addRecordForm.$setPristine(); + $scope.recordOwnerShipForm.$setPristine(); $("#record_modal_ownership").modal('hide'); + } }; $scope.submitRequestedOwnerShipTransfer = function () { var record = angular.copy($scope.currentRecord); record['onlyFour'] = true; - if ($scope.addRecordForm.$valid) { + if ($scope.recordOwnerShipForm.$valid) { updateRecordSet(record); - $scope.addRecordForm.$setPristine(); + $scope.recordOwnerShipForm.$setPristine(); $("#record_modal_ownership_transfer").modal('hide'); } }; diff --git a/modules/portal/public/templates/record-modal.html b/modules/portal/public/templates/record-modal.html index e148b9b26..37af139a4 100644 --- a/modules/portal/public/templates/record-modal.html +++ b/modules/portal/public/templates/record-modal.html @@ -1,3 +1,5 @@ +
+
@@ -514,12 +516,13 @@ +
+
- - - - - - Record Owner Group is required for records in shared zones - - + + + + + + Record Owner Group is required for records in shared zones + + - - - - - Record Owner Group is required for records in shared zones - - - - + + + + + Record Owner Group is required for records in shared zones + + + + - - - - + +
+
\ No newline at end of file From 6d109733722eb76c6f02ec1ae40dbcaa2fd7744d Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Mon, 18 Mar 2024 14:36:24 +0530 Subject: [PATCH 09/25] bug fix --- .../zones/zoneTabs/manageRecords.scala.html | 4 ++-- .../lib/controllers/controller.records.js | 8 ++++++- .../portal/public/templates/record-modal.html | 24 ++++++++++--------- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/modules/portal/app/views/zones/zoneTabs/manageRecords.scala.html b/modules/portal/app/views/zones/zoneTabs/manageRecords.scala.html index f5fe8b8e9..aca2896ff 100644 --- a/modules/portal/app/views/zones/zoneTabs/manageRecords.scala.html +++ b/modules/portal/app/views/zones/zoneTabs/manageRecords.scala.html @@ -364,10 +364,10 @@ ng-if="zoneInfo.shared && record.ownerGroupId != undefined && !canReadZone && record.canBeEdited" ng-switch="record.isCurrentRecordSetOwner"> - + - + diff --git a/modules/portal/public/lib/controllers/controller.records.js b/modules/portal/public/lib/controllers/controller.records.js index b7178284b..dad088060 100644 --- a/modules/portal/public/lib/controllers/controller.records.js +++ b/modules/portal/public/lib/controllers/controller.records.js @@ -47,6 +47,7 @@ angular.module('controller.records', []) {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.records = {}; + $scope.isOwnerShipRequest = true; $scope.recordsetChangesPreview = {}; $scope.recordsetChanges = {}; $scope.currentRecord = {}; @@ -127,6 +128,7 @@ angular.module('controller.records', []) $log.log('groupsService::getGroup-success'); function success(response) { $scope.recordSetRequestedOwnerShipName = response.data.name; + } return groupsService .getGroup(groupId) @@ -207,7 +209,7 @@ angular.module('controller.records', []) $("#record_modal_ownership").modal("show"); }; - $scope.requestOwnerShipTransfer = function(record) { + $scope.requestOwnerShipTransfer = function(record, isOwnerShipRequest) { $scope.currentRecord = angular.copy(record); $scope.currentRecord.recordSetGroupChange = angular.copy(record.recordSetGroupChange); $scope.recordModal = { @@ -222,6 +224,10 @@ angular.module('controller.records', []) }; var currentRecordOwnerGroupId = $scope.currentRecord.ownerGroupId; + if (isOwnerShipRequest) { + $scope.currentRecord.recordSetGroupChange.requestedOwnerGroupId = angular.copy(null); + $scope.currentRecord.recordSetGroupChange.ownerShipTransferStatus = angular.copy(null); + } getGroup($scope.currentRecord.recordSetGroupChange.requestedOwnerGroupId); $scope.ownerShipTransferApprover = false; $scope.ownerShipTransferRequestor = false; diff --git a/modules/portal/public/templates/record-modal.html b/modules/portal/public/templates/record-modal.html index 37af139a4..b438e7f7f 100644 --- a/modules/portal/public/templates/record-modal.html +++ b/modules/portal/public/templates/record-modal.html @@ -549,7 +549,7 @@ + + Record Owner Group is required for records in shared zones + - - Record Owner Group is required for records in shared zones - + > - - Record Owner Group is required for records in shared zones From 974434e053c0b32219f62c89f8669a2c6a7596ed Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Mon, 18 Mar 2024 21:37:21 +0530 Subject: [PATCH 10/25] Allow super user to request/approve ownership transfer --- .../scala/vinyldns/api/domain/record/RecordSetService.scala | 2 +- .../portal/app/views/zones/zoneTabs/manageRecords.scala.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetService.scala b/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetService.scala index 86bfc15e5..73c9011a3 100644 --- a/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetService.scala +++ b/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetService.scala @@ -155,7 +155,7 @@ class RecordSetService( && !auth.isSuper) unchangedRecordSet(existing, recordSet).toResult else ().toResult _ <- if(existing.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("") == OwnerShipTransferStatus.Cancelled && !auth.isSuper) recordSetOwnerShipApproveStatus(recordSet).toResult else ().toResult - recordSet <- if (auth.isSuper) recordSet.toResult else updateRecordSetGroupChangeStatus(recordSet, existing, zone) + recordSet <- updateRecordSetGroupChangeStatus(recordSet, existing, zone) change <- RecordSetChangeGenerator.forUpdate(existing, recordSet, zone, Some(auth)).toResult // because changes happen to the RS in forUpdate itself, converting 1st and validating on that rsForValidations = change.recordSet diff --git a/modules/portal/app/views/zones/zoneTabs/manageRecords.scala.html b/modules/portal/app/views/zones/zoneTabs/manageRecords.scala.html index aca2896ff..1a306fe02 100644 --- a/modules/portal/app/views/zones/zoneTabs/manageRecords.scala.html +++ b/modules/portal/app/views/zones/zoneTabs/manageRecords.scala.html @@ -357,11 +357,11 @@ - + From 2ab7e85c6653464a8b83779c8515beb2307f475c Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Fri, 22 Mar 2024 11:30:10 +0530 Subject: [PATCH 11/25] update --- .../api/domain/record/RecordSetService.scala | 18 +++++++++++------- .../lib/controllers/controller.records.js | 2 +- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetService.scala b/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetService.scala index 73c9011a3..77571036c 100644 --- a/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetService.scala +++ b/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetService.scala @@ -152,8 +152,10 @@ class RecordSetService( _ <- unchangedRecordType(existing, recordSet).toResult _ <- unchangedZoneId(existing, recordSet).toResult _ <- if(requestorOwnerShipTransferStatus.contains(recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("")) - && !auth.isSuper) unchangedRecordSet(existing, recordSet).toResult else ().toResult - _ <- if(existing.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("") == OwnerShipTransferStatus.Cancelled && !auth.isSuper) + && !auth.isSuper && !auth.isGroupMember(existing.ownerGroupId.getOrElse("None"))) + unchangedRecordSet(existing, recordSet).toResult else ().toResult + _ <- if(existing.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("") == OwnerShipTransferStatus.Cancelled + && !auth.isSuper && !auth.isGroupMember(existing.ownerGroupId.getOrElse("None"))) recordSetOwnerShipApproveStatus(recordSet).toResult else ().toResult recordSet <- updateRecordSetGroupChangeStatus(recordSet, existing, zone) change <- RecordSetChangeGenerator.forUpdate(existing, recordSet, zone, Some(auth)).toResult @@ -161,13 +163,15 @@ class RecordSetService( rsForValidations = change.recordSet superUserCanUpdateOwnerGroup = canSuperUserUpdateOwnerGroup(existing, recordSet, zone, auth) _ <- isNotHighValueDomain(recordSet, zone, highValueDomainConfig).toResult - _ <- if(requestorOwnerShipTransferStatus.contains(recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("")) && !auth.isSuper && !auth.isGroupMember(existing.ownerGroupId.getOrElse("None"))) - ().toResult else canUpdateRecordSet(auth, existing.name, existing.typ, zone, existing.ownerGroupId, superUserCanUpdateOwnerGroup).toResult + _ <- if(requestorOwnerShipTransferStatus.contains(recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("")) + && !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) - _ <- if(requestorOwnerShipTransferStatus.contains(recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("")) && !auth.isSuper && !auth.isGroupMember(existing.ownerGroupId.getOrElse("None"))) + _ <- if(requestorOwnerShipTransferStatus.contains(recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("")) + && !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("")) && !auth.isSuper) - canUseOwnerGroup(existing.ownerGroupId, ownerGroup, auth).toResult + else if(approverOwnerShipTransferStatus.contains(recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("")) + && !auth.isSuper) canUseOwnerGroup(existing.ownerGroupId, ownerGroup, auth).toResult else canUseOwnerGroup(rsForValidations.ownerGroupId, ownerGroup, auth).toResult _ <- notPending(existing).toResult existingRecordsWithName <- recordSetRepository diff --git a/modules/portal/public/lib/controllers/controller.records.js b/modules/portal/public/lib/controllers/controller.records.js index dad088060..a167fc516 100644 --- a/modules/portal/public/lib/controllers/controller.records.js +++ b/modules/portal/public/lib/controllers/controller.records.js @@ -227,7 +227,7 @@ angular.module('controller.records', []) if (isOwnerShipRequest) { $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; From 1c816346c8468a36f2c2580ec3bc4ee5b25ae606 Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Fri, 22 Mar 2024 12:24:46 +0530 Subject: [PATCH 12/25] update --- .../scala/vinyldns/api/domain/record/RecordSetService.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetService.scala b/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetService.scala index 77571036c..4d85ebcbb 100644 --- a/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetService.scala +++ b/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetService.scala @@ -155,7 +155,7 @@ class RecordSetService( && !auth.isSuper && !auth.isGroupMember(existing.ownerGroupId.getOrElse("None"))) unchangedRecordSet(existing, recordSet).toResult else ().toResult _ <- if(existing.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("") == OwnerShipTransferStatus.Cancelled - && !auth.isSuper && !auth.isGroupMember(existing.ownerGroupId.getOrElse("None"))) + && !auth.isSuper) recordSetOwnerShipApproveStatus(recordSet).toResult else ().toResult recordSet <- updateRecordSetGroupChangeStatus(recordSet, existing, zone) change <- RecordSetChangeGenerator.forUpdate(existing, recordSet, zone, Some(auth)).toResult From db2af0accae8c9c404b31aaebb763487b263387f Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Fri, 22 Mar 2024 17:25:40 +0530 Subject: [PATCH 13/25] update --- .../test/functional/tests/recordsets/update_recordset_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/api/src/test/functional/tests/recordsets/update_recordset_test.py b/modules/api/src/test/functional/tests/recordsets/update_recordset_test.py index f4f854b4d..77a71d667 100644 --- a/modules/api/src/test/functional/tests/recordsets/update_recordset_test.py +++ b/modules/api/src/test/functional/tests/recordsets/update_recordset_test.py @@ -2457,7 +2457,7 @@ def test_update_owner_group_transfer_and_ttl_on_user_not_in_owner_group_in_fails 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 + shared_group = shared_zone_test_context.dummy_group ok_group = shared_zone_test_context.ok_group update_rs = None From cc7612497d8539587493da10a980a505bb0b2bbb Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Fri, 22 Mar 2024 19:02:47 +0530 Subject: [PATCH 14/25] update --- .../functional/tests/recordsets/update_recordset_test.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/modules/api/src/test/functional/tests/recordsets/update_recordset_test.py b/modules/api/src/test/functional/tests/recordsets/update_recordset_test.py index 77a71d667..b27e8dfdc 100644 --- a/modules/api/src/test/functional/tests/recordsets/update_recordset_test.py +++ b/modules/api/src/test/functional/tests/recordsets/update_recordset_test.py @@ -2474,9 +2474,8 @@ def test_update_owner_group_transfer_and_ttl_on_user_not_in_owner_group_in_fails update["recordSetGroupChange"] = recordset_group_change_json update["ttl"] = update["ttl"] + 100 - error = ok_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")) + error = ok_client.update_recordset(update, status=202) + assert_that(error, is_(f"User not in record owner group with id \"{shared_group['id']}\"")) finally: if update_rs: From 885e135b318fbd9284bea8f63841ce9c9a9979de Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Fri, 22 Mar 2024 19:20:44 +0530 Subject: [PATCH 15/25] Resolved func tests --- .../test/functional/tests/recordsets/update_recordset_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/api/src/test/functional/tests/recordsets/update_recordset_test.py b/modules/api/src/test/functional/tests/recordsets/update_recordset_test.py index b27e8dfdc..a476ff35a 100644 --- a/modules/api/src/test/functional/tests/recordsets/update_recordset_test.py +++ b/modules/api/src/test/functional/tests/recordsets/update_recordset_test.py @@ -2474,7 +2474,7 @@ def test_update_owner_group_transfer_and_ttl_on_user_not_in_owner_group_in_fails update["recordSetGroupChange"] = recordset_group_change_json update["ttl"] = update["ttl"] + 100 - error = ok_client.update_recordset(update, status=202) + error = ok_client.update_recordset(update, status=422) assert_that(error, is_(f"User not in record owner group with id \"{shared_group['id']}\"")) finally: From 029ffbfd5d13a83c39c8f0e4da44ad2e00b3df9a Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Fri, 22 Mar 2024 21:47:06 +0530 Subject: [PATCH 16/25] update --- .../functional/tests/recordsets/update_recordset_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/api/src/test/functional/tests/recordsets/update_recordset_test.py b/modules/api/src/test/functional/tests/recordsets/update_recordset_test.py index a476ff35a..f18d70634 100644 --- a/modules/api/src/test/functional/tests/recordsets/update_recordset_test.py +++ b/modules/api/src/test/functional/tests/recordsets/update_recordset_test.py @@ -2457,8 +2457,8 @@ def test_update_owner_group_transfer_and_ttl_on_user_not_in_owner_group_in_fails 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.dummy_group - ok_group = shared_zone_test_context.ok_group + shared_group = shared_zone_test_context.ok_group + dummy_group = shared_zone_test_context.dummy_group update_rs = None try: @@ -2470,7 +2470,7 @@ def test_update_owner_group_transfer_and_ttl_on_user_not_in_owner_group_in_fails assert_that(update["ownerGroupId"], is_(shared_group["id"])) recordset_group_change_json = {"ownerShipTransferStatus": "Requested", - "requestedOwnerGroupId": ok_group["id"]} + "requestedOwnerGroupId": dummy_group["id"]} update["recordSetGroupChange"] = recordset_group_change_json update["ttl"] = update["ttl"] + 100 From 21667ebbf3e773ad929ccd4922cdcc6c78084e65 Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Fri, 22 Mar 2024 22:36:01 +0530 Subject: [PATCH 17/25] update --- .../functional/tests/recordsets/update_recordset_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/api/src/test/functional/tests/recordsets/update_recordset_test.py b/modules/api/src/test/functional/tests/recordsets/update_recordset_test.py index f18d70634..aafe69614 100644 --- a/modules/api/src/test/functional/tests/recordsets/update_recordset_test.py +++ b/modules/api/src/test/functional/tests/recordsets/update_recordset_test.py @@ -2455,9 +2455,9 @@ def test_update_owner_group_transfer_and_ttl_on_user_not_in_owner_group_in_fails 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 - ok_client = shared_zone_test_context.ok_vinyldns_client + dummy_client = shared_zone_test_context.dummy_vinyldns_client zone = shared_zone_test_context.shared_zone - shared_group = shared_zone_test_context.ok_group + shared_group = shared_zone_test_context.shared_record_group dummy_group = shared_zone_test_context.dummy_group update_rs = None @@ -2474,7 +2474,7 @@ def test_update_owner_group_transfer_and_ttl_on_user_not_in_owner_group_in_fails update["recordSetGroupChange"] = recordset_group_change_json update["ttl"] = update["ttl"] + 100 - error = ok_client.update_recordset(update, status=422) + error = dummy_client.update_recordset(update, status=422) assert_that(error, is_(f"User not in record owner group with id \"{shared_group['id']}\"")) finally: From f0931f4817b0b46715c3430d8431cc69a2042cf2 Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Fri, 22 Mar 2024 22:46:02 +0530 Subject: [PATCH 18/25] update --- .../test/functional/tests/recordsets/update_recordset_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/api/src/test/functional/tests/recordsets/update_recordset_test.py b/modules/api/src/test/functional/tests/recordsets/update_recordset_test.py index aafe69614..4139ce260 100644 --- a/modules/api/src/test/functional/tests/recordsets/update_recordset_test.py +++ b/modules/api/src/test/functional/tests/recordsets/update_recordset_test.py @@ -2475,7 +2475,7 @@ def test_update_owner_group_transfer_and_ttl_on_user_not_in_owner_group_in_fails update["ttl"] = update["ttl"] + 100 error = dummy_client.update_recordset(update, status=422) - assert_that(error, is_(f"User not in record owner group with id \"{shared_group['id']}\"")) + 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: From bd549df516ee52eb2644a1c8f9fa4e39b8a3693b Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Wed, 3 Apr 2024 23:10:48 +0530 Subject: [PATCH 19/25] update --- .../public/lib/controllers/controller.records.js | 5 ++++- modules/portal/public/templates/record-modal.html | 10 ++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/modules/portal/public/lib/controllers/controller.records.js b/modules/portal/public/lib/controllers/controller.records.js index a167fc516..9b537d0b0 100644 --- a/modules/portal/public/lib/controllers/controller.records.js +++ b/modules/portal/public/lib/controllers/controller.records.js @@ -228,6 +228,7 @@ angular.module('controller.records', []) $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; @@ -259,7 +260,9 @@ angular.module('controller.records', []) $scope.submitRequestedOwnerShipTransfer = function () { var record = angular.copy($scope.currentRecord); record['onlyFour'] = true; - if ($scope.recordOwnerShipForm.$valid) { + 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'); diff --git a/modules/portal/public/templates/record-modal.html b/modules/portal/public/templates/record-modal.html index b438e7f7f..c81977ded 100644 --- a/modules/portal/public/templates/record-modal.html +++ b/modules/portal/public/templates/record-modal.html @@ -575,23 +575,25 @@ - - From 9c55dfa81741d179f92f5d8ee5493f3886604030 Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Thu, 4 Apr 2024 14:41:20 +0530 Subject: [PATCH 20/25] update --- modules/portal/public/lib/controllers/controller.records.js | 3 +-- modules/portal/public/templates/record-modal.html | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/modules/portal/public/lib/controllers/controller.records.js b/modules/portal/public/lib/controllers/controller.records.js index 9b537d0b0..9b2c3121a 100644 --- a/modules/portal/public/lib/controllers/controller.records.js +++ b/modules/portal/public/lib/controllers/controller.records.js @@ -224,11 +224,10 @@ angular.module('controller.records', []) }; var currentRecordOwnerGroupId = $scope.currentRecord.ownerGroupId; - if (isOwnerShipRequest) { + 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; diff --git a/modules/portal/public/templates/record-modal.html b/modules/portal/public/templates/record-modal.html index c81977ded..f1d216d8a 100644 --- a/modules/portal/public/templates/record-modal.html +++ b/modules/portal/public/templates/record-modal.html @@ -551,7 +551,7 @@ + ng-switch="currentOwnerShipTransferApprover || recordModal.previous.recordSetGroupChange.ownerShipTransferStatus == 'PendingReview'"> - Record Owner Group is required for records in shared zones + Owner Group transfer status is required for records in shared zones Date: Wed, 24 Apr 2024 14:50:28 +0530 Subject: [PATCH 22/25] update --- modules/portal/public/templates/record-modal.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/portal/public/templates/record-modal.html b/modules/portal/public/templates/record-modal.html index eaf384514..5197a859f 100644 --- a/modules/portal/public/templates/record-modal.html +++ b/modules/portal/public/templates/record-modal.html @@ -598,7 +598,7 @@ - Record Owner Group is required for records in shared zones + Owner Group transfer status is required for records in shared zones From 959ab286498ee8bc7b63eb2489605f733b58a9ce Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Tue, 27 Aug 2024 17:37:57 +0530 Subject: [PATCH 23/25] updated owner transfer email notifier --- .../api/notifier/email/EmailNotifier.scala | 94 ++++++++++--------- .../zones/zoneTabs/manageRecords.scala.html | 9 +- 2 files changed, 53 insertions(+), 50 deletions(-) diff --git a/modules/api/src/main/scala/vinyldns/api/notifier/email/EmailNotifier.scala b/modules/api/src/main/scala/vinyldns/api/notifier/email/EmailNotifier.scala index 3744d0f2f..057054a86 100644 --- a/modules/api/src/main/scala/vinyldns/api/notifier/email/EmailNotifier.scala +++ b/modules/api/src/main/scala/vinyldns/api/notifier/email/EmailNotifier.scala @@ -18,24 +18,21 @@ package vinyldns.api.notifier.email import vinyldns.core.notifier.{Notification, Notifier} import cats.effect.IO -import vinyldns.core.domain.batch.{ - BatchChange, - BatchChangeApprovalStatus, - SingleAddChange, - SingleChange, - SingleDeleteRRSetChange -} +import cats.implicits.toFoldableOps +import vinyldns.core.domain.batch.{BatchChange, BatchChangeApprovalStatus, SingleAddChange, SingleChange, SingleDeleteRRSetChange} import vinyldns.core.domain.membership.{GroupRepository, User, UserRepository} import org.slf4j.LoggerFactory + import javax.mail.internet.{InternetAddress, MimeMessage} import javax.mail.{Address, Message, Session} - import scala.util.Try -import vinyldns.core.domain.record.{AAAAData, AData, CNAMEData, MXData, PTRData, RecordData, RecordSetChange, TXTData, OwnerShipTransferStatus} +import vinyldns.core.domain.record.{AAAAData, AData, CNAMEData, MXData, OwnerShipTransferStatus, PTRData, RecordData, RecordSetChange, TXTData} import vinyldns.core.domain.record.OwnerShipTransferStatus.OwnerShipTransferStatus + import java.time.format.{DateTimeFormatter, FormatStyle} import vinyldns.core.domain.batch.BatchChangeStatus._ import vinyldns.core.domain.batch.BatchChangeApprovalStatus._ + import java.time.ZoneId class EmailNotifier(config: EmailNotifierConfig, session: Session, userRepository: UserRepository, groupRepository: GroupRepository) @@ -86,44 +83,51 @@ class EmailNotifier(config: EmailNotifierConfig, session: Session, userRepositor } def sendRecordSetOwnerTransferNotification(rsc: RecordSetChange): IO[Unit] = { - val toUser = - for{ - group <- groupRepository.getGroup(rsc.recordSet.ownerGroupId.getOrElse("")) - member <- userRepository.getUser(group.map(_.memberIds.head).getOrElse("")) - user <- userRepository.getUser(member.get.id)} - yield user - - val cCUser = - for{ - group <- groupRepository.getGroup(rsc.recordSet.recordSetGroupChange.map(_.requestedOwnerGroupId.getOrElse("")).getOrElse("")) - member <- userRepository.getUser(group.map(_.memberIds.head).getOrElse("")) - user <- userRepository.getUser(member.get.id)} - yield user - - toUser.flatMap { - case Some(UserWithEmail(email)) => - cCUser.flatMap { ccEmail => - send(email)(new InternetAddress(ccEmail.get.email.get)) { message => - message.setSubject(s"VinylDNS RecordSet change ${rsc.id} results") - message.setContent(formatRecordSetChange(rsc), "text/html") - message + for { + toGroup <- groupRepository.getGroup(rsc.recordSet.ownerGroupId.getOrElse("")) + ccGroup <- groupRepository.getGroup(rsc.recordSet.recordSetGroupChange.map(_.requestedOwnerGroupId.getOrElse("")).getOrElse("")) + _ <- 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_ { ccId => + userRepository.getUser(ccId).flatMap { + case Some(ccUser) => + val ccEmail = ccUser.email.get + 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("")}" + ) + } + case None => + IO { + logger.warn(s"Unable to find user: ${rsc.userId}") + } + case _ => + 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("")}" - ) - } - case None => - IO { - logger.warn(s"Unable to find user: ${rsc.userId}") - - } - case _ => - IO.unit - } + case None => IO.unit // Handle case where toGroup is None + } + } yield () } + + def formatBatchChange(bc: BatchChange): String = { val sb = new StringBuilder // Batch change info diff --git a/modules/portal/app/views/zones/zoneTabs/manageRecords.scala.html b/modules/portal/app/views/zones/zoneTabs/manageRecords.scala.html index 457b27c1a..aa05eae95 100644 --- a/modules/portal/app/views/zones/zoneTabs/manageRecords.scala.html +++ b/modules/portal/app/views/zones/zoneTabs/manageRecords.scala.html @@ -355,13 +355,12 @@
+ + +
- - - - From 28de709b497b28d8d2b8aa0918779da7ca06ea7b Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Tue, 27 Aug 2024 17:47:27 +0530 Subject: [PATCH 24/25] Removed email notifier in config --- modules/api/src/main/resources/reference.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/api/src/main/resources/reference.conf b/modules/api/src/main/resources/reference.conf index 776fd6da9..817c668e1 100644 --- a/modules/api/src/main/resources/reference.conf +++ b/modules/api/src/main/resources/reference.conf @@ -161,7 +161,7 @@ vinyldns { } } - notifiers = ["email"] + notifiers = [] email = { class-name = "vinyldns.api.notifier.email.EmailNotifierProvider" From 52dcb88663d3c456b38db7f1216b2d978b9a4b8b Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Tue, 27 Aug 2024 23:23:50 +0530 Subject: [PATCH 25/25] Resolved tests --- .../api/notifier/email/EmailNotifier.scala | 21 ++++++++-------- .../notifier/email/EmailNotifierSpec.scala | 25 ++++++++----------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/modules/api/src/main/scala/vinyldns/api/notifier/email/EmailNotifier.scala b/modules/api/src/main/scala/vinyldns/api/notifier/email/EmailNotifier.scala index 057054a86..3abcd5ca8 100644 --- a/modules/api/src/main/scala/vinyldns/api/notifier/email/EmailNotifier.scala +++ b/modules/api/src/main/scala/vinyldns/api/notifier/email/EmailNotifier.scala @@ -18,7 +18,8 @@ package vinyldns.api.notifier.email import vinyldns.core.notifier.{Notification, Notifier} import cats.effect.IO -import cats.implicits.toFoldableOps +import cats.implicits._ +import cats.effect.IO import vinyldns.core.domain.batch.{BatchChange, BatchChangeApprovalStatus, SingleAddChange, SingleChange, SingleDeleteRRSetChange} import vinyldns.core.domain.membership.{GroupRepository, User, UserRepository} import org.slf4j.LoggerFactory @@ -88,24 +89,24 @@ class EmailNotifier(config: EmailNotifierConfig, session: Session, userRepositor ccGroup <- groupRepository.getGroup(rsc.recordSet.recordSetGroupChange.map(_.requestedOwnerGroupId.getOrElse("")).getOrElse("")) _ <- toGroup match { case Some(group) => - group.memberIds.toList.traverse_ { id => + group.memberIds.toList.traverse { id => userRepository.getUser(id).flatMap { case Some(UserWithEmail(toEmail)) => ccGroup match { case Some(ccg) => - ccg.memberIds.toList.traverse_ { ccId => - userRepository.getUser(ccId).flatMap { - case Some(ccUser) => - val ccEmail = ccUser.email.get - send(toEmail)(new InternetAddress(ccEmail) ){ message => + ccg.memberIds.toList.traverse { id => + userRepository.getUser(id).flatMap { + case Some(ccUser) => + val ccEmail = ccUser.email.getOrElse("") + 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 None => IO.unit } case Some(user: User) if user.email.isDefined => diff --git a/modules/api/src/test/scala/vinyldns/api/notifier/email/EmailNotifierSpec.scala b/modules/api/src/test/scala/vinyldns/api/notifier/email/EmailNotifierSpec.scala index 62a1f4c46..243ac3546 100644 --- a/modules/api/src/test/scala/vinyldns/api/notifier/email/EmailNotifierSpec.scala +++ b/modules/api/src/test/scala/vinyldns/api/notifier/email/EmailNotifierSpec.scala @@ -20,6 +20,7 @@ import org.scalatest.wordspec.AnyWordSpec import org.scalatest.BeforeAndAfterEach import org.scalatestplus.mockito.MockitoSugar import vinyldns.api.CatsHelpers + import javax.mail.{Provider, Session, Transport, URLName} import java.util.Properties import vinyldns.core.domain.membership.{GroupRepository, User, UserRepository} @@ -30,8 +31,10 @@ import org.mockito.Matchers.{eq => eqArg, _} import org.mockito.Mockito._ import org.mockito.ArgumentCaptor import cats.effect.IO + import javax.mail.{Address, Message} import _root_.vinyldns.core.domain.batch._ + import java.time.Instant import java.time.temporal.ChronoUnit import vinyldns.core.domain.record.{AData, OwnerShipTransferStatus, RecordSetChange, RecordSetChangeStatus, RecordSetChangeType, RecordType} @@ -157,8 +160,10 @@ class EmailNotifierSpec 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) @@ -167,28 +172,20 @@ class EmailNotifierSpec doReturn(IO.pure(Some(okGroup))) .when(mockGroupRepository) .getGroup(okGroup.id) - doReturn(IO.pure(Some(okUser))) - .when(mockUserRepository) - .getUser(okGroup.memberIds.head) + doReturn(IO.pure(Some(dummyGrp))) + .when(mockGroupRepository) + .getGroup(dummyGrp.id) doReturn(IO.pure(Some(okUser))) .when(mockUserRepository) .getUser(okUser.id) - doReturn(IO.pure(Some(dummyGroup))) - .when(mockGroupRepository) - .getGroup(dummyGroup.id) - doReturn(IO.pure(Some(dummyUser.copy(email = Some("test@test.com"))))) + doReturn(IO.pure(Some(dummyUsr))) .when(mockUserRepository) - .getUser(dummyGroup.memberIds.head) - doReturn(IO.pure(Some(dummyUser.copy(email = Some("test@test.com"))))) - .when(mockUserRepository) - .getUser(dummyUser.id) + .getUser(dummyGrp.memberIds.head) val rsc = reccordSetChange.copy(userId = okUser.id) notifier.notify(Notification(rsc)).unsafeRunSync() val message = messageArgument.getValue - println(message) - message.getFrom should be(Array(fromAddress)) message.getContentType should be("text/html; charset=us-ascii") message.getAllRecipients should be(expectedAddresses)