diff --git a/modules/api/functional_test/live_tests/membership/list_my_groups_test.py b/modules/api/functional_test/live_tests/membership/list_my_groups_test.py
index 4ef0b018a..5b33135f3 100644
--- a/modules/api/functional_test/live_tests/membership/list_my_groups_test.py
+++ b/modules/api/functional_test/live_tests/membership/list_my_groups_test.py
@@ -9,6 +9,7 @@ from vinyldns_context import VinylDNSTestContext
class ListGroupsSearchContext(object):
def __init__(self):
self.client = VinylDNSClient(VinylDNSTestContext.vinyldns_url, access_key='listGroupAccessKey', secret_key='listGroupSecretKey')
+ self.support_user_client = VinylDNSClient(VinylDNSTestContext.vinyldns_url, 'supportUserAccessKey', 'supportUserSecretKey')
self.tear_down() # ensures that the environment is clean before starting
try:
@@ -29,6 +30,15 @@ class ListGroupsSearchContext(object):
pass
raise
+ def verify_ignore_access(self, results):
+ assert_that(results, has_length(3)) # 3 fields
+
+ assert_that(len(results['groups']), greater_than(50))
+ assert_that(results, is_not(has_key('groupNameFilter')))
+ assert_that(results, is_not(has_key('startFrom')))
+ assert_that(results, is_not(has_key('nextId')))
+ assert_that(results['maxItems'], is_(100))
+
def tear_down(self):
clear_zones(self.client)
clear_groups(self.client)
@@ -51,7 +61,7 @@ def test_list_my_groups_no_parameters(list_my_groups_context):
results = list_my_groups_context.client.list_my_groups(status=200)
- assert_that(results, has_length(2)) # 2 fields
+ assert_that(results, has_length(3)) # 3 fields
assert_that(results['groups'], has_length(50))
assert_that(results, is_not(has_key('groupNameFilter')))
@@ -70,7 +80,7 @@ def test_get_my_groups_using_old_account_auth(list_my_groups_context):
Test passing in an account will return an empty set
"""
results = list_my_groups_context.client.list_my_groups(status=200)
- assert_that(results, has_length(2))
+ assert_that(results, has_length(3))
assert_that(results, is_not(has_key('groupNameFilter')))
assert_that(results, is_not(has_key('startFrom')))
assert_that(results, is_not(has_key('nextId')))
@@ -83,7 +93,7 @@ def test_list_my_groups_max_items(list_my_groups_context):
"""
results = list_my_groups_context.client.list_my_groups(max_items=5, status=200)
- assert_that(results, has_length(3)) # 3 fields
+ assert_that(results, has_length(4)) # 4 fields
assert_that(results, has_key('groups'))
assert_that(results, is_not(has_key('groupNameFilter')))
@@ -98,7 +108,7 @@ def test_list_my_groups_paging(list_my_groups_context):
"""
results=list_my_groups_context.client.list_my_groups(max_items=20, status=200)
- assert_that(results, has_length(3)) # 3 fields
+ assert_that(results, has_length(4)) # 4 fields
assert_that(results, has_key('groups'))
assert_that(results, is_not(has_key('groupNameFilter')))
assert_that(results, is_not(has_key('startFrom')))
@@ -110,7 +120,7 @@ def test_list_my_groups_paging(list_my_groups_context):
results = list_my_groups_context.client.list_my_groups(max_items=20, start_from=results['nextId'], status=200)
if 'nextId' in results:
- assert_that(results, has_length(4)) # 4 fields
+ assert_that(results, has_length(5)) # 5 fields
assert_that(results, has_key('groups'))
assert_that(results, is_not(has_key('groupNameFilter')))
assert_that(results['startFrom'], is_(prev['nextId']))
@@ -118,7 +128,7 @@ def test_list_my_groups_paging(list_my_groups_context):
assert_that(results['maxItems'], is_(20))
else:
- assert_that(results, has_length(3)) # 3 fields
+ assert_that(results, has_length(4)) # 4 fields
assert_that(results, has_key('groups'))
assert_that(results, is_not(has_key('groupNameFilter')))
assert_that(results['startFrom'], is_(prev['nextId']))
@@ -132,7 +142,7 @@ def test_list_my_groups_filter_matches(list_my_groups_context):
"""
results = list_my_groups_context.client.list_my_groups(group_name_filter="test-list-my-groups-01", status=200)
- assert_that(results, has_length(3)) # 3 fields
+ assert_that(results, has_length(4)) # 4 fields
assert_that(results['groups'], has_length(10))
assert_that(results['groupNameFilter'], is_('test-list-my-groups-01'))
@@ -163,3 +173,35 @@ def test_list_my_groups_no_deleted(list_my_groups_context):
for g in results['groups']:
assert_that(g['status'], is_not('Deleted'))
+def test_list_my_groups_with_ignore_access_true(list_my_groups_context):
+ """
+ Test that we can get all the groups whether a user is a member or not
+ """
+
+ results = list_my_groups_context.client.list_my_groups(ignore_access=True, status=200)
+
+ list_my_groups_context.verify_ignore_access(results)
+
+ my_results = list_my_groups_context.client.list_my_groups(status=200)
+ my_results['groups'] = sorted(my_results['groups'], key=lambda x: x['name'])
+
+ for i in range(0, 50):
+ assert_that(my_results['groups'][i]['name'], is_("test-list-my-groups-{0:0>3}".format(i)))
+
+def test_list_my_groups_as_support_user(list_my_groups_context):
+ """
+ Test that we can get all the groups as a support user, even without ignore_access
+ """
+
+ results = list_my_groups_context.support_user_client.list_my_groups(status=200)
+
+ list_my_groups_context.verify_ignore_access(results)
+
+def test_list_my_groups_as_support_user_with_ignore_access_true(list_my_groups_context):
+ """
+ Test that we can get all the groups as a support user
+ """
+
+ results = list_my_groups_context.support_user_client.list_my_groups(ignore_access=True, status=200)
+
+ list_my_groups_context.verify_ignore_access(results)
diff --git a/modules/api/functional_test/vinyldns_python.py b/modules/api/functional_test/vinyldns_python.py
index e8c3eeb55..b4cac4477 100644
--- a/modules/api/functional_test/vinyldns_python.py
+++ b/modules/api/functional_test/vinyldns_python.py
@@ -226,12 +226,13 @@ class VinylDNSClient(object):
return data
- def list_my_groups(self, group_name_filter=None, start_from=None, max_items=None, **kwargs):
+ def list_my_groups(self, group_name_filter=None, start_from=None, max_items=None, ignore_access=False, **kwargs):
"""
Retrieves my groups
:param start_from: the start key of the page
:param max_items: the page limit
:param group_name_filter: only returns groups whose names contain filter string
+ :param ignore_access: determines if groups should be retrieved based on requester's membership
:return: the content of the response
"""
@@ -242,6 +243,8 @@ class VinylDNSClient(object):
args.append(u'startFrom={0}'.format(start_from))
if max_items is not None:
args.append(u'maxItems={0}'.format(max_items))
+ if ignore_access is not False:
+ args.append(u'ignoreAccess={0}'.format(ignore_access))
url = urljoin(self.index_url, u'/groups') + u'?' + u'&'.join(args)
response, data = self.make_request(url, u'GET', self.headers, **kwargs)
diff --git a/modules/api/src/main/scala/vinyldns/api/domain/membership/MembershipProtocol.scala b/modules/api/src/main/scala/vinyldns/api/domain/membership/MembershipProtocol.scala
index 676164875..6e69dabec 100644
--- a/modules/api/src/main/scala/vinyldns/api/domain/membership/MembershipProtocol.scala
+++ b/modules/api/src/main/scala/vinyldns/api/domain/membership/MembershipProtocol.scala
@@ -140,7 +140,8 @@ final case class ListMyGroupsResponse(
groupNameFilter: Option[String] = None,
startFrom: Option[String] = None,
nextId: Option[String] = None,
- maxItems: Int)
+ maxItems: Int,
+ ignoreAccess: Boolean)
final case class GroupNotFoundError(msg: String) extends Throwable(msg)
diff --git a/modules/api/src/main/scala/vinyldns/api/domain/membership/MembershipService.scala b/modules/api/src/main/scala/vinyldns/api/domain/membership/MembershipService.scala
index 9d7b5e7aa..f9e3d619e 100644
--- a/modules/api/src/main/scala/vinyldns/api/domain/membership/MembershipService.scala
+++ b/modules/api/src/main/scala/vinyldns/api/domain/membership/MembershipService.scala
@@ -135,16 +135,17 @@ class MembershipService(
groupNameFilter: Option[String],
startFrom: Option[String],
maxItems: Int,
- authPrincipal: AuthPrincipal): Result[ListMyGroupsResponse] = {
+ authPrincipal: AuthPrincipal,
+ ignoreAccess: Boolean): Result[ListMyGroupsResponse] = {
val groupsCall =
- if (authPrincipal.isSystemAdmin) {
+ if (authPrincipal.isSystemAdmin || ignoreAccess) {
groupRepo.getAllGroups()
} else {
groupRepo.getGroups(authPrincipal.memberGroupIds.toSet)
}
groupsCall.map { grp =>
- pageListGroupsResponse(grp.toList, groupNameFilter, startFrom, maxItems)
+ pageListGroupsResponse(grp.toList, groupNameFilter, startFrom, maxItems, ignoreAccess)
}
}.toResult
@@ -152,7 +153,8 @@ class MembershipService(
allGroups: Seq[Group],
groupNameFilter: Option[String],
startFrom: Option[String],
- maxItems: Int): ListMyGroupsResponse = {
+ maxItems: Int,
+ ignoreAccess: Boolean): ListMyGroupsResponse = {
val allMyGroups = allGroups
.filter(_.status == GroupStatus.Active)
.sortBy(_.id)
@@ -165,7 +167,7 @@ class MembershipService(
val nextId = if (filtered.length > maxItems) Some(filtered(maxItems - 1).id) else None
val groups = filtered.take(maxItems)
- ListMyGroupsResponse(groups, groupNameFilter, startFrom, nextId, maxItems)
+ ListMyGroupsResponse(groups, groupNameFilter, startFrom, nextId, maxItems, ignoreAccess)
}
def getGroupActivity(
diff --git a/modules/api/src/main/scala/vinyldns/api/domain/membership/MembershipServiceAlgebra.scala b/modules/api/src/main/scala/vinyldns/api/domain/membership/MembershipServiceAlgebra.scala
index b5961d32a..b5c7245d7 100644
--- a/modules/api/src/main/scala/vinyldns/api/domain/membership/MembershipServiceAlgebra.scala
+++ b/modules/api/src/main/scala/vinyldns/api/domain/membership/MembershipServiceAlgebra.scala
@@ -42,7 +42,8 @@ trait MembershipServiceAlgebra {
groupNameFilter: Option[String],
startFrom: Option[String],
maxItems: Int,
- authPrincipal: AuthPrincipal): Result[ListMyGroupsResponse]
+ authPrincipal: AuthPrincipal,
+ ignoreAccess: Boolean): Result[ListMyGroupsResponse]
def listMembers(
groupId: String,
diff --git a/modules/api/src/main/scala/vinyldns/api/route/MembershipRouting.scala b/modules/api/src/main/scala/vinyldns/api/route/MembershipRouting.scala
index a5b6a0746..29e857fef 100644
--- a/modules/api/src/main/scala/vinyldns/api/route/MembershipRouting.scala
+++ b/modules/api/src/main/scala/vinyldns/api/route/MembershipRouting.scala
@@ -69,8 +69,16 @@ class MembershipRoute(
}
} ~
(get & monitor("Endpoint.listMyGroups")) {
- parameters("startFrom".?, "maxItems".as[Int].?(DEFAULT_MAX_ITEMS), "groupNameFilter".?) {
- (startFrom: Option[String], maxItems: Int, groupNameFilter: Option[String]) =>
+ parameters(
+ "startFrom".?,
+ "maxItems".as[Int].?(DEFAULT_MAX_ITEMS),
+ "groupNameFilter".?,
+ "ignoreAccess".as[Boolean].?(false)) {
+ (
+ startFrom: Option[String],
+ maxItems: Int,
+ groupNameFilter: Option[String],
+ ignoreAccess: Boolean) =>
{
handleRejections(invalidQueryHandler) {
validate(
@@ -81,8 +89,9 @@ class MembershipRoute(
""".stripMargin
) {
authenticateAndExecute(membershipService
- .listMyGroups(groupNameFilter, startFrom, maxItems, _)) { groups =>
- complete(StatusCodes.OK, groups)
+ .listMyGroups(groupNameFilter, startFrom, maxItems, _, ignoreAccess)) {
+ groups =>
+ complete(StatusCodes.OK, groups)
}
}
}
diff --git a/modules/api/src/test/scala/vinyldns/api/domain/membership/MembershipServiceSpec.scala b/modules/api/src/test/scala/vinyldns/api/domain/membership/MembershipServiceSpec.scala
index 312405a56..1c2327520 100644
--- a/modules/api/src/test/scala/vinyldns/api/domain/membership/MembershipServiceSpec.scala
+++ b/modules/api/src/test/scala/vinyldns/api/domain/membership/MembershipServiceSpec.scala
@@ -529,14 +529,15 @@ class MembershipServiceSpec
.when(mockGroupRepo)
.getGroups(any[Set[String]])
val result: ListMyGroupsResponse =
- rightResultOf(underTest.listMyGroups(None, None, 100, listOfDummyGroupsAuth).value)
+ rightResultOf(underTest.listMyGroups(None, None, 100, listOfDummyGroupsAuth, false).value)
verify(mockGroupRepo, never()).getAllGroups()
result shouldBe ListMyGroupsResponse(
groups = listOfDummyGroupInfo.take(100),
None,
None,
nextId = Some(listOfDummyGroups(99).id),
- maxItems = 100)
+ maxItems = 100,
+ ignoreAccess = false)
}
"return only return groups whose name matches the filter" in {
doReturn(IO.pure(listOfDummyGroups.toSet))
@@ -548,14 +549,16 @@ class MembershipServiceSpec
groupNameFilter = Some("name-dummy01"),
startFrom = None,
maxItems = 100,
- listOfDummyGroupsAuth)
+ listOfDummyGroupsAuth,
+ false)
.value)
result shouldBe ListMyGroupsResponse(
groups = listOfDummyGroupInfo.slice(10, 20),
groupNameFilter = Some("name-dummy01"),
startFrom = None,
nextId = None,
- maxItems = 100)
+ maxItems = 100,
+ ignoreAccess = false)
}
"return only return groups after startFrom" in {
doReturn(IO.pure(listOfDummyGroups.toSet))
@@ -567,14 +570,16 @@ class MembershipServiceSpec
groupNameFilter = None,
startFrom = Some(listOfDummyGroups(99).id),
maxItems = 100,
- listOfDummyGroupsAuth)
+ listOfDummyGroupsAuth,
+ ignoreAccess = false)
.value)
result shouldBe ListMyGroupsResponse(
groups = listOfDummyGroupInfo.slice(100, 200),
groupNameFilter = None,
startFrom = Some(listOfDummyGroups(99).id),
nextId = None,
- maxItems = 100)
+ maxItems = 100,
+ ignoreAccess = false)
}
"return only return maxItems groups" in {
doReturn(IO.pure(listOfDummyGroups.toSet))
@@ -586,35 +591,65 @@ class MembershipServiceSpec
groupNameFilter = None,
startFrom = None,
maxItems = 10,
- listOfDummyGroupsAuth)
+ listOfDummyGroupsAuth,
+ ignoreAccess = false)
.value)
result shouldBe ListMyGroupsResponse(
groups = listOfDummyGroupInfo.slice(0, 10),
groupNameFilter = None,
startFrom = None,
nextId = Some(listOfDummyGroups(9).id),
- maxItems = 10)
+ maxItems = 10,
+ ignoreAccess = false)
}
"return an empty set if the user is not a member of any groups" in {
doReturn(IO.pure(Set())).when(mockGroupRepo).getGroups(any[Set[String]])
val result: ListMyGroupsResponse =
- rightResultOf(underTest.listMyGroups(None, None, 100, notAuth).value)
- result shouldBe ListMyGroupsResponse(Seq(), None, None, None, 100)
+ rightResultOf(underTest.listMyGroups(None, None, 100, notAuth, false).value)
+ result shouldBe ListMyGroupsResponse(Seq(), None, None, None, 100, false)
}
- "return groups from the database for super users" in {
+ "return all groups from the database if ignoreAccess is true" in {
doReturn(IO.pure(Set(okGroup, dummyGroup))).when(mockGroupRepo).getAllGroups()
val result: ListMyGroupsResponse =
- rightResultOf(underTest.listMyGroups(None, None, 100, superUserAuth).value)
+ rightResultOf(underTest.listMyGroups(None, None, 100, notAuth, true).value)
verify(mockGroupRepo).getAllGroups()
result.groups should contain theSameElementsAs Seq(
GroupInfo(dummyGroup),
GroupInfo(okGroup))
}
- "return groups from the database for support users" in {
+ "return all groups from the database for super users even if ignoreAccess is false" in {
+ doReturn(IO.pure(Set(okGroup, dummyGroup))).when(mockGroupRepo).getAllGroups()
+ val result: ListMyGroupsResponse =
+ rightResultOf(underTest.listMyGroups(None, None, 100, superUserAuth, false).value)
+ verify(mockGroupRepo).getAllGroups()
+ result.groups should contain theSameElementsAs Seq(
+ GroupInfo(dummyGroup),
+ GroupInfo(okGroup))
+ }
+ "return all groups from the database for super users if ignoreAccess is true" in {
+ doReturn(IO.pure(Set(okGroup, dummyGroup))).when(mockGroupRepo).getAllGroups()
+ val result: ListMyGroupsResponse =
+ rightResultOf(underTest.listMyGroups(None, None, 100, superUserAuth, true).value)
+ verify(mockGroupRepo).getAllGroups()
+ result.groups should contain theSameElementsAs Seq(
+ GroupInfo(dummyGroup),
+ GroupInfo(okGroup))
+ }
+ "return all groups from the database for support users even if ignoreAccess is false" in {
val supportAuth = AuthPrincipal(okUser.copy(isSupport = true), Seq())
doReturn(IO.pure(Set(okGroup, dummyGroup))).when(mockGroupRepo).getAllGroups()
val result: ListMyGroupsResponse =
- rightResultOf(underTest.listMyGroups(None, None, 100, supportAuth).value)
+ rightResultOf(underTest.listMyGroups(None, None, 100, supportAuth, false).value)
+ verify(mockGroupRepo).getAllGroups()
+ result.groups should contain theSameElementsAs Seq(
+ GroupInfo(dummyGroup),
+ GroupInfo(okGroup))
+ }
+ "return all groups from the database for support users if ignoreAccess is true" in {
+ val supportAuth = AuthPrincipal(okUser.copy(isSupport = true), Seq())
+ doReturn(IO.pure(Set(okGroup, dummyGroup))).when(mockGroupRepo).getAllGroups()
+ val result: ListMyGroupsResponse =
+ rightResultOf(underTest.listMyGroups(None, None, 100, supportAuth, true).value)
verify(mockGroupRepo).getAllGroups()
result.groups should contain theSameElementsAs Seq(
GroupInfo(dummyGroup),
@@ -626,8 +661,8 @@ class MembershipServiceSpec
.when(mockGroupRepo)
.getGroups(any[Set[String]])
val result: ListMyGroupsResponse =
- rightResultOf(underTest.listMyGroups(None, None, 100, deletedGroupAuth).value)
- result shouldBe ListMyGroupsResponse(Seq(), None, None, None, 100)
+ rightResultOf(underTest.listMyGroups(None, None, 100, deletedGroupAuth, false).value)
+ result shouldBe ListMyGroupsResponse(Seq(), None, None, None, 100, false)
}
}
diff --git a/modules/api/src/test/scala/vinyldns/api/route/MembershipRoutingSpec.scala b/modules/api/src/test/scala/vinyldns/api/route/MembershipRoutingSpec.scala
index 33641a549..6eed8911a 100644
--- a/modules/api/src/test/scala/vinyldns/api/route/MembershipRoutingSpec.scala
+++ b/modules/api/src/test/scala/vinyldns/api/route/MembershipRoutingSpec.scala
@@ -188,15 +188,16 @@ class MembershipRoutingSpec
"return a 200 response with the groups when no optional parameters are used" in {
val twoUserGroupInfo = GroupInfo(twoUserGroup)
doReturn(
- result(ListMyGroupsResponse(Seq(okGroupInfo, twoUserGroupInfo), None, None, None, 100)))
+ result(
+ ListMyGroupsResponse(Seq(okGroupInfo, twoUserGroupInfo), None, None, None, 100, false)))
.when(membershipService)
- .listMyGroups(None, None, 100, okAuth)
+ .listMyGroups(None, None, 100, okAuth, false)
Get("/groups") ~> Route.seal(membershipRoute) ~> check {
status shouldBe StatusCodes.OK
val result = responseAs[ListMyGroupsResponse]
val expected =
- ListMyGroupsResponse(Seq(okGroupInfo, twoUserGroupInfo), None, None, None, 100)
+ ListMyGroupsResponse(Seq(okGroupInfo, twoUserGroupInfo), None, None, None, 100, false)
result shouldBe expected
}
@@ -209,13 +210,15 @@ class MembershipRoutingSpec
groupNameFilter = Some("ok"),
startFrom = Some("anyString"),
nextId = None,
- maxItems = 100)))
+ maxItems = 100,
+ ignoreAccess = false)))
.when(membershipService)
.listMyGroups(
groupNameFilter = Some("ok"),
startFrom = Some("anyString"),
maxItems = 100,
- okAuth)
+ okAuth,
+ ignoreAccess = false)
Get("/groups?startFrom=anyString&maxItems=100&groupNameFilter=ok") ~> Route.seal(
membershipRoute) ~> check {
status shouldBe StatusCodes.OK
@@ -226,7 +229,8 @@ class MembershipRoutingSpec
groupNameFilter = Some("ok"),
startFrom = Some("anyString"),
maxItems = 100,
- nextId = None)
+ nextId = None,
+ ignoreAccess = false)
result shouldBe expected
}
@@ -244,7 +248,7 @@ class MembershipRoutingSpec
"return a 500 response when fails" in {
doReturn(result(new IllegalArgumentException("fail")))
.when(membershipService)
- .listMyGroups(None, None, 100, okAuth)
+ .listMyGroups(None, None, 100, okAuth, false)
Get("/groups") ~> Route.seal(membershipRoute) ~> check {
status shouldBe StatusCodes.InternalServerError
diff --git a/modules/portal/app/controllers/VinylDNS.scala b/modules/portal/app/controllers/VinylDNS.scala
index 7267fa57f..b993996db 100644
--- a/modules/portal/app/controllers/VinylDNS.scala
+++ b/modules/portal/app/controllers/VinylDNS.scala
@@ -67,6 +67,7 @@ object VinylDNS {
lastName: Option[String],
email: Option[String],
isSuper: Boolean,
+ isSupport: Boolean,
id: String,
lockStatus: LockStatus)
object UserInfo {
@@ -77,6 +78,7 @@ object VinylDNS {
lastName = user.lastName,
email = user.email,
isSuper = user.isSuper,
+ isSupport = user.isSupport,
id = user.id,
lockStatus = user.lockStatus
)
@@ -218,7 +220,7 @@ class VinylDNS @Inject()(
})
}
- def getMyGroups(): Action[AnyContent] = userAction.async { implicit request =>
+ def getGroups(): Action[AnyContent] = userAction.async { implicit request =>
val queryParameters = new HashMap[String, java.util.List[String]]()
for {
(name, values) <- request.queryString
diff --git a/modules/portal/app/views/groups/groups.scala.html b/modules/portal/app/views/groups/groups.scala.html
index b6227ad46..a3182f256 100644
--- a/modules/portal/app/views/groups/groups.scala.html
+++ b/modules/portal/app/views/groups/groups.scala.html
@@ -22,7 +22,17 @@