From 6a1e91d5dfa888538d26aea998c22696cff69ede Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Mon, 24 Jul 2023 18:24:02 +0530 Subject: [PATCH 01/22] list abandoned zones --- .../domain/zone/ListZoneChangesResponse.scala | 9 + .../api/domain/zone/ZoneProtocol.scala | 23 +- .../api/domain/zone/ZoneService.scala | 54 ++++- .../api/domain/zone/ZoneServiceAlgebra.scala | 8 + .../vinyldns/api/route/ZoneRouting.scala | 32 +++ .../api/domain/zone/ZoneServiceSpec.scala | 191 ++++++++++++++++ .../vinyldns/api/route/ZoneRoutingSpec.scala | 183 ++++++++++++++++ .../domain/zone/ListZoneChangesResults.scala | 9 + .../domain/zone/ZoneChangeRepository.scala | 9 + .../scala/vinyldns/core/TestZoneData.scala | 28 +++ ...lZoneChangeRepositoryIntegrationSpec.scala | 154 +++++++++++++ .../MySqlZoneRepositoryIntegrationSpec.scala | 5 +- .../migration/V3.30__AddStatusZoneAccess.sql | 8 + modules/mysql/src/main/resources/test/ddl.sql | 5 +- .../MySqlZoneChangeRepository.scala | 121 +++++++++++ .../repository/MySqlZoneRepository.scala | 26 ++- modules/portal/app/controllers/VinylDNS.scala | 14 ++ .../portal/app/views/zones/zones.scala.html | 204 ++++++++++++++++++ modules/portal/conf/routes | 1 + .../controller.manageZones.spec.js | 7 + .../lib/controllers/controller.zones.js | 118 ++++++++++ .../lib/controllers/controller.zones.spec.js | 74 ++++++- .../lib/services/zones/service.zones.js | 14 ++ .../lib/services/zones/service.zones.spec.js | 9 + .../controllers/TestApplicationData.scala | 18 ++ .../test/controllers/VinylDNSSpec.scala | 68 ++++++ 26 files changed, 1377 insertions(+), 15 deletions(-) create mode 100644 modules/mysql/src/main/resources/db/migration/V3.30__AddStatusZoneAccess.sql diff --git a/modules/api/src/main/scala/vinyldns/api/domain/zone/ListZoneChangesResponse.scala b/modules/api/src/main/scala/vinyldns/api/domain/zone/ListZoneChangesResponse.scala index e6fe8fd03..7c84acb0c 100644 --- a/modules/api/src/main/scala/vinyldns/api/domain/zone/ListZoneChangesResponse.scala +++ b/modules/api/src/main/scala/vinyldns/api/domain/zone/ListZoneChangesResponse.scala @@ -38,6 +38,15 @@ object ListZoneChangesResponse { ) } +case class ListDeletedZoneChangesResponse( + zonesDeletedInfo: List[ZoneChangeDeletedInfo], + zoneChangeFilter: Option[String] = None, + nextId: Option[String] = None, + startFrom: Option[String] = None, + maxItems: Int = 100, + ignoreAccess: Boolean = false +) + case class ListFailedZoneChangesResponse( failedZoneChanges: List[ZoneChange] = Nil, nextId: Int, 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 faf2b16be..dda0bc6b3 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 @@ -22,7 +22,7 @@ import vinyldns.core.domain.record.RecordSetChangeType.RecordSetChangeType import vinyldns.core.domain.record.RecordSetStatus.RecordSetStatus import vinyldns.core.domain.record.RecordType.RecordType import vinyldns.core.domain.record.{RecordData, RecordSet, RecordSetChange} -import vinyldns.core.domain.zone.{ACLRuleInfo, AccessLevel, Zone, ZoneACL, ZoneConnection} +import vinyldns.core.domain.zone.{ACLRuleInfo, AccessLevel, Zone, ZoneACL, ZoneChange, ZoneConnection} import vinyldns.core.domain.zone.AccessLevel.AccessLevel import vinyldns.core.domain.zone.ZoneStatus.ZoneStatus @@ -145,6 +145,27 @@ object ZoneSummaryInfo { ) } +case class ZoneChangeDeletedInfo( + zoneChange: ZoneChange, + adminGroupName: String, + userName: String, + accessLevel: AccessLevel + ) + +object ZoneChangeDeletedInfo { + def apply(zoneChange: List[ZoneChange], + groupName: String, + userName: String, + accessLevel: AccessLevel) + : ZoneChangeDeletedInfo = + ZoneChangeDeletedInfo( + zoneChange= zoneChange, + groupName = groupName, + userName = userName, + accessLevel = accessLevel + ) +} + case class RecordSetListInfo( zoneId: String, name: String, diff --git a/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneService.scala b/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneService.scala index f2080410b..c4e99782c 100644 --- a/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneService.scala +++ b/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneService.scala @@ -23,7 +23,7 @@ import vinyldns.api.Interfaces import vinyldns.core.domain.auth.AuthPrincipal import vinyldns.api.repository.ApiDataAccessor import vinyldns.core.crypto.CryptoAlgebra -import vinyldns.core.domain.membership.{Group, GroupRepository, User, UserRepository} +import vinyldns.core.domain.membership.{Group, GroupRepository, ListUsersResults, User, UserRepository} import vinyldns.core.domain.zone._ import vinyldns.core.queue.MessageQueue import vinyldns.core.domain.DomainHelpers.ensureTrailingDot @@ -226,6 +226,58 @@ class ZoneService( } }.toResult + def listDeletedZones( + authPrincipal: AuthPrincipal, + nameFilter: Option[String] = None, + startFrom: Option[String] = None, + maxItems: Int = 100, + ignoreAccess: Boolean = false + ): Result[ListDeletedZoneChangesResponse] = { + for { + listZonesChangeResult <- zoneChangeRepository.listDeletedZones( + authPrincipal, + nameFilter, + startFrom, + maxItems, + ignoreAccess + ) + zoneChanges = listZonesChangeResult.zoneDeleted + groupIds = zoneChanges.map(_.zone.adminGroupId).toSet + groups <- groupRepository.getGroups(groupIds) + userId = zoneChanges.map(_.userId).toSet + users <- userRepository.getUsers(userId,None,None) + zoneDeleteSummaryInfos = ZoneChangeDeletedInfoMapping(zoneChanges, authPrincipal, groups, users) + } yield { + ListDeletedZoneChangesResponse( + zoneDeleteSummaryInfos, + listZonesChangeResult.zoneChangeFilter, + listZonesChangeResult.nextId, + listZonesChangeResult.startFrom, + listZonesChangeResult.maxItems, + listZonesChangeResult.ignoreAccess + ) + } + }.toResult + + private def ZoneChangeDeletedInfoMapping( + zoneChange: List[ZoneChange], + auth: AuthPrincipal, + groups: Set[Group], + users: ListUsersResults + ): List[ZoneChangeDeletedInfo] = + zoneChange.map { zc => + val groupName = groups.find(_.id == zc.zone.adminGroupId) match { + case Some(group) => group.name + case None => "Unknown group name" + } + val userName = users.users.find(_.id == zc.userId) match { + case Some(user) => user.userName + case None => "Unknown user name" + } + val zoneAccess = getZoneAccess(auth, zc.zone) + ZoneChangeDeletedInfo(zc, groupName,userName, zoneAccess) + } + def zoneSummaryInfoMapping( zones: List[Zone], auth: AuthPrincipal, diff --git a/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneServiceAlgebra.scala b/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneServiceAlgebra.scala index 01a15ace5..daede2241 100644 --- a/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneServiceAlgebra.scala +++ b/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneServiceAlgebra.scala @@ -49,6 +49,14 @@ trait ZoneServiceAlgebra { includeReverse: Boolean ): Result[ListZonesResponse] + def listDeletedZones( + authPrincipal: AuthPrincipal, + nameFilter: Option[String], + startFrom: Option[String], + maxItems: Int, + ignoreAccess: Boolean + ): Result[ListDeletedZoneChangesResponse] + def listZoneChanges( zoneId: String, authPrincipal: AuthPrincipal, diff --git a/modules/api/src/main/scala/vinyldns/api/route/ZoneRouting.scala b/modules/api/src/main/scala/vinyldns/api/route/ZoneRouting.scala index 5ba1b0aef..9cc289b5f 100644 --- a/modules/api/src/main/scala/vinyldns/api/route/ZoneRouting.scala +++ b/modules/api/src/main/scala/vinyldns/api/route/ZoneRouting.scala @@ -111,6 +111,38 @@ class ZoneRoute( } } } ~ + path("zones" / "deleted" / "changes") { + (get & monitor("Endpoint.listDeletedZones")) { + parameters( + "nameFilter".?, + "startFrom".as[String].?, + "maxItems".as[Int].?(DEFAULT_MAX_ITEMS), + "ignoreAccess".as[Boolean].?(false) + ) { + ( + nameFilter: Option[String], + startFrom: Option[String], + maxItems: Int, + ignoreAccess: Boolean + ) => + { + handleRejections(invalidQueryHandler) { + validate( + 0 < maxItems && maxItems <= MAX_ITEMS_LIMIT, + s"maxItems was $maxItems, maxItems must be between 0 and $MAX_ITEMS_LIMIT" + ) { + authenticateAndExecute( + zoneService + .listDeletedZones(_, nameFilter, startFrom, maxItems, ignoreAccess) + ) { result => + complete(StatusCodes.OK, result) + } + } + } + } + } + } + } ~ path("zones" / "backendids") { (get & monitor("Endpoint.getBackendIds")) { authenticateAndExecute(_ => zoneService.getBackendIds()) { ids => diff --git a/modules/api/src/test/scala/vinyldns/api/domain/zone/ZoneServiceSpec.scala b/modules/api/src/test/scala/vinyldns/api/domain/zone/ZoneServiceSpec.scala index dc8789eb1..6e4a4c8ae 100644 --- a/modules/api/src/test/scala/vinyldns/api/domain/zone/ZoneServiceSpec.scala +++ b/modules/api/src/test/scala/vinyldns/api/domain/zone/ZoneServiceSpec.scala @@ -57,6 +57,8 @@ class ZoneServiceSpec private val badConnection = ZoneConnection("bad", "bad", Encrypted("bad"), "bad") private val abcZoneSummary = ZoneSummaryInfo(abcZone, abcGroup.name, AccessLevel.Delete) private val xyzZoneSummary = ZoneSummaryInfo(xyzZone, xyzGroup.name, AccessLevel.NoAccess) + private val abcDeletedZoneSummary = ZoneChangeDeletedInfo(abcDeletedZoneChange, abcGroup.name, okUser.userName, AccessLevel.Delete) + private val xyzDeletedZoneSummary = ZoneChangeDeletedInfo(xyzDeletedZoneChange, xyzGroup.name, okUser.userName, AccessLevel.NoAccess) private val zoneIp4ZoneSummary = ZoneSummaryInfo(zoneIp4, abcGroup.name, AccessLevel.Delete) private val zoneIp6ZoneSummary = ZoneSummaryInfo(zoneIp6, abcGroup.name, AccessLevel.Delete) private val mockMembershipRepo = mock[MembershipRepository] @@ -1084,6 +1086,195 @@ class ZoneServiceSpec } } + "ListDeletedZones" should { + "not fail with no zones returned" in { + + doReturn(IO.pure(ListDeletedZonesChangeResults(List()))) + .when(mockZoneChangeRepo) + .listDeletedZones(abcAuth, None, None, 100, false) + doReturn(IO.pure(Set(abcGroup))).when(mockGroupRepo).getGroups(any[Set[String]]) + doReturn(IO.pure(ListUsersResults(Seq(okUser), None))) + .when(mockUserRepo) + .getUsers(any[Set[String]], any[Option[String]], any[Option[Int]]) + + val result: ListDeletedZoneChangesResponse = rightResultOf(underTest.listDeletedZones(abcAuth).value) + result.zonesDeletedInfo shouldBe List() + result.maxItems shouldBe 100 + result.startFrom shouldBe None + result.zoneChangeFilter shouldBe None + result.nextId shouldBe None + result.ignoreAccess shouldBe false + } + + "return the appropriate zones" in { + doReturn(IO.pure(ListDeletedZonesChangeResults(List(abcDeletedZoneChange)))) + .when(mockZoneChangeRepo) + .listDeletedZones(abcAuth, None, None, 100, false) + doReturn(IO.pure(Set(abcGroup))) + .when(mockGroupRepo) + .getGroups(any[Set[String]]) + doReturn(IO.pure(ListUsersResults(Seq(okUser), None))) + .when(mockUserRepo) + .getUsers(any[Set[String]], any[Option[String]], any[Option[Int]]) + + val result: ListDeletedZoneChangesResponse = rightResultOf(underTest.listDeletedZones(abcAuth).value) + + result.zonesDeletedInfo shouldBe List(abcDeletedZoneSummary) + result.maxItems shouldBe 100 + result.startFrom shouldBe None + result.zoneChangeFilter shouldBe None + result.nextId shouldBe None + result.ignoreAccess shouldBe false + } + + "return all zones" in { + doReturn(IO.pure(ListDeletedZonesChangeResults(List(abcDeletedZoneChange, xyzDeletedZoneChange), ignoreAccess = true))) + .when(mockZoneChangeRepo) + .listDeletedZones(abcAuth, None, None, 100, true) + doReturn(IO.pure(Set(abcGroup, xyzGroup))) + .when(mockGroupRepo) + .getGroups(any[Set[String]]) + doReturn(IO.pure(ListUsersResults(Seq(okUser), None))) + .when(mockUserRepo) + .getUsers(any[Set[String]], any[Option[String]], any[Option[Int]]) + + val result: ListDeletedZoneChangesResponse = + rightResultOf(underTest.listDeletedZones(abcAuth, ignoreAccess = true).value) + result.zonesDeletedInfo shouldBe List(abcDeletedZoneSummary,xyzDeletedZoneSummary) + result.maxItems shouldBe 100 + result.startFrom shouldBe None + result.zoneChangeFilter shouldBe None + result.nextId shouldBe None + result.ignoreAccess shouldBe true + } + + "return Unknown group name if zone admin group cannot be found" in { + doReturn(IO.pure(ListDeletedZonesChangeResults(List(abcDeletedZoneChange, xyzDeletedZoneChange)))) + .when(mockZoneChangeRepo) + .listDeletedZones(abcAuth, None, None, 100, false) + doReturn(IO.pure(Set(okGroup))).when(mockGroupRepo).getGroups(any[Set[String]]) + doReturn(IO.pure(ListUsersResults(Seq(okUser), None))) + .when(mockUserRepo) + .getUsers(any[Set[String]], any[Option[String]], any[Option[Int]]) + + val result: ListDeletedZoneChangesResponse = rightResultOf(underTest.listDeletedZones(abcAuth).value) + val expectedZones = + List(abcDeletedZoneSummary, xyzDeletedZoneSummary).map(_.copy(adminGroupName = "Unknown group name")) + result.zonesDeletedInfo shouldBe expectedZones + result.maxItems shouldBe 100 + result.startFrom shouldBe None + result.zoneChangeFilter shouldBe None + result.nextId shouldBe None + } + + "set the nextId appropriately" in { + doReturn( + IO.pure( + ListDeletedZonesChangeResults( + List(abcDeletedZoneChange, xyzDeletedZoneChange), + maxItems = 2, + nextId = Some("zone2."), + ignoreAccess = false + ) + ) + ).when(mockZoneChangeRepo) + .listDeletedZones(abcAuth, None, None, 2, false) + doReturn(IO.pure(Set(abcGroup, xyzGroup))) + .when(mockGroupRepo) + .getGroups(any[Set[String]]) + doReturn(IO.pure(ListUsersResults(Seq(okUser), None))) + .when(mockUserRepo) + .getUsers(any[Set[String]], any[Option[String]], any[Option[Int]]) + + val result: ListDeletedZoneChangesResponse = + rightResultOf(underTest.listDeletedZones(abcAuth, maxItems = 2).value) + result.zonesDeletedInfo shouldBe List(abcDeletedZoneSummary, xyzDeletedZoneSummary) + result.maxItems shouldBe 2 + result.startFrom shouldBe None + result.zoneChangeFilter shouldBe None + result.nextId shouldBe Some("zone2.") + } + + "set the nameFilter when provided" in { + doReturn( + IO.pure( + ListDeletedZonesChangeResults( + List(abcDeletedZoneChange, xyzDeletedZoneChange), + zoneChangeFilter = Some("foo"), + maxItems = 2, + nextId = Some("zone2."), + ignoreAccess = false + ) + ) + ).when(mockZoneChangeRepo) + .listDeletedZones(abcAuth, Some("foo"), None, 2, false) + doReturn(IO.pure(Set(abcGroup, xyzGroup))) + .when(mockGroupRepo) + .getGroups(any[Set[String]]) + doReturn(IO.pure(ListUsersResults(Seq(okUser), None))) + .when(mockUserRepo) + .getUsers(any[Set[String]], any[Option[String]], any[Option[Int]]) + + val result: ListDeletedZoneChangesResponse = + rightResultOf(underTest.listDeletedZones(abcAuth, nameFilter = Some("foo"), maxItems = 2).value) + result.zonesDeletedInfo shouldBe List(abcDeletedZoneSummary, xyzDeletedZoneSummary) + result.zoneChangeFilter shouldBe Some("foo") + result.nextId shouldBe Some("zone2.") + result.maxItems shouldBe 2 + } + + "set the startFrom when provided" in { + doReturn( + IO.pure( + ListDeletedZonesChangeResults( + List(abcDeletedZoneChange, xyzDeletedZoneChange), + startFrom = Some("zone4."), + maxItems = 2, + ignoreAccess = false + ) + ) + ).when(mockZoneChangeRepo) + .listDeletedZones(abcAuth, None, Some("zone4."), 2, false) + doReturn(IO.pure(Set(abcGroup, xyzGroup))) + .when(mockGroupRepo) + .getGroups(any[Set[String]]) + doReturn(IO.pure(ListUsersResults(Seq(okUser), None))) + .when(mockUserRepo) + .getUsers(any[Set[String]], any[Option[String]], any[Option[Int]]) + + val result: ListDeletedZoneChangesResponse = + rightResultOf(underTest.listDeletedZones(abcAuth, startFrom = Some("zone4."), maxItems = 2).value) + result.zonesDeletedInfo shouldBe List(abcDeletedZoneSummary, xyzDeletedZoneSummary) + result.startFrom shouldBe Some("zone4.") + } + + "set the nextId to be the current result set size plus the start from" in { + doReturn( + IO.pure( + ListDeletedZonesChangeResults( + List(abcDeletedZoneChange, xyzDeletedZoneChange), + startFrom = Some("zone4."), + maxItems = 2, + nextId = Some("zone6."), + ignoreAccess = false + ) + ) + ).when(mockZoneChangeRepo) + .listDeletedZones(abcAuth, None, Some("zone4."), 2, false) + doReturn(IO.pure(Set(abcGroup, xyzGroup))) + .when(mockGroupRepo) + .getGroups(any[Set[String]]) + doReturn(IO.pure(ListUsersResults(Seq(okUser), None))) + .when(mockUserRepo) + .getUsers(any[Set[String]], any[Option[String]], any[Option[Int]]) + + val result: ListDeletedZoneChangesResponse = + rightResultOf(underTest.listDeletedZones(abcAuth, startFrom = Some("zone4."), maxItems = 2).value) + result.zonesDeletedInfo shouldBe List(abcDeletedZoneSummary, xyzDeletedZoneSummary) + result.nextId shouldBe Some("zone6.") + } + } + "listZoneChanges" should { "retrieve the zone changes" in { doReturn(IO.pure(Some(okZone))) diff --git a/modules/api/src/test/scala/vinyldns/api/route/ZoneRoutingSpec.scala b/modules/api/src/test/scala/vinyldns/api/route/ZoneRoutingSpec.scala index 9b9f0673a..f3bdcdbdd 100644 --- a/modules/api/src/test/scala/vinyldns/api/route/ZoneRoutingSpec.scala +++ b/modules/api/src/test/scala/vinyldns/api/route/ZoneRoutingSpec.scala @@ -117,6 +117,26 @@ class ZoneRoutingSpec Zone("zone6.in-addr.arpa.", "zone6@test.com", ZoneStatus.Active, adminGroupId = xyzGroup.id) private val zoneSummaryInfo6 = ZoneSummaryInfo(zone6, xyzGroup.name, AccessLevel.NoAccess) private val error = Zone("error.", "error@test.com") + private val deletedZone1 = Zone("ok1.", "ok1@test.com", ZoneStatus.Deleted , acl = zoneAcl) + private val deletedZoneChange1 = ZoneChange(deletedZone1, "ok1", ZoneChangeType.Create, ZoneChangeStatus.Complete) + private val ZoneChangeDeletedInfo1 = ZoneChangeDeletedInfo( + deletedZoneChange1, okGroup.name, okUser.userName, AccessLevel.NoAccess) + private val deletedZone2 = Zone("ok2.", "ok2@test.com", ZoneStatus.Deleted , acl = zoneAcl) + private val deletedZoneChange2 = ZoneChange(deletedZone2, "ok2", ZoneChangeType.Create, ZoneChangeStatus.Complete) + private val ZoneChangeDeletedInfo2 = ZoneChangeDeletedInfo( + deletedZoneChange2, okGroup.name, okUser.userName, AccessLevel.NoAccess) + private val deletedZone3 = Zone("ok3.", "ok3@test.com", ZoneStatus.Deleted , acl = zoneAcl) + private val deletedZoneChange3 = ZoneChange(deletedZone3, "ok3", ZoneChangeType.Create, ZoneChangeStatus.Complete) + private val ZoneChangeDeletedInfo3= ZoneChangeDeletedInfo( + deletedZoneChange3, okGroup.name, okUser.userName, AccessLevel.NoAccess) + private val deletedZone4 = Zone("ok4.", "ok4@test.com", ZoneStatus.Deleted , acl = zoneAcl, adminGroupId = xyzGroup.id) + private val deletedZoneChange4 = ZoneChange(deletedZone4, "ok4", ZoneChangeType.Create, ZoneChangeStatus.Complete) + private val ZoneChangeDeletedInfo4 = ZoneChangeDeletedInfo( + deletedZoneChange4, okGroup.name, okUser.userName, AccessLevel.NoAccess) + private val deletedZone5 = Zone("ok5.", "ok5@test.com", ZoneStatus.Deleted , acl = zoneAcl, adminGroupId = xyzGroup.id) + private val deletedZoneChange5 = ZoneChange(deletedZone5, "ok5", ZoneChangeType.Create, ZoneChangeStatus.Complete) + private val ZoneChangeDeletedInfo5 = ZoneChangeDeletedInfo( + deletedZoneChange5, okGroup.name, okUser.userName, AccessLevel.NoAccess) private val missingFields: JValue = ("invalidField" -> "randomValue") ~~ @@ -395,6 +415,92 @@ class ZoneRoutingSpec outcome.toResult } + def listDeletedZones( + authPrincipal: AuthPrincipal, + nameFilter: Option[String], + startFrom: Option[String], + maxItems: Int, + ignoreAccess: Boolean = false + ): Result[ListDeletedZoneChangesResponse] = { + + val outcome = (authPrincipal, nameFilter, startFrom, maxItems, ignoreAccess) match { + case (_, None, Some("zone3."), 3, false) => + Right( + ListDeletedZoneChangesResponse( + zonesDeletedInfo = List(ZoneChangeDeletedInfo1,ZoneChangeDeletedInfo2,ZoneChangeDeletedInfo3), + zoneChangeFilter = None, + startFrom = Some("zone3."), + nextId = Some("zone6."), + maxItems = 3, + ignoreAccess = false + ) + ) + case (_, None, Some("zone4."), 4, false) => + Right( + ListDeletedZoneChangesResponse( + zonesDeletedInfo = List(ZoneChangeDeletedInfo1,ZoneChangeDeletedInfo2,ZoneChangeDeletedInfo3), + zoneChangeFilter = None, + startFrom = Some("zone4."), + nextId = None, + maxItems = 4, + ignoreAccess = false + ) + ) + + case (_, None, None, 3, false) => + Right( + ListDeletedZoneChangesResponse( + zonesDeletedInfo = List(ZoneChangeDeletedInfo1,ZoneChangeDeletedInfo2,ZoneChangeDeletedInfo3), + zoneChangeFilter = None, + startFrom = None, + nextId = Some("zone3."), + maxItems = 3, + ignoreAccess = false + ) + ) + + case (_, None, None, 5, true) => + Right( + ListDeletedZoneChangesResponse( + zonesDeletedInfo = + List(ZoneChangeDeletedInfo1,ZoneChangeDeletedInfo2,ZoneChangeDeletedInfo3, ZoneChangeDeletedInfo4,ZoneChangeDeletedInfo5), + zoneChangeFilter = None, + startFrom = None, + nextId = None, + maxItems = 5, + ignoreAccess = true + ) + ) + + case (_, Some(filter), Some("zone4."), 4, false) => + Right( + ListDeletedZoneChangesResponse( + zonesDeletedInfo = List(ZoneChangeDeletedInfo1,ZoneChangeDeletedInfo2,ZoneChangeDeletedInfo3), + zoneChangeFilter = Some(filter), + startFrom = Some("zone4."), + nextId = None, + maxItems = 4, + ignoreAccess = false + ) + ) + + case (_, None, None, _, _) => + Right( + ListDeletedZoneChangesResponse( + zonesDeletedInfo = List(ZoneChangeDeletedInfo1,ZoneChangeDeletedInfo2,ZoneChangeDeletedInfo3), + zoneChangeFilter = None, + startFrom = None, + nextId = None, + ignoreAccess = false + ) + ) + + case _ => Left(InvalidRequest("shouldnt get here")) + } + + outcome.toResult + } + def listZoneChanges( zoneId: String, authPrincipal: AuthPrincipal, @@ -1078,6 +1184,83 @@ class ZoneRoutingSpec } } + "GET Deleted zones" should { + "return the next id when more results exist" in { + Get(s"/zones/deleted/changes?startFrom=zone3.&maxItems=3") ~> zoneRoute ~> check { + val resp = responseAs[ListDeletedZoneChangesResponse] + val deletedZones = resp.zonesDeletedInfo + (deletedZones.map(_.zoneChange.zone.id) should contain) + .only(deletedZone1.id, deletedZone2.id, deletedZone3.id) + resp.nextId shouldBe Some("zone6.") + resp.maxItems shouldBe 3 + resp.startFrom shouldBe Some("zone3.") + } + } + + "not return the next id when there are no more results" in { + Get(s"/zones/deleted/changes?startFrom=zone4.&maxItems=4") ~> zoneRoute ~> check { + val resp = responseAs[ListDeletedZoneChangesResponse] + val deletedZones = resp.zonesDeletedInfo + (deletedZones.map(_.zoneChange.zone.id) should contain) + .only(deletedZone1.id, deletedZone2.id, deletedZone3.id) + resp.nextId shouldBe None + resp.maxItems shouldBe 4 + resp.startFrom shouldBe Some("zone4.") + resp.ignoreAccess shouldBe false + } + } + + "not return the start from when not provided" in { + Get(s"/zones/deleted/changes?maxItems=3") ~> zoneRoute ~> check { + val resp = responseAs[ListDeletedZoneChangesResponse] + val deletedZones = resp.zonesDeletedInfo + (deletedZones.map(_.zoneChange.zone.id) should contain) + .only(deletedZone1.id, deletedZone2.id, deletedZone3.id) + resp.nextId shouldBe Some("zone3.") + resp.maxItems shouldBe 3 + resp.startFrom shouldBe None + resp.ignoreAccess shouldBe false + } + } + + "return the name filter when provided" in { + Get(s"/zones/deleted/changes?nameFilter=foo&startFrom=zone4.&maxItems=4") ~> zoneRoute ~> check { + val resp = responseAs[ListDeletedZoneChangesResponse] + val deletedZones = resp.zonesDeletedInfo + (deletedZones.map(_.zoneChange.zone.id) should contain) + .only(deletedZone1.id, deletedZone2.id, deletedZone3.id) + resp.nextId shouldBe None + resp.maxItems shouldBe 4 + resp.startFrom shouldBe Some("zone4.") + resp.zoneChangeFilter shouldBe Some("foo") + resp.ignoreAccess shouldBe false + } + } + + "return all zones when list all is true" in { + Get(s"/zones/deleted/changes?maxItems=5&ignoreAccess=true") ~> zoneRoute ~> check { + val resp = responseAs[ListDeletedZoneChangesResponse] + val deletedZones = resp.zonesDeletedInfo + (deletedZones.map(_.zoneChange.zone.id) should contain) + .only(deletedZone1.id, deletedZone2.id, deletedZone3.id, deletedZone4.id, deletedZone5.id) + resp.nextId shouldBe None + resp.maxItems shouldBe 5 + resp.startFrom shouldBe None + resp.zoneChangeFilter shouldBe None + resp.ignoreAccess shouldBe true + } + } + + "return an error if the max items is out of range" in { + Get(s"/zones/deleted/changes?maxItems=700") ~> zoneRoute ~> check { + status shouldBe BadRequest + responseEntity.toString should include( + "maxItems was 700, maxItems must be between 0 and 100" + ) + } + } + } + "GET zone changes" should { "return the zone changes" in { Get(s"/zones/${ok.id}/changes") ~> zoneRoute ~> check { diff --git a/modules/core/src/main/scala/vinyldns/core/domain/zone/ListZoneChangesResults.scala b/modules/core/src/main/scala/vinyldns/core/domain/zone/ListZoneChangesResults.scala index 5a44e4109..002f7280f 100644 --- a/modules/core/src/main/scala/vinyldns/core/domain/zone/ListZoneChangesResults.scala +++ b/modules/core/src/main/scala/vinyldns/core/domain/zone/ListZoneChangesResults.scala @@ -23,6 +23,15 @@ case class ListZoneChangesResults( maxItems: Int = 100 ) +case class ListDeletedZonesChangeResults( + zoneDeleted: List[ZoneChange] = List[ZoneChange](), + nextId: Option[String] = None, + startFrom: Option[String] = None, + maxItems: Int = 100, + ignoreAccess: Boolean = false, + zoneChangeFilter: Option[String] = None +) + case class ListFailedZoneChangesResults( items: List[ZoneChange] = List[ZoneChange](), nextId: Int = 0, diff --git a/modules/core/src/main/scala/vinyldns/core/domain/zone/ZoneChangeRepository.scala b/modules/core/src/main/scala/vinyldns/core/domain/zone/ZoneChangeRepository.scala index 79a55ebf6..15ffcbebd 100644 --- a/modules/core/src/main/scala/vinyldns/core/domain/zone/ZoneChangeRepository.scala +++ b/modules/core/src/main/scala/vinyldns/core/domain/zone/ZoneChangeRepository.scala @@ -17,6 +17,7 @@ package vinyldns.core.domain.zone import cats.effect._ +import vinyldns.core.domain.auth.AuthPrincipal import vinyldns.core.repository.Repository trait ZoneChangeRepository extends Repository { @@ -29,6 +30,14 @@ trait ZoneChangeRepository extends Repository { maxItems: Int = 100 ): IO[ListZoneChangesResults] + def listDeletedZones( + authPrincipal: AuthPrincipal, + zoneNameFilter: Option[String] = None, + startFrom: Option[String] = None, + maxItems: Int = 100, + ignoreAccess: Boolean = false + ): IO[ListDeletedZonesChangeResults] + def listFailedZoneChanges( maxItems: Int = 100, startFrom: Int= 0 diff --git a/modules/core/src/test/scala/vinyldns/core/TestZoneData.scala b/modules/core/src/test/scala/vinyldns/core/TestZoneData.scala index d27a84fe5..c0837bc4b 100644 --- a/modules/core/src/test/scala/vinyldns/core/TestZoneData.scala +++ b/modules/core/src/test/scala/vinyldns/core/TestZoneData.scala @@ -52,9 +52,21 @@ object TestZoneData { connection = testConnection ) + val abcZoneDeleted: Zone = Zone("abc.zone.recordsets.", "test@test.com", adminGroupId = abcGroup.id, status = ZoneStatus.Deleted) + val xyzZoneDeleted: Zone = Zone("xyz.zone.recordsets.", "abc@xyz.com", adminGroupId = xyzGroup.id, status = ZoneStatus.Deleted) + val zoneDeleted: Zone = Zone( "some.deleted.zone.", "test@test.com", + adminGroupId = abcGroup.id, + status = ZoneStatus.Deleted, + connection = testConnection + ) + + val zoneDeletedOkGroup: Zone = Zone( + "some.deleted.zone.", + "test@test.com", + adminGroupId = okGroup.id, status = ZoneStatus.Deleted, connection = testConnection ) @@ -89,6 +101,22 @@ object TestZoneData { val zoneUpdate: ZoneChange = zoneChangePending.copy(status = ZoneChangeStatus.Synced) + val abcDeletedZoneChange: ZoneChange = ZoneChange( + abcZoneDeleted, + "ok", + ZoneChangeType.Create, + ZoneChangeStatus.Complete, + created = DateTime.now.minus(1000) + ) + + val xyzDeletedZoneChange: ZoneChange = ZoneChange( + xyzZoneDeleted, + "ok", + ZoneChangeType.Create, + ZoneChangeStatus.Complete, + created = DateTime.now.minus(1000) + ) + def makeTestPendingZoneChange(zone: Zone): ZoneChange = ZoneChange(zone, "userId", ZoneChangeType.Update, ZoneChangeStatus.Pending) diff --git a/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlZoneChangeRepositoryIntegrationSpec.scala b/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlZoneChangeRepositoryIntegrationSpec.scala index ab1758f76..17698da5f 100644 --- a/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlZoneChangeRepositoryIntegrationSpec.scala +++ b/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlZoneChangeRepositoryIntegrationSpec.scala @@ -33,6 +33,9 @@ import vinyldns.core.TestZoneData.okZone import vinyldns.core.TestZoneData.testConnection import vinyldns.core.domain.Encrypted import vinyldns.mysql.TestMySqlInstance +import vinyldns.core.TestZoneData.{okZone, testConnection} +import vinyldns.core.domain.auth.AuthPrincipal +import vinyldns.core.TestMembershipData.{dummyAuth, dummyGroup, okGroup, okUser} import scala.concurrent.duration._ import scala.util.Random @@ -92,6 +95,7 @@ class MySqlZoneChangeRepositoryIntegrationSpec status= ZoneChangeStatus.Failed, created = Instant.now.truncatedTo(ChronoUnit.MILLIS).minusSeconds(Random.nextInt(1000)) ) + val successChanges : IndexedSeq[ZoneChange] = for { zone <- zones } yield ZoneChange( zone, @@ -100,6 +104,84 @@ class MySqlZoneChangeRepositoryIntegrationSpec status= ZoneChangeStatus.Synced, created = Instant.now.truncatedTo(ChronoUnit.MILLIS).minusSeconds(Random.nextInt(1000)) ) + + val groups = (11 until 20) + .map(num => okGroup.copy(name = num.toString, id = UUID.randomUUID().toString)) + .toList + + // generate some ACLs + private val groupAclRules = groups.map( + g => + ACLRule( + accessLevel = AccessLevel.Read, + groupId = Some(g.id) + ) + ) + + private val userOnlyAclRule = + ACLRule( + accessLevel = AccessLevel.Read, + userId = Some(okUser.id) + ) + + // the zone acl rule will have the user rule and all of the group rules + private val testZoneAcl = ZoneACL( + rules = Set(userOnlyAclRule) ++ groupAclRules + ) + + private val testZoneAdminGroupId = "foo" + + val dummyAclRule = + ACLRule( + accessLevel = AccessLevel.Read, + groupId = Some(dummyGroup.id) + ) + + val testZone = (11 until 20).map { num => + val z = + okZone.copy( + name = num.toString + ".", + id = UUID.randomUUID().toString, + adminGroupId = testZoneAdminGroupId, + acl = testZoneAcl + ) + // add the dummy acl rule to the first zone + if (num == 11) z.addACLRule(dummyAclRule) else z + } + + val deletedZoneChanges + : IndexedSeq[ZoneChange] = for { testZone <- testZone } yield { + ZoneChange( + testZone.copy(status = ZoneStatus.Deleted), + testZone.account, + ZoneChangeType.Create, + ZoneChangeStatus.Synced, + created = DateTime.now().minusSeconds(Random.nextInt(1000)) + )} + + def saveZones(zones: Seq[Zone]): IO[Unit] = + zones.foldLeft(IO.unit) { + case (acc, z) => + acc.flatMap { _ => + zoneRepo.save(z).map(_ => ()) + } + } + + def deleteZones(zones: Seq[Zone]): IO[Unit] = + zones.foldLeft(IO.unit) { + case (acc, z) => + acc.flatMap { _ => + zoneRepo.deleteTx(z).map(_ => ()) + } + } + + def saveZoneChanges(zoneChanges: Seq[ZoneChange]): IO[Unit] = + zoneChanges.foldLeft(IO.unit) { + case (acc, zc) => + acc.flatMap { _ => + repo.save(zc).map(_ => ()) + } + } } import TestData._ @@ -303,5 +385,77 @@ class MySqlZoneChangeRepositoryIntegrationSpec pageThree.nextId should equal(None) pageThree.startFrom should equal(pageTwo.nextId) } + "get authorized zones" in { + // store all of the zones + saveZones(testZone).unsafeRunSync() + // save the change + saveZoneChanges(deletedZoneChanges).unsafeRunSync() + + // query for all zones for the ok user, he should have access to all of the zones + val okUserAuth = AuthPrincipal( + signedInUser = okUser, + memberGroupIds = groups.map(_.id) + ) + + repo.listDeletedZones(okUserAuth).unsafeRunSync().zoneDeleted should contain theSameElementsAs deletedZoneChanges + + // dummy user only has access to one zone + (repo.listDeletedZones(dummyAuth).unsafeRunSync().zoneDeleted should contain).only(deletedZoneChanges.head) + + deleteZones(testZone).unsafeRunSync() + + + } + "return an empty list of zones if the user is not authorized to any" in { + val unauthorized = AuthPrincipal( + signedInUser = User("not-authorized", "not-authorized", "not-authorized"), + memberGroupIds = Seq.empty + ) + + val f = + for { + _ <- saveZones(testZone) + _ <- saveZoneChanges(deletedZoneChanges) + zones <- repo.listDeletedZones(unauthorized) + } yield zones + + f.unsafeRunSync().zoneDeleted shouldBe empty + deleteZones(testZone).unsafeRunSync() + + } + + "not return zones when access is revoked" in { + // ok user can access both zones, dummy can only access first zone + val zones = testZone.take(2) + val zoneChange = deletedZoneChanges.take(2) + val addACL = saveZones(zones) + val addACLZc = saveZoneChanges(zoneChange) + + + val okUserAuth = AuthPrincipal( + signedInUser = okUser, + memberGroupIds = groups.map(_.id) + ) + addACL.unsafeRunSync() + addACLZc.unsafeRunSync() + + (repo.listDeletedZones(okUserAuth).unsafeRunSync().zoneDeleted should contain). allElementsOf(zoneChange) + + // dummy user only has access to first zone + (repo.listDeletedZones(dummyAuth).unsafeRunSync().zoneDeleted should contain).only(zoneChange.head) + + // revoke the access for the dummy user + val revoked = zones(0).deleteACLRule(dummyAclRule) + val revokedZc = zoneChange(0).copy(zone=revoked) + zoneRepo.save(revoked).unsafeRunSync() + repo.save(revokedZc).unsafeRunSync() + + // ok user can still access zones + (repo.listDeletedZones(okUserAuth).unsafeRunSync().zoneDeleted should contain).allElementsOf(Seq( zoneChange(1))) + + // dummy user can not access the revoked zone + repo.listDeletedZones(dummyAuth).unsafeRunSync().zoneDeleted shouldBe empty + deleteZones(testZone).unsafeRunSync() + } } } diff --git a/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlZoneRepositoryIntegrationSpec.scala b/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlZoneRepositoryIntegrationSpec.scala index 7b0c19b32..9c54eef8f 100644 --- a/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlZoneRepositoryIntegrationSpec.scala +++ b/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlZoneRepositoryIntegrationSpec.scala @@ -952,12 +952,13 @@ class MySqlZoneRepositoryIntegrationSpec "check if an id has an ACL rule for at least one of the zones" in { val zoneId = UUID.randomUUID().toString + val adminId = UUID.randomUUID().toString val testZones = (1 until 3).map { num => okZone.copy( name = num.toString + ".", id = zoneId, - adminGroupId = testZoneAdminGroupId, + adminGroupId = adminId, acl = testZoneAcl ) } @@ -965,7 +966,7 @@ class MySqlZoneRepositoryIntegrationSpec val f = for { _ <- saveZones(testZones) - zones <- repo.getFirstOwnedZoneAclGroupId(testZoneAdminGroupId) + zones <- repo.getFirstOwnedZoneAclGroupId(adminId) } yield zones f.unsafeRunSync() shouldBe Some(zoneId) diff --git a/modules/mysql/src/main/resources/db/migration/V3.30__AddStatusZoneAccess.sql b/modules/mysql/src/main/resources/db/migration/V3.30__AddStatusZoneAccess.sql new file mode 100644 index 000000000..eca6b16f8 --- /dev/null +++ b/modules/mysql/src/main/resources/db/migration/V3.30__AddStatusZoneAccess.sql @@ -0,0 +1,8 @@ +CREATE SCHEMA IF NOT EXISTS ${dbName}; + +USE ${dbName}; + +ALTER TABLE zone_access +DROP CONSTRAINT fk_zone_access, +ADD zone_status CHAR(36) NOT NULL +; \ No newline at end of file diff --git a/modules/mysql/src/main/resources/test/ddl.sql b/modules/mysql/src/main/resources/test/ddl.sql index 4889127a9..d310c48d0 100644 --- a/modules/mysql/src/main/resources/test/ddl.sql +++ b/modules/mysql/src/main/resources/test/ddl.sql @@ -237,10 +237,7 @@ CREATE TABLE IF NOT EXISTS zone_access ( accessor_id char(36) not null, zone_id char(36) not null, - primary key (accessor_id, zone_id), - constraint fk_zone_access_zone_id - foreign key (zone_id) references zone (id) - on delete cascade + primary key (accessor_id, zone_id) ); CREATE INDEX IF NOT EXISTS zone_access_accessor_id_index diff --git a/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlZoneChangeRepository.scala b/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlZoneChangeRepository.scala index cf0614431..bba875da3 100644 --- a/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlZoneChangeRepository.scala +++ b/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlZoneChangeRepository.scala @@ -17,10 +17,13 @@ package vinyldns.mysql.repository import cats.effect.IO + import java.time.Instant import java.time.temporal.ChronoUnit import org.slf4j.LoggerFactory import scalikejdbc._ +import vinyldns.core.domain.auth.AuthPrincipal +import vinyldns.core.domain.membership.User import vinyldns.core.domain.zone._ import vinyldns.core.protobuf._ import vinyldns.core.route.Monitored @@ -32,12 +35,26 @@ class MySqlZoneChangeRepository with Monitored { private final val logger = LoggerFactory.getLogger(classOf[MySqlZoneChangeRepository]) + private final val MAX_ACCESSORS = 30 + private final val PUT_ZONE_CHANGE = sql""" |REPLACE INTO zone_change (change_id, zone_id, data, created_timestamp) | VALUES ({change_id}, {zone_id}, {data}, {created_timestamp}) """.stripMargin + private final val BASE_ZONE_CHANGE_SEARCH_SQL = + """ + |SELECT zc.data + | FROM zone_change zc + """.stripMargin + + private final val BASE_GET_ZONES_SQL = + """ + |SELECT z.data + | FROM zone z + """.stripMargin + private final val LIST_ZONES_CHANGES = sql""" |SELECT zc.data @@ -110,6 +127,106 @@ class MySqlZoneChangeRepository } } + private def withAccessors( + user: User, + groupIds: Seq[String], + ignoreAccessZones: Boolean + ): (String, Seq[Any]) = + // Super users do not need to join across to check zone access as they have access to all of the zones + if (ignoreAccessZones || user.isSuper || user.isSupport) { + (BASE_ZONE_CHANGE_SEARCH_SQL, Seq.empty) + } else { + // User is not super or support, + // let's join across to the zone access table so we return only zones a user has access to + val accessors = buildZoneSearchAccessorList(user, groupIds) + val questionMarks = List.fill(accessors.size)("?").mkString(",") + val withAccessorCheck = BASE_ZONE_CHANGE_SEARCH_SQL + + s""" + | JOIN zone_access za ON zc.zone_id = za.zone_id + | AND za.accessor_id IN ($questionMarks) + """.stripMargin + (withAccessorCheck, accessors) + } + + /* Limit the accessors so that we don't have boundless parameterized queries */ + private def buildZoneSearchAccessorList(user: User, groupIds: Seq[String]): Seq[String] = { + val allAccessors = user.id +: groupIds + + if (allAccessors.length > MAX_ACCESSORS) { + logger.warn( + s"User ${user.userName} with id ${user.id} is in more than $MAX_ACCESSORS groups, no all zones maybe returned!" + ) + } + + // Take the top 30 accessors, but add "EVERYONE" to the list so that we include zones that have everyone access + allAccessors.take(MAX_ACCESSORS) :+ "EVERYONE" + } + + def listDeletedZones( + authPrincipal: AuthPrincipal, + zoneNameFilter: Option[String] = None, + startFrom: Option[String] = None, + maxItems: Int = 100, + ignoreAccess: Boolean = false + ): IO[ListDeletedZonesChangeResults] = + monitor("repo.ZoneChange.listDeletedZoneInZoneChanges") { + IO { + DB.readOnly { implicit s => + val (withAccessorCheck, accessors) = + withAccessors(authPrincipal.signedInUser, authPrincipal.memberGroupIds, ignoreAccess) + val sb = new StringBuilder + sb.append(withAccessorCheck) + + val query = sb.toString + + val zoneChangeResults: List[ZoneChange] = + SQL(query) + .bind(accessors: _*) + .map(extractZoneChange(1)) + .list() + .apply() + + val zoneResults: List[Zone] = + SQL(BASE_GET_ZONES_SQL) + .map(extractZone(1)) + .list() + .apply() + + val zoneNotInZoneChange: List[ZoneChange] = + zoneChangeResults.filter(z=> !zoneResults.map(_.name).contains(z.zone.name) && z.zone.status != ZoneStatus.Active) + + val deletedZoneResults: List[ZoneChange] = + zoneNotInZoneChange.filter(_.zone.status.equals(ZoneStatus.Deleted)).distinct + + val results: List[ZoneChange] = + if (zoneNameFilter.nonEmpty) { + deletedZoneResults.filter(r => r.zone.name.contains(zoneNameFilter.getOrElse("not found"))) + } else { + deletedZoneResults + } + + val deletedZonesWithStartFrom: List[ZoneChange] = startFrom match { + case Some(zoneName) => results.dropWhile(_.zone.name != zoneName) + case None => results + } + + val deletedZonesWithMaxItems = deletedZonesWithStartFrom.take(maxItems + 1) + + val (newResults, nextId) = + if (deletedZonesWithMaxItems.size > maxItems) + (deletedZonesWithMaxItems.dropRight(1), deletedZonesWithMaxItems.lastOption.map(_.zone.name)) + else (deletedZonesWithMaxItems, None) + + ListDeletedZonesChangeResults( + zoneDeleted = newResults, + nextId = nextId, + startFrom = startFrom, + maxItems = maxItems, + zoneChangeFilter = zoneNameFilter, + ignoreAccess = ignoreAccess + ) + }}} + def listFailedZoneChanges(maxItems: Int, startFrom: Int): IO[ListFailedZoneChangesResults] = monitor("repo.ZoneChange.listFailedZoneChanges") { IO { @@ -130,4 +247,8 @@ class MySqlZoneChangeRepository private def extractZoneChange(colIndex: Int): WrappedResultSet => ZoneChange = res => { fromPB(VinylDNSProto.ZoneChange.parseFrom(res.bytes(colIndex))) } + + private def extractZone(columnIndex: Int): WrappedResultSet => Zone = res => { + fromPB(VinylDNSProto.Zone.parseFrom(res.bytes(columnIndex))) + } } diff --git a/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlZoneRepository.scala b/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlZoneRepository.scala index c61a1ea14..6a15a9853 100644 --- a/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlZoneRepository.scala +++ b/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlZoneRepository.scala @@ -71,10 +71,17 @@ class MySqlZoneRepository extends ZoneRepository with ProtobufConversions with M */ private final val PUT_ZONE_ACCESS = sql""" - |REPLACE INTO zone_access(accessor_id, zone_id) - | VALUES ({accessorId}, {zoneId}) + |REPLACE INTO zone_access(accessor_id, zone_id, zone_status) + | VALUES ({accessorId}, {zoneId}, {zoneStatus}) """.stripMargin + private final val UPDATE_ZONE_ACCESS = + sql""" + |UPDATE zone_access + | SET zone_status = {zoneStatus} + | WHERE zone_id = {zoneId} + """.stripMargin + private final val DELETE_ZONE_ACCESS = sql""" |DELETE @@ -127,8 +134,8 @@ class MySqlZoneRepository extends ZoneRepository with ProtobufConversions with M private final val GET_ZONE_ACCESS_BY_ADMIN_GROUP_ID = sql""" |SELECT zone_id - | FROM zone_access z - | WHERE z.accessor_id = (?) + | FROM zone_access za + | WHERE za.accessor_id = (?) AND za.zone_status <> 'Deleted' | LIMIT 1 """.stripMargin @@ -503,10 +510,10 @@ class MySqlZoneRepository extends ZoneRepository with ProtobufConversions with M val sqlParameters: Seq[Seq[(Symbol, Any)]] = zone.acl.rules.toSeq .map(r => r.userId.orElse(r.groupId).getOrElse("EVERYONE")) // if the user and group are empty, assert everyone - .map(userOrGroupId => Seq('accessorId -> userOrGroupId, 'zoneId -> zone.id)) + .map(userOrGroupId => Seq('accessorId -> userOrGroupId, 'zoneId -> zone.id, 'zoneStatus -> zone.status.toString)) // we MUST make sure that we put the admin group id as an accessor to this zone - val allAccessors = sqlParameters :+ Seq('accessorId -> zone.adminGroupId, 'zoneId -> zone.id) + val allAccessors = sqlParameters :+ Seq('accessorId -> zone.adminGroupId, 'zoneId -> zone.id,'zoneStatus -> zone.status.toString) // make sure that we do a distinct, so that we don't generate unnecessary inserts PUT_ZONE_ACCESS.batchByName(allAccessors.distinct: _*).apply() @@ -518,6 +525,12 @@ class MySqlZoneRepository extends ZoneRepository with ProtobufConversions with M zone } + private def updateZoneAccess(zone: Zone)(implicit session: DBSession): Zone = { + UPDATE_ZONE_ACCESS.bindByName( + 'zoneStatus ->zone.status.toString, 'zoneId ->zone.id).update().apply() + zone + } + private def deleteZoneAccess(zone: Zone)(implicit session: DBSession): Zone = { DELETE_ZONE_ACCESS.bind(zone.id).update().apply() zone @@ -532,6 +545,7 @@ class MySqlZoneRepository extends ZoneRepository with ProtobufConversions with M IO { DB.localTx { implicit s => deleteZone(zone) + updateZoneAccess(zone) } } } diff --git a/modules/portal/app/controllers/VinylDNS.scala b/modules/portal/app/controllers/VinylDNS.scala index 7daa0c0d0..f21b1c620 100644 --- a/modules/portal/app/controllers/VinylDNS.scala +++ b/modules/portal/app/controllers/VinylDNS.scala @@ -477,6 +477,20 @@ class VinylDNS @Inject() ( }) } + def getDeletedZones: Action[AnyContent] = userAction.async { implicit request => + val queryParameters = new HashMap[String, java.util.List[String]]() + for { + (name, values) <- request.queryString + } queryParameters.put(name, values.asJava) + val vinyldnsRequest = + new VinylDNSRequest("GET", s"$vinyldnsServiceBackend", "zones/deleted/changes", parameters = queryParameters) + executeRequest(vinyldnsRequest, request.user).map(response => { + Status(response.status)(response.body) + .withHeaders(cacheHeaders: _*) + }) + // $COVERAGE-ON$ + } + def getZoneChange(id: String): Action[AnyContent] = userAction.async { implicit request => val queryParameters = new HashMap[String, java.util.List[String]]() for { diff --git a/modules/portal/app/views/zones/zones.scala.html b/modules/portal/app/views/zones/zones.scala.html index ec8a442a7..df2e2fafb 100644 --- a/modules/portal/app/views/zones/zones.scala.html +++ b/modules/portal/app/views/zones/zones.scala.html @@ -29,6 +29,7 @@
@@ -289,6 +290,209 @@
+
+
+
+ + +
+
+ + + + +
+
+
+ + + + +
+
+
+ + + +
+ +
+
+
+

Loading zones...

+

No zones match the search criteria.

+ + +
+ {{ getZonesPageNumber("myDeletedZones") }} + +
+ + + + + + + + + + + + + @if(meta.sharedDisplayEnabled) { + + } + + + + + + + + + + + + @if(meta.sharedDisplayEnabled) { + + } + + +
NameEmailAdmin GroupCreatedAbandonedStatusAbandoned ByAccess
+ + + + + + {{deletedZone.zoneChange.zone.created}} + + {{deletedZone.zoneChange.zone.updated}} + + {{deletedZone.userName}} + {{zone.shared ? "Shared" : "Private"}}
+ + +
+ {{ getZonesPageNumber("myDeletedZones") }} + +
+ + +
+
+
+
+

Loading zones...

+

No zones match the search criteria.

+ + +
+ {{ getZonesPageNumber("allDeletedZones") }} + +
+ + + + + + + + + + + + + @if(meta.sharedDisplayEnabled) { + + } + + + + + + + + + + + + @if(meta.sharedDisplayEnabled) { + + } + + +
NameEmailAdmin GroupCreatedAbandonedStatusAbandoned ByAccess
+ + + + + + {{deletedZone.zoneChange.zone.created}} + + {{deletedZone.zoneChange.zone.updated}} + + {{deletedZone.userName}} + {{zone.shared ? "Shared" : "Private"}}
+ + +
+ {{ getZonesPageNumber("allDeletedZones") }} + +
+ + +
+
+
+
+ +
+ +
+ + +
+
+
diff --git a/modules/portal/conf/routes b/modules/portal/conf/routes index f0dc50335..895622486 100644 --- a/modules/portal/conf/routes +++ b/modules/portal/conf/routes @@ -31,6 +31,7 @@ GET /api/zones/backendids @controllers.VinylDNS.getBacken GET /api/zones/:id @controllers.VinylDNS.getZone(id: String) GET /api/zones/:id/details @controllers.VinylDNS.getCommonZoneDetails(id: String) GET /api/zones/name/:name @controllers.VinylDNS.getZoneByName(name: String) +GET /api/zones/deleted/changes @controllers.VinylDNS.getDeletedZones GET /api/zones/:id/changes @controllers.VinylDNS.getZoneChange(id: String) POST /api/zones @controllers.VinylDNS.addZone PUT /api/zones/:id @controllers.VinylDNS.updateZone(id: String) diff --git a/modules/portal/public/lib/controllers/controller.manageZones.spec.js b/modules/portal/public/lib/controllers/controller.manageZones.spec.js index 0fb0f28b4..63499d18d 100644 --- a/modules/portal/public/lib/controllers/controller.manageZones.spec.js +++ b/modules/portal/public/lib/controllers/controller.manageZones.spec.js @@ -42,6 +42,13 @@ describe('Controller: ManageZonesController', function () { } }); }; + zonesService.getDeletedZones = function() { + return $q.when({ + data: { + zonesDeletedInfo: ["all my deleted zones"] + } + }); + }; zonesService.getBackendIds = function() { return $q.when({ data: ['backend-1', 'backend-2'] diff --git a/modules/portal/public/lib/controllers/controller.zones.js b/modules/portal/public/lib/controllers/controller.zones.js index 51466c09d..e017d10f6 100644 --- a/modules/portal/public/lib/controllers/controller.zones.js +++ b/modules/portal/public/lib/controllers/controller.zones.js @@ -23,6 +23,8 @@ angular.module('controller.zones', []) $scope.allZonesLoaded = false; $scope.hasZones = false; // Re-assigned each time zones are fetched without a query $scope.allGroups = []; + $scope.myDeletedZones = []; + $scope.allDeletedZones = []; $scope.ignoreAccess = false; $scope.validEmailDomains= []; $scope.allZonesAccess = function () { @@ -41,6 +43,8 @@ angular.module('controller.zones', []) // Paging status for zone sets var zonesPaging = pagingService.getNewPagingParams(100); var allZonesPaging = pagingService.getNewPagingParams(100); + var myDeleteZonesPaging = pagingService.getNewPagingParams(100); + var allDeleteZonesPaging = pagingService.getNewPagingParams(100); profileService.getAuthenticatedUserData().then(function (results) { if (results.data) { $scope.profile = results.data; @@ -197,8 +201,52 @@ angular.module('controller.zones', []) .catch(function (error) { handleError(error, 'zonesService::getZones-failure'); }); + + zonesService + .getDeletedZones(myDeleteZonesPaging.maxItems, undefined, $scope.query, false) + .then(function (response) { + $log.debug('zonesService::getMyDeletedZones-success (' + response.data.zonesDeletedInfo.length + ' zones)'); + myDeleteZonesPaging.next = response.data.nextId; + updateMyDeletedZoneDisplay(response.data.zonesDeletedInfo); + }) + .catch(function (error) { + handleError(error, 'zonesService::getDeletedZones-failure'); + }); + zonesService + .getDeletedZones(allDeleteZonesPaging.maxItems, undefined, $scope.query, true) + .then(function (response) { + $log.debug('zonesService::getAllDeletedZones-success (' + response.data.zonesDeletedInfo.length + ' zones)'); + allDeleteZonesPaging.next = response.data.nextId; + updateAllDeletedZoneDisplay(response.data.zonesDeletedInfo); + }) + .catch(function (error) { + handleError(error, 'zonesService::getDeletedZones-failure'); + }); + }; + function updateMyDeletedZoneDisplay (myDeletedZones) { + $scope.myDeletedZones = myDeletedZones; + $scope.myDeletedZonesLoaded = true; + $log.debug("Displaying my Deleted zones: ", $scope.myDeletedZones); + if($scope.myDeletedZones.length > 0) { + $("td.dataTables_empty").hide(); + } else { + $("td.dataTables_empty").show(); + } + } + + function updateAllDeletedZoneDisplay (allDeletedZones) { + $scope.allDeletedZones = allDeletedZones; + $scope.allDeletedZonesLoaded = true; + $log.debug("Displaying all Deleted zones: ", $scope.allDeletedZones); + if($scope.allDeletedZones.length > 0) { + $("td.dataTables_empty").hide(); + } else { + $("td.dataTables_empty").show(); + } + } + function updateZoneDisplay (zones) { $scope.zones = zones; $scope.myZoneIds = zones.map(function(zone) {return zone['id']}); @@ -283,6 +331,10 @@ angular.module('controller.zones', []) return pagingService.getPanelTitle(zonesPaging); case 'allZones': return pagingService.getPanelTitle(allZonesPaging); + case 'myDeletedZones': + return pagingService.getPanelTitle(myDeleteZonesPaging); + case 'allDeletedZones': + return pagingService.getPanelTitle(allDeleteZonesPaging); } }; @@ -292,6 +344,10 @@ angular.module('controller.zones', []) return pagingService.prevPageEnabled(zonesPaging); case 'allZones': return pagingService.prevPageEnabled(allZonesPaging); + case 'myDeletedZones': + return pagingService.prevPageEnabled(myDeleteZonesPaging); + case 'allDeletedZones': + return pagingService.prevPageEnabled(allDeleteZonesPaging); } }; @@ -301,6 +357,10 @@ angular.module('controller.zones', []) return pagingService.nextPageEnabled(zonesPaging); case 'allZones': return pagingService.nextPageEnabled(allZonesPaging); + case 'myDeletedZones': + return pagingService.nextPageEnabled(myDeleteZonesPaging); + case 'allDeletedZones': + return pagingService.nextPageEnabled(allDeleteZonesPaging); } }; @@ -330,6 +390,32 @@ angular.module('controller.zones', []) }); } + $scope.prevPageMyDeletedZones = function() { + var startFrom = pagingService.getPrevStartFrom(myDeleteZonesPaging); + return zonesService + .getDeletedZones(myDeleteZonesPaging.maxItems, startFrom, $scope.query, false) + .then(function(response) { + myDeleteZonesPaging = pagingService.prevPageUpdate(response.data.nextId, myDeleteZonesPaging); + updateMyDeletedZoneDisplay(response.data.zonesDeletedInfo); + }) + .catch(function (error) { + handleError(error,'zonesService::prevPage-failure'); + }); + } + + $scope.prevPageAllDeletedZones = function() { + var startFrom = pagingService.getPrevStartFrom(allDeleteZonesPaging); + return zonesService + .getDeletedZones(allDeleteZonesPaging.maxItems, startFrom, $scope.query, true) + .then(function(response) { + allDeleteZonesPaging = pagingService.prevPageUpdate(response.data.nextId, allDeleteZonesPaging); + updateAllDeletedZoneDisplay(response.data.zonesDeletedInfo); + }) + .catch(function (error) { + handleError(error,'zonesService::prevPage-failure'); + }); + } + $scope.nextPageMyZones = function () { return zonesService .getZones(zonesPaging.maxItems, zonesPaging.next, $scope.query, $scope.searchByAdminGroup, false, true) @@ -362,5 +448,37 @@ angular.module('controller.zones', []) }); }; + $scope.nextPageMyDeletedZones = function () { + return zonesService + .getDeletedZones(myDeleteZonesPaging.maxItems, myDeleteZonesPaging.next, $scope.query, false) + .then(function(response) { + var myDeletedZoneSets = response.data.zonesDeletedInfo; + myDeleteZonesPaging = pagingService.nextPageUpdate(myDeletedZoneSets, response.data.nextId, myDeleteZonesPaging); + + if (myDeletedZoneSets.length > 0) { + updateMyDeletedZoneDisplay(response.data.zonesDeletedInfo); + } + }) + .catch(function (error) { + handleError(error,'zonesService::nextPage-failure') + }); + }; + + $scope.nextPageAllDeletedZones = function () { + return zonesService + .getDeletedZones(allDeleteZonesPaging.maxItems, allDeleteZonesPaging.next, $scope.query, false) + .then(function(response) { + var allDeletedZoneSets = response.data.zonesDeletedInfo; + allDeleteZonesPaging = pagingService.nextPageUpdate(allDeletedZoneSets, response.data.nextId, allDeleteZonesPaging); + + if (allDeletedZoneSets.length > 0) { + updateAllDeletedZoneDisplay(response.data.zonesDeletedInfo); + } + }) + .catch(function (error) { + handleError(error,'zonesService::nextPage-failure') + }); + }; + $timeout($scope.refreshZones, 0); }); diff --git a/modules/portal/public/lib/controllers/controller.zones.spec.js b/modules/portal/public/lib/controllers/controller.zones.spec.js index 2064769c9..cc89ffb38 100644 --- a/modules/portal/public/lib/controllers/controller.zones.spec.js +++ b/modules/portal/public/lib/controllers/controller.zones.spec.js @@ -1,4 +1,4 @@ -/* +c/* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -114,4 +114,76 @@ describe('Controller: ZonesController', function () { expect(getZoneSets.calls.mostRecent().args).toEqual( [expectedMaxItems, expectedStartFrom, expectedQuery, expectedSearchByAdminGroup, expectedignoreAccess, expectedincludeReverse]); }); + + it('nextPageMyZones should call getDeletedZones with the correct parameters', function () { + mockDeletedZone = {zonesDeletedInfo:[ { + zoneChanges: [{ zone: { + name: "dummy.", + email: "test@test.com", + status: "Deleted", + created: "2017-02-15T14:58:39Z", + account: "c8234503-bfda-4b80-897f-d74129051eaa", + acl: {rules: []}, + adminGroupId: "c8234503-bfda-4b80-897f-d74129051eaa", + id: "c5c87405-2ec8-4e03-b2dc-c6758a5d9666", + shared: false, + status: "Active", + latestSync: "2017-02-15T14:58:39Z", + isTest: true + }}],maxItems: 100}]}; + var getDeletedZoneSets = spyOn(this.zonesService, 'getDeletedZones') + .and.stub() + .and.returnValue(this.zonesService.q.when(mockDeletedZone)); + + var expectedMaxItems = 100; + var expectedStartFrom = undefined; + var expectedQuery = this.scope.query; + var expectedignoreAccess = true; + + this.scope.nextPageMyDeletedZones(); + + expect(getDeletedZoneSets.calls.count()).toBe(1); + expect(getDeletedZoneSets.calls.mostRecent().args).toEqual( + [expectedMaxItems, expectedStartFrom, expectedQuery, expectedignoreAccess]); + }); + + it('prevPageMyZones should call getZones with the correct parameters', function () { + + mockDeletedZone = {zonesDeletedInfo:[ { + zoneChanges: [{ zone: { + name: "dummy.", + email: "test@test.com", + status: "Deleted", + created: "2017-02-15T14:58:39Z", + account: "c8234503-bfda-4b80-897f-d74129051eaa", + acl: {rules: []}, + adminGroupId: "c8234503-bfda-4b80-897f-d74129051eaa", + id: "c5c87405-2ec8-4e03-b2dc-c6758a5d9666", + shared: false, + status: "Active", + latestSync: "2017-02-15T14:58:39Z", + isTest: true + }}],maxItems: 100}]}; + var getDeletedZoneSets = spyOn(this.zonesService, 'getDeletedZones') + .and.stub() + .and.returnValue(this.zonesService.q.when(mockDeletedZone)); + + var expectedMaxItems = 100; + var expectedStartFrom = undefined; + var expectedQuery = this.scope.query; + var expectedignoreAccess = true; + + this.scope.prevPageMyDeletedZones(); + + expect(getDeletedZoneSets.calls.count()).toBe(1); + expect(getDeletedZoneSets.calls.mostRecent().args).toEqual( + [expectedMaxItems, expectedStartFrom, expectedQuery, expectedignoreAccess]); + + this.scope.nextPageMyDeletedZones(); + this.scope.prevPageMyDeletedZones(); + + expect(getDeletedZoneSets.calls.count()).toBe(3); + expect(getDeletedZoneSets.calls.mostRecent().args).toEqual( + [expectedMaxItems, expectedStartFrom, expectedQuery, expectedignoreAccess]); + }); }); diff --git a/modules/portal/public/lib/services/zones/service.zones.js b/modules/portal/public/lib/services/zones/service.zones.js index a34a734ee..ebc3fa5a8 100644 --- a/modules/portal/public/lib/services/zones/service.zones.js +++ b/modules/portal/public/lib/services/zones/service.zones.js @@ -53,6 +53,20 @@ angular.module('service.zones', []) return $http.get(url); }; + this.getDeletedZones = function (limit, startFrom, query, ignoreAccess) { + if (query == "") { + query = null; + } + var params = { + "maxItems": limit, + "startFrom": startFrom, + "nameFilter": query, + "ignoreAccess": ignoreAccess + }; + var url = groupsService.urlBuilder("/api/zones/deleted/changes", params); + return $http.get(url); + }; + this.getBackendIds = function() { var url = "/api/zones/backendids"; return $http.get(url); diff --git a/modules/portal/public/lib/services/zones/service.zones.spec.js b/modules/portal/public/lib/services/zones/service.zones.spec.js index 967299d9d..ed41ebb26 100644 --- a/modules/portal/public/lib/services/zones/service.zones.spec.js +++ b/modules/portal/public/lib/services/zones/service.zones.spec.js @@ -54,6 +54,15 @@ describe('Service: zoneService', function () { this.$httpBackend.flush(); }); + it('http backend gets called properly when getting deleted zones', function () { + this.$httpBackend.expectGET('/api/zones/deleted/changes?maxItems=100&startFrom=start&nameFilter=someQuery&ignoreAccess=false').respond('deleted zone returned'); + this.zonesService.getDeletedZones('100', 'start', 'someQuery', true) + .then(function(response) { + expect(response.data).toBe('deleted zone returned'); + }); + this.$httpBackend.flush(); + }); + it('http backend gets called properly when deleting zone', function (done) { this.$httpBackend.expectDELETE('/api/zones/id').respond('zone deleted'); this.zonesService.delZone('id') diff --git a/modules/portal/test/controllers/TestApplicationData.scala b/modules/portal/test/controllers/TestApplicationData.scala index 9b06c4e8a..fee7bd4eb 100644 --- a/modules/portal/test/controllers/TestApplicationData.scala +++ b/modules/portal/test/controllers/TestApplicationData.scala @@ -240,6 +240,24 @@ trait TestApplicationData { this: Mockito => | } """.stripMargin) + val hobbitDeletedZoneChange: JsValue = Json.parse(s"""{ + | "zoneId": "$hobbitZoneId", + | "zoneChanges": + | [{ "zone": { + | "name": "$hobbitZoneName", + | "email": "hobbitAdmin@shire.me", + | "status": "Active", + | "account": "system", + | "acl": "rules", + | "adminGroupId": "$hobbitGroupId", + | "id": "$hobbitZoneId", + | "shared": false, + | "status": "Deleted" + | }}], + | "maxItems": 100 + | } + """.stripMargin) + val hobbitZoneChange: JsValue = Json.parse(s"""{ | "zoneId": "$hobbitZoneId", | "zoneChanges": diff --git a/modules/portal/test/controllers/VinylDNSSpec.scala b/modules/portal/test/controllers/VinylDNSSpec.scala index 8c9aef9c0..edaf6135b 100644 --- a/modules/portal/test/controllers/VinylDNSSpec.scala +++ b/modules/portal/test/controllers/VinylDNSSpec.scala @@ -1621,6 +1621,74 @@ class VinylDNSSpec extends Specification with Mockito with TestApplicationData w } } + ".getDeletedZones" should { + + "return ok (200) if the DeletedZones is found" in new WithApplication(app) { + val client = MockWS { + case (GET, u) if u == s"http://localhost:9001/zones/deleted/changes" => + defaultActionBuilder { Results.Ok(hobbitDeletedZoneChange) } + } + val mockUserAccessor = mock[UserAccountAccessor] + mockUserAccessor.get(anyString).returns(IO.pure(Some(frodoUser))) + mockUserAccessor.getUserByKey(anyString).returns(IO.pure(Some(frodoUser))) + val underTest = withClient(client) + val result = + underTest.getDeletedZones()( + FakeRequest(GET, s"/zones/deleted/changes") + .withSession("username" -> frodoUser.userName, "accessKey" -> frodoUser.accessKey) + ) + + status(result) must beEqualTo(OK) + hasCacheHeaders(result) + contentAsJson(result) must beEqualTo(hobbitDeletedZoneChange) + } + + "return a not found (404) if the DeletedZones does not exist" in new WithApplication(app) { + val client = MockWS { + case (GET, u) if u == s"http://localhost:9001/zones/deleted/changes" => + defaultActionBuilder { Results.NotFound } + } + val mockUserAccessor = mock[UserAccountAccessor] + mockUserAccessor.get(anyString).returns(IO.pure(Some(frodoUser))) + mockUserAccessor.getUserByKey(anyString).returns(IO.pure(Some(frodoUser))) + val underTest = withClient(client) + val result = + underTest.getDeletedZones()( + FakeRequest(GET, "zones/deleted/changes") + .withSession("username" -> frodoUser.userName, "accessKey" -> frodoUser.accessKey) + ) + + status(result) must beEqualTo(NOT_FOUND) + hasCacheHeaders(result) + } + + "return unauthorized (401) if requesting user is not logged in" in new WithApplication(app) { + val client = mock[WSClient] + val underTest = withClient(client) + val result = underTest.getDeletedZones()(FakeRequest(GET, s"/api/zones/deleted/changes")) + + status(result) mustEqual 401 + hasCacheHeaders(result) + contentAsString(result) must beEqualTo("You are not logged in. Please login to continue.") + } + "return forbidden (403) if user account is locked" in new WithApplication(app) { + val client = mock[WSClient] + val underTest = withLockedClient(client) + val result = underTest.getDeletedZones()( + FakeRequest(GET, s"/api/zones/deleted/changes").withSession( + "username" -> lockedFrodoUser.userName, + "accessKey" -> lockedFrodoUser.accessKey + ) + ) + + status(result) mustEqual 403 + hasCacheHeaders(result) + contentAsString(result) must beEqualTo( + s"User account for `${lockedFrodoUser.userName}` is locked." + ) + } + } + ".getZone" should { "return unauthorized (401) if requesting user is not logged in" in new WithApplication(app) { val client = mock[WSClient] From 490849c5ff801153026c51ac315b38d75d2709e6 Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Mon, 24 Jul 2023 19:34:38 +0530 Subject: [PATCH 02/22] update --- .../core/src/test/scala/vinyldns/core/TestZoneData.scala | 8 ++++---- .../MySqlZoneChangeRepositoryIntegrationSpec.scala | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/core/src/test/scala/vinyldns/core/TestZoneData.scala b/modules/core/src/test/scala/vinyldns/core/TestZoneData.scala index c0837bc4b..c489938f5 100644 --- a/modules/core/src/test/scala/vinyldns/core/TestZoneData.scala +++ b/modules/core/src/test/scala/vinyldns/core/TestZoneData.scala @@ -105,16 +105,16 @@ object TestZoneData { abcZoneDeleted, "ok", ZoneChangeType.Create, - ZoneChangeStatus.Complete, - created = DateTime.now.minus(1000) + ZoneChangeStatus.Synced, + created = Instant.now.truncatedTo(ChronoUnit.MILLIS).minusMillis(1000) ) val xyzDeletedZoneChange: ZoneChange = ZoneChange( xyzZoneDeleted, "ok", ZoneChangeType.Create, - ZoneChangeStatus.Complete, - created = DateTime.now.minus(1000) + ZoneChangeStatus.Synced, + created = Instant.now.truncatedTo(ChronoUnit.MILLIS).minusMillis(1000) ) def makeTestPendingZoneChange(zone: Zone): ZoneChange = diff --git a/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlZoneChangeRepositoryIntegrationSpec.scala b/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlZoneChangeRepositoryIntegrationSpec.scala index 17698da5f..eecb00f86 100644 --- a/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlZoneChangeRepositoryIntegrationSpec.scala +++ b/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlZoneChangeRepositoryIntegrationSpec.scala @@ -156,7 +156,7 @@ class MySqlZoneChangeRepositoryIntegrationSpec testZone.account, ZoneChangeType.Create, ZoneChangeStatus.Synced, - created = DateTime.now().minusSeconds(Random.nextInt(1000)) + created = Instant.now.truncatedTo(ChronoUnit.MILLIS).minusMillis(1000) )} def saveZones(zones: Seq[Zone]): IO[Unit] = @@ -408,7 +408,7 @@ class MySqlZoneChangeRepositoryIntegrationSpec } "return an empty list of zones if the user is not authorized to any" in { val unauthorized = AuthPrincipal( - signedInUser = User("not-authorized", "not-authorized", "not-authorized"), + signedInUser = User("not-authorized", "not-authorized", Encrypted("not-authorized")), memberGroupIds = Seq.empty ) From 19ef51bd16f6937f8128b586769ac552394b0f52 Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Mon, 24 Jul 2023 19:48:02 +0530 Subject: [PATCH 03/22] update --- .../api/domain/zone/ZoneServiceSpec.scala | 16 ++++++++-------- .../vinyldns/api/route/ZoneRoutingSpec.scala | 10 +++++----- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/modules/api/src/test/scala/vinyldns/api/domain/zone/ZoneServiceSpec.scala b/modules/api/src/test/scala/vinyldns/api/domain/zone/ZoneServiceSpec.scala index 6e4a4c8ae..d45198ee5 100644 --- a/modules/api/src/test/scala/vinyldns/api/domain/zone/ZoneServiceSpec.scala +++ b/modules/api/src/test/scala/vinyldns/api/domain/zone/ZoneServiceSpec.scala @@ -1097,7 +1097,7 @@ class ZoneServiceSpec .when(mockUserRepo) .getUsers(any[Set[String]], any[Option[String]], any[Option[Int]]) - val result: ListDeletedZoneChangesResponse = rightResultOf(underTest.listDeletedZones(abcAuth).value) + val result: ListDeletedZoneChangesResponse = underTest.listDeletedZones(abcAuth).value.unsafeRunSync().toOption.get result.zonesDeletedInfo shouldBe List() result.maxItems shouldBe 100 result.startFrom shouldBe None @@ -1117,7 +1117,7 @@ class ZoneServiceSpec .when(mockUserRepo) .getUsers(any[Set[String]], any[Option[String]], any[Option[Int]]) - val result: ListDeletedZoneChangesResponse = rightResultOf(underTest.listDeletedZones(abcAuth).value) + val result: ListDeletedZoneChangesResponse = underTest.listDeletedZones(abcAuth).value.unsafeRunSync().toOption.get result.zonesDeletedInfo shouldBe List(abcDeletedZoneSummary) result.maxItems shouldBe 100 @@ -1139,7 +1139,7 @@ class ZoneServiceSpec .getUsers(any[Set[String]], any[Option[String]], any[Option[Int]]) val result: ListDeletedZoneChangesResponse = - rightResultOf(underTest.listDeletedZones(abcAuth, ignoreAccess = true).value) + underTest.listDeletedZones(abcAuth, ignoreAccess = true).value.unsafeRunSync().toOption.get result.zonesDeletedInfo shouldBe List(abcDeletedZoneSummary,xyzDeletedZoneSummary) result.maxItems shouldBe 100 result.startFrom shouldBe None @@ -1157,7 +1157,7 @@ class ZoneServiceSpec .when(mockUserRepo) .getUsers(any[Set[String]], any[Option[String]], any[Option[Int]]) - val result: ListDeletedZoneChangesResponse = rightResultOf(underTest.listDeletedZones(abcAuth).value) + val result: ListDeletedZoneChangesResponse = underTest.listDeletedZones(abcAuth).value.unsafeRunSync().toOption.get val expectedZones = List(abcDeletedZoneSummary, xyzDeletedZoneSummary).map(_.copy(adminGroupName = "Unknown group name")) result.zonesDeletedInfo shouldBe expectedZones @@ -1187,7 +1187,7 @@ class ZoneServiceSpec .getUsers(any[Set[String]], any[Option[String]], any[Option[Int]]) val result: ListDeletedZoneChangesResponse = - rightResultOf(underTest.listDeletedZones(abcAuth, maxItems = 2).value) + underTest.listDeletedZones(abcAuth, maxItems = 2).value.unsafeRunSync().toOption.get result.zonesDeletedInfo shouldBe List(abcDeletedZoneSummary, xyzDeletedZoneSummary) result.maxItems shouldBe 2 result.startFrom shouldBe None @@ -1216,7 +1216,7 @@ class ZoneServiceSpec .getUsers(any[Set[String]], any[Option[String]], any[Option[Int]]) val result: ListDeletedZoneChangesResponse = - rightResultOf(underTest.listDeletedZones(abcAuth, nameFilter = Some("foo"), maxItems = 2).value) + underTest.listDeletedZones(abcAuth, nameFilter = Some("foo"), maxItems = 2).value.unsafeRunSync().toOption.get result.zonesDeletedInfo shouldBe List(abcDeletedZoneSummary, xyzDeletedZoneSummary) result.zoneChangeFilter shouldBe Some("foo") result.nextId shouldBe Some("zone2.") @@ -1243,7 +1243,7 @@ class ZoneServiceSpec .getUsers(any[Set[String]], any[Option[String]], any[Option[Int]]) val result: ListDeletedZoneChangesResponse = - rightResultOf(underTest.listDeletedZones(abcAuth, startFrom = Some("zone4."), maxItems = 2).value) + underTest.listDeletedZones(abcAuth, startFrom = Some("zone4."), maxItems = 2).value.unsafeRunSync().toOption.get result.zonesDeletedInfo shouldBe List(abcDeletedZoneSummary, xyzDeletedZoneSummary) result.startFrom shouldBe Some("zone4.") } @@ -1269,7 +1269,7 @@ class ZoneServiceSpec .getUsers(any[Set[String]], any[Option[String]], any[Option[Int]]) val result: ListDeletedZoneChangesResponse = - rightResultOf(underTest.listDeletedZones(abcAuth, startFrom = Some("zone4."), maxItems = 2).value) + underTest.listDeletedZones(abcAuth, startFrom = Some("zone4."), maxItems = 2).value.unsafeRunSync().toOption.get result.zonesDeletedInfo shouldBe List(abcDeletedZoneSummary, xyzDeletedZoneSummary) result.nextId shouldBe Some("zone6.") } diff --git a/modules/api/src/test/scala/vinyldns/api/route/ZoneRoutingSpec.scala b/modules/api/src/test/scala/vinyldns/api/route/ZoneRoutingSpec.scala index f3bdcdbdd..e3b15da57 100644 --- a/modules/api/src/test/scala/vinyldns/api/route/ZoneRoutingSpec.scala +++ b/modules/api/src/test/scala/vinyldns/api/route/ZoneRoutingSpec.scala @@ -118,23 +118,23 @@ class ZoneRoutingSpec private val zoneSummaryInfo6 = ZoneSummaryInfo(zone6, xyzGroup.name, AccessLevel.NoAccess) private val error = Zone("error.", "error@test.com") private val deletedZone1 = Zone("ok1.", "ok1@test.com", ZoneStatus.Deleted , acl = zoneAcl) - private val deletedZoneChange1 = ZoneChange(deletedZone1, "ok1", ZoneChangeType.Create, ZoneChangeStatus.Complete) + private val deletedZoneChange1 = ZoneChange(deletedZone1, "ok1", ZoneChangeType.Create, ZoneChangeStatus.Synced) private val ZoneChangeDeletedInfo1 = ZoneChangeDeletedInfo( deletedZoneChange1, okGroup.name, okUser.userName, AccessLevel.NoAccess) private val deletedZone2 = Zone("ok2.", "ok2@test.com", ZoneStatus.Deleted , acl = zoneAcl) - private val deletedZoneChange2 = ZoneChange(deletedZone2, "ok2", ZoneChangeType.Create, ZoneChangeStatus.Complete) + private val deletedZoneChange2 = ZoneChange(deletedZone2, "ok2", ZoneChangeType.Create, ZoneChangeStatus.Synced) private val ZoneChangeDeletedInfo2 = ZoneChangeDeletedInfo( deletedZoneChange2, okGroup.name, okUser.userName, AccessLevel.NoAccess) private val deletedZone3 = Zone("ok3.", "ok3@test.com", ZoneStatus.Deleted , acl = zoneAcl) - private val deletedZoneChange3 = ZoneChange(deletedZone3, "ok3", ZoneChangeType.Create, ZoneChangeStatus.Complete) + private val deletedZoneChange3 = ZoneChange(deletedZone3, "ok3", ZoneChangeType.Create, ZoneChangeStatus.Synced) private val ZoneChangeDeletedInfo3= ZoneChangeDeletedInfo( deletedZoneChange3, okGroup.name, okUser.userName, AccessLevel.NoAccess) private val deletedZone4 = Zone("ok4.", "ok4@test.com", ZoneStatus.Deleted , acl = zoneAcl, adminGroupId = xyzGroup.id) - private val deletedZoneChange4 = ZoneChange(deletedZone4, "ok4", ZoneChangeType.Create, ZoneChangeStatus.Complete) + private val deletedZoneChange4 = ZoneChange(deletedZone4, "ok4", ZoneChangeType.Create, ZoneChangeStatus.Synced) private val ZoneChangeDeletedInfo4 = ZoneChangeDeletedInfo( deletedZoneChange4, okGroup.name, okUser.userName, AccessLevel.NoAccess) private val deletedZone5 = Zone("ok5.", "ok5@test.com", ZoneStatus.Deleted , acl = zoneAcl, adminGroupId = xyzGroup.id) - private val deletedZoneChange5 = ZoneChange(deletedZone5, "ok5", ZoneChangeType.Create, ZoneChangeStatus.Complete) + private val deletedZoneChange5 = ZoneChange(deletedZone5, "ok5", ZoneChangeType.Create, ZoneChangeStatus.Synced) private val ZoneChangeDeletedInfo5 = ZoneChangeDeletedInfo( deletedZoneChange5, okGroup.name, okUser.userName, AccessLevel.NoAccess) From 2a2deda29f5d236f0e4455cb3481a4586560f190 Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Tue, 25 Jul 2023 12:13:02 +0530 Subject: [PATCH 04/22] update --- ...lZoneChangeRepositoryIntegrationSpec.scala | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlZoneChangeRepositoryIntegrationSpec.scala b/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlZoneChangeRepositoryIntegrationSpec.scala index eecb00f86..6b86a6ea1 100644 --- a/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlZoneChangeRepositoryIntegrationSpec.scala +++ b/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlZoneChangeRepositoryIntegrationSpec.scala @@ -388,6 +388,8 @@ class MySqlZoneChangeRepositoryIntegrationSpec "get authorized zones" in { // store all of the zones saveZones(testZone).unsafeRunSync() + // delete all stored zones + deleteZones(testZone).unsafeRunSync() // save the change saveZoneChanges(deletedZoneChanges).unsafeRunSync() @@ -402,8 +404,24 @@ class MySqlZoneChangeRepositoryIntegrationSpec // dummy user only has access to one zone (repo.listDeletedZones(dummyAuth).unsafeRunSync().zoneDeleted should contain).only(deletedZoneChanges.head) - deleteZones(testZone).unsafeRunSync() + } + "return empty in deleted zone if zone is created again" in { + // store all of the zones + saveZones(testZone).unsafeRunSync() + // save the change + saveZoneChanges(deletedZoneChanges).unsafeRunSync() + + // query for all zones for the ok user, he should have access to all of the zones + val okUserAuth = AuthPrincipal( + signedInUser = okUser, + memberGroupIds = groups.map(_.id) + ) + + repo.listDeletedZones(okUserAuth).unsafeRunSync().zoneDeleted should contain theSameElementsAs List() + + // delete all stored zones + deleteZones(testZone).unsafeRunSync() } "return an empty list of zones if the user is not authorized to any" in { @@ -415,6 +433,7 @@ class MySqlZoneChangeRepositoryIntegrationSpec val f = for { _ <- saveZones(testZone) + _ <- deleteZones(testZone) _ <- saveZoneChanges(deletedZoneChanges) zones <- repo.listDeletedZones(unauthorized) } yield zones @@ -429,6 +448,7 @@ class MySqlZoneChangeRepositoryIntegrationSpec val zones = testZone.take(2) val zoneChange = deletedZoneChanges.take(2) val addACL = saveZones(zones) + val deleteZone= deleteZones(zones) val addACLZc = saveZoneChanges(zoneChange) @@ -437,6 +457,7 @@ class MySqlZoneChangeRepositoryIntegrationSpec memberGroupIds = groups.map(_.id) ) addACL.unsafeRunSync() + deleteZone.unsafeRunSync() addACLZc.unsafeRunSync() (repo.listDeletedZones(okUserAuth).unsafeRunSync().zoneDeleted should contain). allElementsOf(zoneChange) @@ -455,7 +476,6 @@ class MySqlZoneChangeRepositoryIntegrationSpec // dummy user can not access the revoked zone repo.listDeletedZones(dummyAuth).unsafeRunSync().zoneDeleted shouldBe empty - deleteZones(testZone).unsafeRunSync() } } } From b466b97507b98e1a6d4b0f3f95c0854ab13e048d Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Tue, 25 Jul 2023 17:31:26 +0530 Subject: [PATCH 05/22] update --- .../lib/controllers/controller.zones.spec.js | 18 +++++++++--------- .../lib/services/zones/service.zones.spec.js | 15 ++++++++++++--- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/modules/portal/public/lib/controllers/controller.zones.spec.js b/modules/portal/public/lib/controllers/controller.zones.spec.js index cc89ffb38..841b59213 100644 --- a/modules/portal/public/lib/controllers/controller.zones.spec.js +++ b/modules/portal/public/lib/controllers/controller.zones.spec.js @@ -1,4 +1,4 @@ -c/* +/* * Copyright 2018 Comcast Cable Communications Management, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -115,7 +115,7 @@ describe('Controller: ZonesController', function () { [expectedMaxItems, expectedStartFrom, expectedQuery, expectedSearchByAdminGroup, expectedignoreAccess, expectedincludeReverse]); }); - it('nextPageMyZones should call getDeletedZones with the correct parameters', function () { + it('nextPageZones should call getDeletedZones with the correct parameters', function () { mockDeletedZone = {zonesDeletedInfo:[ { zoneChanges: [{ zone: { name: "dummy.", @@ -138,16 +138,16 @@ describe('Controller: ZonesController', function () { var expectedMaxItems = 100; var expectedStartFrom = undefined; var expectedQuery = this.scope.query; - var expectedignoreAccess = true; + var expectedIgnoreAccess = false; this.scope.nextPageMyDeletedZones(); expect(getDeletedZoneSets.calls.count()).toBe(1); expect(getDeletedZoneSets.calls.mostRecent().args).toEqual( - [expectedMaxItems, expectedStartFrom, expectedQuery, expectedignoreAccess]); + [expectedMaxItems, expectedStartFrom, expectedQuery, expectedIgnoreAccess]); }); - it('prevPageMyZones should call getZones with the correct parameters', function () { + it('prevPageZones should call getDeletedZones with the correct parameters', function () { mockDeletedZone = {zonesDeletedInfo:[ { zoneChanges: [{ zone: { @@ -171,19 +171,19 @@ describe('Controller: ZonesController', function () { var expectedMaxItems = 100; var expectedStartFrom = undefined; var expectedQuery = this.scope.query; - var expectedignoreAccess = true; + var expectedIgnoreAccess = false; - this.scope.prevPageMyDeletedZones(); + this.scope.prevPageDeletedZones(); expect(getDeletedZoneSets.calls.count()).toBe(1); expect(getDeletedZoneSets.calls.mostRecent().args).toEqual( - [expectedMaxItems, expectedStartFrom, expectedQuery, expectedignoreAccess]); + [expectedMaxItems, expectedStartFrom, expectedQuery, expectedIgnoreAccess]); this.scope.nextPageMyDeletedZones(); this.scope.prevPageMyDeletedZones(); expect(getDeletedZoneSets.calls.count()).toBe(3); expect(getDeletedZoneSets.calls.mostRecent().args).toEqual( - [expectedMaxItems, expectedStartFrom, expectedQuery, expectedignoreAccess]); + [expectedMaxItems, expectedStartFrom, expectedQuery, expectedIgnoreAccess]); }); }); diff --git a/modules/portal/public/lib/services/zones/service.zones.spec.js b/modules/portal/public/lib/services/zones/service.zones.spec.js index ed41ebb26..2971178b5 100644 --- a/modules/portal/public/lib/services/zones/service.zones.spec.js +++ b/modules/portal/public/lib/services/zones/service.zones.spec.js @@ -54,11 +54,20 @@ describe('Service: zoneService', function () { this.$httpBackend.flush(); }); - it('http backend gets called properly when getting deleted zones', function () { - this.$httpBackend.expectGET('/api/zones/deleted/changes?maxItems=100&startFrom=start&nameFilter=someQuery&ignoreAccess=false').respond('deleted zone returned'); + it('http backend gets called properly when getting my deleted zones', function () { + this.$httpBackend.expectGET('/api/zones/deleted/changes?maxItems=100&startFrom=start&nameFilter=someQuery&ignoreAccess=true').respond('deleted my zone returned'); this.zonesService.getDeletedZones('100', 'start', 'someQuery', true) .then(function(response) { - expect(response.data).toBe('deleted zone returned'); + expect(response.data).toBe('deleted my zone returned'); + }); + this.$httpBackend.flush(); + }); + + it('http backend gets called properly when getting all deleted zones', function () { + this.$httpBackend.expectGET('/api/zones/deleted/changes?maxItems=100&startFrom=start&nameFilter=someQuery&ignoreAccess=false').respond('deleted all zone returned'); + this.zonesService.getDeletedZones('100', 'start', 'someQuery', false) + .then(function(response) { + expect(response.data).toBe('deleted all zone returned'); }); this.$httpBackend.flush(); }); From 6e527e9e7240caf19c2705a6144f21d469e3642e Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Tue, 25 Jul 2023 17:51:44 +0530 Subject: [PATCH 06/22] dummy commit --- modules/portal/public/lib/controllers/controller.zones.spec.js | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/portal/public/lib/controllers/controller.zones.spec.js b/modules/portal/public/lib/controllers/controller.zones.spec.js index 841b59213..19c8aa822 100644 --- a/modules/portal/public/lib/controllers/controller.zones.spec.js +++ b/modules/portal/public/lib/controllers/controller.zones.spec.js @@ -185,5 +185,6 @@ describe('Controller: ZonesController', function () { expect(getDeletedZoneSets.calls.count()).toBe(3); expect(getDeletedZoneSets.calls.mostRecent().args).toEqual( [expectedMaxItems, expectedStartFrom, expectedQuery, expectedIgnoreAccess]); + }); }); From 930e3c6d46331da2359ca2a1e9ca3e95bb4d0970 Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Tue, 25 Jul 2023 19:41:34 +0530 Subject: [PATCH 07/22] Update --- modules/portal/public/lib/controllers/controller.zones.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/portal/public/lib/controllers/controller.zones.spec.js b/modules/portal/public/lib/controllers/controller.zones.spec.js index 19c8aa822..d30b03d08 100644 --- a/modules/portal/public/lib/controllers/controller.zones.spec.js +++ b/modules/portal/public/lib/controllers/controller.zones.spec.js @@ -173,7 +173,7 @@ describe('Controller: ZonesController', function () { var expectedQuery = this.scope.query; var expectedIgnoreAccess = false; - this.scope.prevPageDeletedZones(); + this.scope.prevPageMyDeletedZones(); expect(getDeletedZoneSets.calls.count()).toBe(1); expect(getDeletedZoneSets.calls.mostRecent().args).toEqual( From 5af3b5d6f0039be7c868ec794930fd4173fa57c3 Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Tue, 25 Jul 2023 22:25:53 +0530 Subject: [PATCH 08/22] added descending sort with abandoned date --- .../vinyldns/mysql/repository/MySqlZoneChangeRepository.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlZoneChangeRepository.scala b/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlZoneChangeRepository.scala index bba875da3..2c01dcd75 100644 --- a/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlZoneChangeRepository.scala +++ b/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlZoneChangeRepository.scala @@ -196,7 +196,7 @@ class MySqlZoneChangeRepository zoneChangeResults.filter(z=> !zoneResults.map(_.name).contains(z.zone.name) && z.zone.status != ZoneStatus.Active) val deletedZoneResults: List[ZoneChange] = - zoneNotInZoneChange.filter(_.zone.status.equals(ZoneStatus.Deleted)).distinct + zoneNotInZoneChange.filter(_.zone.status.equals(ZoneStatus.Deleted)).distinct.sortBy(_.zone.updated).reverse val results: List[ZoneChange] = if (zoneNameFilter.nonEmpty) { From 050c79ebc9b599fbdae101811b9d07042cb1bc2f Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Fri, 4 Aug 2023 15:45:41 +0530 Subject: [PATCH 09/22] updated next id --- .../vinyldns/mysql/repository/MySqlZoneChangeRepository.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlZoneChangeRepository.scala b/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlZoneChangeRepository.scala index 2c01dcd75..b0b5a4192 100644 --- a/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlZoneChangeRepository.scala +++ b/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlZoneChangeRepository.scala @@ -206,7 +206,7 @@ class MySqlZoneChangeRepository } val deletedZonesWithStartFrom: List[ZoneChange] = startFrom match { - case Some(zoneName) => results.dropWhile(_.zone.name != zoneName) + case Some(zoneId) => results.dropWhile(_.zone.id != zoneId) case None => results } @@ -214,7 +214,7 @@ class MySqlZoneChangeRepository val (newResults, nextId) = if (deletedZonesWithMaxItems.size > maxItems) - (deletedZonesWithMaxItems.dropRight(1), deletedZonesWithMaxItems.lastOption.map(_.zone.name)) + (deletedZonesWithMaxItems.dropRight(1), deletedZonesWithMaxItems.lastOption.map(_.zone.id)) else (deletedZonesWithMaxItems, None) ListDeletedZonesChangeResults( From b9b5b10397649adbb8cca7263b142ab6662ec712 Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Fri, 4 Aug 2023 18:24:04 +0530 Subject: [PATCH 10/22] added pagination test in api --- ...lZoneChangeRepositoryIntegrationSpec.scala | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlZoneChangeRepositoryIntegrationSpec.scala b/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlZoneChangeRepositoryIntegrationSpec.scala index 6b86a6ea1..b40c50775 100644 --- a/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlZoneChangeRepositoryIntegrationSpec.scala +++ b/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlZoneChangeRepositoryIntegrationSpec.scala @@ -403,7 +403,54 @@ class MySqlZoneChangeRepositoryIntegrationSpec // dummy user only has access to one zone (repo.listDeletedZones(dummyAuth).unsafeRunSync().zoneDeleted should contain).only(deletedZoneChanges.head) + } + "page deleted zones using a startFrom and maxItems" in { + // store all of the zones + saveZones(testZone).unsafeRunSync() + // delete all stored zones + deleteZones(testZone).unsafeRunSync() + // save the change + saveZoneChanges(deletedZoneChanges).unsafeRunSync() + + // query for all zones for the ok user, he should have access to all of the zones + val okUserAuth = AuthPrincipal( + signedInUser = okUser, + memberGroupIds = groups.map(_.id) + ) + + val listDeletedZones = repo.listDeletedZones(okUserAuth).unsafeRunSync() + + val expectedPageOne = List(listDeletedZones.zoneDeleted(0)) + val expectedPageOneNext = Some(listDeletedZones.zoneDeleted(1).zone.id) + val expectedPageTwo = List(listDeletedZones.zoneDeleted(1)) + val expectedPageTwoNext = Some(listDeletedZones.zoneDeleted(2).zone.id) + val expectedPageThree = List(listDeletedZones.zoneDeleted(2)) + val expectedPageThreeNext = Some(listDeletedZones.zoneDeleted(3).zone.id) + + // get first page + val pageOne = repo.listDeletedZones(okUserAuth,startFrom = None, maxItems = 1 ).unsafeRunSync() + pageOne.zoneDeleted.size should equal(1) + pageOne.zoneDeleted should equal(expectedPageOne) + pageOne.nextId should equal(expectedPageOneNext) + pageOne.startFrom should equal(None) + + // get second page + val pageTwo = + repo.listDeletedZones(okUserAuth, startFrom = pageOne.nextId, maxItems = 1).unsafeRunSync() + pageTwo.zoneDeleted.size should equal(1) + pageTwo.zoneDeleted should equal(expectedPageTwo) + pageTwo.nextId should equal(expectedPageTwoNext) + pageTwo.startFrom should equal(pageOne.nextId) + + // get final page + // next id should be none now + val pageThree = + repo.listDeletedZones(okUserAuth, startFrom = pageTwo.nextId, maxItems = 1).unsafeRunSync() + pageThree.zoneDeleted.size should equal(1) + pageThree.zoneDeleted should equal(expectedPageThree) + pageThree.nextId should equal(expectedPageThreeNext) + pageThree.startFrom should equal(pageTwo.nextId) } "return empty in deleted zone if zone is created again" in { @@ -424,6 +471,7 @@ class MySqlZoneChangeRepositoryIntegrationSpec deleteZones(testZone).unsafeRunSync() } + "return an empty list of zones if the user is not authorized to any" in { val unauthorized = AuthPrincipal( signedInUser = User("not-authorized", "not-authorized", Encrypted("not-authorized")), From 6a8e5884179cc9b55a328683ab90f4fd32068671 Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Tue, 22 Aug 2023 23:57:44 +0530 Subject: [PATCH 11/22] ignore access fix --- modules/portal/public/lib/controllers/controller.zones.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/portal/public/lib/controllers/controller.zones.js b/modules/portal/public/lib/controllers/controller.zones.js index e017d10f6..0dd5ad470 100644 --- a/modules/portal/public/lib/controllers/controller.zones.js +++ b/modules/portal/public/lib/controllers/controller.zones.js @@ -178,7 +178,7 @@ angular.module('controller.zones', []) allZonesPaging = pagingService.resetPaging(allZonesPaging); zonesService - .getZones(zonesPaging.maxItems, undefined, $scope.query, $scope.searchByAdminGroup, true, $scope.includeReverse) + .getZones(zonesPaging.maxItems, undefined, $scope.query, $scope.searchByAdminGroup, false, $scope.includeReverse) .then(function (response) { $log.debug('zonesService::getZones-success (' + response.data.zones.length + ' zones)'); zonesPaging.next = response.data.nextId; @@ -212,6 +212,7 @@ angular.module('controller.zones', []) .catch(function (error) { handleError(error, 'zonesService::getDeletedZones-failure'); }); + zonesService .getDeletedZones(allDeleteZonesPaging.maxItems, undefined, $scope.query, true) .then(function (response) { From 092b825a7c076792ef7c87e846e115e9a4eda3d0 Mon Sep 17 00:00:00 2001 From: Jay07GIT Date: Wed, 23 Aug 2023 00:07:53 +0530 Subject: [PATCH 12/22] collapse fix in manage records --- .../app/views/zones/zoneTabs/manageRecords.scala.html | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/modules/portal/app/views/zones/zoneTabs/manageRecords.scala.html b/modules/portal/app/views/zones/zoneTabs/manageRecords.scala.html index 4508c760d..5c45b4a29 100644 --- a/modules/portal/app/views/zones/zoneTabs/manageRecords.scala.html +++ b/modules/portal/app/views/zones/zoneTabs/manageRecords.scala.html @@ -5,13 +5,15 @@
- +
@@ -52,6 +54,7 @@
+
From e1c12d37a24cf879b04dbbf249dfea80a8c2d531 Mon Sep 17 00:00:00 2001 From: Aravindh-Raju Date: Wed, 23 Aug 2023 18:58:04 +0530 Subject: [PATCH 13/22] gui changes --- .../views/dnsChanges/dnsChanges.scala.html | 8 +++++--- .../app/views/groups/groupDetail.scala.html | 8 +++++--- .../views/recordsets/recordSets.scala.html | 6 ++++-- .../zones/zoneTabs/changeHistory.scala.html | 8 +++++--- .../zones/zoneTabs/manageZone.scala.html | 2 +- .../zoneTabs/zoneChangeHistory.scala.html | 19 +++++++++++++++++-- .../lib/recordset/recordsets.controller.js | 3 +++ 7 files changed, 40 insertions(+), 14 deletions(-) diff --git a/modules/portal/app/views/dnsChanges/dnsChanges.scala.html b/modules/portal/app/views/dnsChanges/dnsChanges.scala.html index c3a143d71..bf5ceafb0 100644 --- a/modules/portal/app/views/dnsChanges/dnsChanges.scala.html +++ b/modules/portal/app/views/dnsChanges/dnsChanges.scala.html @@ -12,7 +12,7 @@ -

DNS Changes {{ getPageTitle() }}

+

DNS Changes

@@ -58,7 +58,8 @@
-
+
+ {{ getPageTitle() }}