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 74d7b3c4a..3b8d66a36 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 @@ -16,6 +16,7 @@ package vinyldns.api.domain.zone +import cats.effect.IO import cats.implicits._ import vinyldns.api.domain.access.AccessValidationsAlgebra import vinyldns.api.Interfaces @@ -142,33 +143,59 @@ class ZoneService( accessLevel = getZoneAccess(auth, zone) } yield ZoneInfo(zone, aclInfo, groupName, accessLevel) + // List zones. Uses zone name as default while using search to list zones or by admin group name if selected. def listZones( authPrincipal: AuthPrincipal, nameFilter: Option[String] = None, startFrom: Option[String] = None, maxItems: Int = 100, + searchByAdminGroup: Boolean = false, ignoreAccess: Boolean = false ): Result[ListZonesResponse] = { - for { - listZonesResult <- zoneRepository.listZones( - authPrincipal, - nameFilter, - startFrom, - maxItems, - ignoreAccess + if(!searchByAdminGroup || nameFilter.isEmpty){ + for { + listZonesResult <- zoneRepository.listZones( + authPrincipal, + nameFilter, + startFrom, + maxItems, + ignoreAccess + ) + zones = listZonesResult.zones + groupIds = zones.map(_.adminGroupId).toSet + groups <- groupRepository.getGroups(groupIds) + zoneSummaryInfos = zoneSummaryInfoMapping(zones, authPrincipal, groups) + } yield ListZonesResponse( + zoneSummaryInfos, + listZonesResult.zonesFilter, + listZonesResult.startFrom, + listZonesResult.nextId, + listZonesResult.maxItems, + listZonesResult.ignoreAccess ) - zones = listZonesResult.zones - groupIds = zones.map(_.adminGroupId).toSet - groups <- groupRepository.getGroups(groupIds) - zoneSummaryInfos = zoneSummaryInfoMapping(zones, authPrincipal, groups) - } yield ListZonesResponse( - zoneSummaryInfos, - listZonesResult.zonesFilter, - listZonesResult.startFrom, - listZonesResult.nextId, - listZonesResult.maxItems, - listZonesResult.ignoreAccess - ) + } + else { + for { + groupIds <- getGroupsIdsByName(nameFilter.get) + listZonesResult <- zoneRepository.listZonesByAdminGroupIds( + authPrincipal, + startFrom, + maxItems, + groupIds, + ignoreAccess + ) + zones = listZonesResult.zones + groups <- groupRepository.getGroups(groupIds) + zoneSummaryInfos = zoneSummaryInfoMapping(zones, authPrincipal, groups) + } yield ListZonesResponse( + zoneSummaryInfos, + nameFilter, + listZonesResult.startFrom, + listZonesResult.nextId, + listZonesResult.maxItems, + listZonesResult.ignoreAccess + ) + } }.toResult def zoneSummaryInfoMapping( @@ -242,6 +269,10 @@ class ZoneService( } yield zoneChange } + def getGroupsIdsByName(groupName: String): IO[Set[String]] = { + groupRepository.getGroupsByName(groupName).map(x => x.map(_.id)) + } + def getBackendIds(): Result[List[String]] = backendResolver.ids.toList.toResult 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 01457a64b..cbcec8f92 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 @@ -42,6 +42,7 @@ trait ZoneServiceAlgebra { nameFilter: Option[String], startFrom: Option[String], maxItems: Int, + searchByAdminGroup: Boolean, ignoreAccess: Boolean ): Result[ListZonesResponse] 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 fe9365839..440f346c3 100644 --- a/modules/api/src/main/scala/vinyldns/api/route/ZoneRouting.scala +++ b/modules/api/src/main/scala/vinyldns/api/route/ZoneRouting.scala @@ -78,12 +78,14 @@ class ZoneRoute( "nameFilter".?, "startFrom".as[String].?, "maxItems".as[Int].?(DEFAULT_MAX_ITEMS), + "searchByAdminGroup".as[Boolean].?(false), "ignoreAccess".as[Boolean].?(false) ) { ( nameFilter: Option[String], startFrom: Option[String], maxItems: Int, + searchByAdminGroup: Boolean, ignoreAccess: Boolean ) => { @@ -94,7 +96,7 @@ class ZoneRoute( ) { authenticateAndExecute( zoneService - .listZones(_, nameFilter, startFrom, maxItems, ignoreAccess) + .listZones(_, nameFilter, startFrom, maxItems, searchByAdminGroup, ignoreAccess) ) { result => complete(StatusCodes.OK, result) } diff --git a/modules/api/src/test/functional/tests/zones/list_zones_test.py b/modules/api/src/test/functional/tests/zones/list_zones_test.py index 1279f5b2f..1caf96e31 100644 --- a/modules/api/src/test/functional/tests/zones/list_zones_test.py +++ b/modules/api/src/test/functional/tests/zones/list_zones_test.py @@ -23,6 +23,44 @@ def test_list_zones_success(list_zone_context, shared_zone_test_context): assert_that(result["nameFilter"], is_(f"*{shared_zone_test_context.partition_id}")) +def test_list_zones_by_admin_group_name(list_zone_context, shared_zone_test_context): + """ + Test that we can retrieve list of zones by searching with admin group name + """ + result = shared_zone_test_context.list_zones_client.list_zones(name_filter=f"list-zones-group{shared_zone_test_context.partition_id}", search_by_admin_group=True, status=200) + retrieved = result["zones"] + + assert_that(retrieved, has_length(5)) + assert_that(retrieved, has_item(has_entry("name", list_zone_context.search_zone1["name"]))) + assert_that(retrieved, has_item(has_entry("name", list_zone_context.search_zone2["name"]))) + assert_that(retrieved, has_item(has_entry("name", list_zone_context.search_zone3["name"]))) + assert_that(retrieved, has_item(has_entry("name", list_zone_context.non_search_zone1["name"]))) + assert_that(retrieved, has_item(has_entry("name", list_zone_context.non_search_zone2["name"]))) + assert_that(retrieved, has_item(has_entry("adminGroupName", list_zone_context.list_zones_group["name"]))) + assert_that(retrieved, has_item(has_entry("backendId", "func-test-backend"))) + + assert_that(result["nameFilter"], is_(f"list-zones-group{shared_zone_test_context.partition_id}")) + + +def test_list_zones_by_admin_group_name_with_wildcard(list_zone_context, shared_zone_test_context): + """ + Test that we can retrieve list of zones by searching with admin group name with wildcard character + """ + result = shared_zone_test_context.list_zones_client.list_zones(name_filter=f"*group{shared_zone_test_context.partition_id}", search_by_admin_group=True, status=200) + retrieved = result["zones"] + + assert_that(retrieved, has_length(5)) + assert_that(retrieved, has_item(has_entry("name", list_zone_context.search_zone1["name"]))) + assert_that(retrieved, has_item(has_entry("name", list_zone_context.search_zone2["name"]))) + assert_that(retrieved, has_item(has_entry("name", list_zone_context.search_zone3["name"]))) + assert_that(retrieved, has_item(has_entry("name", list_zone_context.non_search_zone1["name"]))) + assert_that(retrieved, has_item(has_entry("name", list_zone_context.non_search_zone2["name"]))) + assert_that(retrieved, has_item(has_entry("adminGroupName", list_zone_context.list_zones_group["name"]))) + assert_that(retrieved, has_item(has_entry("backendId", "func-test-backend"))) + + assert_that(result["nameFilter"], is_(f"*group{shared_zone_test_context.partition_id}")) + + def test_list_zones_max_items_100(shared_zone_test_context): """ Test that the default max items for a list zones request is 100 diff --git a/modules/api/src/test/functional/vinyldns_python.py b/modules/api/src/test/functional/vinyldns_python.py index 63df14233..c6e29e135 100644 --- a/modules/api/src/test/functional/vinyldns_python.py +++ b/modules/api/src/test/functional/vinyldns_python.py @@ -445,7 +445,7 @@ class VinylDNSClient(object): response, data = self.make_request(url, "GET", self.headers, not_found_ok=True, **kwargs) return data - def list_zones(self, name_filter=None, start_from=None, max_items=None, ignore_access=False, **kwargs): + def list_zones(self, name_filter=None, start_from=None, max_items=None, search_by_admin_group=False, ignore_access=False, **kwargs): """ Gets a list of zones that currently exist :return: a list of zones @@ -462,6 +462,9 @@ class VinylDNSClient(object): if max_items: query.append("maxItems=" + str(max_items)) + if search_by_admin_group: + query.append("searchByAdminGroup=" + str(search_by_admin_group)) + if ignore_access: query.append("ignoreAccess=" + str(ignore_access)) 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 c6ab3e935..69faad71a 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 @@ -562,6 +562,45 @@ class ZoneServiceSpec result.ignoreAccess shouldBe true } + "name filter must be used to return zones by admin group name, when search by admin group option is true" in { + doReturn(IO.pure(Set(abcGroup))) + .when(mockGroupRepo) + .getGroupsByName(any[String]) + doReturn(IO.pure(ListZonesResults(List(abcZone), ignoreAccess = true, zonesFilter = Some("abcGroup")))) + .when(mockZoneRepo) + .listZonesByAdminGroupIds(abcAuth, None, 100, Set(abcGroup.id), ignoreAccess = true) + doReturn(IO.pure(Set(abcGroup))).when(mockGroupRepo).getGroups(any[Set[String]]) + + // When searchByAdminGroup is true, zones are filtered by admin group name given in nameFilter + val result: ListZonesResponse = + rightResultOf(underTest.listZones(abcAuth, Some("abcGroup"), None, 100, searchByAdminGroup = true, ignoreAccess = true).value) + result.zones shouldBe List(abcZoneSummary) + result.maxItems shouldBe 100 + result.startFrom shouldBe None + result.nameFilter shouldBe Some("abcGroup") + result.nextId shouldBe None + result.ignoreAccess shouldBe true + } + + "name filter must be used to return zone by zone name, when search by admin group option is false" in { + doReturn(IO.pure(Set(abcGroup))) + .when(mockGroupRepo) + .getGroups(any[Set[String]]) + doReturn(IO.pure(ListZonesResults(List(abcZone), ignoreAccess = true, zonesFilter = Some("abcZone")))) + .when(mockZoneRepo) + .listZones(abcAuth, Some("abcZone"), None, 100, true) + + // When searchByAdminGroup is false, zone name given in nameFilter is returned + val result: ListZonesResponse = + rightResultOf(underTest.listZones(abcAuth, Some("abcZone"), None, 100, searchByAdminGroup = false, ignoreAccess = true).value) + result.zones shouldBe List(abcZoneSummary) + result.maxItems shouldBe 100 + result.startFrom shouldBe None + result.nameFilter shouldBe Some("abcZone") + result.nextId shouldBe None + result.ignoreAccess shouldBe true + } + "return Unknown group name if zone admin group cannot be found" in { doReturn(IO.pure(ListZonesResults(List(abcZone, xyzZone)))) .when(mockZoneRepo) diff --git a/modules/api/src/test/scala/vinyldns/api/repository/EmptyRepositories.scala b/modules/api/src/test/scala/vinyldns/api/repository/EmptyRepositories.scala index a1239cc71..ad3d79310 100644 --- a/modules/api/src/test/scala/vinyldns/api/repository/EmptyRepositories.scala +++ b/modules/api/src/test/scala/vinyldns/api/repository/EmptyRepositories.scala @@ -84,6 +84,14 @@ trait EmptyZoneRepo extends ZoneRepository { def getZoneByName(zoneName: String): IO[Option[Zone]] = IO.pure(None) + def listZonesByAdminGroupIds( + authPrincipal: AuthPrincipal, + startFrom: Option[String] = None, + maxItems: Int = 100, + adminGroupIds: Set[String], + ignoreAccess: Boolean = false + ): IO[ListZonesResults] = IO.pure(ListZonesResults()) + def listZones( authPrincipal: AuthPrincipal, zoneNameFilter: Option[String] = None, @@ -113,6 +121,8 @@ trait EmptyGroupRepo extends GroupRepository { def getGroupByName(groupName: String): IO[Option[Group]] = IO.pure(None) + def getGroupsByName(groupName: String): IO[Set[Group]] = IO.pure(Set()) + def getAllGroups(): IO[Set[Group]] = IO.pure(Set()) } 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 38927d2d8..1ffc6f537 100644 --- a/modules/api/src/test/scala/vinyldns/api/route/ZoneRoutingSpec.scala +++ b/modules/api/src/test/scala/vinyldns/api/route/ZoneRoutingSpec.scala @@ -252,6 +252,7 @@ class ZoneRoutingSpec nameFilter: Option[String], startFrom: Option[String], maxItems: Int, + searchByAdminGroup: Boolean = false, ignoreAccess: Boolean = false ): Result[ListZonesResponse] = { @@ -920,6 +921,20 @@ class ZoneRoutingSpec } } + "return zones by admin group name when searchByAdminGroup is true" in { + Get(s"/zones?nameFilter=ok&startFrom=zone4.&maxItems=4&searchByAdminGroup=true") ~> zoneRoute ~> check { + val resp = responseAs[ListZonesResponse] + val zones = resp.zones + (zones.map(_.id) should contain) + .only(zone1.id, zone2.id, zone3.id) + resp.nextId shouldBe None + resp.maxItems shouldBe 4 + resp.startFrom shouldBe Some("zone4.") + resp.nameFilter shouldBe Some("ok") + resp.ignoreAccess shouldBe false + } + } + "return all zones when list all is true" in { Get(s"/zones?maxItems=5&ignoreAccess=true") ~> zoneRoute ~> check { val resp = responseAs[ListZonesResponse] diff --git a/modules/core/src/main/scala/vinyldns/core/domain/membership/GroupRepository.scala b/modules/core/src/main/scala/vinyldns/core/domain/membership/GroupRepository.scala index 233a2f6b5..128792a8c 100644 --- a/modules/core/src/main/scala/vinyldns/core/domain/membership/GroupRepository.scala +++ b/modules/core/src/main/scala/vinyldns/core/domain/membership/GroupRepository.scala @@ -33,6 +33,8 @@ trait GroupRepository extends Repository { def getGroupByName(groupName: String): IO[Option[Group]] + def getGroupsByName(groupName: String): IO[Set[Group]] + def getAllGroups(): IO[Set[Group]] } diff --git a/modules/core/src/main/scala/vinyldns/core/domain/zone/ZoneRepository.scala b/modules/core/src/main/scala/vinyldns/core/domain/zone/ZoneRepository.scala index 86a2f289c..ec8e82af9 100644 --- a/modules/core/src/main/scala/vinyldns/core/domain/zone/ZoneRepository.scala +++ b/modules/core/src/main/scala/vinyldns/core/domain/zone/ZoneRepository.scala @@ -35,6 +35,14 @@ trait ZoneRepository extends Repository { def getZonesByFilters(zoneNames: Set[String]): IO[Set[Zone]] + def listZonesByAdminGroupIds( + authPrincipal: AuthPrincipal, + startFrom: Option[String] = None, + maxItems: Int = 100, + adminGroupIds: Set[String], + ignoreAccess: Boolean = false + ): IO[ListZonesResults] + def listZones( authPrincipal: AuthPrincipal, zoneNameFilter: Option[String] = None, diff --git a/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlGroupRepositoryIntegrationSpec.scala b/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlGroupRepositoryIntegrationSpec.scala index b658c741a..76b3469b4 100644 --- a/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlGroupRepositoryIntegrationSpec.scala +++ b/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlGroupRepositoryIntegrationSpec.scala @@ -103,6 +103,20 @@ class MySqlGroupRepositoryIntegrationSpec } } + "MySqlGroupRepository.getGroupsByName" should { + "retrieve a group" in { + repo.getGroupsByName(groups.head.name).unsafeRunSync() shouldBe Set(groups.head) + } + + "retrieve groups with wildcard character" in { + repo.getGroupsByName("*-group-*").unsafeRunSync() shouldBe groups.toSet + } + + "returns empty set when group does not exist" in { + repo.getGroupsByName("no-existo").unsafeRunSync() shouldBe Set() + } + } + "MySqlGroupRepository.getAllGroups" should { "retrieve all groups" in { repo.getAllGroups().unsafeRunSync() should contain theSameElementsAs groups.toSet 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 369c35443..8daa0e58b 100644 --- a/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlZoneRepositoryIntegrationSpec.scala +++ b/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlZoneRepositoryIntegrationSpec.scala @@ -29,14 +29,16 @@ import vinyldns.core.domain.zone._ import vinyldns.core.TestZoneData.okZone import vinyldns.core.TestMembershipData._ import vinyldns.core.domain.zone.ZoneRepository.DuplicateZoneError -import vinyldns.mysql.TestMySqlInstance +import vinyldns.mysql.{TestMySqlInstance, TransactionProvider} +import vinyldns.mysql.TestMySqlInstance.groupRepository class MySqlZoneRepositoryIntegrationSpec extends AnyWordSpec with BeforeAndAfterAll with BeforeAndAfterEach with Matchers - with Inspectors { + with Inspectors + with TransactionProvider { private var repo: ZoneRepository = _ @@ -221,6 +223,32 @@ class MySqlZoneRepositoryIntegrationSpec (repo.listZones(dummyAuth).unsafeRunSync().zones should contain).only(testZones.head) } + "get authorized zone by admin group name" in { + + executeWithinTransaction { db: DB => + groupRepository.save(db, okGroup.copy(id = testZoneAdminGroupId)) + }.unsafeRunSync() + + // store all of the zones + + val f = saveZones(testZones) + + // 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) + ) + + f.unsafeRunSync() + repo.listZonesByAdminGroupIds(okUserAuth, None, 100, Set(testZoneAdminGroupId)).unsafeRunSync().zones should contain theSameElementsAs testZones + + // dummy user only has access to one zone + (repo.listZonesByAdminGroupIds(dummyAuth, None, 100, Set(testZoneAdminGroupId)).unsafeRunSync().zones should contain).only(testZones.head) + + // delete the group created to test + groupRepository.delete(okGroup).unsafeRunSync() + } + "get all zones" in { // store all of the zones val privateZone = okZone.copy( @@ -259,6 +287,82 @@ class MySqlZoneRepositoryIntegrationSpec .zones should contain theSameElementsAs testZones } + "get all zones by admin group name" in { + + executeWithinTransaction { db: DB => + groupRepository.save(db, okGroup) + }.unsafeRunSync() + + val group = groupRepository.getGroupsByName(okGroup.name).unsafeRunSync() + val groupId = group.head.id + + // store all of the zones + val privateZone = okZone.copy( + name = "private-zone.", + id = UUID.randomUUID().toString, + acl = ZoneACL(), + adminGroupId = groupId + ) + + val sharedZone = okZone.copy( + name = "shared-zone.", + id = UUID.randomUUID().toString, + acl = ZoneACL(), + shared = true, + adminGroupId = groupId + ) + + val testZones = Seq(privateZone, sharedZone) + + val f = saveZones(testZones) + + // query for all zones for the ok user, should have all of the zones returned + val okUserAuth = AuthPrincipal( + signedInUser = okUser, + memberGroupIds = groups.map(_.id) + ) + + f.unsafeRunSync() + + repo + .listZonesByAdminGroupIds(okUserAuth, None, 100, Set(groupId), ignoreAccess = true) + .unsafeRunSync() + .zones should contain theSameElementsAs testZones + + // dummy user only have all of the zones returned + repo + .listZonesByAdminGroupIds(dummyAuth, None, 100, Set(groupId), ignoreAccess = true) + .unsafeRunSync() + .zones should contain theSameElementsAs testZones + + + // delete the group created to test + groupRepository.delete(okGroup).unsafeRunSync() + } + + "get empty list when no matching admin group name is found while filtering zones by group name" in { + + executeWithinTransaction { db: DB => + groupRepository.save(db, okGroup.copy(id = testZoneAdminGroupId)) + }.unsafeRunSync() + + // store all of the zones + + val f = saveZones(testZones) + + // 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) + ) + + f.unsafeRunSync() + repo.listZonesByAdminGroupIds(okUserAuth, None, 100, Set()).unsafeRunSync().zones shouldBe empty + + // delete the group created to test + groupRepository.delete(okGroup).unsafeRunSync() + } + "get zones that are accessible by everyone" in { //user and group id being set to None implies EVERYONE access diff --git a/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlGroupRepository.scala b/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlGroupRepository.scala index 83ead82f7..ae61e201c 100644 --- a/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlGroupRepository.scala +++ b/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlGroupRepository.scala @@ -155,6 +155,30 @@ class MySqlGroupRepository extends GroupRepository with GroupProtobufConversions } } + def getGroupsByName(nameFilter: String): IO[Set[Group]] = + monitor("repo.Group.getGroupByName") { + IO { + logger.debug(s"Getting groups with name: $nameFilter") + val initialQuery = "SELECT data FROM groups WHERE name" + val sb = new StringBuilder + sb.append(initialQuery) + val groupsLike = if (nameFilter.contains('*')) { + s" LIKE '${nameFilter.replace('*', '%')}'" + } else { + s" LIKE '$nameFilter%'" + } + sb.append(groupsLike) + val query = sb.toString() + + DB.readOnly { implicit s => + SQL(query) + .map(toGroup(1)) + .list() + .apply() + }.toSet + } + } + def getAllGroups(): IO[Set[Group]] = monitor("repo.Group.getAllGroups") { IO { 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 e80402632..d08b90309 100644 --- a/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlZoneRepository.scala +++ b/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlZoneRepository.scala @@ -229,6 +229,65 @@ class MySqlZoneRepository extends ZoneRepository with ProtobufConversions with M } } + /** + * This is somewhat complicated due to how we need to build the SQL. + * + * - Dynamically build the accessor list combining the user id and group ids + * - Dynamically build the LIMIT clause. We cannot specify an offset if this is the first page (offset == 0) + * + * @return a ListZonesResults + */ + def listZonesByAdminGroupIds( + authPrincipal: AuthPrincipal, + startFrom: Option[String] = None, + maxItems: Int = 100, + adminGroupIds: Set[String], + ignoreAccess: Boolean = false + ): IO[ListZonesResults] = + monitor("repo.ZoneJDBC.listZonesByAdminGroupIds") { + IO { + DB.readOnly { implicit s => + val (withAccessorCheck, accessors) = + withAccessors(authPrincipal.signedInUser, authPrincipal.memberGroupIds, ignoreAccess) + val sb = new StringBuilder + sb.append(withAccessorCheck) + + if(adminGroupIds.nonEmpty) { + val groupIds = adminGroupIds.map(x => "'" + x + "'").mkString(",") + sb.append(s" WHERE admin_group_id IN ($groupIds) ") + } else { + sb.append(s" WHERE admin_group_id IN ('') ") + } + + sb.append(s" GROUP BY z.name ") + sb.append(s" LIMIT ${maxItems + 1}") + + val query = sb.toString + + val results: List[Zone] = SQL(query) + .bind(accessors: _*) + .map(extractZone(1)) + .list() + .apply() + + val (newResults, nextId) = + if (results.size > maxItems) + (results.dropRight(1), results.dropRight(1).lastOption.map(_.name)) + else (results, None) + + + ListZonesResults( + zones = newResults, + nextId = nextId, + startFrom = startFrom, + maxItems = maxItems, + zonesFilter = None, + ignoreAccess = ignoreAccess, + ) + } + } + } + /** * This is somewhat complicated due to how we need to build the SQL. * diff --git a/modules/portal/app/views/zones/zones.scala.html b/modules/portal/app/views/zones/zones.scala.html index 9c56548df..4bf31e9f1 100644 --- a/modules/portal/app/views/zones/zones.scala.html +++ b/modules/portal/app/views/zones/zones.scala.html @@ -59,7 +59,12 @@ - + + +
+
@@ -168,8 +173,14 @@ - + + +
+ +
diff --git a/modules/portal/public/lib/controllers/controller.zones.js b/modules/portal/public/lib/controllers/controller.zones.js index d4057fcfd..0900d86d7 100644 --- a/modules/portal/public/lib/controllers/controller.zones.js +++ b/modules/portal/public/lib/controllers/controller.zones.js @@ -87,7 +87,7 @@ angular.module('controller.zones', []) allZonesPaging = pagingService.resetPaging(allZonesPaging); zonesService - .getZones(zonesPaging.maxItems, undefined, $scope.query) + .getZones(zonesPaging.maxItems, undefined, $scope.query, $scope.searchByAdminGroup) .then(function (response) { $log.log('zonesService::getZones-success (' + response.data.zones.length + ' zones)'); zonesPaging.next = response.data.nextId; @@ -101,7 +101,7 @@ angular.module('controller.zones', []) }); zonesService - .getZones(zonesPaging.maxItems, undefined, $scope.query, true) + .getZones(zonesPaging.maxItems, undefined, $scope.query, $scope.searchByAdminGroup, true) .then(function (response) { $log.log('zonesService::getZones-success (' + response.data.zones.length + ' zones)'); allZonesPaging.next = response.data.nextId; @@ -207,7 +207,7 @@ angular.module('controller.zones', []) $scope.prevPageMyZones = function() { var startFrom = pagingService.getPrevStartFrom(zonesPaging); return zonesService - .getZones(zonesPaging.maxItems, startFrom, $scope.query, false) + .getZones(zonesPaging.maxItems, startFrom, $scope.query, $scope.searchByAdminGroup, false) .then(function(response) { zonesPaging = pagingService.prevPageUpdate(response.data.nextId, zonesPaging); updateZoneDisplay(response.data.zones); @@ -220,7 +220,7 @@ angular.module('controller.zones', []) $scope.prevPageAllZones = function() { var startFrom = pagingService.getPrevStartFrom(allZonesPaging); return zonesService - .getZones(allZonesPaging.maxItems, startFrom, $scope.query, true) + .getZones(allZonesPaging.maxItems, startFrom, $scope.query, $scope.searchByAdminGroup, true) .then(function(response) { allZonesPaging = pagingService.prevPageUpdate(response.data.nextId, allZonesPaging); updateAllZonesDisplay(response.data.zones); @@ -232,7 +232,7 @@ angular.module('controller.zones', []) $scope.nextPageMyZones = function () { return zonesService - .getZones(zonesPaging.maxItems, zonesPaging.next, $scope.query, false) + .getZones(zonesPaging.maxItems, zonesPaging.next, $scope.query, $scope.searchByAdminGroup, false) .then(function(response) { var zoneSets = response.data.zones; zonesPaging = pagingService.nextPageUpdate(zoneSets, response.data.nextId, zonesPaging); @@ -248,7 +248,7 @@ angular.module('controller.zones', []) $scope.nextPageAllZones = function () { return zonesService - .getZones(allZonesPaging.maxItems, allZonesPaging.next, $scope.query, true) + .getZones(allZonesPaging.maxItems, allZonesPaging.next, $scope.query, $scope.searchByAdminGroup, true) .then(function(response) { var zoneSets = response.data.zones; allZonesPaging = pagingService.nextPageUpdate(zoneSets, response.data.nextId, allZonesPaging); diff --git a/modules/portal/public/lib/controllers/controller.zones.spec.js b/modules/portal/public/lib/controllers/controller.zones.spec.js index 47197e078..bbd790ebc 100644 --- a/modules/portal/public/lib/controllers/controller.zones.spec.js +++ b/modules/portal/public/lib/controllers/controller.zones.spec.js @@ -76,13 +76,14 @@ describe('Controller: ZonesController', function () { var expectedMaxItems = 100; var expectedStartFrom = undefined; var expectedQuery = this.scope.query; + var expectedSearchByAdminGroup = this.scope.searchByAdminGroup; var expectedignoreAccess = false; this.scope.nextPageMyZones(); expect(getZoneSets.calls.count()).toBe(1); expect(getZoneSets.calls.mostRecent().args).toEqual( - [expectedMaxItems, expectedStartFrom, expectedQuery, expectedignoreAccess]); + [expectedMaxItems, expectedStartFrom, expectedQuery, expectedSearchByAdminGroup, expectedignoreAccess]); }); it('prevPageMyZones should call getZones with the correct parameters', function () { @@ -93,19 +94,20 @@ describe('Controller: ZonesController', function () { var expectedMaxItems = 100; var expectedStartFrom = undefined; var expectedQuery = this.scope.query; + var expectedSearchByAdminGroup = this.scope.searchByAdminGroup; var expectedignoreAccess = false; this.scope.prevPageMyZones(); expect(getZoneSets.calls.count()).toBe(1); expect(getZoneSets.calls.mostRecent().args).toEqual( - [expectedMaxItems, expectedStartFrom, expectedQuery, expectedignoreAccess]); + [expectedMaxItems, expectedStartFrom, expectedQuery, expectedSearchByAdminGroup, expectedignoreAccess]); this.scope.nextPageMyZones(); this.scope.prevPageMyZones(); expect(getZoneSets.calls.count()).toBe(3); expect(getZoneSets.calls.mostRecent().args).toEqual( - [expectedMaxItems, expectedStartFrom, expectedQuery, expectedignoreAccess]); + [expectedMaxItems, expectedStartFrom, expectedQuery, expectedSearchByAdminGroup, expectedignoreAccess]); }); }); diff --git a/modules/portal/public/lib/services/zones/service.zones.js b/modules/portal/public/lib/services/zones/service.zones.js index 2273421e9..28585a56f 100644 --- a/modules/portal/public/lib/services/zones/service.zones.js +++ b/modules/portal/public/lib/services/zones/service.zones.js @@ -19,7 +19,7 @@ angular.module('service.zones', []) .service('zonesService', function ($http, groupsService, $log, utilityService) { - this.getZones = function (limit, startFrom, query, ignoreAccess) { + this.getZones = function (limit, startFrom, query, searchByAdminGroup, ignoreAccess) { if (query == "") { query = null; } @@ -27,6 +27,7 @@ angular.module('service.zones', []) "maxItems": limit, "startFrom": startFrom, "nameFilter": query, + "searchByAdminGroup": searchByAdminGroup, "ignoreAccess": ignoreAccess }; var url = groupsService.urlBuilder("/api/zones", params); 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 5149008a8..c20e717ef 100644 --- a/modules/portal/public/lib/services/zones/service.zones.spec.js +++ b/modules/portal/public/lib/services/zones/service.zones.spec.js @@ -27,8 +27,8 @@ describe('Service: zoneService', function () { })); it('http backend gets called properly when getting zones', function () { - this.$httpBackend.expectGET('/api/zones?maxItems=100&startFrom=start&nameFilter=someQuery&ignoreAccess=false').respond('zone returned'); - this.zonesService.getZones('100', 'start', 'someQuery', false) + this.$httpBackend.expectGET('/api/zones?maxItems=100&startFrom=start&nameFilter=someQuery&searchByAdminGroup=false&ignoreAccess=false').respond('zone returned'); + this.zonesService.getZones('100', 'start', 'someQuery', false, false) .then(function(response) { expect(response.data).toBe('zone returned'); });