mirror of
https://github.com/VinylDNS/vinyldns
synced 2025-08-31 14:25:30 +00:00
Open list groups access (#809)
This commit is contained in:
@@ -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)
|
||||
|
@@ -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)
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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(
|
||||
|
@@ -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,
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user