mirror of
https://github.com/VinylDNS/vinyldns
synced 2025-09-03 07:45:15 +00:00
Merge pull request #1124 from Aravindh-Raju/aravindhr/filter-zones
Add ability to search/filter zones
This commit is contained in:
@@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package vinyldns.api.domain.zone
|
package vinyldns.api.domain.zone
|
||||||
|
|
||||||
|
import cats.effect.IO
|
||||||
import cats.implicits._
|
import cats.implicits._
|
||||||
import vinyldns.api.domain.access.AccessValidationsAlgebra
|
import vinyldns.api.domain.access.AccessValidationsAlgebra
|
||||||
import vinyldns.api.Interfaces
|
import vinyldns.api.Interfaces
|
||||||
@@ -142,33 +143,59 @@ class ZoneService(
|
|||||||
accessLevel = getZoneAccess(auth, zone)
|
accessLevel = getZoneAccess(auth, zone)
|
||||||
} yield ZoneInfo(zone, aclInfo, groupName, accessLevel)
|
} 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(
|
def listZones(
|
||||||
authPrincipal: AuthPrincipal,
|
authPrincipal: AuthPrincipal,
|
||||||
nameFilter: Option[String] = None,
|
nameFilter: Option[String] = None,
|
||||||
startFrom: Option[String] = None,
|
startFrom: Option[String] = None,
|
||||||
maxItems: Int = 100,
|
maxItems: Int = 100,
|
||||||
|
searchByAdminGroup: Boolean = false,
|
||||||
ignoreAccess: Boolean = false
|
ignoreAccess: Boolean = false
|
||||||
): Result[ListZonesResponse] = {
|
): Result[ListZonesResponse] = {
|
||||||
for {
|
if(!searchByAdminGroup || nameFilter.isEmpty){
|
||||||
listZonesResult <- zoneRepository.listZones(
|
for {
|
||||||
authPrincipal,
|
listZonesResult <- zoneRepository.listZones(
|
||||||
nameFilter,
|
authPrincipal,
|
||||||
startFrom,
|
nameFilter,
|
||||||
maxItems,
|
startFrom,
|
||||||
ignoreAccess
|
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
|
else {
|
||||||
groups <- groupRepository.getGroups(groupIds)
|
for {
|
||||||
zoneSummaryInfos = zoneSummaryInfoMapping(zones, authPrincipal, groups)
|
groupIds <- getGroupsIdsByName(nameFilter.get)
|
||||||
} yield ListZonesResponse(
|
listZonesResult <- zoneRepository.listZonesByAdminGroupIds(
|
||||||
zoneSummaryInfos,
|
authPrincipal,
|
||||||
listZonesResult.zonesFilter,
|
startFrom,
|
||||||
listZonesResult.startFrom,
|
maxItems,
|
||||||
listZonesResult.nextId,
|
groupIds,
|
||||||
listZonesResult.maxItems,
|
ignoreAccess
|
||||||
listZonesResult.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
|
}.toResult
|
||||||
|
|
||||||
def zoneSummaryInfoMapping(
|
def zoneSummaryInfoMapping(
|
||||||
@@ -242,6 +269,10 @@ class ZoneService(
|
|||||||
} yield zoneChange
|
} yield zoneChange
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def getGroupsIdsByName(groupName: String): IO[Set[String]] = {
|
||||||
|
groupRepository.getGroupsByName(groupName).map(x => x.map(_.id))
|
||||||
|
}
|
||||||
|
|
||||||
def getBackendIds(): Result[List[String]] =
|
def getBackendIds(): Result[List[String]] =
|
||||||
backendResolver.ids.toList.toResult
|
backendResolver.ids.toList.toResult
|
||||||
|
|
||||||
|
@@ -42,6 +42,7 @@ trait ZoneServiceAlgebra {
|
|||||||
nameFilter: Option[String],
|
nameFilter: Option[String],
|
||||||
startFrom: Option[String],
|
startFrom: Option[String],
|
||||||
maxItems: Int,
|
maxItems: Int,
|
||||||
|
searchByAdminGroup: Boolean,
|
||||||
ignoreAccess: Boolean
|
ignoreAccess: Boolean
|
||||||
): Result[ListZonesResponse]
|
): Result[ListZonesResponse]
|
||||||
|
|
||||||
|
@@ -78,12 +78,14 @@ class ZoneRoute(
|
|||||||
"nameFilter".?,
|
"nameFilter".?,
|
||||||
"startFrom".as[String].?,
|
"startFrom".as[String].?,
|
||||||
"maxItems".as[Int].?(DEFAULT_MAX_ITEMS),
|
"maxItems".as[Int].?(DEFAULT_MAX_ITEMS),
|
||||||
|
"searchByAdminGroup".as[Boolean].?(false),
|
||||||
"ignoreAccess".as[Boolean].?(false)
|
"ignoreAccess".as[Boolean].?(false)
|
||||||
) {
|
) {
|
||||||
(
|
(
|
||||||
nameFilter: Option[String],
|
nameFilter: Option[String],
|
||||||
startFrom: Option[String],
|
startFrom: Option[String],
|
||||||
maxItems: Int,
|
maxItems: Int,
|
||||||
|
searchByAdminGroup: Boolean,
|
||||||
ignoreAccess: Boolean
|
ignoreAccess: Boolean
|
||||||
) =>
|
) =>
|
||||||
{
|
{
|
||||||
@@ -94,7 +96,7 @@ class ZoneRoute(
|
|||||||
) {
|
) {
|
||||||
authenticateAndExecute(
|
authenticateAndExecute(
|
||||||
zoneService
|
zoneService
|
||||||
.listZones(_, nameFilter, startFrom, maxItems, ignoreAccess)
|
.listZones(_, nameFilter, startFrom, maxItems, searchByAdminGroup, ignoreAccess)
|
||||||
) { result =>
|
) { result =>
|
||||||
complete(StatusCodes.OK, result)
|
complete(StatusCodes.OK, result)
|
||||||
}
|
}
|
||||||
|
@@ -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}"))
|
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):
|
def test_list_zones_max_items_100(shared_zone_test_context):
|
||||||
"""
|
"""
|
||||||
Test that the default max items for a list zones request is 100
|
Test that the default max items for a list zones request is 100
|
||||||
|
@@ -445,7 +445,7 @@ class VinylDNSClient(object):
|
|||||||
response, data = self.make_request(url, "GET", self.headers, not_found_ok=True, **kwargs)
|
response, data = self.make_request(url, "GET", self.headers, not_found_ok=True, **kwargs)
|
||||||
return data
|
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
|
Gets a list of zones that currently exist
|
||||||
:return: a list of zones
|
:return: a list of zones
|
||||||
@@ -462,6 +462,9 @@ class VinylDNSClient(object):
|
|||||||
if max_items:
|
if max_items:
|
||||||
query.append("maxItems=" + str(max_items))
|
query.append("maxItems=" + str(max_items))
|
||||||
|
|
||||||
|
if search_by_admin_group:
|
||||||
|
query.append("searchByAdminGroup=" + str(search_by_admin_group))
|
||||||
|
|
||||||
if ignore_access:
|
if ignore_access:
|
||||||
query.append("ignoreAccess=" + str(ignore_access))
|
query.append("ignoreAccess=" + str(ignore_access))
|
||||||
|
|
||||||
|
@@ -562,6 +562,45 @@ class ZoneServiceSpec
|
|||||||
result.ignoreAccess shouldBe true
|
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 {
|
"return Unknown group name if zone admin group cannot be found" in {
|
||||||
doReturn(IO.pure(ListZonesResults(List(abcZone, xyzZone))))
|
doReturn(IO.pure(ListZonesResults(List(abcZone, xyzZone))))
|
||||||
.when(mockZoneRepo)
|
.when(mockZoneRepo)
|
||||||
|
@@ -84,6 +84,14 @@ trait EmptyZoneRepo extends ZoneRepository {
|
|||||||
|
|
||||||
def getZoneByName(zoneName: String): IO[Option[Zone]] = IO.pure(None)
|
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(
|
def listZones(
|
||||||
authPrincipal: AuthPrincipal,
|
authPrincipal: AuthPrincipal,
|
||||||
zoneNameFilter: Option[String] = None,
|
zoneNameFilter: Option[String] = None,
|
||||||
@@ -113,6 +121,8 @@ trait EmptyGroupRepo extends GroupRepository {
|
|||||||
|
|
||||||
def getGroupByName(groupName: String): IO[Option[Group]] = IO.pure(None)
|
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())
|
def getAllGroups(): IO[Set[Group]] = IO.pure(Set())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -252,6 +252,7 @@ class ZoneRoutingSpec
|
|||||||
nameFilter: Option[String],
|
nameFilter: Option[String],
|
||||||
startFrom: Option[String],
|
startFrom: Option[String],
|
||||||
maxItems: Int,
|
maxItems: Int,
|
||||||
|
searchByAdminGroup: Boolean = false,
|
||||||
ignoreAccess: Boolean = false
|
ignoreAccess: Boolean = false
|
||||||
): Result[ListZonesResponse] = {
|
): 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 {
|
"return all zones when list all is true" in {
|
||||||
Get(s"/zones?maxItems=5&ignoreAccess=true") ~> zoneRoute ~> check {
|
Get(s"/zones?maxItems=5&ignoreAccess=true") ~> zoneRoute ~> check {
|
||||||
val resp = responseAs[ListZonesResponse]
|
val resp = responseAs[ListZonesResponse]
|
||||||
|
@@ -33,6 +33,8 @@ trait GroupRepository extends Repository {
|
|||||||
|
|
||||||
def getGroupByName(groupName: String): IO[Option[Group]]
|
def getGroupByName(groupName: String): IO[Option[Group]]
|
||||||
|
|
||||||
|
def getGroupsByName(groupName: String): IO[Set[Group]]
|
||||||
|
|
||||||
def getAllGroups(): IO[Set[Group]]
|
def getAllGroups(): IO[Set[Group]]
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -35,6 +35,14 @@ trait ZoneRepository extends Repository {
|
|||||||
|
|
||||||
def getZonesByFilters(zoneNames: Set[String]): IO[Set[Zone]]
|
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(
|
def listZones(
|
||||||
authPrincipal: AuthPrincipal,
|
authPrincipal: AuthPrincipal,
|
||||||
zoneNameFilter: Option[String] = None,
|
zoneNameFilter: Option[String] = None,
|
||||||
|
@@ -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 {
|
"MySqlGroupRepository.getAllGroups" should {
|
||||||
"retrieve all groups" in {
|
"retrieve all groups" in {
|
||||||
repo.getAllGroups().unsafeRunSync() should contain theSameElementsAs groups.toSet
|
repo.getAllGroups().unsafeRunSync() should contain theSameElementsAs groups.toSet
|
||||||
|
@@ -29,14 +29,16 @@ import vinyldns.core.domain.zone._
|
|||||||
import vinyldns.core.TestZoneData.okZone
|
import vinyldns.core.TestZoneData.okZone
|
||||||
import vinyldns.core.TestMembershipData._
|
import vinyldns.core.TestMembershipData._
|
||||||
import vinyldns.core.domain.zone.ZoneRepository.DuplicateZoneError
|
import vinyldns.core.domain.zone.ZoneRepository.DuplicateZoneError
|
||||||
import vinyldns.mysql.TestMySqlInstance
|
import vinyldns.mysql.{TestMySqlInstance, TransactionProvider}
|
||||||
|
import vinyldns.mysql.TestMySqlInstance.groupRepository
|
||||||
|
|
||||||
class MySqlZoneRepositoryIntegrationSpec
|
class MySqlZoneRepositoryIntegrationSpec
|
||||||
extends AnyWordSpec
|
extends AnyWordSpec
|
||||||
with BeforeAndAfterAll
|
with BeforeAndAfterAll
|
||||||
with BeforeAndAfterEach
|
with BeforeAndAfterEach
|
||||||
with Matchers
|
with Matchers
|
||||||
with Inspectors {
|
with Inspectors
|
||||||
|
with TransactionProvider {
|
||||||
|
|
||||||
private var repo: ZoneRepository = _
|
private var repo: ZoneRepository = _
|
||||||
|
|
||||||
@@ -221,6 +223,32 @@ class MySqlZoneRepositoryIntegrationSpec
|
|||||||
(repo.listZones(dummyAuth).unsafeRunSync().zones should contain).only(testZones.head)
|
(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 {
|
"get all zones" in {
|
||||||
// store all of the zones
|
// store all of the zones
|
||||||
val privateZone = okZone.copy(
|
val privateZone = okZone.copy(
|
||||||
@@ -259,6 +287,82 @@ class MySqlZoneRepositoryIntegrationSpec
|
|||||||
.zones should contain theSameElementsAs testZones
|
.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 {
|
"get zones that are accessible by everyone" in {
|
||||||
|
|
||||||
//user and group id being set to None implies EVERYONE access
|
//user and group id being set to None implies EVERYONE access
|
||||||
|
@@ -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]] =
|
def getAllGroups(): IO[Set[Group]] =
|
||||||
monitor("repo.Group.getAllGroups") {
|
monitor("repo.Group.getAllGroups") {
|
||||||
IO {
|
IO {
|
||||||
|
@@ -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.
|
* This is somewhat complicated due to how we need to build the SQL.
|
||||||
*
|
*
|
||||||
|
@@ -59,7 +59,12 @@
|
|||||||
<span class="fa fa-search"></span>
|
<span class="fa fa-search"></span>
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
<input id="zone-search-text" ng-model="query" type="text" class="form-control" placeholder="Zone Name"/>
|
<input id="zone-search-text" ng-model="query" type="text" class="form-control" placeholder="{{!searchByAdminGroup ? 'Zone Name' : 'Admin Group Name'}}"/>
|
||||||
|
</div>
|
||||||
|
<div class="checkbox pull-right">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" ng-model="searchByAdminGroup" ng-change="refreshZones()"> Search by admin group
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -168,8 +173,14 @@
|
|||||||
<span class="fa fa-search"></span>
|
<span class="fa fa-search"></span>
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
<input id="all-zones-search-text" ng-model="query" type="text" class="form-control" placeholder="Zone Name"/>
|
<input id="all-zones-search-text" ng-model="query" type="text" class="form-control" placeholder="{{!searchByAdminGroup ? 'Zone Name' : 'Admin Group Name'}}"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="checkbox pull-right">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" ng-model="searchByAdminGroup" ng-change="refreshZones()"> Search by admin group
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<!-- END SEARCH BOX -->
|
<!-- END SEARCH BOX -->
|
||||||
|
@@ -87,7 +87,7 @@ angular.module('controller.zones', [])
|
|||||||
allZonesPaging = pagingService.resetPaging(allZonesPaging);
|
allZonesPaging = pagingService.resetPaging(allZonesPaging);
|
||||||
|
|
||||||
zonesService
|
zonesService
|
||||||
.getZones(zonesPaging.maxItems, undefined, $scope.query)
|
.getZones(zonesPaging.maxItems, undefined, $scope.query, $scope.searchByAdminGroup)
|
||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
$log.log('zonesService::getZones-success (' + response.data.zones.length + ' zones)');
|
$log.log('zonesService::getZones-success (' + response.data.zones.length + ' zones)');
|
||||||
zonesPaging.next = response.data.nextId;
|
zonesPaging.next = response.data.nextId;
|
||||||
@@ -101,7 +101,7 @@ angular.module('controller.zones', [])
|
|||||||
});
|
});
|
||||||
|
|
||||||
zonesService
|
zonesService
|
||||||
.getZones(zonesPaging.maxItems, undefined, $scope.query, true)
|
.getZones(zonesPaging.maxItems, undefined, $scope.query, $scope.searchByAdminGroup, true)
|
||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
$log.log('zonesService::getZones-success (' + response.data.zones.length + ' zones)');
|
$log.log('zonesService::getZones-success (' + response.data.zones.length + ' zones)');
|
||||||
allZonesPaging.next = response.data.nextId;
|
allZonesPaging.next = response.data.nextId;
|
||||||
@@ -207,7 +207,7 @@ angular.module('controller.zones', [])
|
|||||||
$scope.prevPageMyZones = function() {
|
$scope.prevPageMyZones = function() {
|
||||||
var startFrom = pagingService.getPrevStartFrom(zonesPaging);
|
var startFrom = pagingService.getPrevStartFrom(zonesPaging);
|
||||||
return zonesService
|
return zonesService
|
||||||
.getZones(zonesPaging.maxItems, startFrom, $scope.query, false)
|
.getZones(zonesPaging.maxItems, startFrom, $scope.query, $scope.searchByAdminGroup, false)
|
||||||
.then(function(response) {
|
.then(function(response) {
|
||||||
zonesPaging = pagingService.prevPageUpdate(response.data.nextId, zonesPaging);
|
zonesPaging = pagingService.prevPageUpdate(response.data.nextId, zonesPaging);
|
||||||
updateZoneDisplay(response.data.zones);
|
updateZoneDisplay(response.data.zones);
|
||||||
@@ -220,7 +220,7 @@ angular.module('controller.zones', [])
|
|||||||
$scope.prevPageAllZones = function() {
|
$scope.prevPageAllZones = function() {
|
||||||
var startFrom = pagingService.getPrevStartFrom(allZonesPaging);
|
var startFrom = pagingService.getPrevStartFrom(allZonesPaging);
|
||||||
return zonesService
|
return zonesService
|
||||||
.getZones(allZonesPaging.maxItems, startFrom, $scope.query, true)
|
.getZones(allZonesPaging.maxItems, startFrom, $scope.query, $scope.searchByAdminGroup, true)
|
||||||
.then(function(response) {
|
.then(function(response) {
|
||||||
allZonesPaging = pagingService.prevPageUpdate(response.data.nextId, allZonesPaging);
|
allZonesPaging = pagingService.prevPageUpdate(response.data.nextId, allZonesPaging);
|
||||||
updateAllZonesDisplay(response.data.zones);
|
updateAllZonesDisplay(response.data.zones);
|
||||||
@@ -232,7 +232,7 @@ angular.module('controller.zones', [])
|
|||||||
|
|
||||||
$scope.nextPageMyZones = function () {
|
$scope.nextPageMyZones = function () {
|
||||||
return zonesService
|
return zonesService
|
||||||
.getZones(zonesPaging.maxItems, zonesPaging.next, $scope.query, false)
|
.getZones(zonesPaging.maxItems, zonesPaging.next, $scope.query, $scope.searchByAdminGroup, false)
|
||||||
.then(function(response) {
|
.then(function(response) {
|
||||||
var zoneSets = response.data.zones;
|
var zoneSets = response.data.zones;
|
||||||
zonesPaging = pagingService.nextPageUpdate(zoneSets, response.data.nextId, zonesPaging);
|
zonesPaging = pagingService.nextPageUpdate(zoneSets, response.data.nextId, zonesPaging);
|
||||||
@@ -248,7 +248,7 @@ angular.module('controller.zones', [])
|
|||||||
|
|
||||||
$scope.nextPageAllZones = function () {
|
$scope.nextPageAllZones = function () {
|
||||||
return zonesService
|
return zonesService
|
||||||
.getZones(allZonesPaging.maxItems, allZonesPaging.next, $scope.query, true)
|
.getZones(allZonesPaging.maxItems, allZonesPaging.next, $scope.query, $scope.searchByAdminGroup, true)
|
||||||
.then(function(response) {
|
.then(function(response) {
|
||||||
var zoneSets = response.data.zones;
|
var zoneSets = response.data.zones;
|
||||||
allZonesPaging = pagingService.nextPageUpdate(zoneSets, response.data.nextId, allZonesPaging);
|
allZonesPaging = pagingService.nextPageUpdate(zoneSets, response.data.nextId, allZonesPaging);
|
||||||
|
@@ -76,13 +76,14 @@ describe('Controller: ZonesController', function () {
|
|||||||
var expectedMaxItems = 100;
|
var expectedMaxItems = 100;
|
||||||
var expectedStartFrom = undefined;
|
var expectedStartFrom = undefined;
|
||||||
var expectedQuery = this.scope.query;
|
var expectedQuery = this.scope.query;
|
||||||
|
var expectedSearchByAdminGroup = this.scope.searchByAdminGroup;
|
||||||
var expectedignoreAccess = false;
|
var expectedignoreAccess = false;
|
||||||
|
|
||||||
this.scope.nextPageMyZones();
|
this.scope.nextPageMyZones();
|
||||||
|
|
||||||
expect(getZoneSets.calls.count()).toBe(1);
|
expect(getZoneSets.calls.count()).toBe(1);
|
||||||
expect(getZoneSets.calls.mostRecent().args).toEqual(
|
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 () {
|
it('prevPageMyZones should call getZones with the correct parameters', function () {
|
||||||
@@ -93,19 +94,20 @@ describe('Controller: ZonesController', function () {
|
|||||||
var expectedMaxItems = 100;
|
var expectedMaxItems = 100;
|
||||||
var expectedStartFrom = undefined;
|
var expectedStartFrom = undefined;
|
||||||
var expectedQuery = this.scope.query;
|
var expectedQuery = this.scope.query;
|
||||||
|
var expectedSearchByAdminGroup = this.scope.searchByAdminGroup;
|
||||||
var expectedignoreAccess = false;
|
var expectedignoreAccess = false;
|
||||||
|
|
||||||
this.scope.prevPageMyZones();
|
this.scope.prevPageMyZones();
|
||||||
|
|
||||||
expect(getZoneSets.calls.count()).toBe(1);
|
expect(getZoneSets.calls.count()).toBe(1);
|
||||||
expect(getZoneSets.calls.mostRecent().args).toEqual(
|
expect(getZoneSets.calls.mostRecent().args).toEqual(
|
||||||
[expectedMaxItems, expectedStartFrom, expectedQuery, expectedignoreAccess]);
|
[expectedMaxItems, expectedStartFrom, expectedQuery, expectedSearchByAdminGroup, expectedignoreAccess]);
|
||||||
|
|
||||||
this.scope.nextPageMyZones();
|
this.scope.nextPageMyZones();
|
||||||
this.scope.prevPageMyZones();
|
this.scope.prevPageMyZones();
|
||||||
|
|
||||||
expect(getZoneSets.calls.count()).toBe(3);
|
expect(getZoneSets.calls.count()).toBe(3);
|
||||||
expect(getZoneSets.calls.mostRecent().args).toEqual(
|
expect(getZoneSets.calls.mostRecent().args).toEqual(
|
||||||
[expectedMaxItems, expectedStartFrom, expectedQuery, expectedignoreAccess]);
|
[expectedMaxItems, expectedStartFrom, expectedQuery, expectedSearchByAdminGroup, expectedignoreAccess]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -19,7 +19,7 @@
|
|||||||
angular.module('service.zones', [])
|
angular.module('service.zones', [])
|
||||||
.service('zonesService', function ($http, groupsService, $log, utilityService) {
|
.service('zonesService', function ($http, groupsService, $log, utilityService) {
|
||||||
|
|
||||||
this.getZones = function (limit, startFrom, query, ignoreAccess) {
|
this.getZones = function (limit, startFrom, query, searchByAdminGroup, ignoreAccess) {
|
||||||
if (query == "") {
|
if (query == "") {
|
||||||
query = null;
|
query = null;
|
||||||
}
|
}
|
||||||
@@ -27,6 +27,7 @@ angular.module('service.zones', [])
|
|||||||
"maxItems": limit,
|
"maxItems": limit,
|
||||||
"startFrom": startFrom,
|
"startFrom": startFrom,
|
||||||
"nameFilter": query,
|
"nameFilter": query,
|
||||||
|
"searchByAdminGroup": searchByAdminGroup,
|
||||||
"ignoreAccess": ignoreAccess
|
"ignoreAccess": ignoreAccess
|
||||||
};
|
};
|
||||||
var url = groupsService.urlBuilder("/api/zones", params);
|
var url = groupsService.urlBuilder("/api/zones", params);
|
||||||
|
@@ -27,8 +27,8 @@ describe('Service: zoneService', function () {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
it('http backend gets called properly when getting zones', 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.$httpBackend.expectGET('/api/zones?maxItems=100&startFrom=start&nameFilter=someQuery&searchByAdminGroup=false&ignoreAccess=false').respond('zone returned');
|
||||||
this.zonesService.getZones('100', 'start', 'someQuery', false)
|
this.zonesService.getZones('100', 'start', 'someQuery', false, false)
|
||||||
.then(function(response) {
|
.then(function(response) {
|
||||||
expect(response.data).toBe('zone returned');
|
expect(response.data).toBe('zone returned');
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user