mirror of
https://github.com/VinylDNS/vinyldns
synced 2025-08-29 13:27:43 +00:00
Merge branch 'master' into show_recordsets_count
This commit is contained in:
commit
fb6db99685
@ -57,6 +57,8 @@ class Route53ApiIntegrationSpec
|
||||
"test",
|
||||
Some("access"),
|
||||
Some("secret"),
|
||||
None,
|
||||
None,
|
||||
sys.env.getOrElse("R53_SERVICE_ENDPOINT", "http://localhost:19003"),
|
||||
"us-east-1"
|
||||
)
|
||||
|
@ -158,8 +158,8 @@ final case class ListAdminsResponse(admins: Seq[UserInfo])
|
||||
|
||||
final case class ListGroupChangesResponse(
|
||||
changes: Seq[GroupChangeInfo],
|
||||
startFrom: Option[String] = None,
|
||||
nextId: Option[String] = None,
|
||||
startFrom: Option[Int] = None,
|
||||
nextId: Option[Int] = None,
|
||||
maxItems: Int
|
||||
)
|
||||
|
||||
|
@ -266,7 +266,7 @@ class MembershipService(
|
||||
|
||||
def getGroupActivity(
|
||||
groupId: String,
|
||||
startFrom: Option[String],
|
||||
startFrom: Option[Int],
|
||||
maxItems: Int,
|
||||
authPrincipal: AuthPrincipal
|
||||
): Result[ListGroupChangesResponse] =
|
||||
@ -285,7 +285,7 @@ class MembershipService(
|
||||
} yield ListGroupChangesResponse(
|
||||
groupChanges.map(change => GroupChangeInfo.apply(change.copy(userName = userMap.get(change.userId)))),
|
||||
startFrom,
|
||||
result.lastEvaluatedTimeStamp,
|
||||
result.nextId,
|
||||
maxItems
|
||||
)
|
||||
|
||||
|
@ -63,7 +63,7 @@ trait MembershipServiceAlgebra {
|
||||
|
||||
def getGroupActivity(
|
||||
groupId: String,
|
||||
startFrom: Option[String],
|
||||
startFrom: Option[Int],
|
||||
maxItems: Int,
|
||||
authPrincipal: AuthPrincipal
|
||||
): Result[ListGroupChangesResponse]
|
||||
|
@ -38,6 +38,15 @@ object ListZoneChangesResponse {
|
||||
)
|
||||
}
|
||||
|
||||
case class ListDeletedZoneChangesResponse(
|
||||
zonesDeletedInfo: List[ZoneChangeDeletedInfo],
|
||||
zoneChangeFilter: Option[String] = None,
|
||||
nextId: Option[String] = None,
|
||||
startFrom: Option[String] = None,
|
||||
maxItems: Int = 100,
|
||||
ignoreAccess: Boolean = false
|
||||
)
|
||||
|
||||
case class ListFailedZoneChangesResponse(
|
||||
failedZoneChanges: List[ZoneChange] = Nil,
|
||||
nextId: Int,
|
||||
|
@ -22,7 +22,7 @@ import vinyldns.core.domain.record.RecordSetChangeType.RecordSetChangeType
|
||||
import vinyldns.core.domain.record.RecordSetStatus.RecordSetStatus
|
||||
import vinyldns.core.domain.record.RecordType.RecordType
|
||||
import vinyldns.core.domain.record.{RecordData, RecordSet, RecordSetChange}
|
||||
import vinyldns.core.domain.zone.{ACLRuleInfo, AccessLevel, Zone, ZoneACL, ZoneConnection}
|
||||
import vinyldns.core.domain.zone.{ACLRuleInfo, AccessLevel, Zone, ZoneACL, ZoneChange, ZoneConnection}
|
||||
import vinyldns.core.domain.zone.AccessLevel.AccessLevel
|
||||
import vinyldns.core.domain.zone.ZoneStatus.ZoneStatus
|
||||
|
||||
@ -145,6 +145,27 @@ object ZoneSummaryInfo {
|
||||
)
|
||||
}
|
||||
|
||||
case class ZoneChangeDeletedInfo(
|
||||
zoneChange: ZoneChange,
|
||||
adminGroupName: String,
|
||||
userName: String,
|
||||
accessLevel: AccessLevel
|
||||
)
|
||||
|
||||
object ZoneChangeDeletedInfo {
|
||||
def apply(zoneChange: List[ZoneChange],
|
||||
groupName: String,
|
||||
userName: String,
|
||||
accessLevel: AccessLevel)
|
||||
: ZoneChangeDeletedInfo =
|
||||
ZoneChangeDeletedInfo(
|
||||
zoneChange= zoneChange,
|
||||
groupName = groupName,
|
||||
userName = userName,
|
||||
accessLevel = accessLevel
|
||||
)
|
||||
}
|
||||
|
||||
case class RecordSetListInfo(
|
||||
zoneId: String,
|
||||
name: String,
|
||||
|
@ -23,7 +23,7 @@ import vinyldns.api.Interfaces
|
||||
import vinyldns.core.domain.auth.AuthPrincipal
|
||||
import vinyldns.api.repository.ApiDataAccessor
|
||||
import vinyldns.core.crypto.CryptoAlgebra
|
||||
import vinyldns.core.domain.membership.{Group, GroupRepository, User, UserRepository}
|
||||
import vinyldns.core.domain.membership.{Group, GroupRepository, ListUsersResults, User, UserRepository}
|
||||
import vinyldns.core.domain.zone._
|
||||
import vinyldns.core.queue.MessageQueue
|
||||
import vinyldns.core.domain.DomainHelpers.ensureTrailingDot
|
||||
@ -226,6 +226,58 @@ class ZoneService(
|
||||
}
|
||||
}.toResult
|
||||
|
||||
def listDeletedZones(
|
||||
authPrincipal: AuthPrincipal,
|
||||
nameFilter: Option[String] = None,
|
||||
startFrom: Option[String] = None,
|
||||
maxItems: Int = 100,
|
||||
ignoreAccess: Boolean = false
|
||||
): Result[ListDeletedZoneChangesResponse] = {
|
||||
for {
|
||||
listZonesChangeResult <- zoneChangeRepository.listDeletedZones(
|
||||
authPrincipal,
|
||||
nameFilter,
|
||||
startFrom,
|
||||
maxItems,
|
||||
ignoreAccess
|
||||
)
|
||||
zoneChanges = listZonesChangeResult.zoneDeleted
|
||||
groupIds = zoneChanges.map(_.zone.adminGroupId).toSet
|
||||
groups <- groupRepository.getGroups(groupIds)
|
||||
userId = zoneChanges.map(_.userId).toSet
|
||||
users <- userRepository.getUsers(userId,None,None)
|
||||
zoneDeleteSummaryInfos = ZoneChangeDeletedInfoMapping(zoneChanges, authPrincipal, groups, users)
|
||||
} yield {
|
||||
ListDeletedZoneChangesResponse(
|
||||
zoneDeleteSummaryInfos,
|
||||
listZonesChangeResult.zoneChangeFilter,
|
||||
listZonesChangeResult.nextId,
|
||||
listZonesChangeResult.startFrom,
|
||||
listZonesChangeResult.maxItems,
|
||||
listZonesChangeResult.ignoreAccess
|
||||
)
|
||||
}
|
||||
}.toResult
|
||||
|
||||
private def ZoneChangeDeletedInfoMapping(
|
||||
zoneChange: List[ZoneChange],
|
||||
auth: AuthPrincipal,
|
||||
groups: Set[Group],
|
||||
users: ListUsersResults
|
||||
): List[ZoneChangeDeletedInfo] =
|
||||
zoneChange.map { zc =>
|
||||
val groupName = groups.find(_.id == zc.zone.adminGroupId) match {
|
||||
case Some(group) => group.name
|
||||
case None => "Unknown group name"
|
||||
}
|
||||
val userName = users.users.find(_.id == zc.userId) match {
|
||||
case Some(user) => user.userName
|
||||
case None => "Unknown user name"
|
||||
}
|
||||
val zoneAccess = getZoneAccess(auth, zc.zone)
|
||||
ZoneChangeDeletedInfo(zc, groupName,userName, zoneAccess)
|
||||
}
|
||||
|
||||
def zoneSummaryInfoMapping(
|
||||
zones: List[Zone],
|
||||
auth: AuthPrincipal,
|
||||
|
@ -49,6 +49,14 @@ trait ZoneServiceAlgebra {
|
||||
includeReverse: Boolean
|
||||
): Result[ListZonesResponse]
|
||||
|
||||
def listDeletedZones(
|
||||
authPrincipal: AuthPrincipal,
|
||||
nameFilter: Option[String],
|
||||
startFrom: Option[String],
|
||||
maxItems: Int,
|
||||
ignoreAccess: Boolean
|
||||
): Result[ListDeletedZoneChangesResponse]
|
||||
|
||||
def listZoneChanges(
|
||||
zoneId: String,
|
||||
authPrincipal: AuthPrincipal,
|
||||
|
@ -162,8 +162,8 @@ class MembershipRoute(
|
||||
} ~
|
||||
path("groups" / Segment / "activity") { groupId =>
|
||||
(get & monitor("Endpoint.groupActivity")) {
|
||||
parameters("startFrom".?, "maxItems".as[Int].?(DEFAULT_MAX_ITEMS)) {
|
||||
(startFrom: Option[String], maxItems: Int) =>
|
||||
parameters("startFrom".as[Int].?, "maxItems".as[Int].?(DEFAULT_MAX_ITEMS)) {
|
||||
(startFrom: Option[Int], maxItems: Int) =>
|
||||
handleRejections(invalidQueryHandler) {
|
||||
validate(
|
||||
0 < maxItems && maxItems <= MAX_ITEMS_LIMIT,
|
||||
|
@ -249,33 +249,6 @@ class RecordSetRoute(
|
||||
}
|
||||
}
|
||||
} ~
|
||||
path("recordsetchange" / "history") {
|
||||
(get & monitor("Endpoint.listRecordSetChangeHistory")) {
|
||||
parameters("startFrom".as[Int].?, "maxItems".as[Int].?(DEFAULT_MAX_ITEMS), "fqdn".as[String].?, "recordType".as[String].?) {
|
||||
(startFrom: Option[Int], maxItems: Int, fqdn: Option[String], recordType: Option[String]) =>
|
||||
handleRejections(invalidQueryHandler) {
|
||||
val errorMessage = if(fqdn.isEmpty || recordType.isEmpty) {
|
||||
"recordType and fqdn cannot be empty"
|
||||
} else {
|
||||
s"maxItems was $maxItems, maxItems must be between 0 exclusive " +
|
||||
s"and $DEFAULT_MAX_ITEMS inclusive"
|
||||
}
|
||||
val isValid = (0 < maxItems && maxItems <= DEFAULT_MAX_ITEMS) && (fqdn.nonEmpty && recordType.nonEmpty)
|
||||
validate(
|
||||
check = isValid,
|
||||
errorMsg = errorMessage
|
||||
){
|
||||
authenticateAndExecute(
|
||||
recordSetService
|
||||
.listRecordSetChangeHistory(None, startFrom, maxItems, fqdn, RecordType.find(recordType.get), _)
|
||||
) { changes =>
|
||||
complete(StatusCodes.OK, changes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} ~
|
||||
path("metrics" / "health" / "zones" / Segment / "recordsetchangesfailure") {zoneId =>
|
||||
(get & monitor("Endpoint.listFailedRecordSetChanges")) {
|
||||
parameters("startFrom".as[Int].?(0), "maxItems".as[Int].?(DEFAULT_MAX_ITEMS)) {
|
||||
|
@ -111,6 +111,38 @@ class ZoneRoute(
|
||||
}
|
||||
}
|
||||
} ~
|
||||
path("zones" / "deleted" / "changes") {
|
||||
(get & monitor("Endpoint.listDeletedZones")) {
|
||||
parameters(
|
||||
"nameFilter".?,
|
||||
"startFrom".as[String].?,
|
||||
"maxItems".as[Int].?(DEFAULT_MAX_ITEMS),
|
||||
"ignoreAccess".as[Boolean].?(false)
|
||||
) {
|
||||
(
|
||||
nameFilter: Option[String],
|
||||
startFrom: Option[String],
|
||||
maxItems: Int,
|
||||
ignoreAccess: Boolean
|
||||
) =>
|
||||
{
|
||||
handleRejections(invalidQueryHandler) {
|
||||
validate(
|
||||
0 < maxItems && maxItems <= MAX_ITEMS_LIMIT,
|
||||
s"maxItems was $maxItems, maxItems must be between 0 and $MAX_ITEMS_LIMIT"
|
||||
) {
|
||||
authenticateAndExecute(
|
||||
zoneService
|
||||
.listDeletedZones(_, nameFilter, startFrom, maxItems, ignoreAccess)
|
||||
) { result =>
|
||||
complete(StatusCodes.OK, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} ~
|
||||
path("zones" / "backendids") {
|
||||
(get & monitor("Endpoint.getBackendIds")) {
|
||||
authenticateAndExecute(_ => zoneService.getBackendIds()) { ids =>
|
||||
|
@ -26,12 +26,6 @@ def test_list_group_activity_start_from_success(group_activity_context, shared_z
|
||||
# we grab 3 items, which when sorted by most recent will give the 3 most recent items
|
||||
page_one = client.get_group_changes(created_group["id"], max_items=3, status=200)
|
||||
|
||||
# our start from will align with the created on the 3rd change in the list
|
||||
start_from_index = 2
|
||||
# using epoch time to start from a timestamp
|
||||
epoch = datetime.utcfromtimestamp(0)
|
||||
# start from a known good timestamp
|
||||
start_from = str(int((datetime.strptime(page_one["changes"][start_from_index]["created"], "%Y-%m-%dT%H:%M:%S.%fZ") - epoch).total_seconds() * 1000))
|
||||
# now, we say give me all changes since the start_from, which should yield 8-7-6-5-4
|
||||
result = client.get_group_changes(created_group["id"], start_from=page_one["nextId"], max_items=5, status=200)
|
||||
|
||||
@ -50,26 +44,19 @@ def test_list_group_activity_start_from_success(group_activity_context, shared_z
|
||||
assert_that(result["changes"][i]["oldGroup"], is_(updated_groups[expected_start - i - 1]))
|
||||
|
||||
|
||||
def test_list_group_activity_start_from_fake_time(group_activity_context, shared_zone_test_context):
|
||||
def test_list_group_activity_start_from_random_number(group_activity_context, shared_zone_test_context):
|
||||
"""
|
||||
Test that we can start from a fake time stamp
|
||||
Test that we can start from a random number, but it returns no changes
|
||||
"""
|
||||
client = shared_zone_test_context.ok_vinyldns_client
|
||||
created_group = group_activity_context["created_group"]
|
||||
updated_groups = group_activity_context["updated_groups"]
|
||||
start_from = "9999999999999" # start from a random timestamp far in the future
|
||||
|
||||
result = client.get_group_changes(created_group["id"], start_from=start_from, max_items=5, status=200)
|
||||
result = client.get_group_changes(created_group["id"], start_from=999, max_items=5, status=200)
|
||||
|
||||
# there are 10 updates, proceeded by 1 create
|
||||
assert_that(result["changes"], has_length(5))
|
||||
assert_that(result["changes"], has_length(0))
|
||||
assert_that(result["maxItems"], is_(5))
|
||||
assert_that(result["startFrom"], is_(start_from))
|
||||
assert_that(result["nextId"], is_not(none()))
|
||||
|
||||
for i in range(0, 5):
|
||||
assert_that(result["changes"][i]["newGroup"], is_(updated_groups[9 - i]))
|
||||
assert_that(result["changes"][i]["oldGroup"], is_(updated_groups[9 - i - 1]))
|
||||
|
||||
|
||||
def test_list_group_activity_max_item_success(group_activity_context, shared_zone_test_context):
|
||||
|
@ -1013,11 +1013,11 @@ class MembershipServiceSpec
|
||||
"return the group activity" in {
|
||||
val groupChangeRepoResponse = ListGroupChangesResults(
|
||||
listOfDummyGroupChanges.take(100),
|
||||
Some(listOfDummyGroupChanges(100).id)
|
||||
Some(listOfDummyGroupChanges.size)
|
||||
)
|
||||
doReturn(IO.pure(groupChangeRepoResponse))
|
||||
.when(mockGroupChangeRepo)
|
||||
.getGroupChanges(anyString, any[Option[String]], anyInt)
|
||||
.getGroupChanges(anyString, any[Option[Int]], anyInt)
|
||||
|
||||
doReturn(IO.pure(ListUsersResults(Seq(dummyUser), Some("1"))))
|
||||
.when(mockUserRepo)
|
||||
@ -1031,18 +1031,18 @@ class MembershipServiceSpec
|
||||
underTest.getGroupActivity(dummyGroup.id, None, 100, dummyAuth).value.unsafeRunSync().toOption.get
|
||||
result.changes should contain theSameElementsAs expected
|
||||
result.maxItems shouldBe 100
|
||||
result.nextId shouldBe Some(listOfDummyGroupChanges(100).id)
|
||||
result.nextId shouldBe Some(listOfDummyGroupChanges.size)
|
||||
result.startFrom shouldBe None
|
||||
}
|
||||
|
||||
"return group activity even if the user is not authorized" in {
|
||||
val groupChangeRepoResponse = ListGroupChangesResults(
|
||||
listOfDummyGroupChanges.take(100),
|
||||
Some(listOfDummyGroupChanges(100).id)
|
||||
Some(listOfDummyGroupChanges.size)
|
||||
)
|
||||
doReturn(IO.pure(groupChangeRepoResponse))
|
||||
.when(mockGroupChangeRepo)
|
||||
.getGroupChanges(anyString, any[Option[String]], anyInt)
|
||||
.getGroupChanges(anyString, any[Option[Int]], anyInt)
|
||||
|
||||
doReturn(IO.pure(ListUsersResults(Seq(dummyUser), Some("1"))))
|
||||
.when(mockUserRepo)
|
||||
@ -1056,7 +1056,7 @@ class MembershipServiceSpec
|
||||
underTest.getGroupActivity(dummyGroup.id, None, 100, okAuth).value.unsafeRunSync().toOption.get
|
||||
result.changes should contain theSameElementsAs expected
|
||||
result.maxItems shouldBe 100
|
||||
result.nextId shouldBe Some(listOfDummyGroupChanges(100).id)
|
||||
result.nextId shouldBe Some(listOfDummyGroupChanges.size)
|
||||
result.startFrom shouldBe None
|
||||
}
|
||||
}
|
||||
|
@ -57,6 +57,8 @@ class ZoneServiceSpec
|
||||
private val badConnection = ZoneConnection("bad", "bad", Encrypted("bad"), "bad")
|
||||
private val abcZoneSummary = ZoneSummaryInfo(abcZone, abcGroup.name, AccessLevel.Delete)
|
||||
private val xyzZoneSummary = ZoneSummaryInfo(xyzZone, xyzGroup.name, AccessLevel.NoAccess)
|
||||
private val abcDeletedZoneSummary = ZoneChangeDeletedInfo(abcDeletedZoneChange, abcGroup.name, okUser.userName, AccessLevel.Delete)
|
||||
private val xyzDeletedZoneSummary = ZoneChangeDeletedInfo(xyzDeletedZoneChange, xyzGroup.name, okUser.userName, AccessLevel.NoAccess)
|
||||
private val zoneIp4ZoneSummary = ZoneSummaryInfo(zoneIp4, abcGroup.name, AccessLevel.Delete)
|
||||
private val zoneIp6ZoneSummary = ZoneSummaryInfo(zoneIp6, abcGroup.name, AccessLevel.Delete)
|
||||
private val mockMembershipRepo = mock[MembershipRepository]
|
||||
@ -1084,6 +1086,195 @@ class ZoneServiceSpec
|
||||
}
|
||||
}
|
||||
|
||||
"ListDeletedZones" should {
|
||||
"not fail with no zones returned" in {
|
||||
|
||||
doReturn(IO.pure(ListDeletedZonesChangeResults(List())))
|
||||
.when(mockZoneChangeRepo)
|
||||
.listDeletedZones(abcAuth, None, None, 100, false)
|
||||
doReturn(IO.pure(Set(abcGroup))).when(mockGroupRepo).getGroups(any[Set[String]])
|
||||
doReturn(IO.pure(ListUsersResults(Seq(okUser), None)))
|
||||
.when(mockUserRepo)
|
||||
.getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
|
||||
|
||||
val result: ListDeletedZoneChangesResponse = underTest.listDeletedZones(abcAuth).value.unsafeRunSync().toOption.get
|
||||
result.zonesDeletedInfo shouldBe List()
|
||||
result.maxItems shouldBe 100
|
||||
result.startFrom shouldBe None
|
||||
result.zoneChangeFilter shouldBe None
|
||||
result.nextId shouldBe None
|
||||
result.ignoreAccess shouldBe false
|
||||
}
|
||||
|
||||
"return the appropriate zones" in {
|
||||
doReturn(IO.pure(ListDeletedZonesChangeResults(List(abcDeletedZoneChange))))
|
||||
.when(mockZoneChangeRepo)
|
||||
.listDeletedZones(abcAuth, None, None, 100, false)
|
||||
doReturn(IO.pure(Set(abcGroup)))
|
||||
.when(mockGroupRepo)
|
||||
.getGroups(any[Set[String]])
|
||||
doReturn(IO.pure(ListUsersResults(Seq(okUser), None)))
|
||||
.when(mockUserRepo)
|
||||
.getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
|
||||
|
||||
val result: ListDeletedZoneChangesResponse = underTest.listDeletedZones(abcAuth).value.unsafeRunSync().toOption.get
|
||||
|
||||
result.zonesDeletedInfo shouldBe List(abcDeletedZoneSummary)
|
||||
result.maxItems shouldBe 100
|
||||
result.startFrom shouldBe None
|
||||
result.zoneChangeFilter shouldBe None
|
||||
result.nextId shouldBe None
|
||||
result.ignoreAccess shouldBe false
|
||||
}
|
||||
|
||||
"return all zones" in {
|
||||
doReturn(IO.pure(ListDeletedZonesChangeResults(List(abcDeletedZoneChange, xyzDeletedZoneChange), ignoreAccess = true)))
|
||||
.when(mockZoneChangeRepo)
|
||||
.listDeletedZones(abcAuth, None, None, 100, true)
|
||||
doReturn(IO.pure(Set(abcGroup, xyzGroup)))
|
||||
.when(mockGroupRepo)
|
||||
.getGroups(any[Set[String]])
|
||||
doReturn(IO.pure(ListUsersResults(Seq(okUser), None)))
|
||||
.when(mockUserRepo)
|
||||
.getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
|
||||
|
||||
val result: ListDeletedZoneChangesResponse =
|
||||
underTest.listDeletedZones(abcAuth, ignoreAccess = true).value.unsafeRunSync().toOption.get
|
||||
result.zonesDeletedInfo shouldBe List(abcDeletedZoneSummary,xyzDeletedZoneSummary)
|
||||
result.maxItems shouldBe 100
|
||||
result.startFrom shouldBe None
|
||||
result.zoneChangeFilter shouldBe None
|
||||
result.nextId shouldBe None
|
||||
result.ignoreAccess shouldBe true
|
||||
}
|
||||
|
||||
"return Unknown group name if zone admin group cannot be found" in {
|
||||
doReturn(IO.pure(ListDeletedZonesChangeResults(List(abcDeletedZoneChange, xyzDeletedZoneChange))))
|
||||
.when(mockZoneChangeRepo)
|
||||
.listDeletedZones(abcAuth, None, None, 100, false)
|
||||
doReturn(IO.pure(Set(okGroup))).when(mockGroupRepo).getGroups(any[Set[String]])
|
||||
doReturn(IO.pure(ListUsersResults(Seq(okUser), None)))
|
||||
.when(mockUserRepo)
|
||||
.getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
|
||||
|
||||
val result: ListDeletedZoneChangesResponse = underTest.listDeletedZones(abcAuth).value.unsafeRunSync().toOption.get
|
||||
val expectedZones =
|
||||
List(abcDeletedZoneSummary, xyzDeletedZoneSummary).map(_.copy(adminGroupName = "Unknown group name"))
|
||||
result.zonesDeletedInfo shouldBe expectedZones
|
||||
result.maxItems shouldBe 100
|
||||
result.startFrom shouldBe None
|
||||
result.zoneChangeFilter shouldBe None
|
||||
result.nextId shouldBe None
|
||||
}
|
||||
|
||||
"set the nextId appropriately" in {
|
||||
doReturn(
|
||||
IO.pure(
|
||||
ListDeletedZonesChangeResults(
|
||||
List(abcDeletedZoneChange, xyzDeletedZoneChange),
|
||||
maxItems = 2,
|
||||
nextId = Some("zone2."),
|
||||
ignoreAccess = false
|
||||
)
|
||||
)
|
||||
).when(mockZoneChangeRepo)
|
||||
.listDeletedZones(abcAuth, None, None, 2, false)
|
||||
doReturn(IO.pure(Set(abcGroup, xyzGroup)))
|
||||
.when(mockGroupRepo)
|
||||
.getGroups(any[Set[String]])
|
||||
doReturn(IO.pure(ListUsersResults(Seq(okUser), None)))
|
||||
.when(mockUserRepo)
|
||||
.getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
|
||||
|
||||
val result: ListDeletedZoneChangesResponse =
|
||||
underTest.listDeletedZones(abcAuth, maxItems = 2).value.unsafeRunSync().toOption.get
|
||||
result.zonesDeletedInfo shouldBe List(abcDeletedZoneSummary, xyzDeletedZoneSummary)
|
||||
result.maxItems shouldBe 2
|
||||
result.startFrom shouldBe None
|
||||
result.zoneChangeFilter shouldBe None
|
||||
result.nextId shouldBe Some("zone2.")
|
||||
}
|
||||
|
||||
"set the nameFilter when provided" in {
|
||||
doReturn(
|
||||
IO.pure(
|
||||
ListDeletedZonesChangeResults(
|
||||
List(abcDeletedZoneChange, xyzDeletedZoneChange),
|
||||
zoneChangeFilter = Some("foo"),
|
||||
maxItems = 2,
|
||||
nextId = Some("zone2."),
|
||||
ignoreAccess = false
|
||||
)
|
||||
)
|
||||
).when(mockZoneChangeRepo)
|
||||
.listDeletedZones(abcAuth, Some("foo"), None, 2, false)
|
||||
doReturn(IO.pure(Set(abcGroup, xyzGroup)))
|
||||
.when(mockGroupRepo)
|
||||
.getGroups(any[Set[String]])
|
||||
doReturn(IO.pure(ListUsersResults(Seq(okUser), None)))
|
||||
.when(mockUserRepo)
|
||||
.getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
|
||||
|
||||
val result: ListDeletedZoneChangesResponse =
|
||||
underTest.listDeletedZones(abcAuth, nameFilter = Some("foo"), maxItems = 2).value.unsafeRunSync().toOption.get
|
||||
result.zonesDeletedInfo shouldBe List(abcDeletedZoneSummary, xyzDeletedZoneSummary)
|
||||
result.zoneChangeFilter shouldBe Some("foo")
|
||||
result.nextId shouldBe Some("zone2.")
|
||||
result.maxItems shouldBe 2
|
||||
}
|
||||
|
||||
"set the startFrom when provided" in {
|
||||
doReturn(
|
||||
IO.pure(
|
||||
ListDeletedZonesChangeResults(
|
||||
List(abcDeletedZoneChange, xyzDeletedZoneChange),
|
||||
startFrom = Some("zone4."),
|
||||
maxItems = 2,
|
||||
ignoreAccess = false
|
||||
)
|
||||
)
|
||||
).when(mockZoneChangeRepo)
|
||||
.listDeletedZones(abcAuth, None, Some("zone4."), 2, false)
|
||||
doReturn(IO.pure(Set(abcGroup, xyzGroup)))
|
||||
.when(mockGroupRepo)
|
||||
.getGroups(any[Set[String]])
|
||||
doReturn(IO.pure(ListUsersResults(Seq(okUser), None)))
|
||||
.when(mockUserRepo)
|
||||
.getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
|
||||
|
||||
val result: ListDeletedZoneChangesResponse =
|
||||
underTest.listDeletedZones(abcAuth, startFrom = Some("zone4."), maxItems = 2).value.unsafeRunSync().toOption.get
|
||||
result.zonesDeletedInfo shouldBe List(abcDeletedZoneSummary, xyzDeletedZoneSummary)
|
||||
result.startFrom shouldBe Some("zone4.")
|
||||
}
|
||||
|
||||
"set the nextId to be the current result set size plus the start from" in {
|
||||
doReturn(
|
||||
IO.pure(
|
||||
ListDeletedZonesChangeResults(
|
||||
List(abcDeletedZoneChange, xyzDeletedZoneChange),
|
||||
startFrom = Some("zone4."),
|
||||
maxItems = 2,
|
||||
nextId = Some("zone6."),
|
||||
ignoreAccess = false
|
||||
)
|
||||
)
|
||||
).when(mockZoneChangeRepo)
|
||||
.listDeletedZones(abcAuth, None, Some("zone4."), 2, false)
|
||||
doReturn(IO.pure(Set(abcGroup, xyzGroup)))
|
||||
.when(mockGroupRepo)
|
||||
.getGroups(any[Set[String]])
|
||||
doReturn(IO.pure(ListUsersResults(Seq(okUser), None)))
|
||||
.when(mockUserRepo)
|
||||
.getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
|
||||
|
||||
val result: ListDeletedZoneChangesResponse =
|
||||
underTest.listDeletedZones(abcAuth, startFrom = Some("zone4."), maxItems = 2).value.unsafeRunSync().toOption.get
|
||||
result.zonesDeletedInfo shouldBe List(abcDeletedZoneSummary, xyzDeletedZoneSummary)
|
||||
result.nextId shouldBe Some("zone6.")
|
||||
}
|
||||
}
|
||||
|
||||
"listZoneChanges" should {
|
||||
"retrieve the zone changes" in {
|
||||
doReturn(IO.pure(Some(okZone)))
|
||||
|
@ -705,14 +705,14 @@ class MembershipRoutingSpec
|
||||
)
|
||||
doReturn(result(expected))
|
||||
.when(membershipService)
|
||||
.getGroupActivity(anyString, any[Option[String]], anyInt, any[AuthPrincipal])
|
||||
.getGroupActivity(anyString, any[Option[Int]], anyInt, any[AuthPrincipal])
|
||||
|
||||
Get(s"/groups/pageSize/activity") ~> Route.seal(membershipRoute) ~> check {
|
||||
status shouldBe StatusCodes.OK
|
||||
val maxItemsCaptor = ArgumentCaptor.forClass(classOf[Int])
|
||||
verify(membershipService).getGroupActivity(
|
||||
anyString,
|
||||
any[Option[String]],
|
||||
any[Option[Int]],
|
||||
maxItemsCaptor.capture(),
|
||||
any[AuthPrincipal]
|
||||
)
|
||||
|
@ -117,6 +117,26 @@ class ZoneRoutingSpec
|
||||
Zone("zone6.in-addr.arpa.", "zone6@test.com", ZoneStatus.Active, adminGroupId = xyzGroup.id)
|
||||
private val zoneSummaryInfo6 = ZoneSummaryInfo(zone6, xyzGroup.name, AccessLevel.NoAccess)
|
||||
private val error = Zone("error.", "error@test.com")
|
||||
private val deletedZone1 = Zone("ok1.", "ok1@test.com", ZoneStatus.Deleted , acl = zoneAcl)
|
||||
private val deletedZoneChange1 = ZoneChange(deletedZone1, "ok1", ZoneChangeType.Create, ZoneChangeStatus.Synced)
|
||||
private val ZoneChangeDeletedInfo1 = ZoneChangeDeletedInfo(
|
||||
deletedZoneChange1, okGroup.name, okUser.userName, AccessLevel.NoAccess)
|
||||
private val deletedZone2 = Zone("ok2.", "ok2@test.com", ZoneStatus.Deleted , acl = zoneAcl)
|
||||
private val deletedZoneChange2 = ZoneChange(deletedZone2, "ok2", ZoneChangeType.Create, ZoneChangeStatus.Synced)
|
||||
private val ZoneChangeDeletedInfo2 = ZoneChangeDeletedInfo(
|
||||
deletedZoneChange2, okGroup.name, okUser.userName, AccessLevel.NoAccess)
|
||||
private val deletedZone3 = Zone("ok3.", "ok3@test.com", ZoneStatus.Deleted , acl = zoneAcl)
|
||||
private val deletedZoneChange3 = ZoneChange(deletedZone3, "ok3", ZoneChangeType.Create, ZoneChangeStatus.Synced)
|
||||
private val ZoneChangeDeletedInfo3= ZoneChangeDeletedInfo(
|
||||
deletedZoneChange3, okGroup.name, okUser.userName, AccessLevel.NoAccess)
|
||||
private val deletedZone4 = Zone("ok4.", "ok4@test.com", ZoneStatus.Deleted , acl = zoneAcl, adminGroupId = xyzGroup.id)
|
||||
private val deletedZoneChange4 = ZoneChange(deletedZone4, "ok4", ZoneChangeType.Create, ZoneChangeStatus.Synced)
|
||||
private val ZoneChangeDeletedInfo4 = ZoneChangeDeletedInfo(
|
||||
deletedZoneChange4, okGroup.name, okUser.userName, AccessLevel.NoAccess)
|
||||
private val deletedZone5 = Zone("ok5.", "ok5@test.com", ZoneStatus.Deleted , acl = zoneAcl, adminGroupId = xyzGroup.id)
|
||||
private val deletedZoneChange5 = ZoneChange(deletedZone5, "ok5", ZoneChangeType.Create, ZoneChangeStatus.Synced)
|
||||
private val ZoneChangeDeletedInfo5 = ZoneChangeDeletedInfo(
|
||||
deletedZoneChange5, okGroup.name, okUser.userName, AccessLevel.NoAccess)
|
||||
|
||||
private val missingFields: JValue =
|
||||
("invalidField" -> "randomValue") ~~
|
||||
@ -395,6 +415,92 @@ class ZoneRoutingSpec
|
||||
outcome.toResult
|
||||
}
|
||||
|
||||
def listDeletedZones(
|
||||
authPrincipal: AuthPrincipal,
|
||||
nameFilter: Option[String],
|
||||
startFrom: Option[String],
|
||||
maxItems: Int,
|
||||
ignoreAccess: Boolean = false
|
||||
): Result[ListDeletedZoneChangesResponse] = {
|
||||
|
||||
val outcome = (authPrincipal, nameFilter, startFrom, maxItems, ignoreAccess) match {
|
||||
case (_, None, Some("zone3."), 3, false) =>
|
||||
Right(
|
||||
ListDeletedZoneChangesResponse(
|
||||
zonesDeletedInfo = List(ZoneChangeDeletedInfo1,ZoneChangeDeletedInfo2,ZoneChangeDeletedInfo3),
|
||||
zoneChangeFilter = None,
|
||||
startFrom = Some("zone3."),
|
||||
nextId = Some("zone6."),
|
||||
maxItems = 3,
|
||||
ignoreAccess = false
|
||||
)
|
||||
)
|
||||
case (_, None, Some("zone4."), 4, false) =>
|
||||
Right(
|
||||
ListDeletedZoneChangesResponse(
|
||||
zonesDeletedInfo = List(ZoneChangeDeletedInfo1,ZoneChangeDeletedInfo2,ZoneChangeDeletedInfo3),
|
||||
zoneChangeFilter = None,
|
||||
startFrom = Some("zone4."),
|
||||
nextId = None,
|
||||
maxItems = 4,
|
||||
ignoreAccess = false
|
||||
)
|
||||
)
|
||||
|
||||
case (_, None, None, 3, false) =>
|
||||
Right(
|
||||
ListDeletedZoneChangesResponse(
|
||||
zonesDeletedInfo = List(ZoneChangeDeletedInfo1,ZoneChangeDeletedInfo2,ZoneChangeDeletedInfo3),
|
||||
zoneChangeFilter = None,
|
||||
startFrom = None,
|
||||
nextId = Some("zone3."),
|
||||
maxItems = 3,
|
||||
ignoreAccess = false
|
||||
)
|
||||
)
|
||||
|
||||
case (_, None, None, 5, true) =>
|
||||
Right(
|
||||
ListDeletedZoneChangesResponse(
|
||||
zonesDeletedInfo =
|
||||
List(ZoneChangeDeletedInfo1,ZoneChangeDeletedInfo2,ZoneChangeDeletedInfo3, ZoneChangeDeletedInfo4,ZoneChangeDeletedInfo5),
|
||||
zoneChangeFilter = None,
|
||||
startFrom = None,
|
||||
nextId = None,
|
||||
maxItems = 5,
|
||||
ignoreAccess = true
|
||||
)
|
||||
)
|
||||
|
||||
case (_, Some(filter), Some("zone4."), 4, false) =>
|
||||
Right(
|
||||
ListDeletedZoneChangesResponse(
|
||||
zonesDeletedInfo = List(ZoneChangeDeletedInfo1,ZoneChangeDeletedInfo2,ZoneChangeDeletedInfo3),
|
||||
zoneChangeFilter = Some(filter),
|
||||
startFrom = Some("zone4."),
|
||||
nextId = None,
|
||||
maxItems = 4,
|
||||
ignoreAccess = false
|
||||
)
|
||||
)
|
||||
|
||||
case (_, None, None, _, _) =>
|
||||
Right(
|
||||
ListDeletedZoneChangesResponse(
|
||||
zonesDeletedInfo = List(ZoneChangeDeletedInfo1,ZoneChangeDeletedInfo2,ZoneChangeDeletedInfo3),
|
||||
zoneChangeFilter = None,
|
||||
startFrom = None,
|
||||
nextId = None,
|
||||
ignoreAccess = false
|
||||
)
|
||||
)
|
||||
|
||||
case _ => Left(InvalidRequest("shouldnt get here"))
|
||||
}
|
||||
|
||||
outcome.toResult
|
||||
}
|
||||
|
||||
def listZoneChanges(
|
||||
zoneId: String,
|
||||
authPrincipal: AuthPrincipal,
|
||||
@ -1078,6 +1184,83 @@ class ZoneRoutingSpec
|
||||
}
|
||||
}
|
||||
|
||||
"GET Deleted zones" should {
|
||||
"return the next id when more results exist" in {
|
||||
Get(s"/zones/deleted/changes?startFrom=zone3.&maxItems=3") ~> zoneRoute ~> check {
|
||||
val resp = responseAs[ListDeletedZoneChangesResponse]
|
||||
val deletedZones = resp.zonesDeletedInfo
|
||||
(deletedZones.map(_.zoneChange.zone.id) should contain)
|
||||
.only(deletedZone1.id, deletedZone2.id, deletedZone3.id)
|
||||
resp.nextId shouldBe Some("zone6.")
|
||||
resp.maxItems shouldBe 3
|
||||
resp.startFrom shouldBe Some("zone3.")
|
||||
}
|
||||
}
|
||||
|
||||
"not return the next id when there are no more results" in {
|
||||
Get(s"/zones/deleted/changes?startFrom=zone4.&maxItems=4") ~> zoneRoute ~> check {
|
||||
val resp = responseAs[ListDeletedZoneChangesResponse]
|
||||
val deletedZones = resp.zonesDeletedInfo
|
||||
(deletedZones.map(_.zoneChange.zone.id) should contain)
|
||||
.only(deletedZone1.id, deletedZone2.id, deletedZone3.id)
|
||||
resp.nextId shouldBe None
|
||||
resp.maxItems shouldBe 4
|
||||
resp.startFrom shouldBe Some("zone4.")
|
||||
resp.ignoreAccess shouldBe false
|
||||
}
|
||||
}
|
||||
|
||||
"not return the start from when not provided" in {
|
||||
Get(s"/zones/deleted/changes?maxItems=3") ~> zoneRoute ~> check {
|
||||
val resp = responseAs[ListDeletedZoneChangesResponse]
|
||||
val deletedZones = resp.zonesDeletedInfo
|
||||
(deletedZones.map(_.zoneChange.zone.id) should contain)
|
||||
.only(deletedZone1.id, deletedZone2.id, deletedZone3.id)
|
||||
resp.nextId shouldBe Some("zone3.")
|
||||
resp.maxItems shouldBe 3
|
||||
resp.startFrom shouldBe None
|
||||
resp.ignoreAccess shouldBe false
|
||||
}
|
||||
}
|
||||
|
||||
"return the name filter when provided" in {
|
||||
Get(s"/zones/deleted/changes?nameFilter=foo&startFrom=zone4.&maxItems=4") ~> zoneRoute ~> check {
|
||||
val resp = responseAs[ListDeletedZoneChangesResponse]
|
||||
val deletedZones = resp.zonesDeletedInfo
|
||||
(deletedZones.map(_.zoneChange.zone.id) should contain)
|
||||
.only(deletedZone1.id, deletedZone2.id, deletedZone3.id)
|
||||
resp.nextId shouldBe None
|
||||
resp.maxItems shouldBe 4
|
||||
resp.startFrom shouldBe Some("zone4.")
|
||||
resp.zoneChangeFilter shouldBe Some("foo")
|
||||
resp.ignoreAccess shouldBe false
|
||||
}
|
||||
}
|
||||
|
||||
"return all zones when list all is true" in {
|
||||
Get(s"/zones/deleted/changes?maxItems=5&ignoreAccess=true") ~> zoneRoute ~> check {
|
||||
val resp = responseAs[ListDeletedZoneChangesResponse]
|
||||
val deletedZones = resp.zonesDeletedInfo
|
||||
(deletedZones.map(_.zoneChange.zone.id) should contain)
|
||||
.only(deletedZone1.id, deletedZone2.id, deletedZone3.id, deletedZone4.id, deletedZone5.id)
|
||||
resp.nextId shouldBe None
|
||||
resp.maxItems shouldBe 5
|
||||
resp.startFrom shouldBe None
|
||||
resp.zoneChangeFilter shouldBe None
|
||||
resp.ignoreAccess shouldBe true
|
||||
}
|
||||
}
|
||||
|
||||
"return an error if the max items is out of range" in {
|
||||
Get(s"/zones/deleted/changes?maxItems=700") ~> zoneRoute ~> check {
|
||||
status shouldBe BadRequest
|
||||
responseEntity.toString should include(
|
||||
"maxItems was 700, maxItems must be between 0 and 100"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"GET zone changes" should {
|
||||
"return the zone changes" in {
|
||||
Get(s"/zones/${ok.id}/changes") ~> zoneRoute ~> check {
|
||||
|
@ -28,7 +28,7 @@ trait GroupChangeRepository extends Repository {
|
||||
|
||||
def getGroupChanges(
|
||||
groupId: String,
|
||||
startFrom: Option[String],
|
||||
startFrom: Option[Int],
|
||||
maxItems: Int
|
||||
): IO[ListGroupChangesResults]
|
||||
|
||||
|
@ -18,5 +18,5 @@ package vinyldns.core.domain.membership
|
||||
|
||||
final case class ListGroupChangesResults(
|
||||
changes: Seq[GroupChange],
|
||||
lastEvaluatedTimeStamp: Option[String]
|
||||
nextId: Option[Int]
|
||||
)
|
||||
|
@ -23,6 +23,15 @@ case class ListZoneChangesResults(
|
||||
maxItems: Int = 100
|
||||
)
|
||||
|
||||
case class ListDeletedZonesChangeResults(
|
||||
zoneDeleted: List[ZoneChange] = List[ZoneChange](),
|
||||
nextId: Option[String] = None,
|
||||
startFrom: Option[String] = None,
|
||||
maxItems: Int = 100,
|
||||
ignoreAccess: Boolean = false,
|
||||
zoneChangeFilter: Option[String] = None
|
||||
)
|
||||
|
||||
case class ListFailedZoneChangesResults(
|
||||
items: List[ZoneChange] = List[ZoneChange](),
|
||||
nextId: Int = 0,
|
||||
|
@ -17,6 +17,7 @@
|
||||
package vinyldns.core.domain.zone
|
||||
|
||||
import cats.effect._
|
||||
import vinyldns.core.domain.auth.AuthPrincipal
|
||||
import vinyldns.core.repository.Repository
|
||||
|
||||
trait ZoneChangeRepository extends Repository {
|
||||
@ -29,6 +30,14 @@ trait ZoneChangeRepository extends Repository {
|
||||
maxItems: Int = 100
|
||||
): IO[ListZoneChangesResults]
|
||||
|
||||
def listDeletedZones(
|
||||
authPrincipal: AuthPrincipal,
|
||||
zoneNameFilter: Option[String] = None,
|
||||
startFrom: Option[String] = None,
|
||||
maxItems: Int = 100,
|
||||
ignoreAccess: Boolean = false
|
||||
): IO[ListDeletedZonesChangeResults]
|
||||
|
||||
def listFailedZoneChanges(
|
||||
maxItems: Int = 100,
|
||||
startFrom: Int= 0
|
||||
|
@ -52,9 +52,21 @@ object TestZoneData {
|
||||
connection = testConnection
|
||||
)
|
||||
|
||||
val abcZoneDeleted: Zone = Zone("abc.zone.recordsets.", "test@test.com", adminGroupId = abcGroup.id, status = ZoneStatus.Deleted)
|
||||
val xyzZoneDeleted: Zone = Zone("xyz.zone.recordsets.", "abc@xyz.com", adminGroupId = xyzGroup.id, status = ZoneStatus.Deleted)
|
||||
|
||||
val zoneDeleted: Zone = Zone(
|
||||
"some.deleted.zone.",
|
||||
"test@test.com",
|
||||
adminGroupId = abcGroup.id,
|
||||
status = ZoneStatus.Deleted,
|
||||
connection = testConnection
|
||||
)
|
||||
|
||||
val zoneDeletedOkGroup: Zone = Zone(
|
||||
"some.deleted.zone.",
|
||||
"test@test.com",
|
||||
adminGroupId = okGroup.id,
|
||||
status = ZoneStatus.Deleted,
|
||||
connection = testConnection
|
||||
)
|
||||
@ -89,6 +101,22 @@ object TestZoneData {
|
||||
|
||||
val zoneUpdate: ZoneChange = zoneChangePending.copy(status = ZoneChangeStatus.Synced)
|
||||
|
||||
val abcDeletedZoneChange: ZoneChange = ZoneChange(
|
||||
abcZoneDeleted,
|
||||
"ok",
|
||||
ZoneChangeType.Create,
|
||||
ZoneChangeStatus.Synced,
|
||||
created = Instant.now.truncatedTo(ChronoUnit.MILLIS).minusMillis(1000)
|
||||
)
|
||||
|
||||
val xyzDeletedZoneChange: ZoneChange = ZoneChange(
|
||||
xyzZoneDeleted,
|
||||
"ok",
|
||||
ZoneChangeType.Create,
|
||||
ZoneChangeStatus.Synced,
|
||||
created = Instant.now.truncatedTo(ChronoUnit.MILLIS).minusMillis(1000)
|
||||
)
|
||||
|
||||
def makeTestPendingZoneChange(zone: Zone): ZoneChange =
|
||||
ZoneChange(zone, "userId", ZoneChangeType.Update, ZoneChangeStatus.Pending)
|
||||
|
||||
|
@ -102,7 +102,7 @@ class MySqlGroupChangeRepositoryIntegrationSpec
|
||||
}
|
||||
|
||||
"MySqlGroupChangeRepository.getGroupChanges" should {
|
||||
"don't return lastEvaluatedTimeStamp if page size < maxItems" in {
|
||||
"don't return nextId if page size < maxItems" in {
|
||||
val groupId = "group-id-1"
|
||||
val changes = generateGroupChanges(groupId, 50)
|
||||
changes.map(saveGroupChangeData(repo, _).unsafeRunSync())
|
||||
@ -113,7 +113,7 @@ class MySqlGroupChangeRepositoryIntegrationSpec
|
||||
|
||||
val listResponse = repo.getGroupChanges(groupId, None, 100).unsafeRunSync()
|
||||
listResponse.changes shouldBe expectedChanges
|
||||
listResponse.lastEvaluatedTimeStamp shouldBe None
|
||||
listResponse.nextId shouldBe None
|
||||
}
|
||||
|
||||
"get group changes properly using a maxItems of 1" in {
|
||||
@ -130,9 +130,7 @@ class MySqlGroupChangeRepositoryIntegrationSpec
|
||||
val listResponse =
|
||||
repo.getGroupChanges(groupId, startFrom = None, maxItems = 1).unsafeRunSync()
|
||||
listResponse.changes shouldBe expectedChanges
|
||||
listResponse.lastEvaluatedTimeStamp shouldBe Some(
|
||||
expectedChanges.head.created.toEpochMilli.toString
|
||||
)
|
||||
listResponse.nextId shouldBe Some(1)
|
||||
}
|
||||
|
||||
"page group changes using a startFrom and maxItems" in {
|
||||
@ -145,33 +143,33 @@ class MySqlGroupChangeRepositoryIntegrationSpec
|
||||
.reverse
|
||||
|
||||
val expectedPageOne = Seq(changesSorted(0))
|
||||
val expectedPageOneNext = Some(changesSorted(0).created.toEpochMilli.toString)
|
||||
val expectedPageOneNext = Some(1)
|
||||
val expectedPageTwo = Seq(changesSorted(1))
|
||||
val expectedPageTwoNext = Some(changesSorted(1).created.toEpochMilli.toString)
|
||||
val expectedPageTwoNext = Some(2)
|
||||
val expectedPageThree = Seq(changesSorted(2))
|
||||
val expectedPageThreeNext = Some(changesSorted(2).created.toEpochMilli.toString)
|
||||
val expectedPageThreeNext = Some(3)
|
||||
|
||||
// get first page
|
||||
val pageOne =
|
||||
repo.getGroupChanges(groupId, startFrom = None, maxItems = 1).unsafeRunSync()
|
||||
pageOne.changes shouldBe expectedPageOne
|
||||
pageOne.lastEvaluatedTimeStamp shouldBe expectedPageOneNext
|
||||
pageOne.nextId shouldBe expectedPageOneNext
|
||||
|
||||
// get second page
|
||||
val pageTwo =
|
||||
repo
|
||||
.getGroupChanges(groupId, startFrom = pageOne.lastEvaluatedTimeStamp, maxItems = 1)
|
||||
.getGroupChanges(groupId, startFrom = pageOne.nextId, maxItems = 1)
|
||||
.unsafeRunSync()
|
||||
pageTwo.changes shouldBe expectedPageTwo
|
||||
pageTwo.lastEvaluatedTimeStamp shouldBe expectedPageTwoNext
|
||||
pageTwo.nextId shouldBe expectedPageTwoNext
|
||||
|
||||
// get final page
|
||||
val pageThree =
|
||||
repo
|
||||
.getGroupChanges(groupId, startFrom = pageTwo.lastEvaluatedTimeStamp, maxItems = 1)
|
||||
.getGroupChanges(groupId, startFrom = pageTwo.nextId, maxItems = 1)
|
||||
.unsafeRunSync()
|
||||
pageThree.changes shouldBe expectedPageThree
|
||||
pageThree.lastEvaluatedTimeStamp shouldBe expectedPageThreeNext
|
||||
pageThree.nextId shouldBe expectedPageThreeNext
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,6 +33,9 @@ import vinyldns.core.TestZoneData.okZone
|
||||
import vinyldns.core.TestZoneData.testConnection
|
||||
import vinyldns.core.domain.Encrypted
|
||||
import vinyldns.mysql.TestMySqlInstance
|
||||
import vinyldns.core.TestZoneData.{okZone, testConnection}
|
||||
import vinyldns.core.domain.auth.AuthPrincipal
|
||||
import vinyldns.core.TestMembershipData.{dummyAuth, dummyGroup, okGroup, okUser}
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.Random
|
||||
@ -92,6 +95,7 @@ class MySqlZoneChangeRepositoryIntegrationSpec
|
||||
status= ZoneChangeStatus.Failed,
|
||||
created = Instant.now.truncatedTo(ChronoUnit.MILLIS).minusSeconds(Random.nextInt(1000))
|
||||
)
|
||||
|
||||
val successChanges
|
||||
: IndexedSeq[ZoneChange] = for { zone <- zones } yield ZoneChange(
|
||||
zone,
|
||||
@ -100,6 +104,84 @@ class MySqlZoneChangeRepositoryIntegrationSpec
|
||||
status= ZoneChangeStatus.Synced,
|
||||
created = Instant.now.truncatedTo(ChronoUnit.MILLIS).minusSeconds(Random.nextInt(1000))
|
||||
)
|
||||
|
||||
val groups = (11 until 20)
|
||||
.map(num => okGroup.copy(name = num.toString, id = UUID.randomUUID().toString))
|
||||
.toList
|
||||
|
||||
// generate some ACLs
|
||||
private val groupAclRules = groups.map(
|
||||
g =>
|
||||
ACLRule(
|
||||
accessLevel = AccessLevel.Read,
|
||||
groupId = Some(g.id)
|
||||
)
|
||||
)
|
||||
|
||||
private val userOnlyAclRule =
|
||||
ACLRule(
|
||||
accessLevel = AccessLevel.Read,
|
||||
userId = Some(okUser.id)
|
||||
)
|
||||
|
||||
// the zone acl rule will have the user rule and all of the group rules
|
||||
private val testZoneAcl = ZoneACL(
|
||||
rules = Set(userOnlyAclRule) ++ groupAclRules
|
||||
)
|
||||
|
||||
private val testZoneAdminGroupId = "foo"
|
||||
|
||||
val dummyAclRule =
|
||||
ACLRule(
|
||||
accessLevel = AccessLevel.Read,
|
||||
groupId = Some(dummyGroup.id)
|
||||
)
|
||||
|
||||
val testZone = (11 until 20).map { num =>
|
||||
val z =
|
||||
okZone.copy(
|
||||
name = num.toString + ".",
|
||||
id = UUID.randomUUID().toString,
|
||||
adminGroupId = testZoneAdminGroupId,
|
||||
acl = testZoneAcl
|
||||
)
|
||||
// add the dummy acl rule to the first zone
|
||||
if (num == 11) z.addACLRule(dummyAclRule) else z
|
||||
}
|
||||
|
||||
val deletedZoneChanges
|
||||
: IndexedSeq[ZoneChange] = for { testZone <- testZone } yield {
|
||||
ZoneChange(
|
||||
testZone.copy(status = ZoneStatus.Deleted),
|
||||
testZone.account,
|
||||
ZoneChangeType.Create,
|
||||
ZoneChangeStatus.Synced,
|
||||
created = Instant.now.truncatedTo(ChronoUnit.MILLIS).minusMillis(1000)
|
||||
)}
|
||||
|
||||
def saveZones(zones: Seq[Zone]): IO[Unit] =
|
||||
zones.foldLeft(IO.unit) {
|
||||
case (acc, z) =>
|
||||
acc.flatMap { _ =>
|
||||
zoneRepo.save(z).map(_ => ())
|
||||
}
|
||||
}
|
||||
|
||||
def deleteZones(zones: Seq[Zone]): IO[Unit] =
|
||||
zones.foldLeft(IO.unit) {
|
||||
case (acc, z) =>
|
||||
acc.flatMap { _ =>
|
||||
zoneRepo.deleteTx(z).map(_ => ())
|
||||
}
|
||||
}
|
||||
|
||||
def saveZoneChanges(zoneChanges: Seq[ZoneChange]): IO[Unit] =
|
||||
zoneChanges.foldLeft(IO.unit) {
|
||||
case (acc, zc) =>
|
||||
acc.flatMap { _ =>
|
||||
repo.save(zc).map(_ => ())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
import TestData._
|
||||
@ -303,5 +385,145 @@ class MySqlZoneChangeRepositoryIntegrationSpec
|
||||
pageThree.nextId should equal(None)
|
||||
pageThree.startFrom should equal(pageTwo.nextId)
|
||||
}
|
||||
"get authorized zones" in {
|
||||
// store all of the zones
|
||||
saveZones(testZone).unsafeRunSync()
|
||||
// delete all stored zones
|
||||
deleteZones(testZone).unsafeRunSync()
|
||||
// save the change
|
||||
saveZoneChanges(deletedZoneChanges).unsafeRunSync()
|
||||
|
||||
// query for all zones for the ok user, he should have access to all of the zones
|
||||
val okUserAuth = AuthPrincipal(
|
||||
signedInUser = okUser,
|
||||
memberGroupIds = groups.map(_.id)
|
||||
)
|
||||
|
||||
repo.listDeletedZones(okUserAuth).unsafeRunSync().zoneDeleted should contain theSameElementsAs deletedZoneChanges
|
||||
|
||||
// dummy user only has access to one zone
|
||||
(repo.listDeletedZones(dummyAuth).unsafeRunSync().zoneDeleted should contain).only(deletedZoneChanges.head)
|
||||
}
|
||||
|
||||
"page deleted zones using a startFrom and maxItems" in {
|
||||
// store all of the zones
|
||||
saveZones(testZone).unsafeRunSync()
|
||||
// delete all stored zones
|
||||
deleteZones(testZone).unsafeRunSync()
|
||||
// save the change
|
||||
saveZoneChanges(deletedZoneChanges).unsafeRunSync()
|
||||
|
||||
// query for all zones for the ok user, he should have access to all of the zones
|
||||
val okUserAuth = AuthPrincipal(
|
||||
signedInUser = okUser,
|
||||
memberGroupIds = groups.map(_.id)
|
||||
)
|
||||
|
||||
val listDeletedZones = repo.listDeletedZones(okUserAuth).unsafeRunSync()
|
||||
|
||||
val expectedPageOne = List(listDeletedZones.zoneDeleted(0))
|
||||
val expectedPageOneNext = Some(listDeletedZones.zoneDeleted(1).zone.id)
|
||||
val expectedPageTwo = List(listDeletedZones.zoneDeleted(1))
|
||||
val expectedPageTwoNext = Some(listDeletedZones.zoneDeleted(2).zone.id)
|
||||
val expectedPageThree = List(listDeletedZones.zoneDeleted(2))
|
||||
val expectedPageThreeNext = Some(listDeletedZones.zoneDeleted(3).zone.id)
|
||||
|
||||
// get first page
|
||||
val pageOne = repo.listDeletedZones(okUserAuth,startFrom = None, maxItems = 1 ).unsafeRunSync()
|
||||
pageOne.zoneDeleted.size should equal(1)
|
||||
pageOne.zoneDeleted should equal(expectedPageOne)
|
||||
pageOne.nextId should equal(expectedPageOneNext)
|
||||
pageOne.startFrom should equal(None)
|
||||
|
||||
// get second page
|
||||
val pageTwo =
|
||||
repo.listDeletedZones(okUserAuth, startFrom = pageOne.nextId, maxItems = 1).unsafeRunSync()
|
||||
pageTwo.zoneDeleted.size should equal(1)
|
||||
pageTwo.zoneDeleted should equal(expectedPageTwo)
|
||||
pageTwo.nextId should equal(expectedPageTwoNext)
|
||||
pageTwo.startFrom should equal(pageOne.nextId)
|
||||
|
||||
// get final page
|
||||
// next id should be none now
|
||||
val pageThree =
|
||||
repo.listDeletedZones(okUserAuth, startFrom = pageTwo.nextId, maxItems = 1).unsafeRunSync()
|
||||
pageThree.zoneDeleted.size should equal(1)
|
||||
pageThree.zoneDeleted should equal(expectedPageThree)
|
||||
pageThree.nextId should equal(expectedPageThreeNext)
|
||||
pageThree.startFrom should equal(pageTwo.nextId)
|
||||
}
|
||||
|
||||
"return empty in deleted zone if zone is created again" in {
|
||||
// store all of the zones
|
||||
saveZones(testZone).unsafeRunSync()
|
||||
// save the change
|
||||
saveZoneChanges(deletedZoneChanges).unsafeRunSync()
|
||||
|
||||
// query for all zones for the ok user, he should have access to all of the zones
|
||||
val okUserAuth = AuthPrincipal(
|
||||
signedInUser = okUser,
|
||||
memberGroupIds = groups.map(_.id)
|
||||
)
|
||||
|
||||
repo.listDeletedZones(okUserAuth).unsafeRunSync().zoneDeleted should contain theSameElementsAs List()
|
||||
|
||||
// delete all stored zones
|
||||
deleteZones(testZone).unsafeRunSync()
|
||||
|
||||
}
|
||||
|
||||
"return an empty list of zones if the user is not authorized to any" in {
|
||||
val unauthorized = AuthPrincipal(
|
||||
signedInUser = User("not-authorized", "not-authorized", Encrypted("not-authorized")),
|
||||
memberGroupIds = Seq.empty
|
||||
)
|
||||
|
||||
val f =
|
||||
for {
|
||||
_ <- saveZones(testZone)
|
||||
_ <- deleteZones(testZone)
|
||||
_ <- saveZoneChanges(deletedZoneChanges)
|
||||
zones <- repo.listDeletedZones(unauthorized)
|
||||
} yield zones
|
||||
|
||||
f.unsafeRunSync().zoneDeleted shouldBe empty
|
||||
deleteZones(testZone).unsafeRunSync()
|
||||
|
||||
}
|
||||
|
||||
"not return zones when access is revoked" in {
|
||||
// ok user can access both zones, dummy can only access first zone
|
||||
val zones = testZone.take(2)
|
||||
val zoneChange = deletedZoneChanges.take(2)
|
||||
val addACL = saveZones(zones)
|
||||
val deleteZone= deleteZones(zones)
|
||||
val addACLZc = saveZoneChanges(zoneChange)
|
||||
|
||||
|
||||
val okUserAuth = AuthPrincipal(
|
||||
signedInUser = okUser,
|
||||
memberGroupIds = groups.map(_.id)
|
||||
)
|
||||
addACL.unsafeRunSync()
|
||||
deleteZone.unsafeRunSync()
|
||||
addACLZc.unsafeRunSync()
|
||||
|
||||
(repo.listDeletedZones(okUserAuth).unsafeRunSync().zoneDeleted should contain). allElementsOf(zoneChange)
|
||||
|
||||
// dummy user only has access to first zone
|
||||
(repo.listDeletedZones(dummyAuth).unsafeRunSync().zoneDeleted should contain).only(zoneChange.head)
|
||||
|
||||
// revoke the access for the dummy user
|
||||
val revoked = zones(0).deleteACLRule(dummyAclRule)
|
||||
val revokedZc = zoneChange(0).copy(zone=revoked)
|
||||
zoneRepo.save(revoked).unsafeRunSync()
|
||||
repo.save(revokedZc).unsafeRunSync()
|
||||
|
||||
// ok user can still access zones
|
||||
(repo.listDeletedZones(okUserAuth).unsafeRunSync().zoneDeleted should contain).allElementsOf(Seq( zoneChange(1)))
|
||||
|
||||
// dummy user can not access the revoked zone
|
||||
repo.listDeletedZones(dummyAuth).unsafeRunSync().zoneDeleted shouldBe empty
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -952,12 +952,13 @@ class MySqlZoneRepositoryIntegrationSpec
|
||||
"check if an id has an ACL rule for at least one of the zones" in {
|
||||
|
||||
val zoneId = UUID.randomUUID().toString
|
||||
val adminId = UUID.randomUUID().toString
|
||||
|
||||
val testZones = (1 until 3).map { num =>
|
||||
okZone.copy(
|
||||
name = num.toString + ".",
|
||||
id = zoneId,
|
||||
adminGroupId = testZoneAdminGroupId,
|
||||
adminGroupId = adminId,
|
||||
acl = testZoneAcl
|
||||
)
|
||||
}
|
||||
@ -965,7 +966,7 @@ class MySqlZoneRepositoryIntegrationSpec
|
||||
val f =
|
||||
for {
|
||||
_ <- saveZones(testZones)
|
||||
zones <- repo.getFirstOwnedZoneAclGroupId(testZoneAdminGroupId)
|
||||
zones <- repo.getFirstOwnedZoneAclGroupId(adminId)
|
||||
} yield zones
|
||||
|
||||
f.unsafeRunSync() shouldBe Some(zoneId)
|
||||
|
@ -0,0 +1,8 @@
|
||||
CREATE SCHEMA IF NOT EXISTS ${dbName};
|
||||
|
||||
USE ${dbName};
|
||||
|
||||
ALTER TABLE zone_access
|
||||
DROP CONSTRAINT fk_zone_access,
|
||||
ADD zone_status CHAR(36) NOT NULL
|
||||
;
|
@ -237,10 +237,7 @@ CREATE TABLE IF NOT EXISTS zone_access
|
||||
(
|
||||
accessor_id char(36) not null,
|
||||
zone_id char(36) not null,
|
||||
primary key (accessor_id, zone_id),
|
||||
constraint fk_zone_access_zone_id
|
||||
foreign key (zone_id) references zone (id)
|
||||
on delete cascade
|
||||
primary key (accessor_id, zone_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS zone_access_accessor_id_index
|
||||
|
@ -47,9 +47,9 @@ class MySqlGroupChangeRepository extends GroupChangeRepository with Monitored {
|
||||
sql"""
|
||||
|SELECT data
|
||||
| FROM group_change
|
||||
| WHERE group_id = {groupId} AND created_timestamp < {startFrom}
|
||||
| WHERE group_id = {groupId}
|
||||
| ORDER BY created_timestamp DESC
|
||||
| LIMIT {maxItems}
|
||||
| LIMIT {maxItems} OFFSET {startFrom}
|
||||
""".stripMargin
|
||||
|
||||
private final val LIST_GROUP_CHANGE_NO_START =
|
||||
@ -100,7 +100,7 @@ class MySqlGroupChangeRepository extends GroupChangeRepository with Monitored {
|
||||
|
||||
def getGroupChanges(
|
||||
groupId: String,
|
||||
startFrom: Option[String],
|
||||
startFrom: Option[Int],
|
||||
maxItems: Int
|
||||
): IO[ListGroupChangesResults] =
|
||||
monitor("repo.GroupChange.getGroupChanges") {
|
||||
@ -112,21 +112,25 @@ class MySqlGroupChangeRepository extends GroupChangeRepository with Monitored {
|
||||
val query = startFrom match {
|
||||
case Some(start) =>
|
||||
LIST_GROUP_CHANGES_WITH_START
|
||||
.bindByName('groupId -> groupId, 'startFrom -> start, 'maxItems -> maxItems)
|
||||
.bindByName('groupId -> groupId, 'startFrom -> start, 'maxItems -> (maxItems + 1))
|
||||
case None =>
|
||||
LIST_GROUP_CHANGE_NO_START
|
||||
.bindByName('groupId -> groupId, 'maxItems -> maxItems)
|
||||
.bindByName('groupId -> groupId, 'maxItems -> (maxItems + 1))
|
||||
}
|
||||
val queryResult = query
|
||||
.map(toGroupChange(1))
|
||||
.list()
|
||||
.apply()
|
||||
|
||||
val nextId =
|
||||
if (queryResult.size < maxItems) None
|
||||
else queryResult.lastOption.map(_.created.toEpochMilli.toString)
|
||||
val maxQueries = queryResult.take(maxItems)
|
||||
val startValue = startFrom.getOrElse(0)
|
||||
|
||||
ListGroupChangesResults(queryResult, nextId)
|
||||
val nextId = queryResult match {
|
||||
case _ if queryResult.size <= maxItems | queryResult.isEmpty => None
|
||||
case _ => Some(startValue + maxItems)
|
||||
}
|
||||
|
||||
ListGroupChangesResults(maxQueries, nextId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,10 +17,13 @@
|
||||
package vinyldns.mysql.repository
|
||||
|
||||
import cats.effect.IO
|
||||
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
import org.slf4j.LoggerFactory
|
||||
import scalikejdbc._
|
||||
import vinyldns.core.domain.auth.AuthPrincipal
|
||||
import vinyldns.core.domain.membership.User
|
||||
import vinyldns.core.domain.zone._
|
||||
import vinyldns.core.protobuf._
|
||||
import vinyldns.core.route.Monitored
|
||||
@ -32,12 +35,26 @@ class MySqlZoneChangeRepository
|
||||
with Monitored {
|
||||
private final val logger = LoggerFactory.getLogger(classOf[MySqlZoneChangeRepository])
|
||||
|
||||
private final val MAX_ACCESSORS = 30
|
||||
|
||||
private final val PUT_ZONE_CHANGE =
|
||||
sql"""
|
||||
|REPLACE INTO zone_change (change_id, zone_id, data, created_timestamp)
|
||||
| VALUES ({change_id}, {zone_id}, {data}, {created_timestamp})
|
||||
""".stripMargin
|
||||
|
||||
private final val BASE_ZONE_CHANGE_SEARCH_SQL =
|
||||
"""
|
||||
|SELECT zc.data
|
||||
| FROM zone_change zc
|
||||
""".stripMargin
|
||||
|
||||
private final val BASE_GET_ZONES_SQL =
|
||||
"""
|
||||
|SELECT z.data
|
||||
| FROM zone z
|
||||
""".stripMargin
|
||||
|
||||
private final val LIST_ZONES_CHANGES =
|
||||
sql"""
|
||||
|SELECT zc.data
|
||||
@ -110,6 +127,106 @@ class MySqlZoneChangeRepository
|
||||
}
|
||||
}
|
||||
|
||||
private def withAccessors(
|
||||
user: User,
|
||||
groupIds: Seq[String],
|
||||
ignoreAccessZones: Boolean
|
||||
): (String, Seq[Any]) =
|
||||
// Super users do not need to join across to check zone access as they have access to all of the zones
|
||||
if (ignoreAccessZones || user.isSuper || user.isSupport) {
|
||||
(BASE_ZONE_CHANGE_SEARCH_SQL, Seq.empty)
|
||||
} else {
|
||||
// User is not super or support,
|
||||
// let's join across to the zone access table so we return only zones a user has access to
|
||||
val accessors = buildZoneSearchAccessorList(user, groupIds)
|
||||
val questionMarks = List.fill(accessors.size)("?").mkString(",")
|
||||
val withAccessorCheck = BASE_ZONE_CHANGE_SEARCH_SQL +
|
||||
s"""
|
||||
| JOIN zone_access za ON zc.zone_id = za.zone_id
|
||||
| AND za.accessor_id IN ($questionMarks)
|
||||
""".stripMargin
|
||||
(withAccessorCheck, accessors)
|
||||
}
|
||||
|
||||
/* Limit the accessors so that we don't have boundless parameterized queries */
|
||||
private def buildZoneSearchAccessorList(user: User, groupIds: Seq[String]): Seq[String] = {
|
||||
val allAccessors = user.id +: groupIds
|
||||
|
||||
if (allAccessors.length > MAX_ACCESSORS) {
|
||||
logger.warn(
|
||||
s"User ${user.userName} with id ${user.id} is in more than $MAX_ACCESSORS groups, no all zones maybe returned!"
|
||||
)
|
||||
}
|
||||
|
||||
// Take the top 30 accessors, but add "EVERYONE" to the list so that we include zones that have everyone access
|
||||
allAccessors.take(MAX_ACCESSORS) :+ "EVERYONE"
|
||||
}
|
||||
|
||||
def listDeletedZones(
|
||||
authPrincipal: AuthPrincipal,
|
||||
zoneNameFilter: Option[String] = None,
|
||||
startFrom: Option[String] = None,
|
||||
maxItems: Int = 100,
|
||||
ignoreAccess: Boolean = false
|
||||
): IO[ListDeletedZonesChangeResults] =
|
||||
monitor("repo.ZoneChange.listDeletedZoneInZoneChanges") {
|
||||
IO {
|
||||
DB.readOnly { implicit s =>
|
||||
val (withAccessorCheck, accessors) =
|
||||
withAccessors(authPrincipal.signedInUser, authPrincipal.memberGroupIds, ignoreAccess)
|
||||
val sb = new StringBuilder
|
||||
sb.append(withAccessorCheck)
|
||||
|
||||
val query = sb.toString
|
||||
|
||||
val zoneChangeResults: List[ZoneChange] =
|
||||
SQL(query)
|
||||
.bind(accessors: _*)
|
||||
.map(extractZoneChange(1))
|
||||
.list()
|
||||
.apply()
|
||||
|
||||
val zoneResults: List[Zone] =
|
||||
SQL(BASE_GET_ZONES_SQL)
|
||||
.map(extractZone(1))
|
||||
.list()
|
||||
.apply()
|
||||
|
||||
val zoneNotInZoneChange: List[ZoneChange] =
|
||||
zoneChangeResults.filter(z=> !zoneResults.map(_.name).contains(z.zone.name) && z.zone.status != ZoneStatus.Active)
|
||||
|
||||
val deletedZoneResults: List[ZoneChange] =
|
||||
zoneNotInZoneChange.filter(_.zone.status.equals(ZoneStatus.Deleted)).distinct.sortBy(_.zone.updated).reverse
|
||||
|
||||
val results: List[ZoneChange] =
|
||||
if (zoneNameFilter.nonEmpty) {
|
||||
deletedZoneResults.filter(r => r.zone.name.contains(zoneNameFilter.getOrElse("not found")))
|
||||
} else {
|
||||
deletedZoneResults
|
||||
}
|
||||
|
||||
val deletedZonesWithStartFrom: List[ZoneChange] = startFrom match {
|
||||
case Some(zoneId) => results.dropWhile(_.zone.id != zoneId)
|
||||
case None => results
|
||||
}
|
||||
|
||||
val deletedZonesWithMaxItems = deletedZonesWithStartFrom.take(maxItems + 1)
|
||||
|
||||
val (newResults, nextId) =
|
||||
if (deletedZonesWithMaxItems.size > maxItems)
|
||||
(deletedZonesWithMaxItems.dropRight(1), deletedZonesWithMaxItems.lastOption.map(_.zone.id))
|
||||
else (deletedZonesWithMaxItems, None)
|
||||
|
||||
ListDeletedZonesChangeResults(
|
||||
zoneDeleted = newResults,
|
||||
nextId = nextId,
|
||||
startFrom = startFrom,
|
||||
maxItems = maxItems,
|
||||
zoneChangeFilter = zoneNameFilter,
|
||||
ignoreAccess = ignoreAccess
|
||||
)
|
||||
}}}
|
||||
|
||||
def listFailedZoneChanges(maxItems: Int, startFrom: Int): IO[ListFailedZoneChangesResults] =
|
||||
monitor("repo.ZoneChange.listFailedZoneChanges") {
|
||||
IO {
|
||||
@ -130,4 +247,8 @@ class MySqlZoneChangeRepository
|
||||
private def extractZoneChange(colIndex: Int): WrappedResultSet => ZoneChange = res => {
|
||||
fromPB(VinylDNSProto.ZoneChange.parseFrom(res.bytes(colIndex)))
|
||||
}
|
||||
|
||||
private def extractZone(columnIndex: Int): WrappedResultSet => Zone = res => {
|
||||
fromPB(VinylDNSProto.Zone.parseFrom(res.bytes(columnIndex)))
|
||||
}
|
||||
}
|
||||
|
@ -71,10 +71,17 @@ class MySqlZoneRepository extends ZoneRepository with ProtobufConversions with M
|
||||
*/
|
||||
private final val PUT_ZONE_ACCESS =
|
||||
sql"""
|
||||
|REPLACE INTO zone_access(accessor_id, zone_id)
|
||||
| VALUES ({accessorId}, {zoneId})
|
||||
|REPLACE INTO zone_access(accessor_id, zone_id, zone_status)
|
||||
| VALUES ({accessorId}, {zoneId}, {zoneStatus})
|
||||
""".stripMargin
|
||||
|
||||
private final val UPDATE_ZONE_ACCESS =
|
||||
sql"""
|
||||
|UPDATE zone_access
|
||||
| SET zone_status = {zoneStatus}
|
||||
| WHERE zone_id = {zoneId}
|
||||
""".stripMargin
|
||||
|
||||
private final val DELETE_ZONE_ACCESS =
|
||||
sql"""
|
||||
|DELETE
|
||||
@ -127,8 +134,8 @@ class MySqlZoneRepository extends ZoneRepository with ProtobufConversions with M
|
||||
private final val GET_ZONE_ACCESS_BY_ADMIN_GROUP_ID =
|
||||
sql"""
|
||||
|SELECT zone_id
|
||||
| FROM zone_access z
|
||||
| WHERE z.accessor_id = (?)
|
||||
| FROM zone_access za
|
||||
| WHERE za.accessor_id = (?) AND za.zone_status <> 'Deleted'
|
||||
| LIMIT 1
|
||||
""".stripMargin
|
||||
|
||||
@ -503,10 +510,10 @@ class MySqlZoneRepository extends ZoneRepository with ProtobufConversions with M
|
||||
val sqlParameters: Seq[Seq[(Symbol, Any)]] =
|
||||
zone.acl.rules.toSeq
|
||||
.map(r => r.userId.orElse(r.groupId).getOrElse("EVERYONE")) // if the user and group are empty, assert everyone
|
||||
.map(userOrGroupId => Seq('accessorId -> userOrGroupId, 'zoneId -> zone.id))
|
||||
.map(userOrGroupId => Seq('accessorId -> userOrGroupId, 'zoneId -> zone.id, 'zoneStatus -> zone.status.toString))
|
||||
|
||||
// we MUST make sure that we put the admin group id as an accessor to this zone
|
||||
val allAccessors = sqlParameters :+ Seq('accessorId -> zone.adminGroupId, 'zoneId -> zone.id)
|
||||
val allAccessors = sqlParameters :+ Seq('accessorId -> zone.adminGroupId, 'zoneId -> zone.id,'zoneStatus -> zone.status.toString)
|
||||
|
||||
// make sure that we do a distinct, so that we don't generate unnecessary inserts
|
||||
PUT_ZONE_ACCESS.batchByName(allAccessors.distinct: _*).apply()
|
||||
@ -518,6 +525,12 @@ class MySqlZoneRepository extends ZoneRepository with ProtobufConversions with M
|
||||
zone
|
||||
}
|
||||
|
||||
private def updateZoneAccess(zone: Zone)(implicit session: DBSession): Zone = {
|
||||
UPDATE_ZONE_ACCESS.bindByName(
|
||||
'zoneStatus ->zone.status.toString, 'zoneId ->zone.id).update().apply()
|
||||
zone
|
||||
}
|
||||
|
||||
private def deleteZoneAccess(zone: Zone)(implicit session: DBSession): Zone = {
|
||||
DELETE_ZONE_ACCESS.bind(zone.id).update().apply()
|
||||
zone
|
||||
@ -532,6 +545,7 @@ class MySqlZoneRepository extends ZoneRepository with ProtobufConversions with M
|
||||
IO {
|
||||
DB.localTx { implicit s =>
|
||||
deleteZone(zone)
|
||||
updateZoneAccess(zone)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -486,6 +486,20 @@ class VinylDNS @Inject() (
|
||||
})
|
||||
}
|
||||
|
||||
def getDeletedZones: Action[AnyContent] = userAction.async { implicit request =>
|
||||
val queryParameters = new HashMap[String, java.util.List[String]]()
|
||||
for {
|
||||
(name, values) <- request.queryString
|
||||
} queryParameters.put(name, values.asJava)
|
||||
val vinyldnsRequest =
|
||||
new VinylDNSRequest("GET", s"$vinyldnsServiceBackend", "zones/deleted/changes", parameters = queryParameters)
|
||||
executeRequest(vinyldnsRequest, request.user).map(response => {
|
||||
Status(response.status)(response.body)
|
||||
.withHeaders(cacheHeaders: _*)
|
||||
})
|
||||
// $COVERAGE-ON$
|
||||
}
|
||||
|
||||
def getZoneChange(id: String): Action[AnyContent] = userAction.async { implicit request =>
|
||||
val queryParameters = new HashMap[String, java.util.List[String]]()
|
||||
for {
|
||||
|
@ -12,7 +12,7 @@
|
||||
<!-- END BREADCRUMB -->
|
||||
|
||||
<!-- PAGE TITLE -->
|
||||
<div class="page-title"><h3><span class="fa fa-list-ol"></span> DNS Changes {{ getPageTitle() }}</h3></div>
|
||||
<div class="page-title"><h3><span class="fa fa-list-ol"></span> DNS Changes</h3></div>
|
||||
<!-- END PAGE TITLE -->
|
||||
|
||||
<!-- PAGE CONTENT WRAPPER -->
|
||||
@ -58,7 +58,8 @@
|
||||
<div class="panel-body">
|
||||
|
||||
<!-- PAGINATION -->
|
||||
<div class="dataTables_paginate">
|
||||
<div class="dataTables_paginate vinyldns_paginate">
|
||||
<span class="vinyldns_page_number">{{ getPageTitle() }}</span>
|
||||
<ul class="pagination">
|
||||
<li class="paginate_button previous">
|
||||
<a type="button" ng-if="prevPageEnabled()" ng-click="prevPage()">Previous</a>
|
||||
@ -125,7 +126,8 @@
|
||||
</table>
|
||||
|
||||
<!-- PAGINATION -->
|
||||
<div class="dataTables_paginate">
|
||||
<div class="dataTables_paginate vinyldns_paginate">
|
||||
<span class="vinyldns_page_number">{{ getPageTitle() }}</span>
|
||||
<ul class="pagination">
|
||||
<li class="paginate_button previous">
|
||||
<a ng-if="prevPageEnabled()" ng-click="prevPage()">Previous</a>
|
||||
|
@ -121,7 +121,7 @@
|
||||
<!-- START SIMPLE DATATABLE -->
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">All Group Changes {{ getChangePageTitle() }}</h3>
|
||||
<h3 class="panel-title">All Group Changes</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="btn-group">
|
||||
@ -129,7 +129,8 @@
|
||||
</div>
|
||||
|
||||
<!-- PAGINATION -->
|
||||
<div class="dataTables_paginate">
|
||||
<div class="dataTables_paginate vinyldns_paginate">
|
||||
<span class="vinyldns_page_number">{{ getChangePageTitle() }}</span>
|
||||
<ul class="pagination">
|
||||
<li class="paginate_button previous">
|
||||
<a ng-if="changePrevPageEnabled()" ng-click="changePrevPage()" class="paginate_button">Previous</a>
|
||||
@ -171,7 +172,8 @@
|
||||
</table>
|
||||
|
||||
<!-- PAGINATION -->
|
||||
<div class="dataTables_paginate">
|
||||
<div class="dataTables_paginate vinyldns_paginate">
|
||||
<span class="vinyldns_page_number">{{ getChangePageTitle() }}</span>
|
||||
<ul class="pagination">
|
||||
<li class="paginate_button previous">
|
||||
<a ng-if="changePrevPageEnabled()" ng-click="changePrevPage()" class="paginate_button">Previous</a>
|
||||
|
@ -426,7 +426,8 @@
|
||||
</div>
|
||||
|
||||
<!-- PAGINATION -->
|
||||
<div class="dataTables_paginate">
|
||||
<div class="dataTables_paginate vinyldns_paginate">
|
||||
<span class="vinyldns_page_number">{{ getRecordChangePageTitle() }}</span>
|
||||
<ul class="pagination">
|
||||
<li class="paginate_button previous">
|
||||
<a ng-if="changeHistoryPrevPageEnabled()" ng-click="changeHistoryPrevPage()" class="paginate_button">Previous</a>
|
||||
@ -476,7 +477,8 @@
|
||||
</table>
|
||||
|
||||
<!-- PAGINATION -->
|
||||
<div class="dataTables_paginate">
|
||||
<div class="dataTables_paginate vinyldns_paginate">
|
||||
<span class="vinyldns_page_number">{{ getRecordChangePageTitle() }}</span>
|
||||
<ul class="pagination">
|
||||
<li class="paginate_button previous">
|
||||
<a ng-if="changeHistoryPrevPageEnabled()" ng-click="changeHistoryPrevPage()" class="paginate_button">Previous</a>
|
||||
|
@ -3,7 +3,7 @@
|
||||
<!-- START SIMPLE DATATABLE -->
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">All Record Changes {{ getChangePageTitle() }}</h3>
|
||||
<h3 class="panel-title">All Record Changes</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="btn-group">
|
||||
@ -11,7 +11,8 @@
|
||||
</div>
|
||||
|
||||
<!-- PAGINATION -->
|
||||
<div class="dataTables_paginate">
|
||||
<div class="dataTables_paginate vinyldns_paginate">
|
||||
<span class="vinyldns_page_number">{{ getChangePageTitle() }}</span>
|
||||
<ul class="pagination">
|
||||
<li class="paginate_button previous">
|
||||
<a ng-if="changePrevPageEnabled()" ng-click="changePrevPage()" class="paginate_button">Previous</a>
|
||||
@ -61,7 +62,8 @@
|
||||
</table>
|
||||
|
||||
<!-- PAGINATION -->
|
||||
<div class="dataTables_paginate">
|
||||
<div class="dataTables_paginate vinyldns_paginate">
|
||||
<span class="vinyldns_page_number">{{ getChangePageTitle() }}</span>
|
||||
<ul class="pagination">
|
||||
<li class="paginate_button previous">
|
||||
<a ng-if="changePrevPageEnabled()" ng-click="changePrevPage()" class="paginate_button">Previous</a>
|
||||
|
@ -5,13 +5,13 @@
|
||||
<div class="panel panel-default x_panel" ng-init="sharedDisplayEnabled = @meta.sharedDisplayEnabled; defaultTtl = @meta.defaultTtl">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
<a class="collapse-link">
|
||||
<a class="collapse-link" href data-toggle="collapse" data-target="#recordChangePreviewTableCollapse" aria-expanded="false" aria-controls="recordChangePreviewTableCollapse">
|
||||
<i class="fa fa-chevron-down"></i>
|
||||
Recent Record Changes
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="panel-collapse collapse in" id="recordChangePreviewTableCollapse">
|
||||
<div class="panel-body x_content panel-body-open">
|
||||
<button class="btn btn-default" ng-click="refreshRecordChangesPreview()"><span class="fa fa-refresh"></span> Refresh</button>
|
||||
<table id="recordChangePreviewTable" class="table table-condensed">
|
||||
@ -52,6 +52,7 @@
|
||||
</table>
|
||||
<!-- <button type="button" class="btn btn-info btn-sm">View full change log</button> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- START RECENT RECORDSET CHANGES TABLE PANEL -->
|
||||
<!-- END ACCORDION -->
|
||||
|
@ -75,7 +75,7 @@
|
||||
<label class="col-md-3 control-label">Access</label>
|
||||
<div class="col-md-9">
|
||||
<div>
|
||||
<select class="form-control" ng-model="updateZoneInfo.shared">
|
||||
<select class="form-control" ng-model="updateZoneInfo.shared" ng-disabled="!isZoneAdmin">
|
||||
<option ng-value="true" ng-model="updateZoneInfo.shared"
|
||||
ng-selected="updateZoneInfo.shared == true">
|
||||
Shared</option>
|
||||
|
@ -9,6 +9,21 @@
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-default" ng-click="refreshZoneChange()"><span class="fa fa-refresh"></span> Refresh</button>
|
||||
</div>
|
||||
|
||||
<!-- PAGINATION -->
|
||||
<div class="dataTables_paginate vinyldns_paginate">
|
||||
<span class="vinyldns_page_number">{{ getZoneHistoryPageNumber() }}</span>
|
||||
<ul class="pagination">
|
||||
<li class="paginate_button previous">
|
||||
<a ng-if="prevPageEnabled()" ng-click="prevPageZoneHistory()" class="paginate_button">Previous</a>
|
||||
</li>
|
||||
<li class="paginate_button next">
|
||||
<a ng-if="nextPageEnabled()" ng-click="nextPageZoneHistory()" class="paginate_button">Next</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- END PAGINATION -->
|
||||
|
||||
<table id="zoneChangeDataTable" class="table table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
@ -46,8 +61,8 @@
|
||||
</table>
|
||||
|
||||
<!-- PAGINATION -->
|
||||
<div class="dataTables_paginate vinyldns_zones_paginate">
|
||||
<span class="vinyldns_zones_page_number">{{ getZoneHistoryPageNumber() }}</span>
|
||||
<div class="dataTables_paginate vinyldns_paginate">
|
||||
<span class="vinyldns_page_number">{{ getZoneHistoryPageNumber() }}</span>
|
||||
<ul class="pagination">
|
||||
<li class="paginate_button previous">
|
||||
<a ng-if="prevPageEnabled()" ng-click="prevPageZoneHistory()" class="paginate_button">Previous</a>
|
||||
|
@ -29,6 +29,7 @@
|
||||
<ul class="nav nav-tabs bar_tabs">
|
||||
<li class="active"><a href="#myZones" data-toggle="tab" ng-click="myZonesAccess()">My Zones</a></li>
|
||||
<li><a id="tab2-button" href="#allZones" data-toggle="tab" ng-click="allZonesAccess()">All Zones</a></li>
|
||||
<li><a id="tab3-button" href="#deletedZones" data-toggle="tab">Abandoned Zones</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="panel-body tab-content">
|
||||
@ -278,6 +279,209 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane" id="deletedZones">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
|
||||
<!-- SIMPLE DATATABLE -->
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
|
||||
<button id="zone-refresh-button" class="btn btn-default" ng-click="refreshZones()">
|
||||
<span class="fa fa-refresh"></span> Refresh
|
||||
</button>
|
||||
|
||||
<!-- SEARCH BOX -->
|
||||
<div class="pull-right">
|
||||
<form class="input-group" ng-submit="refreshZones()">
|
||||
<div class="input-group">
|
||||
<span class="input-group-btn">
|
||||
<button id="my-deleted-zones-search-button" type="submit" class="btn btn-primary btn-left-round">
|
||||
<span class="fa fa-search"></span>
|
||||
</button>
|
||||
</span>
|
||||
<input id="deleted-zones-search-text" ng-model="query" type="text" class="form-control" placeholder="Zone Name"/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<!-- END SEARCH BOX -->
|
||||
|
||||
<!-- DELETED ZONES TABS -->
|
||||
<div class="panel panel-default panel-tabs">
|
||||
<ul class="nav nav-tabs bar_tabs">
|
||||
<li class="active"><a href="#myDeletedZones" data-toggle="tab">My Zones</a></li>
|
||||
<li><a id="tab2-button" href="#allDeletedZones" data-toggle="tab">All Zones</a></li>
|
||||
</ul>
|
||||
<div class="panel-body tab-content">
|
||||
<div class="tab-pane active" id="myDeletedZones">
|
||||
<div id="zone-list-table" class="panel-body">
|
||||
<p ng-if="!myDeletedZonesLoaded">Loading zones...</p>
|
||||
<p ng-if="myDeletedZonesLoaded && !myDeletedZones.length">No zones match the search criteria.</p>
|
||||
|
||||
<!-- PAGINATION -->
|
||||
<div class="dataTables_paginate vinyldns_zones_paginate">
|
||||
<span class="vinyldns_zones_page_number">{{ getZonesPageNumber("myDeletedZones") }}</span>
|
||||
<ul class="pagination">
|
||||
<li class="paginate_button previous">
|
||||
<a ng-if="prevPageEnabled('myDeletedZones')" ng-click="prevPageMyDeletedZones()">Previous</a>
|
||||
</li>
|
||||
<li class="paginate_button next">
|
||||
<a ng-if="nextPageEnabled('myDeletedZones')" ng-click="nextPageMyDeletedZones()">Next</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- END PAGINATION -->
|
||||
|
||||
<table class="table" ng-if="myDeletedZones.length">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Admin Group</th>
|
||||
<th>Created</th>
|
||||
<th>Abandoned</th>
|
||||
<th>Status</th>
|
||||
<th>Abandoned By</th>
|
||||
@if(meta.sharedDisplayEnabled) {
|
||||
<th>Access</th>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="deletedZone in myDeletedZones">
|
||||
<td class="wrap-long-text" ng-bind="deletedZone.zoneChange.zone.name">
|
||||
</td>
|
||||
<td class="wrap-long-text" ng-bind="deletedZone.zoneChange.zone.email">
|
||||
</td>
|
||||
<td>
|
||||
<a ng-if="canAccessGroup(deletedZone.zoneChange.zone.adminGroupId)" ng-bind="deletedZone.adminGroupName"
|
||||
href="/groups/{{deletedZone.zoneChange.zone.adminGroupId}}"></a>
|
||||
<span ng-if="!canAccessGroup(deletedZone.zoneChange.zone.adminGroupId)" ng-bind="deletedZone.adminGroupName"
|
||||
style="line-height: 0"></span>
|
||||
</td>
|
||||
<td>
|
||||
{{deletedZone.zoneChange.zone.created}}
|
||||
</td>
|
||||
<td>
|
||||
{{deletedZone.zoneChange.zone.updated}}
|
||||
</td>
|
||||
<td ng-bind="deletedZone.zoneChange.zone.status"></td>
|
||||
<td>
|
||||
{{deletedZone.userName}}
|
||||
</td>
|
||||
@if(meta.sharedDisplayEnabled) {
|
||||
<td>{{zone.shared ? "Shared" : "Private"}}</td>
|
||||
}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- PAGINATION -->
|
||||
<div class="dataTables_paginate vinyldns_zones_paginate">
|
||||
<span class="vinyldns_zones_page_number">{{ getZonesPageNumber("myDeletedZones") }}</span>
|
||||
<ul class="pagination">
|
||||
<li class="paginate_button previous">
|
||||
<a ng-if="prevPageEnabled('myDeletedZones')" ng-click="prevPageMyDeletedZones()">Previous</a>
|
||||
</li>
|
||||
<li class="paginate_button next">
|
||||
<a ng-if="nextPageEnabled('myDeletedZones')" ng-click="nextPageMyDeletedZones()">Next</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- END PAGINATION -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane" id="allDeletedZones">
|
||||
<div id="zone-list-table" class="panel-body">
|
||||
<p ng-if="!allDeletedZonesLoaded">Loading zones...</p>
|
||||
<p ng-if="allDeletedZonesLoaded && !allDeletedZones.length">No zones match the search criteria.</p>
|
||||
|
||||
<!-- PAGINATION -->
|
||||
<div class="dataTables_paginate vinyldns_zones_paginate">
|
||||
<span class="vinyldns_zones_page_number">{{ getZonesPageNumber("allDeletedZones") }}</span>
|
||||
<ul class="pagination">
|
||||
<li class="paginate_button previous">
|
||||
<a ng-if="prevPageEnabled('allDeletedZones')" ng-click="prevPageAllDeletedZones()">Previous</a>
|
||||
</li>
|
||||
<li class="paginate_button next">
|
||||
<a ng-if="nextPageEnabled('allDeletedZones')" ng-click="nextPageAllDeletedZones()">Next</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- END PAGINATION -->
|
||||
|
||||
<table class="table" ng-if="allDeletedZones.length">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Admin Group</th>
|
||||
<th>Created</th>
|
||||
<th>Abandoned</th>
|
||||
<th>Status</th>
|
||||
<th>Abandoned By</th>
|
||||
@if(meta.sharedDisplayEnabled) {
|
||||
<th>Access</th>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="deletedZone in allDeletedZones">
|
||||
<td class="wrap-long-text" ng-bind="deletedZone.zoneChange.zone.name">
|
||||
</td>
|
||||
<td class="wrap-long-text" ng-bind="deletedZone.zoneChange.zone.email">
|
||||
</td>
|
||||
<td>
|
||||
<a ng-if="canAccessGroup(deletedZone.zoneChange.zone.adminGroupId)" ng-bind="deletedZone.adminGroupName"
|
||||
href="/groups/{{deletedZone.zoneChange.zone.adminGroupId}}"></a>
|
||||
<span ng-if="!canAccessGroup(deletedZone.zoneChange.zone.adminGroupId)" ng-bind="deletedZone.adminGroupName"
|
||||
style="line-height: 0"></span>
|
||||
</td>
|
||||
<td>
|
||||
{{deletedZone.zoneChange.zone.created}}
|
||||
</td>
|
||||
<td>
|
||||
{{deletedZone.zoneChange.zone.updated}}
|
||||
</td>
|
||||
<td ng-bind="deletedZone.zoneChange.zone.status"></td>
|
||||
<td>
|
||||
{{deletedZone.userName}}
|
||||
</td>
|
||||
@if(meta.sharedDisplayEnabled) {
|
||||
<td>{{zone.shared ? "Shared" : "Private"}}</td>
|
||||
}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- PAGINATION -->
|
||||
<div class="dataTables_paginate vinyldns_zones_paginate">
|
||||
<span class="vinyldns_zones_page_number">{{ getZonesPageNumber("allDeletedZones") }}</span>
|
||||
<ul class="pagination">
|
||||
<li class="paginate_button previous">
|
||||
<a ng-if="prevPageEnabled('allDeletedZones')" ng-click="prevPageAllDeletedZones()">Previous</a>
|
||||
</li>
|
||||
<li class="paginate_button next">
|
||||
<a ng-if="nextPageEnabled('allDeletedZones')" ng-click="nextPageAllDeletedZones()">Next</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- END PAGINATION -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END DELETED ZONES TABS -->
|
||||
</div>
|
||||
<div class="panel-footer"></div>
|
||||
</div>
|
||||
<!-- END SIMPLE DATATABLE -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END VERTICAL TABS -->
|
||||
|
@ -31,6 +31,7 @@ GET /api/zones/backendids @controllers.VinylDNS.getBacken
|
||||
GET /api/zones/:id @controllers.VinylDNS.getZone(id: String)
|
||||
GET /api/zones/:id/details @controllers.VinylDNS.getCommonZoneDetails(id: String)
|
||||
GET /api/zones/name/:name @controllers.VinylDNS.getZoneByName(name: String)
|
||||
GET /api/zones/deleted/changes @controllers.VinylDNS.getDeletedZones
|
||||
GET /api/zones/:id/changes @controllers.VinylDNS.getZoneChange(id: String)
|
||||
POST /api/zones @controllers.VinylDNS.addZone
|
||||
PUT /api/zones/:id @controllers.VinylDNS.updateZone(id: String)
|
||||
|
@ -42,6 +42,13 @@ describe('Controller: ManageZonesController', function () {
|
||||
}
|
||||
});
|
||||
};
|
||||
zonesService.getDeletedZones = function() {
|
||||
return $q.when({
|
||||
data: {
|
||||
zonesDeletedInfo: ["all my deleted zones"]
|
||||
}
|
||||
});
|
||||
};
|
||||
zonesService.getBackendIds = function() {
|
||||
return $q.when({
|
||||
data: ['backend-1', 'backend-2']
|
||||
|
@ -23,6 +23,8 @@ angular.module('controller.zones', [])
|
||||
$scope.allZonesLoaded = false;
|
||||
$scope.hasZones = false; // Re-assigned each time zones are fetched without a query
|
||||
$scope.allGroups = [];
|
||||
$scope.myDeletedZones = [];
|
||||
$scope.allDeletedZones = [];
|
||||
$scope.ignoreAccess = false;
|
||||
$scope.validEmailDomains= [];
|
||||
$scope.allZonesAccess = function () {
|
||||
@ -41,6 +43,8 @@ angular.module('controller.zones', [])
|
||||
// Paging status for zone sets
|
||||
var zonesPaging = pagingService.getNewPagingParams(100);
|
||||
var allZonesPaging = pagingService.getNewPagingParams(100);
|
||||
var myDeleteZonesPaging = pagingService.getNewPagingParams(100);
|
||||
var allDeleteZonesPaging = pagingService.getNewPagingParams(100);
|
||||
profileService.getAuthenticatedUserData().then(function (results) {
|
||||
if (results.data) {
|
||||
$scope.profile = results.data;
|
||||
@ -197,8 +201,53 @@ angular.module('controller.zones', [])
|
||||
.catch(function (error) {
|
||||
handleError(error, 'zonesService::getZones-failure');
|
||||
});
|
||||
|
||||
zonesService
|
||||
.getDeletedZones(myDeleteZonesPaging.maxItems, undefined, $scope.query, false)
|
||||
.then(function (response) {
|
||||
$log.debug('zonesService::getMyDeletedZones-success (' + response.data.zonesDeletedInfo.length + ' zones)');
|
||||
myDeleteZonesPaging.next = response.data.nextId;
|
||||
updateMyDeletedZoneDisplay(response.data.zonesDeletedInfo);
|
||||
})
|
||||
.catch(function (error) {
|
||||
handleError(error, 'zonesService::getDeletedZones-failure');
|
||||
});
|
||||
|
||||
zonesService
|
||||
.getDeletedZones(allDeleteZonesPaging.maxItems, undefined, $scope.query, true)
|
||||
.then(function (response) {
|
||||
$log.debug('zonesService::getAllDeletedZones-success (' + response.data.zonesDeletedInfo.length + ' zones)');
|
||||
allDeleteZonesPaging.next = response.data.nextId;
|
||||
updateAllDeletedZoneDisplay(response.data.zonesDeletedInfo);
|
||||
})
|
||||
.catch(function (error) {
|
||||
handleError(error, 'zonesService::getDeletedZones-failure');
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
function updateMyDeletedZoneDisplay (myDeletedZones) {
|
||||
$scope.myDeletedZones = myDeletedZones;
|
||||
$scope.myDeletedZonesLoaded = true;
|
||||
$log.debug("Displaying my Deleted zones: ", $scope.myDeletedZones);
|
||||
if($scope.myDeletedZones.length > 0) {
|
||||
$("td.dataTables_empty").hide();
|
||||
} else {
|
||||
$("td.dataTables_empty").show();
|
||||
}
|
||||
}
|
||||
|
||||
function updateAllDeletedZoneDisplay (allDeletedZones) {
|
||||
$scope.allDeletedZones = allDeletedZones;
|
||||
$scope.allDeletedZonesLoaded = true;
|
||||
$log.debug("Displaying all Deleted zones: ", $scope.allDeletedZones);
|
||||
if($scope.allDeletedZones.length > 0) {
|
||||
$("td.dataTables_empty").hide();
|
||||
} else {
|
||||
$("td.dataTables_empty").show();
|
||||
}
|
||||
}
|
||||
|
||||
function updateZoneDisplay (zones) {
|
||||
$scope.zones = zones;
|
||||
$scope.myZoneIds = zones.map(function(zone) {return zone['id']});
|
||||
@ -283,6 +332,10 @@ angular.module('controller.zones', [])
|
||||
return pagingService.getPanelTitle(zonesPaging);
|
||||
case 'allZones':
|
||||
return pagingService.getPanelTitle(allZonesPaging);
|
||||
case 'myDeletedZones':
|
||||
return pagingService.getPanelTitle(myDeleteZonesPaging);
|
||||
case 'allDeletedZones':
|
||||
return pagingService.getPanelTitle(allDeleteZonesPaging);
|
||||
}
|
||||
};
|
||||
|
||||
@ -292,6 +345,10 @@ angular.module('controller.zones', [])
|
||||
return pagingService.prevPageEnabled(zonesPaging);
|
||||
case 'allZones':
|
||||
return pagingService.prevPageEnabled(allZonesPaging);
|
||||
case 'myDeletedZones':
|
||||
return pagingService.prevPageEnabled(myDeleteZonesPaging);
|
||||
case 'allDeletedZones':
|
||||
return pagingService.prevPageEnabled(allDeleteZonesPaging);
|
||||
}
|
||||
};
|
||||
|
||||
@ -301,6 +358,10 @@ angular.module('controller.zones', [])
|
||||
return pagingService.nextPageEnabled(zonesPaging);
|
||||
case 'allZones':
|
||||
return pagingService.nextPageEnabled(allZonesPaging);
|
||||
case 'myDeletedZones':
|
||||
return pagingService.nextPageEnabled(myDeleteZonesPaging);
|
||||
case 'allDeletedZones':
|
||||
return pagingService.nextPageEnabled(allDeleteZonesPaging);
|
||||
}
|
||||
};
|
||||
|
||||
@ -330,6 +391,32 @@ angular.module('controller.zones', [])
|
||||
});
|
||||
}
|
||||
|
||||
$scope.prevPageMyDeletedZones = function() {
|
||||
var startFrom = pagingService.getPrevStartFrom(myDeleteZonesPaging);
|
||||
return zonesService
|
||||
.getDeletedZones(myDeleteZonesPaging.maxItems, startFrom, $scope.query, false)
|
||||
.then(function(response) {
|
||||
myDeleteZonesPaging = pagingService.prevPageUpdate(response.data.nextId, myDeleteZonesPaging);
|
||||
updateMyDeletedZoneDisplay(response.data.zonesDeletedInfo);
|
||||
})
|
||||
.catch(function (error) {
|
||||
handleError(error,'zonesService::prevPage-failure');
|
||||
});
|
||||
}
|
||||
|
||||
$scope.prevPageAllDeletedZones = function() {
|
||||
var startFrom = pagingService.getPrevStartFrom(allDeleteZonesPaging);
|
||||
return zonesService
|
||||
.getDeletedZones(allDeleteZonesPaging.maxItems, startFrom, $scope.query, true)
|
||||
.then(function(response) {
|
||||
allDeleteZonesPaging = pagingService.prevPageUpdate(response.data.nextId, allDeleteZonesPaging);
|
||||
updateAllDeletedZoneDisplay(response.data.zonesDeletedInfo);
|
||||
})
|
||||
.catch(function (error) {
|
||||
handleError(error,'zonesService::prevPage-failure');
|
||||
});
|
||||
}
|
||||
|
||||
$scope.nextPageMyZones = function () {
|
||||
return zonesService
|
||||
.getZones(zonesPaging.maxItems, zonesPaging.next, $scope.query, $scope.searchByAdminGroup, false, true)
|
||||
@ -362,5 +449,37 @@ angular.module('controller.zones', [])
|
||||
});
|
||||
};
|
||||
|
||||
$scope.nextPageMyDeletedZones = function () {
|
||||
return zonesService
|
||||
.getDeletedZones(myDeleteZonesPaging.maxItems, myDeleteZonesPaging.next, $scope.query, false)
|
||||
.then(function(response) {
|
||||
var myDeletedZoneSets = response.data.zonesDeletedInfo;
|
||||
myDeleteZonesPaging = pagingService.nextPageUpdate(myDeletedZoneSets, response.data.nextId, myDeleteZonesPaging);
|
||||
|
||||
if (myDeletedZoneSets.length > 0) {
|
||||
updateMyDeletedZoneDisplay(response.data.zonesDeletedInfo);
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
handleError(error,'zonesService::nextPage-failure')
|
||||
});
|
||||
};
|
||||
|
||||
$scope.nextPageAllDeletedZones = function () {
|
||||
return zonesService
|
||||
.getDeletedZones(allDeleteZonesPaging.maxItems, allDeleteZonesPaging.next, $scope.query, false)
|
||||
.then(function(response) {
|
||||
var allDeletedZoneSets = response.data.zonesDeletedInfo;
|
||||
allDeleteZonesPaging = pagingService.nextPageUpdate(allDeletedZoneSets, response.data.nextId, allDeleteZonesPaging);
|
||||
|
||||
if (allDeletedZoneSets.length > 0) {
|
||||
updateAllDeletedZoneDisplay(response.data.zonesDeletedInfo);
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
handleError(error,'zonesService::nextPage-failure')
|
||||
});
|
||||
};
|
||||
|
||||
$timeout($scope.refreshZones, 0);
|
||||
});
|
||||
|
@ -114,4 +114,77 @@ describe('Controller: ZonesController', function () {
|
||||
expect(getZoneSets.calls.mostRecent().args).toEqual(
|
||||
[expectedMaxItems, expectedStartFrom, expectedQuery, expectedSearchByAdminGroup, expectedignoreAccess, expectedincludeReverse]);
|
||||
});
|
||||
|
||||
it('nextPageZones should call getDeletedZones with the correct parameters', function () {
|
||||
mockDeletedZone = {zonesDeletedInfo:[ {
|
||||
zoneChanges: [{ zone: {
|
||||
name: "dummy.",
|
||||
email: "test@test.com",
|
||||
status: "Deleted",
|
||||
created: "2017-02-15T14:58:39Z",
|
||||
account: "c8234503-bfda-4b80-897f-d74129051eaa",
|
||||
acl: {rules: []},
|
||||
adminGroupId: "c8234503-bfda-4b80-897f-d74129051eaa",
|
||||
id: "c5c87405-2ec8-4e03-b2dc-c6758a5d9666",
|
||||
shared: false,
|
||||
status: "Active",
|
||||
latestSync: "2017-02-15T14:58:39Z",
|
||||
isTest: true
|
||||
}}],maxItems: 100}]};
|
||||
var getDeletedZoneSets = spyOn(this.zonesService, 'getDeletedZones')
|
||||
.and.stub()
|
||||
.and.returnValue(this.zonesService.q.when(mockDeletedZone));
|
||||
|
||||
var expectedMaxItems = 100;
|
||||
var expectedStartFrom = undefined;
|
||||
var expectedQuery = this.scope.query;
|
||||
var expectedIgnoreAccess = false;
|
||||
|
||||
this.scope.nextPageMyDeletedZones();
|
||||
|
||||
expect(getDeletedZoneSets.calls.count()).toBe(1);
|
||||
expect(getDeletedZoneSets.calls.mostRecent().args).toEqual(
|
||||
[expectedMaxItems, expectedStartFrom, expectedQuery, expectedIgnoreAccess]);
|
||||
});
|
||||
|
||||
it('prevPageZones should call getDeletedZones with the correct parameters', function () {
|
||||
|
||||
mockDeletedZone = {zonesDeletedInfo:[ {
|
||||
zoneChanges: [{ zone: {
|
||||
name: "dummy.",
|
||||
email: "test@test.com",
|
||||
status: "Deleted",
|
||||
created: "2017-02-15T14:58:39Z",
|
||||
account: "c8234503-bfda-4b80-897f-d74129051eaa",
|
||||
acl: {rules: []},
|
||||
adminGroupId: "c8234503-bfda-4b80-897f-d74129051eaa",
|
||||
id: "c5c87405-2ec8-4e03-b2dc-c6758a5d9666",
|
||||
shared: false,
|
||||
status: "Active",
|
||||
latestSync: "2017-02-15T14:58:39Z",
|
||||
isTest: true
|
||||
}}],maxItems: 100}]};
|
||||
var getDeletedZoneSets = spyOn(this.zonesService, 'getDeletedZones')
|
||||
.and.stub()
|
||||
.and.returnValue(this.zonesService.q.when(mockDeletedZone));
|
||||
|
||||
var expectedMaxItems = 100;
|
||||
var expectedStartFrom = undefined;
|
||||
var expectedQuery = this.scope.query;
|
||||
var expectedIgnoreAccess = false;
|
||||
|
||||
this.scope.prevPageMyDeletedZones();
|
||||
|
||||
expect(getDeletedZoneSets.calls.count()).toBe(1);
|
||||
expect(getDeletedZoneSets.calls.mostRecent().args).toEqual(
|
||||
[expectedMaxItems, expectedStartFrom, expectedQuery, expectedIgnoreAccess]);
|
||||
|
||||
this.scope.nextPageMyDeletedZones();
|
||||
this.scope.prevPageMyDeletedZones();
|
||||
|
||||
expect(getDeletedZoneSets.calls.count()).toBe(3);
|
||||
expect(getDeletedZoneSets.calls.mostRecent().args).toEqual(
|
||||
[expectedMaxItems, expectedStartFrom, expectedQuery, expectedIgnoreAccess]);
|
||||
|
||||
});
|
||||
});
|
||||
|
@ -264,6 +264,9 @@
|
||||
/**
|
||||
* Record change history paging
|
||||
*/
|
||||
$scope.getRecordChangePageTitle = function() {
|
||||
return pagingService.getPanelTitle(changePaging);
|
||||
};
|
||||
|
||||
$scope.changeHistoryPrevPageEnabled = function() {
|
||||
return pagingService.prevPageEnabled(changePaging);
|
||||
|
@ -53,6 +53,20 @@ angular.module('service.zones', [])
|
||||
return $http.get(url);
|
||||
};
|
||||
|
||||
this.getDeletedZones = function (limit, startFrom, query, ignoreAccess) {
|
||||
if (query == "") {
|
||||
query = null;
|
||||
}
|
||||
var params = {
|
||||
"maxItems": limit,
|
||||
"startFrom": startFrom,
|
||||
"nameFilter": query,
|
||||
"ignoreAccess": ignoreAccess
|
||||
};
|
||||
var url = groupsService.urlBuilder("/api/zones/deleted/changes", params);
|
||||
return $http.get(url);
|
||||
};
|
||||
|
||||
this.getBackendIds = function() {
|
||||
var url = "/api/zones/backendids";
|
||||
return $http.get(url);
|
||||
|
@ -54,6 +54,24 @@ describe('Service: zoneService', function () {
|
||||
this.$httpBackend.flush();
|
||||
});
|
||||
|
||||
it('http backend gets called properly when getting my deleted zones', function () {
|
||||
this.$httpBackend.expectGET('/api/zones/deleted/changes?maxItems=100&startFrom=start&nameFilter=someQuery&ignoreAccess=true').respond('deleted my zone returned');
|
||||
this.zonesService.getDeletedZones('100', 'start', 'someQuery', true)
|
||||
.then(function(response) {
|
||||
expect(response.data).toBe('deleted my zone returned');
|
||||
});
|
||||
this.$httpBackend.flush();
|
||||
});
|
||||
|
||||
it('http backend gets called properly when getting all deleted zones', function () {
|
||||
this.$httpBackend.expectGET('/api/zones/deleted/changes?maxItems=100&startFrom=start&nameFilter=someQuery&ignoreAccess=false').respond('deleted all zone returned');
|
||||
this.zonesService.getDeletedZones('100', 'start', 'someQuery', false)
|
||||
.then(function(response) {
|
||||
expect(response.data).toBe('deleted all zone returned');
|
||||
});
|
||||
this.$httpBackend.flush();
|
||||
});
|
||||
|
||||
it('http backend gets called properly when deleting zone', function (done) {
|
||||
this.$httpBackend.expectDELETE('/api/zones/id').respond('zone deleted');
|
||||
this.zonesService.delZone('id')
|
||||
|
@ -240,6 +240,24 @@ trait TestApplicationData { this: Mockito =>
|
||||
| }
|
||||
""".stripMargin)
|
||||
|
||||
val hobbitDeletedZoneChange: JsValue = Json.parse(s"""{
|
||||
| "zoneId": "$hobbitZoneId",
|
||||
| "zoneChanges":
|
||||
| [{ "zone": {
|
||||
| "name": "$hobbitZoneName",
|
||||
| "email": "hobbitAdmin@shire.me",
|
||||
| "status": "Active",
|
||||
| "account": "system",
|
||||
| "acl": "rules",
|
||||
| "adminGroupId": "$hobbitGroupId",
|
||||
| "id": "$hobbitZoneId",
|
||||
| "shared": false,
|
||||
| "status": "Deleted"
|
||||
| }}],
|
||||
| "maxItems": 100
|
||||
| }
|
||||
""".stripMargin)
|
||||
|
||||
val hobbitZoneChange: JsValue = Json.parse(s"""{
|
||||
| "zoneId": "$hobbitZoneId",
|
||||
| "zoneChanges":
|
||||
|
@ -1621,6 +1621,74 @@ class VinylDNSSpec extends Specification with Mockito with TestApplicationData w
|
||||
}
|
||||
}
|
||||
|
||||
".getDeletedZones" should {
|
||||
|
||||
"return ok (200) if the DeletedZones is found" in new WithApplication(app) {
|
||||
val client = MockWS {
|
||||
case (GET, u) if u == s"http://localhost:9001/zones/deleted/changes" =>
|
||||
defaultActionBuilder { Results.Ok(hobbitDeletedZoneChange) }
|
||||
}
|
||||
val mockUserAccessor = mock[UserAccountAccessor]
|
||||
mockUserAccessor.get(anyString).returns(IO.pure(Some(frodoUser)))
|
||||
mockUserAccessor.getUserByKey(anyString).returns(IO.pure(Some(frodoUser)))
|
||||
val underTest = withClient(client)
|
||||
val result =
|
||||
underTest.getDeletedZones()(
|
||||
FakeRequest(GET, s"/zones/deleted/changes")
|
||||
.withSession("username" -> frodoUser.userName, "accessKey" -> frodoUser.accessKey)
|
||||
)
|
||||
|
||||
status(result) must beEqualTo(OK)
|
||||
hasCacheHeaders(result)
|
||||
contentAsJson(result) must beEqualTo(hobbitDeletedZoneChange)
|
||||
}
|
||||
|
||||
"return a not found (404) if the DeletedZones does not exist" in new WithApplication(app) {
|
||||
val client = MockWS {
|
||||
case (GET, u) if u == s"http://localhost:9001/zones/deleted/changes" =>
|
||||
defaultActionBuilder { Results.NotFound }
|
||||
}
|
||||
val mockUserAccessor = mock[UserAccountAccessor]
|
||||
mockUserAccessor.get(anyString).returns(IO.pure(Some(frodoUser)))
|
||||
mockUserAccessor.getUserByKey(anyString).returns(IO.pure(Some(frodoUser)))
|
||||
val underTest = withClient(client)
|
||||
val result =
|
||||
underTest.getDeletedZones()(
|
||||
FakeRequest(GET, "zones/deleted/changes")
|
||||
.withSession("username" -> frodoUser.userName, "accessKey" -> frodoUser.accessKey)
|
||||
)
|
||||
|
||||
status(result) must beEqualTo(NOT_FOUND)
|
||||
hasCacheHeaders(result)
|
||||
}
|
||||
|
||||
"return unauthorized (401) if requesting user is not logged in" in new WithApplication(app) {
|
||||
val client = mock[WSClient]
|
||||
val underTest = withClient(client)
|
||||
val result = underTest.getDeletedZones()(FakeRequest(GET, s"/api/zones/deleted/changes"))
|
||||
|
||||
status(result) mustEqual 401
|
||||
hasCacheHeaders(result)
|
||||
contentAsString(result) must beEqualTo("You are not logged in. Please login to continue.")
|
||||
}
|
||||
"return forbidden (403) if user account is locked" in new WithApplication(app) {
|
||||
val client = mock[WSClient]
|
||||
val underTest = withLockedClient(client)
|
||||
val result = underTest.getDeletedZones()(
|
||||
FakeRequest(GET, s"/api/zones/deleted/changes").withSession(
|
||||
"username" -> lockedFrodoUser.userName,
|
||||
"accessKey" -> lockedFrodoUser.accessKey
|
||||
)
|
||||
)
|
||||
|
||||
status(result) mustEqual 403
|
||||
hasCacheHeaders(result)
|
||||
contentAsString(result) must beEqualTo(
|
||||
s"User account for `${lockedFrodoUser.userName}` is locked."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
".getZone" should {
|
||||
"return unauthorized (401) if requesting user is not logged in" in new WithApplication(app) {
|
||||
val client = mock[WSClient]
|
||||
|
@ -30,7 +30,7 @@ import vinyldns.core.domain.{Fqdn, record}
|
||||
import vinyldns.core.domain.record.{RecordSet, RecordType}
|
||||
|
||||
class Route53IntegrationSpec
|
||||
extends AnyWordSpec
|
||||
extends AnyWordSpec
|
||||
with BeforeAndAfterAll
|
||||
with BeforeAndAfterEach
|
||||
with Matchers {
|
||||
@ -52,6 +52,8 @@ class Route53IntegrationSpec
|
||||
"test",
|
||||
Option("access"),
|
||||
Option("secret"),
|
||||
None,
|
||||
None,
|
||||
sys.env.getOrElse("R53_SERVICE_ENDPOINT", "http://localhost:19003"),
|
||||
"us-east-1"
|
||||
)
|
||||
|
@ -16,13 +16,9 @@
|
||||
|
||||
package vinyldns.route53.backend
|
||||
|
||||
import java.util.UUID
|
||||
import cats.data.OptionT
|
||||
import cats.effect.IO
|
||||
import com.amazonaws.auth.{
|
||||
AWSStaticCredentialsProvider,
|
||||
BasicAWSCredentials,
|
||||
DefaultAWSCredentialsProviderChain
|
||||
}
|
||||
import com.amazonaws.client.builder.AwsClientBuilder.EndpointConfiguration
|
||||
import com.amazonaws.handlers.AsyncHandler
|
||||
import com.amazonaws.services.route53.{AmazonRoute53Async, AmazonRoute53AsyncClientBuilder}
|
||||
@ -106,10 +102,17 @@ class Route53Backend(
|
||||
* @return A list of record sets matching the name, empty if not found
|
||||
*/
|
||||
def resolve(name: String, zoneName: String, typ: RecordType): IO[List[RecordSet]] = {
|
||||
val fqdn = Fqdn.merge(name, zoneName).fqdn
|
||||
def filterResourceRecordSet(
|
||||
rrs: java.util.List[ResourceRecordSet],
|
||||
rrType: RRType
|
||||
): java.util.List[ResourceRecordSet] =
|
||||
rrs.asScala.filter { r =>
|
||||
r.getName == fqdn && RRType.fromValue(r.getType) == rrType
|
||||
}.asJava
|
||||
for {
|
||||
hostedZoneId <- lookupHostedZone(zoneName)
|
||||
awsRRType <- OptionT.fromOption[IO](toRoute53RecordType(typ))
|
||||
fqdn = Fqdn.merge(name, zoneName).fqdn
|
||||
result <- OptionT.liftF {
|
||||
r53(
|
||||
new ListResourceRecordSetsRequest()
|
||||
@ -119,7 +122,10 @@ class Route53Backend(
|
||||
client.listResourceRecordSetsAsync
|
||||
)
|
||||
}
|
||||
} yield toVinylRecordSets(result.getResourceRecordSets, zoneName: String)
|
||||
} yield toVinylRecordSets(
|
||||
filterResourceRecordSet(result.getResourceRecordSets, awsRRType),
|
||||
zoneName: String
|
||||
)
|
||||
}.getOrElse(Nil)
|
||||
|
||||
/**
|
||||
@ -278,21 +284,23 @@ object Route53Backend {
|
||||
r53ClientBuilder.withEndpointConfiguration(
|
||||
new EndpointConfiguration(config.serviceEndpoint, config.signingRegion)
|
||||
)
|
||||
// If either of accessKey or secretKey are empty in conf file; then use AWSCredentialsProviderChain to figure out
|
||||
// credentials.
|
||||
// https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/auth/DefaultAWSCredentialsProviderChain.html
|
||||
val credProvider = config.accessKey
|
||||
.zip(config.secretKey)
|
||||
.map {
|
||||
case (key, secret) =>
|
||||
new AWSStaticCredentialsProvider(
|
||||
new BasicAWSCredentials(key, secret)
|
||||
)
|
||||
}
|
||||
.headOption
|
||||
.getOrElse {
|
||||
new DefaultAWSCredentialsProviderChain()
|
||||
|
||||
val r53CredBuilder = Route53Credentials.builder
|
||||
for {
|
||||
accessKey <- config.accessKey
|
||||
secretKey <- config.secretKey
|
||||
} r53CredBuilder.basicCredentials(accessKey, secretKey)
|
||||
|
||||
for (role <- config.roleArn) {
|
||||
config.externalId match {
|
||||
case Some(externalId) =>
|
||||
r53CredBuilder.withRole(role, UUID.randomUUID().toString, externalId)
|
||||
case None => r53CredBuilder.withRole(role, UUID.randomUUID().toString)
|
||||
}
|
||||
}
|
||||
|
||||
val credProvider = r53CredBuilder.build().provider
|
||||
|
||||
r53ClientBuilder.withCredentials(credProvider).build()
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright 2018 Comcast Cable Communications Management, LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package vinyldns.route53.backend
|
||||
|
||||
import com.amazonaws.auth._
|
||||
import org.slf4j.LoggerFactory
|
||||
import com.amazonaws.services.securitytoken.{
|
||||
AWSSecurityTokenService,
|
||||
AWSSecurityTokenServiceClientBuilder
|
||||
}
|
||||
|
||||
private[backend] sealed trait Route53Credentials extends Serializable {
|
||||
def provider: AWSCredentialsProvider
|
||||
}
|
||||
|
||||
private[backend] final case object DefaultCredentials extends Route53Credentials {
|
||||
def provider: AWSCredentialsProvider = new DefaultAWSCredentialsProviderChain
|
||||
}
|
||||
|
||||
private[backend] final case class BasicCredentials(accessKeyId: String, secretKey: String)
|
||||
extends Route53Credentials {
|
||||
|
||||
private final val logger = LoggerFactory.getLogger(classOf[Route53Backend])
|
||||
|
||||
def provider: AWSCredentialsProvider =
|
||||
try {
|
||||
new AWSStaticCredentialsProvider(new BasicAWSCredentials(accessKeyId, secretKey))
|
||||
} catch {
|
||||
case e: IllegalArgumentException =>
|
||||
logger.error(
|
||||
"Error when using accessKey/secret: {}. Using DefaultProviderChain.",
|
||||
e.getMessage()
|
||||
)
|
||||
new DefaultAWSCredentialsProviderChain
|
||||
}
|
||||
}
|
||||
|
||||
private[backend] final case class STSCredentials(
|
||||
roleArn: String,
|
||||
sessionName: String,
|
||||
externalId: Option[String] = None,
|
||||
longLivedCreds: Route53Credentials = DefaultCredentials
|
||||
) extends Route53Credentials {
|
||||
|
||||
def provider: AWSCredentialsProvider = {
|
||||
lazy val stsClient: AWSSecurityTokenService =
|
||||
AWSSecurityTokenServiceClientBuilder
|
||||
.standard()
|
||||
.withCredentials(longLivedCreds.provider)
|
||||
.build()
|
||||
val builder = new STSAssumeRoleSessionCredentialsProvider.Builder(roleArn, sessionName)
|
||||
.withStsClient(stsClient)
|
||||
externalId match {
|
||||
case Some(externalId) =>
|
||||
builder
|
||||
.withExternalId(externalId)
|
||||
.build()
|
||||
case None =>
|
||||
builder.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object Route53Credentials {
|
||||
class Builder {
|
||||
private var basicCreds: Option[BasicCredentials] = None
|
||||
private var stsCreds: Option[STSCredentials] = None
|
||||
|
||||
def basicCredentials(accessKeyId: String, secretKey: String): Builder = {
|
||||
basicCreds = Option(BasicCredentials(accessKeyId, secretKey))
|
||||
this
|
||||
}
|
||||
|
||||
def withRole(roleArn: String, sessionName: String): Builder = {
|
||||
stsCreds = Option(STSCredentials(roleArn, sessionName))
|
||||
this
|
||||
}
|
||||
|
||||
def withRole(roleArn: String, sessionName: String, externalId: String): Builder = {
|
||||
stsCreds = Option(
|
||||
STSCredentials(
|
||||
roleArn,
|
||||
sessionName,
|
||||
Option(externalId)
|
||||
)
|
||||
)
|
||||
this
|
||||
}
|
||||
|
||||
def build(): Route53Credentials =
|
||||
stsCreds.map(_.copy(longLivedCreds = longLivedCreds)).getOrElse(longLivedCreds)
|
||||
|
||||
private def longLivedCreds: Route53Credentials = basicCreds.getOrElse(DefaultCredentials)
|
||||
}
|
||||
|
||||
def builder: Builder = new Builder
|
||||
}
|
@ -27,6 +27,8 @@ final case class Route53BackendConfig(
|
||||
id: String,
|
||||
accessKey: Option[String],
|
||||
secretKey: Option[String],
|
||||
roleArn: Option[String],
|
||||
externalId: Option[String],
|
||||
serviceEndpoint: String,
|
||||
signingRegion: String
|
||||
)
|
||||
|
@ -94,6 +94,7 @@ object Dependencies {
|
||||
|
||||
lazy val r53Dependencies = Seq(
|
||||
"com.amazonaws" % "aws-java-sdk-core" % awsV withSources(),
|
||||
"com.amazonaws" % "aws-java-sdk-sts" % awsV withSources(),
|
||||
"com.amazonaws" % "aws-java-sdk-route53" % awsV withSources()
|
||||
)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user