mirror of
https://github.com/VinylDNS/vinyldns
synced 2025-08-30 22:05:21 +00:00
Merge branch 'master' into fix_change_failure_metrics
This commit is contained in:
@@ -256,7 +256,7 @@ class MembershipService(
|
||||
_ <- isGroupChangePresent(result).toResult
|
||||
_ <- canSeeGroup(result.get.newGroup.id, authPrincipal).toResult
|
||||
allUserIds = getGroupUserIds(Seq(result.get))
|
||||
allUserMap <- getUsers(allUserIds).map(_.users.map(x => x.id -> x.userName).toMap)
|
||||
allUserMap <- getUsers(allUserIds).map(_.users.map(x => x.id -> x.userName).toMap.withDefaultValue("unknown user"))
|
||||
groupChangeMessage <- determineGroupDifference(Seq(result.get), allUserMap)
|
||||
groupChanges = (groupChangeMessage, Seq(result.get)).zipped.map{ (a, b) => b.copy(groupChangeMessage = Some(a)) }
|
||||
userIds = Seq(result.get).map(_.userId).toSet
|
||||
@@ -276,7 +276,7 @@ class MembershipService(
|
||||
.getGroupChanges(groupId, startFrom, maxItems)
|
||||
.toResult[ListGroupChangesResults]
|
||||
allUserIds = getGroupUserIds(result.changes)
|
||||
allUserMap <- getUsers(allUserIds).map(_.users.map(x => x.id -> x.userName).toMap)
|
||||
allUserMap <- getUsers(allUserIds).map(_.users.map(x => x.id -> x.userName).toMap.withDefaultValue("unknown user"))
|
||||
groupChangeMessage <- determineGroupDifference(result.changes, allUserMap)
|
||||
groupChanges = (groupChangeMessage, result.changes).zipped.map{ (a, b) => b.copy(groupChangeMessage = Some(a)) }
|
||||
userIds = result.changes.map(_.userId).toSet
|
||||
@@ -316,7 +316,8 @@ class MembershipService(
|
||||
sb.append(s"Group email changed to '${change.newGroup.email}'. ")
|
||||
}
|
||||
if (change.oldGroup.get.description != change.newGroup.description) {
|
||||
sb.append(s"Group description changed to '${change.newGroup.description.get}'. ")
|
||||
val description = if(change.newGroup.description.isEmpty) "" else change.newGroup.description.get
|
||||
sb.append(s"Group description changed to '$description'. ")
|
||||
}
|
||||
val adminAddDifference = change.newGroup.adminUserIds.diff(change.oldGroup.get.adminUserIds)
|
||||
if (adminAddDifference.nonEmpty) {
|
||||
|
@@ -42,6 +42,29 @@ object ListRecordSetChangesResponse {
|
||||
)
|
||||
}
|
||||
|
||||
case class ListRecordSetHistoryResponse(
|
||||
zoneId: Option[String],
|
||||
recordSetChanges: List[RecordSetChangeInfo] = Nil,
|
||||
nextId: Option[Int],
|
||||
startFrom: Option[Int],
|
||||
maxItems: Int
|
||||
)
|
||||
|
||||
object ListRecordSetHistoryResponse {
|
||||
def apply(
|
||||
zoneId: Option[String],
|
||||
listResults: ListRecordSetChangesResults,
|
||||
info: List[RecordSetChangeInfo]
|
||||
): ListRecordSetHistoryResponse =
|
||||
ListRecordSetHistoryResponse(
|
||||
zoneId,
|
||||
info,
|
||||
listResults.nextId,
|
||||
listResults.startFrom,
|
||||
listResults.maxItems
|
||||
)
|
||||
}
|
||||
|
||||
case class ListFailedRecordSetChangesResponse(
|
||||
failedRecordSetChanges: List[RecordSetChange] = Nil,
|
||||
nextId: Int,
|
||||
|
@@ -576,20 +576,38 @@ class RecordSetService(
|
||||
} yield change
|
||||
|
||||
def listRecordSetChanges(
|
||||
zoneId: String,
|
||||
zoneId: Option[String] = None,
|
||||
startFrom: Option[Int] = None,
|
||||
maxItems: Int = 100,
|
||||
fqdn: Option[String] = None,
|
||||
recordType: Option[RecordType] = None,
|
||||
authPrincipal: AuthPrincipal
|
||||
): Result[ListRecordSetChangesResponse] =
|
||||
for {
|
||||
zone <- getZone(zoneId.get)
|
||||
_ <- canSeeZone(authPrincipal, zone).toResult
|
||||
recordSetChangesResults <- recordChangeRepository
|
||||
.listRecordSetChanges(Some(zone.id), startFrom, maxItems, fqdn, recordType)
|
||||
.toResult[ListRecordSetChangesResults]
|
||||
recordSetChangesInfo <- buildRecordSetChangeInfo(recordSetChangesResults.items)
|
||||
} yield ListRecordSetChangesResponse(zoneId.get, recordSetChangesResults, recordSetChangesInfo)
|
||||
|
||||
def listRecordSetChangeHistory(
|
||||
zoneId: Option[String] = None,
|
||||
startFrom: Option[Int] = None,
|
||||
maxItems: Int = 100,
|
||||
fqdn: Option[String] = None,
|
||||
recordType: Option[RecordType] = None,
|
||||
authPrincipal: AuthPrincipal
|
||||
): Result[ListRecordSetHistoryResponse] =
|
||||
for {
|
||||
zone <- getZone(zoneId)
|
||||
_ <- canSeeZone(authPrincipal, zone).toResult
|
||||
recordSetChangesResults <- recordChangeRepository
|
||||
.listRecordSetChanges(zone.id, startFrom, maxItems)
|
||||
.listRecordSetChanges(zoneId, startFrom, maxItems, fqdn, recordType)
|
||||
.toResult[ListRecordSetChangesResults]
|
||||
recordSetChangesInfo <- buildRecordSetChangeInfo(recordSetChangesResults.items)
|
||||
} yield ListRecordSetChangesResponse(zoneId, recordSetChangesResults, recordSetChangesInfo)
|
||||
|
||||
_ <- if(recordSetChangesResults.items.nonEmpty) canSeeZone(authPrincipal, recordSetChangesInfo.map(_.zone).head).toResult else ().toResult
|
||||
zoneId = if(recordSetChangesResults.items.nonEmpty) Some(recordSetChangesResults.items.map(x => x.zone.id).head) else None
|
||||
} yield ListRecordSetHistoryResponse(zoneId, recordSetChangesResults, recordSetChangesInfo)
|
||||
|
||||
def listFailedRecordSetChanges(
|
||||
authPrincipal: AuthPrincipal,
|
||||
|
@@ -101,12 +101,23 @@ trait RecordSetServiceAlgebra {
|
||||
): Result[RecordSetChange]
|
||||
|
||||
def listRecordSetChanges(
|
||||
zoneId: String,
|
||||
zoneId: Option[String],
|
||||
startFrom: Option[Int],
|
||||
maxItems: Int,
|
||||
fqdn: Option[String],
|
||||
recordType: Option[RecordType],
|
||||
authPrincipal: AuthPrincipal
|
||||
): Result[ListRecordSetChangesResponse]
|
||||
|
||||
def listRecordSetChangeHistory(
|
||||
zoneId: Option[String],
|
||||
startFrom: Option[Int],
|
||||
maxItems: Int,
|
||||
fqdn: Option[String],
|
||||
recordType: Option[RecordType],
|
||||
authPrincipal: AuthPrincipal
|
||||
): Result[ListRecordSetHistoryResponse]
|
||||
|
||||
def listFailedRecordSetChanges(
|
||||
authPrincipal: AuthPrincipal,
|
||||
startFrom: Int,
|
||||
|
@@ -78,6 +78,28 @@ object ZoneInfo {
|
||||
)
|
||||
}
|
||||
|
||||
case class ZoneDetails(
|
||||
name: String,
|
||||
email: String,
|
||||
status: ZoneStatus,
|
||||
adminGroupId: String,
|
||||
adminGroupName: String,
|
||||
)
|
||||
|
||||
object ZoneDetails {
|
||||
def apply(
|
||||
zone: Zone,
|
||||
groupName: String,
|
||||
): ZoneDetails =
|
||||
ZoneDetails(
|
||||
name = zone.name,
|
||||
email = zone.email,
|
||||
status = zone.status,
|
||||
adminGroupId = zone.adminGroupId,
|
||||
adminGroupName = groupName,
|
||||
)
|
||||
}
|
||||
|
||||
case class ZoneSummaryInfo(
|
||||
name: String,
|
||||
email: String,
|
||||
|
@@ -153,6 +153,12 @@ class ZoneService(
|
||||
accessLevel = getZoneAccess(auth, zone)
|
||||
} yield ZoneInfo(zone, aclInfo, groupName, accessLevel)
|
||||
|
||||
def getCommonZoneDetails(zoneId: String, auth: AuthPrincipal): Result[ZoneDetails] =
|
||||
for {
|
||||
zone <- getZoneOrFail(zoneId)
|
||||
groupName <- getGroupName(zone.adminGroupId)
|
||||
} yield ZoneDetails(zone, groupName)
|
||||
|
||||
def getZoneByName(zoneName: String, auth: AuthPrincipal): Result[ZoneInfo] =
|
||||
for {
|
||||
zone <- getZoneByNameOrFail(ensureTrailingDot(zoneName))
|
||||
|
@@ -35,6 +35,8 @@ trait ZoneServiceAlgebra {
|
||||
|
||||
def getZone(zoneId: String, auth: AuthPrincipal): Result[ZoneInfo]
|
||||
|
||||
def getCommonZoneDetails(zoneId: String, auth: AuthPrincipal): Result[ZoneDetails]
|
||||
|
||||
def getZoneByName(zoneName: String, auth: AuthPrincipal): Result[ZoneInfo]
|
||||
|
||||
def listZones(
|
||||
|
@@ -223,8 +223,8 @@ class RecordSetRoute(
|
||||
} ~
|
||||
path("zones" / Segment / "recordsetchanges") { zoneId =>
|
||||
(get & monitor("Endpoint.listRecordSetChanges")) {
|
||||
parameters("startFrom".as[Int].?, "maxItems".as[Int].?(DEFAULT_MAX_ITEMS)) {
|
||||
(startFrom: Option[Int], maxItems: Int) =>
|
||||
parameters("startFrom".as[Int].?, "maxItems".as[Int].?(DEFAULT_MAX_ITEMS), "fqdn".as[String].?, "recordType".as[String].?) {
|
||||
(startFrom: Option[Int], maxItems: Int, fqdn: Option[String], _: Option[String]) =>
|
||||
handleRejections(invalidQueryHandler) {
|
||||
validate(
|
||||
check = 0 < maxItems && maxItems <= DEFAULT_MAX_ITEMS,
|
||||
@@ -233,7 +233,34 @@ class RecordSetRoute(
|
||||
) {
|
||||
authenticateAndExecute(
|
||||
recordSetService
|
||||
.listRecordSetChanges(zoneId, startFrom, maxItems, _)
|
||||
.listRecordSetChanges(Some(zoneId), startFrom, maxItems, fqdn, None, _)
|
||||
) { changes =>
|
||||
complete(StatusCodes.OK, changes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} ~
|
||||
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)
|
||||
}
|
||||
|
@@ -29,6 +29,7 @@ import vinyldns.core.domain.zone._
|
||||
import scala.concurrent.duration._
|
||||
|
||||
case class GetZoneResponse(zone: ZoneInfo)
|
||||
case class GetZoneDetailsResponse(zone: ZoneDetails)
|
||||
case class ZoneRejected(zone: Zone, errors: List[String])
|
||||
|
||||
class ZoneRoute(
|
||||
@@ -142,6 +143,13 @@ class ZoneRoute(
|
||||
}
|
||||
}
|
||||
} ~
|
||||
path("zones" / Segment / "details") { id =>
|
||||
(get & monitor("Endpoint.getCommonZoneDetails")) {
|
||||
authenticateAndExecute(zoneService.getCommonZoneDetails(id, _)) { zone =>
|
||||
complete(StatusCodes.OK, GetZoneDetailsResponse(zone))
|
||||
}
|
||||
}
|
||||
} ~
|
||||
path("zones" / Segment / "sync") { id =>
|
||||
(post & monitor("Endpoint.syncZone")) {
|
||||
authenticateAndExecute(zoneService.syncZone(id, _)) { chg =>
|
||||
|
@@ -30,6 +30,38 @@ def check_changes_response(response, recordChanges=False, nextId=False, startFro
|
||||
assert_that(change["userName"], is_("history-user"))
|
||||
|
||||
|
||||
def check_change_history_response(response, fqdn, type, recordChanges=False, nextId=False, startFrom=False, maxItems=100):
|
||||
"""
|
||||
:param type: type of the record
|
||||
:param fqdn: fqdn of the record
|
||||
:param response: return value of list_recordset_changes()
|
||||
:param recordChanges: true if not empty or False if empty, cannot check exact values because don't have access to all attributes
|
||||
:param nextId: true if exists, false if doesn't, wouldn't be able to check exact value
|
||||
:param startFrom: the string for startFrom or false if doesnt exist
|
||||
:param maxItems: maxItems is defined as an Int by default so will always return an Int
|
||||
"""
|
||||
assert_that(response, has_key("zoneId")) # always defined as random string
|
||||
if recordChanges:
|
||||
assert_that(response["recordSetChanges"], is_not(has_length(0)))
|
||||
else:
|
||||
assert_that(response["recordSetChanges"], has_length(0))
|
||||
if nextId:
|
||||
assert_that(response, has_key("nextId"))
|
||||
else:
|
||||
assert_that(response, is_not(has_key("nextId")))
|
||||
if startFrom:
|
||||
assert_that(response["startFrom"], is_(startFrom))
|
||||
else:
|
||||
assert_that(response, is_not(has_key("startFrom")))
|
||||
assert_that(response["maxItems"], is_(maxItems))
|
||||
|
||||
for change in response["recordSetChanges"]:
|
||||
assert_that(change["userName"], is_("history-user"))
|
||||
for recordset in change["recordSet"]:
|
||||
assert_that(change["recordSet"]["type"], is_(type))
|
||||
assert_that(change["recordSet"]["name"] + "." + response["recordSetChanges"][0]["zone"]["name"], is_(fqdn))
|
||||
|
||||
|
||||
def test_list_recordset_changes_no_authorization(shared_zone_test_context):
|
||||
"""
|
||||
Test that recordset changes without authorization fails
|
||||
@@ -174,3 +206,101 @@ def test_list_recordset_changes_max_items_boundaries(shared_zone_test_context):
|
||||
|
||||
assert_that(too_large, is_("maxItems was 101, maxItems must be between 0 exclusive and 100 inclusive"))
|
||||
assert_that(too_small, is_("maxItems was 0, maxItems must be between 0 exclusive and 100 inclusive"))
|
||||
|
||||
|
||||
def test_list_recordset_history_no_authorization(shared_zone_test_context):
|
||||
"""
|
||||
Test that recordset history without authorization fails
|
||||
"""
|
||||
client = shared_zone_test_context.history_client
|
||||
fqdn = "test-create-cname-ok.system-test-history1."
|
||||
type = "CNAME"
|
||||
client.list_recordset_change_history(fqdn, type, sign_request=False, status=401)
|
||||
|
||||
|
||||
def test_list_recordset_history_member_auth_success(shared_zone_test_context):
|
||||
"""
|
||||
Test recordset history succeeds with membership auth for member of admin group
|
||||
"""
|
||||
client = shared_zone_test_context.history_client
|
||||
fqdn = "test-create-cname-ok.system-test-history1."
|
||||
type = "CNAME"
|
||||
response = client.list_recordset_change_history(fqdn, type, status=200)
|
||||
check_change_history_response(response, fqdn, type, recordChanges=True, startFrom=False, nextId=False)
|
||||
|
||||
|
||||
def test_list_recordset_history_member_auth_no_access(shared_zone_test_context):
|
||||
"""
|
||||
Test recordset history fails for user not in admin group with no acl rules
|
||||
"""
|
||||
client = shared_zone_test_context.ok_vinyldns_client
|
||||
fqdn = "test-create-cname-ok.system-test-history1."
|
||||
type = "CNAME"
|
||||
client.list_recordset_change_history(fqdn, type, status=403)
|
||||
|
||||
|
||||
def test_list_recordset_history_success(shared_zone_test_context):
|
||||
"""
|
||||
Test recordset history succeeds with membership auth for member of admin group
|
||||
"""
|
||||
client = shared_zone_test_context.history_client
|
||||
fqdn = "test-create-cname-ok.system-test-history1."
|
||||
type = "CNAME"
|
||||
response = client.list_recordset_change_history(fqdn, type, status=200)
|
||||
check_change_history_response(response, fqdn, type, recordChanges=True, startFrom=False, nextId=False)
|
||||
|
||||
|
||||
def test_list_recordset_history_paging(shared_zone_test_context):
|
||||
"""
|
||||
Test paging for recordset history can use previous nextId as start key of next page
|
||||
"""
|
||||
client = shared_zone_test_context.history_client
|
||||
|
||||
fqdn = "test-create-cname-ok.system-test-history1."
|
||||
type = "CNAME"
|
||||
|
||||
response_1 = client.list_recordset_change_history(fqdn, type, start_from=None, max_items=1)
|
||||
response_2 = client.list_recordset_change_history(fqdn, type, start_from=response_1["nextId"], max_items=1)
|
||||
|
||||
check_change_history_response(response_1, fqdn, type, recordChanges=True, nextId=True, startFrom=False, maxItems=1)
|
||||
check_change_history_response(response_2, fqdn, type, recordChanges=True, nextId=True, startFrom=response_1["nextId"], maxItems=1)
|
||||
|
||||
|
||||
def test_list_recordset_history_returning_no_changes(shared_zone_test_context):
|
||||
"""
|
||||
Pass in startFrom of "2000" should return empty list because start key exceeded number of recordset change history
|
||||
"""
|
||||
client = shared_zone_test_context.history_client
|
||||
fqdn = "test-create-cname-ok.system-test-history1."
|
||||
type = "CNAME"
|
||||
response = client.list_recordset_change_history(fqdn, type, start_from=2000, max_items=None)
|
||||
assert_that(response["recordSetChanges"], has_length(0))
|
||||
assert_that(response["startFrom"], is_(2000))
|
||||
assert_that(response["maxItems"], is_(100))
|
||||
|
||||
|
||||
def test_list_recordset_history_default_max_items(shared_zone_test_context):
|
||||
"""
|
||||
Test default max items is 100
|
||||
"""
|
||||
client = shared_zone_test_context.history_client
|
||||
fqdn = "test-create-cname-ok.system-test-history1."
|
||||
type = "CNAME"
|
||||
|
||||
response = client.list_recordset_change_history(fqdn, type, start_from=None, max_items=None)
|
||||
check_change_history_response(response, fqdn, type, recordChanges=True, startFrom=False, nextId=False, maxItems=100)
|
||||
|
||||
|
||||
def test_list_recordset_history_max_items_boundaries(shared_zone_test_context):
|
||||
"""
|
||||
Test 0 < max_items <= 100
|
||||
"""
|
||||
client = shared_zone_test_context.history_client
|
||||
fqdn = "test-create-cname-ok.system-test-history1."
|
||||
type = "CNAME"
|
||||
|
||||
too_large = client.list_recordset_change_history(fqdn, type, start_from=None, max_items=101, status=400)
|
||||
too_small = client.list_recordset_change_history(fqdn, type, start_from=None, max_items=0, status=400)
|
||||
|
||||
assert_that(too_large, is_("maxItems was 101, maxItems must be between 0 exclusive and 100 inclusive"))
|
||||
assert_that(too_small, is_("maxItems was 0, maxItems must be between 0 exclusive and 100 inclusive"))
|
||||
|
@@ -46,6 +46,7 @@ def test_get_zone_shared_by_id_non_owner(shared_zone_test_context):
|
||||
assert_that(retrieved["shared"], is_(True))
|
||||
assert_that(retrieved["accessLevel"], is_("Read"))
|
||||
|
||||
|
||||
def test_get_zone_private_by_id_fails_without_access(shared_zone_test_context):
|
||||
"""
|
||||
Test get an existing zone by id without access
|
||||
@@ -72,6 +73,29 @@ def test_get_zone_by_id_no_authorization(shared_zone_test_context):
|
||||
client.get_zone("123456", sign_request=False, status=401)
|
||||
|
||||
|
||||
def test_get_common_zone_details_by_id(shared_zone_test_context):
|
||||
"""
|
||||
Test get an existing zone's common details by id
|
||||
"""
|
||||
client = shared_zone_test_context.ok_vinyldns_client
|
||||
|
||||
result = client.get_common_zone_details(shared_zone_test_context.system_test_zone["id"], status=200)
|
||||
retrieved = result["zone"]
|
||||
|
||||
assert_that(retrieved["name"], is_(shared_zone_test_context.system_test_zone["name"]))
|
||||
assert_that(retrieved["email"], is_(shared_zone_test_context.system_test_zone["email"]))
|
||||
assert_that(retrieved["adminGroupName"], is_(shared_zone_test_context.ok_group["name"]))
|
||||
assert_that(retrieved["adminGroupId"], is_(shared_zone_test_context.ok_group["id"]))
|
||||
|
||||
|
||||
def test_get_common_zone_details_by_id_returns_404_when_not_found(shared_zone_test_context):
|
||||
"""
|
||||
Test get an existing zone returns a 404 when the zone is not found
|
||||
"""
|
||||
client = shared_zone_test_context.ok_vinyldns_client
|
||||
client.get_common_zone_details(str(uuid.uuid4()), status=404)
|
||||
|
||||
|
||||
@pytest.mark.serial
|
||||
def test_get_zone_by_id_includes_acl_display_name(shared_zone_test_context):
|
||||
"""
|
||||
|
@@ -51,7 +51,8 @@ class VinylDNSClient(object):
|
||||
self.session.close()
|
||||
self.session_not_found_ok.close()
|
||||
|
||||
def requests_retry_not_found_ok_session(self, retries=20, backoff_factor=0.1, status_forcelist=(500, 502, 504), session=None):
|
||||
def requests_retry_not_found_ok_session(self, retries=20, backoff_factor=0.1, status_forcelist=(500, 502, 504),
|
||||
session=None):
|
||||
session = session or requests.Session()
|
||||
retry = Retry(
|
||||
total=retries,
|
||||
@@ -79,7 +80,8 @@ class VinylDNSClient(object):
|
||||
session.mount("https://", adapter)
|
||||
return session
|
||||
|
||||
def make_request(self, url, method="GET", headers=None, body_string=None, sign_request=True, not_found_ok=False, **kwargs):
|
||||
def make_request(self, url, method="GET", headers=None, body_string=None, sign_request=True, not_found_ok=False,
|
||||
**kwargs):
|
||||
|
||||
# pull out status or None
|
||||
status_code = kwargs.pop("status", None)
|
||||
@@ -100,13 +102,15 @@ class VinylDNSClient(object):
|
||||
for k, v in query.items())
|
||||
|
||||
if sign_request:
|
||||
signed_headers, signed_body = self.sign_request(method, path, body_string, query, with_headers=headers or {}, **kwargs)
|
||||
signed_headers, signed_body = self.sign_request(method, path, body_string, query,
|
||||
with_headers=headers or {}, **kwargs)
|
||||
else:
|
||||
signed_headers = headers or {}
|
||||
signed_body = body_string
|
||||
|
||||
if not_found_ok:
|
||||
response = self.session_not_found_ok.request(method, url, data=signed_body, headers=signed_headers, **kwargs)
|
||||
response = self.session_not_found_ok.request(method, url, data=signed_body, headers=signed_headers,
|
||||
**kwargs)
|
||||
else:
|
||||
response = self.session.request(method, url, data=signed_body, headers=signed_headers, **kwargs)
|
||||
|
||||
@@ -388,6 +392,17 @@ class VinylDNSClient(object):
|
||||
|
||||
return data
|
||||
|
||||
def get_common_zone_details(self, zone_id, **kwargs):
|
||||
"""
|
||||
Gets common zone details which can be seen by all users for the given zone id
|
||||
:param zone_id: the id of the zone to retrieve
|
||||
:return: the zone, or will 404 if not found
|
||||
"""
|
||||
url = urljoin(self.index_url, "/zones/{0}/details".format(zone_id))
|
||||
response, data = self.make_request(url, "GET", self.headers, not_found_ok=True, **kwargs)
|
||||
|
||||
return data
|
||||
|
||||
def get_zone_by_name(self, zone_name, **kwargs):
|
||||
"""
|
||||
Gets a zone for the given zone name
|
||||
@@ -445,7 +460,29 @@ class VinylDNSClient(object):
|
||||
response, data = self.make_request(url, "GET", self.headers, not_found_ok=True, **kwargs)
|
||||
return data
|
||||
|
||||
def list_zones(self, name_filter=None, start_from=None, max_items=None, search_by_admin_group=False, ignore_access=False, **kwargs):
|
||||
def list_recordset_change_history(self, fqdn, record_type, start_from=None, max_items=None, **kwargs):
|
||||
"""
|
||||
Gets the record's change history for the given record fqdn and record type
|
||||
:param fqdn: the record's fqdn
|
||||
:param record_type: the record's type
|
||||
:param start_from: the start key of the page
|
||||
:param max_items: the page limit
|
||||
:return: the zone, or will 404 if not found
|
||||
"""
|
||||
args = []
|
||||
if start_from:
|
||||
args.append("startFrom={0}".format(start_from))
|
||||
if max_items is not None:
|
||||
args.append("maxItems={0}".format(max_items))
|
||||
args.append("fqdn={0}".format(fqdn))
|
||||
args.append("recordType={0}".format(record_type))
|
||||
url = urljoin(self.index_url, "recordsetchange/history") + "?" + "&".join(args)
|
||||
|
||||
response, data = self.make_request(url, "GET", self.headers, not_found_ok=True, **kwargs)
|
||||
return data
|
||||
|
||||
def list_zones(self, name_filter=None, start_from=None, max_items=None, search_by_admin_group=False,
|
||||
ignore_access=False, **kwargs):
|
||||
"""
|
||||
Gets a list of zones that currently exist
|
||||
:return: a list of zones
|
||||
@@ -535,7 +572,8 @@ class VinylDNSClient(object):
|
||||
response, data = self.make_request(url, "GET", self.headers, None, not_found_ok=True, **kwargs)
|
||||
return data
|
||||
|
||||
def list_recordsets_by_zone(self, zone_id, start_from=None, max_items=None, record_name_filter=None, record_type_filter=None, name_sort=None, **kwargs):
|
||||
def list_recordsets_by_zone(self, zone_id, start_from=None, max_items=None, record_name_filter=None,
|
||||
record_type_filter=None, name_sort=None, **kwargs):
|
||||
"""
|
||||
Retrieves all recordsets in a zone
|
||||
:param zone_id: the zone to retrieve
|
||||
@@ -618,7 +656,8 @@ class VinylDNSClient(object):
|
||||
_, data = self.make_request(url, "POST", self.headers, **kwargs)
|
||||
return data
|
||||
|
||||
def list_batch_change_summaries(self, start_from=None, max_items=None, ignore_access=False, approval_status=None, **kwargs):
|
||||
def list_batch_change_summaries(self, start_from=None, max_items=None, ignore_access=False, approval_status=None,
|
||||
**kwargs):
|
||||
"""
|
||||
Gets list of user's batch change summaries
|
||||
:return: the content of the response
|
||||
@@ -660,7 +699,8 @@ class VinylDNSClient(object):
|
||||
:return: the content of the response
|
||||
"""
|
||||
url = urljoin(self.index_url, "/zones/{0}/acl/rules".format(zone_id))
|
||||
response, data = self.make_request(url, "PUT", self.headers, json.dumps(acl_rule), sign_request=sign_request, **kwargs)
|
||||
response, data = self.make_request(url, "PUT", self.headers, json.dumps(acl_rule), sign_request=sign_request,
|
||||
**kwargs)
|
||||
|
||||
return data
|
||||
|
||||
@@ -804,7 +844,8 @@ class VinylDNSClient(object):
|
||||
while change["status"] != expected_status and retries > 0:
|
||||
time.sleep(RETRY_WAIT)
|
||||
retries -= 1
|
||||
latest_change = self.get_recordset_change(change["recordSet"]["zoneId"], change["recordSet"]["id"], change["id"], status=(200, 404))
|
||||
latest_change = self.get_recordset_change(change["recordSet"]["zoneId"], change["recordSet"]["id"],
|
||||
change["id"], status=(200, 404))
|
||||
if type(latest_change) != str:
|
||||
change = latest_change
|
||||
|
||||
|
@@ -184,6 +184,9 @@ class RecordSetServiceSpec
|
||||
doReturn(IO.pure(ListUsersResults(Seq(), None)))
|
||||
.when(mockUserRepo)
|
||||
.getUsers(Set.empty, None, None)
|
||||
doReturn(IO.pure(Some(okGroup)))
|
||||
.when(mockGroupRepo)
|
||||
.getGroup(okGroup.id)
|
||||
|
||||
val result: RecordSetChange =
|
||||
underTest.addRecordSet(record, okAuth).map(_.asInstanceOf[RecordSetChange]).value.unsafeRunSync().toOption.get
|
||||
@@ -468,7 +471,7 @@ class RecordSetServiceSpec
|
||||
|
||||
result.recordSet.ownerGroupId shouldBe Some(okGroup.id)
|
||||
}
|
||||
"fail if user is in not owner group" in {
|
||||
"fail if user is not in owner group" in {
|
||||
val record = aaaa.copy(ownerGroupId = Some(dummyGroup.id))
|
||||
|
||||
doReturn(IO.pure(List()))
|
||||
@@ -583,6 +586,9 @@ class RecordSetServiceSpec
|
||||
doReturn(IO.pure(ListUsersResults(listOfDummyUsers.toSeq, None)))
|
||||
.when(mockUserRepo)
|
||||
.getUsers(dummyGroup.memberIds, None, None)
|
||||
doReturn(IO.pure(Some(xyzGroup)))
|
||||
.when(mockGroupRepo)
|
||||
.getGroup(xyzGroup.id)
|
||||
|
||||
// passes as all three properties within dotted hosts config (allowed zones, users and record types) are satisfied
|
||||
val result: RecordSetChange =
|
||||
@@ -787,6 +793,9 @@ class RecordSetServiceSpec
|
||||
doReturn(IO.pure(ListUsersResults(listOfDummyUsers.toSeq, None)))
|
||||
.when(mockUserRepo)
|
||||
.getUsers(dummyGroup.memberIds, None, None)
|
||||
doReturn(IO.pure(Some(abcGroup)))
|
||||
.when(mockGroupRepo)
|
||||
.getGroup(abcGroup.id)
|
||||
|
||||
// fails as only two properties within dotted hosts config (zones and record types) are satisfied while user is not allowed
|
||||
val result = underTest.addRecordSet(record, abcAuth).value.unsafeRunSync().swap.toOption.get
|
||||
@@ -1932,13 +1941,13 @@ class RecordSetServiceSpec
|
||||
|
||||
doReturn(IO.pure(ListRecordSetChangesResults(completeRecordSetChanges)))
|
||||
.when(mockRecordChangeRepo)
|
||||
.listRecordSetChanges(zoneId = okZone.id, startFrom = None, maxItems = 100)
|
||||
.listRecordSetChanges(zoneId = Some(okZone.id), startFrom = None, maxItems = 100, fqdn = None, recordType = None)
|
||||
doReturn(IO.pure(ListUsersResults(Seq(okUser), None)))
|
||||
.when(mockUserRepo)
|
||||
.getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
|
||||
|
||||
val result: ListRecordSetChangesResponse =
|
||||
underTest.listRecordSetChanges(okZone.id, authPrincipal = okAuth).value.unsafeRunSync().toOption.get
|
||||
underTest.listRecordSetChanges(Some(okZone.id), authPrincipal = okAuth).value.unsafeRunSync().toOption.get
|
||||
val changesWithName =
|
||||
completeRecordSetChanges.map(change => RecordSetChangeInfo(change, Some("ok")))
|
||||
val expectedResults = ListRecordSetChangesResponse(
|
||||
@@ -1951,13 +1960,42 @@ class RecordSetServiceSpec
|
||||
result shouldBe expectedResults
|
||||
}
|
||||
|
||||
"retrieve the recordset changes based on fqdn and record type" in {
|
||||
val filteredRecordSetChanges: List[RecordSetChange] =
|
||||
List(pendingCreateAAAA, completeCreateAAAA)
|
||||
val zoneId = filteredRecordSetChanges.head.zoneId
|
||||
|
||||
doReturn(IO.pure(ListRecordSetChangesResults(filteredRecordSetChanges)))
|
||||
.when(mockRecordChangeRepo)
|
||||
.listRecordSetChanges(zoneId = None, startFrom = None, maxItems = 100, fqdn = Some("aaaa.ok.zone.recordsets."), recordType = Some(RecordType.AAAA))
|
||||
doReturn(IO.pure(ListUsersResults(Seq(okUser), None)))
|
||||
.when(mockUserRepo)
|
||||
.getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
|
||||
|
||||
val result: ListRecordSetHistoryResponse =
|
||||
underTest.listRecordSetChangeHistory(None, fqdn = Some("aaaa.ok.zone.recordsets."), recordType = Some(RecordType.AAAA), authPrincipal = okAuth).value.unsafeRunSync().toOption.get
|
||||
val changesWithName =
|
||||
filteredRecordSetChanges.map(change => RecordSetChangeInfo(change, Some("ok")))
|
||||
val expectedResults = ListRecordSetHistoryResponse(
|
||||
zoneId = Some(zoneId),
|
||||
recordSetChanges = changesWithName,
|
||||
nextId = None,
|
||||
startFrom = None,
|
||||
maxItems = 100
|
||||
)
|
||||
result shouldBe expectedResults
|
||||
}
|
||||
|
||||
"return a zone with no changes if no changes exist" in {
|
||||
doReturn(IO.pure(ListRecordSetChangesResults(items = Nil)))
|
||||
.when(mockRecordChangeRepo)
|
||||
.listRecordSetChanges(zoneId = okZone.id, startFrom = None, maxItems = 100)
|
||||
.listRecordSetChanges(zoneId = Some(okZone.id), startFrom = None, maxItems = 100, fqdn = None, recordType = None)
|
||||
doReturn(IO.pure(ListUsersResults(Seq(okUser), None)))
|
||||
.when(mockUserRepo)
|
||||
.getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
|
||||
|
||||
val result: ListRecordSetChangesResponse =
|
||||
underTest.listRecordSetChanges(okZone.id, authPrincipal = okAuth).value.unsafeRunSync().toOption.get
|
||||
underTest.listRecordSetChanges(Some(okZone.id), authPrincipal = okAuth).value.unsafeRunSync().toOption.get
|
||||
val expectedResults = ListRecordSetChangesResponse(
|
||||
zoneId = okZone.id,
|
||||
recordSetChanges = List(),
|
||||
@@ -2023,7 +2061,7 @@ class RecordSetServiceSpec
|
||||
|
||||
"return a NotAuthorizedError" in {
|
||||
val error =
|
||||
underTest.listRecordSetChanges(zoneNotAuthorized.id, authPrincipal = okAuth).value.unsafeRunSync().swap.toOption.get
|
||||
underTest.listRecordSetChanges(Some(zoneNotAuthorized.id), authPrincipal = okAuth).value.unsafeRunSync().swap.toOption.get
|
||||
|
||||
error shouldBe a[NotAuthorizedError]
|
||||
}
|
||||
@@ -2034,10 +2072,13 @@ class RecordSetServiceSpec
|
||||
|
||||
doReturn(IO.pure(ListRecordSetChangesResults(List(rsChange2, rsChange1))))
|
||||
.when(mockRecordChangeRepo)
|
||||
.listRecordSetChanges(zoneId = okZone.id, startFrom = None, maxItems = 100)
|
||||
.listRecordSetChanges(zoneId = Some(okZone.id), startFrom = None, maxItems = 100, fqdn = None, recordType = None)
|
||||
doReturn(IO.pure(ListUsersResults(Seq(okUser), None)))
|
||||
.when(mockUserRepo)
|
||||
.getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
|
||||
|
||||
val result: ListRecordSetChangesResponse =
|
||||
underTest.listRecordSetChanges(okZone.id, authPrincipal = okAuth).value.unsafeRunSync().toOption.get
|
||||
underTest.listRecordSetChanges(Some(okZone.id), authPrincipal = okAuth).value.unsafeRunSync().toOption.get
|
||||
val changesWithName =
|
||||
List(RecordSetChangeInfo(rsChange2, Some("ok")), RecordSetChangeInfo(rsChange1, Some("ok")))
|
||||
val expectedResults = ListRecordSetChangesResponse(
|
||||
|
@@ -799,6 +799,46 @@ class ZoneServiceSpec
|
||||
}
|
||||
}
|
||||
|
||||
"Getting a zone details" should {
|
||||
"fail with no zone returned" in {
|
||||
doReturn(IO.pure(None)).when(mockZoneRepo).getZone("notAZoneId")
|
||||
|
||||
val error = underTest.getCommonZoneDetails("notAZoneId", okAuth).value.unsafeRunSync().swap.toOption.get
|
||||
error shouldBe a[ZoneNotFoundError]
|
||||
}
|
||||
|
||||
"return zone details even if the user is not authorized for the zone" in {
|
||||
doReturn(IO.pure(Some(okZone))).when(mockZoneRepo).getZone(anyString)
|
||||
|
||||
val noAuth = AuthPrincipal(TestDataLoader.okUser, Seq())
|
||||
|
||||
val result = underTest.getCommonZoneDetails(okZone.id, noAuth).value.unsafeRunSync()
|
||||
val expectedZoneDetails =
|
||||
ZoneDetails(okZone, okGroup.name)
|
||||
result.right.value shouldBe expectedZoneDetails
|
||||
}
|
||||
|
||||
"return the appropriate zone as a ZoneDetails" in {
|
||||
doReturn(IO.pure(Some(abcZone))).when(mockZoneRepo).getZone(abcZone.id)
|
||||
doReturn(IO.pure(Some(abcGroup))).when(mockGroupRepo).getGroup(anyString)
|
||||
|
||||
val expectedZoneDetails =
|
||||
ZoneDetails(abcZone, abcGroup.name)
|
||||
val result = underTest.getCommonZoneDetails(abcZone.id, abcAuth).value.unsafeRunSync()
|
||||
result.right.value shouldBe expectedZoneDetails
|
||||
}
|
||||
|
||||
"return Unknown group name if zone admin group cannot be found" in {
|
||||
doReturn(IO.pure(Some(abcZone))).when(mockZoneRepo).getZone(abcZone.id)
|
||||
doReturn(IO.pure(None)).when(mockGroupRepo).getGroup(anyString)
|
||||
|
||||
val expectedZoneDetails =
|
||||
ZoneDetails(abcZone, "Unknown group name")
|
||||
val result: ZoneDetails = underTest.getCommonZoneDetails(abcZone.id, abcAuth).value.unsafeRunSync().toOption.get
|
||||
result shouldBe expectedZoneDetails
|
||||
}
|
||||
}
|
||||
|
||||
"ListZones" should {
|
||||
"not fail with no zones returned" in {
|
||||
doReturn(IO.pure(ListZonesResults(List())))
|
||||
|
@@ -556,7 +556,8 @@ class ZoneSyncHandlerSpec
|
||||
|
||||
verify(recordChangeRepo).save(any[DB], captor.capture())
|
||||
val req = captor.getValue
|
||||
anonymize(req) shouldBe anonymize(testChangeSet)
|
||||
val expectedRecordSetChanges = testChangeSet.changes
|
||||
anonymize(req) shouldBe anonymize(testChangeSet.withRecordSetChange(expectedRecordSetChanges))
|
||||
|
||||
}
|
||||
"save the record changes to the recordSetCacheRepo" in {
|
||||
@@ -566,7 +567,8 @@ class ZoneSyncHandlerSpec
|
||||
|
||||
verify(recordSetCacheRepo).save(any[DB], captor.capture())
|
||||
val req = captor.getValue
|
||||
anonymize(req) shouldBe anonymize(testChangeSet)
|
||||
val expectedRecordSetChanges = testChangeSet.changes
|
||||
anonymize(req) shouldBe anonymize(testChangeSet.withRecordSetChange(expectedRecordSetChanges))
|
||||
|
||||
}
|
||||
|
||||
@@ -577,7 +579,8 @@ class ZoneSyncHandlerSpec
|
||||
|
||||
verify(recordSetRepo).apply(any[DB], captor.capture())
|
||||
val req = captor.getValue
|
||||
anonymize(req) shouldBe anonymize(testChangeSet)
|
||||
val expectedRecordSetChanges = testChangeSet.changes
|
||||
anonymize(req) shouldBe anonymize(testChangeSet.withRecordSetChange(expectedRecordSetChanges))
|
||||
}
|
||||
|
||||
"returns the zone as active and sets the latest sync" in {
|
||||
|
@@ -29,7 +29,7 @@ import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpec
|
||||
import vinyldns.api.Interfaces._
|
||||
import vinyldns.api.config.LimitsConfig
|
||||
import vinyldns.api.domain.record.{ListFailedRecordSetChangesResponse, ListRecordSetChangesResponse, RecordSetServiceAlgebra}
|
||||
import vinyldns.api.domain.record.{ListFailedRecordSetChangesResponse, ListRecordSetChangesResponse, ListRecordSetHistoryResponse, RecordSetServiceAlgebra}
|
||||
import vinyldns.api.domain.zone._
|
||||
import vinyldns.core.TestMembershipData.okAuth
|
||||
import vinyldns.core.domain.Fqdn
|
||||
@@ -409,6 +409,16 @@ class RecordSetRoutingSpec
|
||||
maxItems = 100
|
||||
)
|
||||
|
||||
private val changeWithUserName =
|
||||
List(RecordSetChangeInfo(rsChange1, Some("ok")))
|
||||
private val listRecordSetChangeHistoryResponse = ListRecordSetHistoryResponse(
|
||||
Some(okZone.id),
|
||||
changeWithUserName,
|
||||
nextId = None,
|
||||
startFrom = None,
|
||||
maxItems = 100
|
||||
)
|
||||
|
||||
private val failedChangesWithUserName =
|
||||
List(rsChange1.copy(status = RecordSetChangeStatus.Failed) , rsChange2.copy(status = RecordSetChangeStatus.Failed))
|
||||
private val listFailedRecordSetChangeResponse = ListFailedRecordSetChangesResponse(
|
||||
@@ -736,18 +746,35 @@ class RecordSetRoutingSpec
|
||||
}.toResult
|
||||
|
||||
def listRecordSetChanges(
|
||||
zoneId: String,
|
||||
zoneId: Option[String],
|
||||
startFrom: Option[Int],
|
||||
maxItems: Int,
|
||||
fqdn: Option[String],
|
||||
recordType: Option[RecordType],
|
||||
authPrincipal: AuthPrincipal
|
||||
): Result[ListRecordSetChangesResponse] = {
|
||||
zoneId match {
|
||||
case zoneNotFound.id => Left(ZoneNotFoundError(s"$zoneId"))
|
||||
case notAuthorizedZone.id => Left(NotAuthorizedError("no way"))
|
||||
case Some(zoneNotFound.id) => Left(ZoneNotFoundError(s"$zoneId"))
|
||||
case Some(notAuthorizedZone.id) => Left(NotAuthorizedError("no way"))
|
||||
case _ => Right(listRecordSetChangesResponse)
|
||||
}
|
||||
}.toResult
|
||||
|
||||
def listRecordSetChangeHistory(
|
||||
zoneId: Option[String],
|
||||
startFrom: Option[Int],
|
||||
maxItems: Int,
|
||||
fqdn: Option[String],
|
||||
recordType: Option[RecordType],
|
||||
authPrincipal: AuthPrincipal
|
||||
): Result[ListRecordSetHistoryResponse] = {
|
||||
zoneId match {
|
||||
case Some(zoneNotFound.id) => Left(ZoneNotFoundError(s"$zoneId"))
|
||||
case Some(notAuthorizedZone.id) => Left(NotAuthorizedError("no way"))
|
||||
case _ => Right(listRecordSetChangeHistoryResponse)
|
||||
}
|
||||
}.toResult
|
||||
|
||||
}
|
||||
|
||||
val recordSetService: RecordSetServiceAlgebra = new TestService
|
||||
@@ -883,6 +910,33 @@ class RecordSetRoutingSpec
|
||||
}
|
||||
}
|
||||
|
||||
"GET recordset change history" should {
|
||||
"return the recordset change" in {
|
||||
Get(s"/recordsetchange/history?fqdn=rs1.ok.&recordType=A") ~> recordSetRoute ~> check {
|
||||
val response = responseAs[ListRecordSetHistoryResponse]
|
||||
|
||||
response.zoneId shouldBe Some(okZone.id)
|
||||
(response.recordSetChanges.map(_.id) should contain)
|
||||
.only(rsChange1.id)
|
||||
}
|
||||
}
|
||||
|
||||
"return an error when the record fqdn and type is not defined" in {
|
||||
Get(s"/recordsetchange/history") ~> recordSetRoute ~> check {
|
||||
status shouldBe StatusCodes.BadRequest
|
||||
}
|
||||
}
|
||||
|
||||
"return a Bad Request when maxItems is out of Bounds" in {
|
||||
Get(s"/recordsetchange/history?maxItems=101") ~> recordSetRoute ~> check {
|
||||
status shouldBe StatusCodes.BadRequest
|
||||
}
|
||||
Get(s"/recordsetchange/history?maxItems=0") ~> recordSetRoute ~> check {
|
||||
status shouldBe StatusCodes.BadRequest
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"GET failed record set changes" should {
|
||||
"return the failed record set changes" in {
|
||||
val rsChangeFailed1 = rsChange1.copy(status = RecordSetChangeStatus.Failed)
|
||||
|
@@ -82,6 +82,7 @@ class ZoneRoutingSpec
|
||||
private val ok = Zone("ok.", "ok@test.com", acl = zoneAcl, adminGroupId = "test")
|
||||
private val aclAsInfo = ZoneACLInfo(zoneAcl.rules.map(ACLRuleInfo(_, Some("name"))))
|
||||
private val okAsZoneInfo = ZoneInfo(ok, aclAsInfo, okGroup.name, AccessLevel.Read)
|
||||
private val okAsZoneDetails = ZoneDetails(ok, okGroup.name)
|
||||
private val badRegex = Zone("ok.", "bad-regex@test.com", adminGroupId = "test")
|
||||
private val trailingDot = Zone("trailing.dot", "trailing-dot@test.com")
|
||||
private val connectionOk = Zone(
|
||||
@@ -249,6 +250,15 @@ class ZoneRoutingSpec
|
||||
outcome.toResult
|
||||
}
|
||||
|
||||
def getCommonZoneDetails(zoneId: String, auth: AuthPrincipal): Result[ZoneDetails] = {
|
||||
val outcome = zoneId match {
|
||||
case notFound.id => Left(ZoneNotFoundError(s"$zoneId"))
|
||||
case ok.id => Right(okAsZoneDetails)
|
||||
case error.id => Left(new RuntimeException("fail"))
|
||||
}
|
||||
outcome.toResult
|
||||
}
|
||||
|
||||
def getZoneByName(zoneName: String, auth: AuthPrincipal): Result[ZoneInfo] = {
|
||||
val outcome = zoneName match {
|
||||
case notFound.name => Left(ZoneNotFoundError(s"$zoneName"))
|
||||
@@ -890,6 +900,27 @@ class ZoneRoutingSpec
|
||||
}
|
||||
}
|
||||
|
||||
"GET zone details" should {
|
||||
"return the zone is retrieved" in {
|
||||
Get(s"/zones/${ok.id}/details") ~> zoneRoute ~> check {
|
||||
status shouldBe OK
|
||||
|
||||
val resultZone = responseAs[GetZoneDetailsResponse].zone
|
||||
resultZone.email shouldBe ok.email
|
||||
resultZone.name shouldBe ok.name
|
||||
Option(resultZone.status) shouldBe defined
|
||||
resultZone.adminGroupId shouldBe "test"
|
||||
resultZone.adminGroupName shouldBe "ok"
|
||||
}
|
||||
}
|
||||
|
||||
"return 404 if the zone does not exist" in {
|
||||
Get(s"/zones/${notFound.id}/details") ~> zoneRoute ~> check {
|
||||
status shouldBe NotFound
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"GET zone by name " should {
|
||||
"return the zone is retrieved" in {
|
||||
Get(s"/zones/name/${ok.name}") ~> zoneRoute ~> check {
|
||||
|
@@ -50,6 +50,11 @@ case class ChangeSet(
|
||||
status: ChangeSetStatus
|
||||
) {
|
||||
|
||||
def withRecordSetChange(recordSetChanges: Seq[RecordSetChange]): ChangeSet =
|
||||
copy(
|
||||
changes = recordSetChanges
|
||||
)
|
||||
|
||||
def complete(change: RecordSetChange): ChangeSet = {
|
||||
val updatedChanges = this.changes.filterNot(_.id == change.id) :+ change
|
||||
if (isFinished)
|
||||
|
@@ -18,6 +18,7 @@ package vinyldns.core.domain.record
|
||||
|
||||
import cats.effect._
|
||||
import scalikejdbc.DB
|
||||
import vinyldns.core.domain.record.RecordType.RecordType
|
||||
import vinyldns.core.repository.Repository
|
||||
|
||||
trait RecordChangeRepository extends Repository {
|
||||
@@ -25,9 +26,11 @@ trait RecordChangeRepository extends Repository {
|
||||
def save(db: DB, changeSet: ChangeSet): IO[ChangeSet]
|
||||
|
||||
def listRecordSetChanges(
|
||||
zoneId: String,
|
||||
zoneId: Option[String],
|
||||
startFrom: Option[Int] = None,
|
||||
maxItems: Int = 100
|
||||
maxItems: Int = 100,
|
||||
fqdn: Option[String] = None,
|
||||
recordType: Option[RecordType] = None
|
||||
): IO[ListRecordSetChangesResults]
|
||||
|
||||
def getRecordSetChange(zoneId: String, changeId: String): IO[Option[RecordSetChange]]
|
||||
|
@@ -15,12 +15,13 @@ section: "operator_menu"
|
||||
- [Queue Configuration](#queue-configuration)
|
||||
- [Database Configuration](#database-configuration)
|
||||
- [Cryptography](#cryptography-settings)
|
||||
- [Zone Connections](#zone-connections)
|
||||
- [Additional Configuration Settings](#additional-configuration-settings)
|
||||
- [Full Example Config](#full-example-config)
|
||||
|
||||
There are a lot of configuration settings in VinylDNS. So much so that it may seem overwhelming to configure vinyldns to
|
||||
your environment. This document describes the configuration settings, highlighting the settings you are _most likely to
|
||||
change_. All of the configuration settings are captured at the end.
|
||||
change_. All the configuration settings are captured at the end.
|
||||
|
||||
It is important to note that the `api` and `portal` have _different_ configuration. We will review the configuration for
|
||||
each separately.
|
||||
@@ -271,7 +272,7 @@ vinyldns {
|
||||
}
|
||||
```
|
||||
|
||||
## Default Zone Connections
|
||||
## Zone Connections
|
||||
|
||||
VinylDNS has three ways of indicating zone connections:
|
||||
|
||||
@@ -291,6 +292,7 @@ VinylDNS also ties in testing network connectivity to the default zone connectio
|
||||
checks. A value for the health check connection timeout in milliseconds can be specified using `health-check-timeout`; a
|
||||
default value of 10000 will be used if not provided.
|
||||
|
||||
### Global Zone Connections Configuration:
|
||||
```yaml
|
||||
vinyldns {
|
||||
|
||||
@@ -347,6 +349,93 @@ vinyldns {
|
||||
]
|
||||
```
|
||||
|
||||
### Alternate Zone Connections Configuration:
|
||||
Below is an alternate way of setting zone connections configuration instead of using the [Global Zone Connections
|
||||
Configuration](#global-zone-connections-configuration)
|
||||
```yaml
|
||||
# configured backend providers
|
||||
backend {
|
||||
# Use "default" when dns backend legacy = true
|
||||
# otherwise, use the id of one of the connections in any of your backends
|
||||
default-backend-id = "default"
|
||||
|
||||
# this is where we can save additional backends
|
||||
backend-providers = [
|
||||
{
|
||||
class-name = "vinyldns.api.backend.dns.DnsBackendProviderLoader"
|
||||
settings = {
|
||||
legacy = false
|
||||
backends = [
|
||||
{
|
||||
id = "default"
|
||||
zone-connection = {
|
||||
name = "vinyldns."
|
||||
key-name = "vinyldns."
|
||||
key = "nzisn+4G2ldMn0q1CV3vsg=="
|
||||
primary-server = "127.0.0.1:19001"
|
||||
}
|
||||
transfer-connection = {
|
||||
name = "vinyldns."
|
||||
key-name = "vinyldns."
|
||||
key = "nzisn+4G2ldMn0q1CV3vsg=="
|
||||
primary-server = "127.0.0.1:19001"
|
||||
},
|
||||
tsig-usage = "always"
|
||||
},
|
||||
{
|
||||
id = "func-test-backend"
|
||||
zone-connection = {
|
||||
name = "vinyldns."
|
||||
key-name = "vinyldns."
|
||||
key = "nzisn+4G2ldMn0q1CV3vsg=="
|
||||
primary-server = "127.0.0.1:19001"
|
||||
}
|
||||
transfer-connection = {
|
||||
name = "vinyldns."
|
||||
key-name = "vinyldns."
|
||||
key = "nzisn+4G2ldMn0q1CV3vsg=="
|
||||
primary-server = "127.0.0.1:19001"
|
||||
},
|
||||
tsig-usage = "always"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Below is an example configuration of backend provider for AWS Route 53, in case we want to use AWS Route 53 as backend.
|
||||
```yaml
|
||||
backend {
|
||||
default-backend-id = "r53"
|
||||
|
||||
backend-providers = [
|
||||
{
|
||||
class-name = "vinyldns.route53.backend.Route53BackendProviderLoader"
|
||||
settings = {
|
||||
backends = [
|
||||
{
|
||||
# AWS access key and secret key.
|
||||
access-key = "your-access-key"
|
||||
secret-key = "your-secret-key"
|
||||
|
||||
# Regional endpoint to make your requests (eg. 'us-west-2', 'us-east-1', etc.). This is the region where your queue is housed.
|
||||
signing-region = "us-east-1"
|
||||
|
||||
# Endpoint to access r53
|
||||
service-endpoint = "https://route53.amazonaws.com/"
|
||||
|
||||
id = "r53"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
Make sure to add AWS name servers in [Approved Name Servers Config](#approved-name-servers).
|
||||
|
||||
## Additional Configuration Settings
|
||||
|
||||
### Approved Name Servers
|
||||
|
@@ -22,10 +22,12 @@ import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpec
|
||||
import scalikejdbc._
|
||||
import vinyldns.core.domain.record.{ChangeSet, RecordChangeRepository, RecordSetChange, RecordSetChangeStatus, RecordSetChangeType}
|
||||
import vinyldns.core.domain.record.{ChangeSet, RecordChangeRepository, RecordSetChange, RecordSetChangeStatus, RecordSetChangeType, RecordType}
|
||||
import vinyldns.core.domain.zone.Zone
|
||||
import vinyldns.mysql.TestMySqlInstance
|
||||
import vinyldns.mysql.TransactionProvider
|
||||
import java.time.Instant
|
||||
|
||||
class MySqlRecordChangeRepositoryIntegrationSpec
|
||||
extends AnyWordSpec
|
||||
with Matchers
|
||||
@@ -60,6 +62,15 @@ class MySqlRecordChangeRepositoryIntegrationSpec
|
||||
newRecordSets.map(makeTestAddChange(_, zone)).toList
|
||||
}
|
||||
|
||||
def generateSameInserts(zone: Zone, count: Int): List[RecordSetChange] = {
|
||||
val newRecordSets =
|
||||
for {
|
||||
i <- 1 to count
|
||||
} yield aaaa.copy(zoneId = zone.id, name = s"apply-test", id = UUID.randomUUID().toString, created = Instant.now.plusSeconds(i))
|
||||
|
||||
newRecordSets.map(makeTestAddChange(_, zone)).toList
|
||||
}
|
||||
|
||||
def generateFailedInserts(zone: Zone, count: Int): List[RecordSetChange] = {
|
||||
val newRecordSets =
|
||||
for {
|
||||
@@ -102,7 +113,7 @@ class MySqlRecordChangeRepositoryIntegrationSpec
|
||||
repo.save(db, ChangeSet(inserts))
|
||||
}
|
||||
saveRecChange.attempt.unsafeRunSync() shouldBe right
|
||||
val result = repo.listRecordSetChanges(okZone.id, None, 5).unsafeRunSync()
|
||||
val result = repo.listRecordSetChanges(Some(okZone.id), None, 5).unsafeRunSync()
|
||||
result.nextId shouldBe defined
|
||||
result.maxItems shouldBe 5
|
||||
(result.items should have).length(5)
|
||||
@@ -121,21 +132,48 @@ class MySqlRecordChangeRepositoryIntegrationSpec
|
||||
repo.save(db, ChangeSet(timeSpaced))
|
||||
}
|
||||
saveRecChange.attempt.unsafeRunSync() shouldBe right
|
||||
val page1 = repo.listRecordSetChanges(okZone.id, None, 2).unsafeRunSync()
|
||||
val page1 = repo.listRecordSetChanges(Some(okZone.id), None, 2).unsafeRunSync()
|
||||
page1.nextId shouldBe Some(2)
|
||||
page1.maxItems shouldBe 2
|
||||
(page1.items should contain).theSameElementsInOrderAs(expectedOrder.take(2))
|
||||
|
||||
val page2 = repo.listRecordSetChanges(okZone.id, page1.nextId, 2).unsafeRunSync()
|
||||
val page2 = repo.listRecordSetChanges(Some(okZone.id), page1.nextId, 2).unsafeRunSync()
|
||||
page2.nextId shouldBe Some(4)
|
||||
page2.maxItems shouldBe 2
|
||||
(page2.items should contain).theSameElementsInOrderAs(expectedOrder.slice(2, 4))
|
||||
|
||||
val page3 = repo.listRecordSetChanges(okZone.id, page2.nextId, 2).unsafeRunSync()
|
||||
val page3 = repo.listRecordSetChanges(Some(okZone.id), page2.nextId, 2).unsafeRunSync()
|
||||
page3.nextId shouldBe None
|
||||
page3.maxItems shouldBe 2
|
||||
page3.items should contain theSameElementsAs expectedOrder.slice(4, 5)
|
||||
}
|
||||
"list a particular recordset's changes by fqdn and record type" in {
|
||||
val inserts = generateInserts(okZone, 10)
|
||||
val saveRecChange = executeWithinTransaction { db: DB =>
|
||||
repo.save(db, ChangeSet(inserts))
|
||||
}
|
||||
saveRecChange.attempt.unsafeRunSync() shouldBe right
|
||||
val result = repo.listRecordSetChanges(None, None, 5, Some("1-apply-test.ok.zone.recordsets."), Some(RecordType.AAAA)).unsafeRunSync()
|
||||
result.nextId shouldBe None
|
||||
result.maxItems shouldBe 5
|
||||
(result.items should have).length(1)
|
||||
}
|
||||
"page through a particular recordset's changes by fqdn and record type" in {
|
||||
val inserts = generateSameInserts(okZone, 8)
|
||||
val saveRecChange = executeWithinTransaction { db: DB =>
|
||||
repo.save(db, ChangeSet(inserts))
|
||||
}
|
||||
saveRecChange.attempt.unsafeRunSync() shouldBe right
|
||||
val page1 = repo.listRecordSetChanges(None, None, 5, Some("apply-test.ok.zone.recordsets."), Some(RecordType.AAAA)).unsafeRunSync()
|
||||
page1.nextId shouldBe defined
|
||||
page1.maxItems shouldBe 5
|
||||
(page1.items should have).length(5)
|
||||
|
||||
val page2 = repo.listRecordSetChanges(None, page1.nextId, 5, Some("apply-test.ok.zone.recordsets."), Some(RecordType.AAAA)).unsafeRunSync()
|
||||
page2.nextId shouldBe None
|
||||
page2.maxItems shouldBe 5
|
||||
(page2.items should have).length(3)
|
||||
}
|
||||
}
|
||||
|
||||
"list failed record changes" should {
|
||||
|
@@ -0,0 +1,8 @@
|
||||
CREATE SCHEMA IF NOT EXISTS ${dbName};
|
||||
|
||||
USE ${dbName};
|
||||
|
||||
ALTER TABLE record_change ADD COLUMN fqdn VARCHAR(255) NOT NULL;
|
||||
ALTER TABLE record_change ADD COLUMN record_type VARCHAR(255) NOT NULL;
|
||||
CREATE INDEX fqdn_index ON record_change (fqdn);
|
||||
CREATE INDEX record_type_index ON record_change (record_type);
|
@@ -19,9 +19,11 @@ package vinyldns.mysql.repository
|
||||
import cats.effect._
|
||||
import scalikejdbc._
|
||||
import vinyldns.core.domain.record.RecordSetChangeType.RecordSetChangeType
|
||||
import vinyldns.core.domain.record.RecordType.RecordType
|
||||
import vinyldns.core.domain.record._
|
||||
import vinyldns.core.protobuf.ProtobufConversions
|
||||
import vinyldns.core.route.Monitored
|
||||
import vinyldns.mysql.repository.MySqlRecordSetRepository.fromRecordType
|
||||
import vinyldns.proto.VinylDNSProto
|
||||
|
||||
class MySqlRecordChangeRepository
|
||||
@@ -39,6 +41,24 @@ class MySqlRecordChangeRepository
|
||||
| LIMIT {limit} OFFSET {startFrom}
|
||||
""".stripMargin
|
||||
|
||||
private val LIST_CHANGES_WITH_START_FQDN_TYPE =
|
||||
sql"""
|
||||
|SELECT data
|
||||
| FROM record_change
|
||||
| WHERE fqdn = {fqdn} AND record_type = {type}
|
||||
| ORDER BY created DESC
|
||||
| LIMIT {limit} OFFSET {startFrom}
|
||||
""".stripMargin
|
||||
|
||||
private val LIST_CHANGES_WITHOUT_START_FQDN_TYPE =
|
||||
sql"""
|
||||
|SELECT data
|
||||
| FROM record_change
|
||||
| WHERE fqdn = {fqdn} AND record_type = {type}
|
||||
| ORDER BY created DESC
|
||||
| LIMIT {limit}
|
||||
""".stripMargin
|
||||
|
||||
private val LIST_RECORD_CHANGES =
|
||||
sql"""
|
||||
|SELECT data
|
||||
@@ -63,7 +83,7 @@ class MySqlRecordChangeRepository
|
||||
""".stripMargin
|
||||
|
||||
private val INSERT_CHANGES =
|
||||
sql"INSERT IGNORE INTO record_change (id, zone_id, created, type, data) VALUES (?, ?, ?, ?, ?)"
|
||||
sql"INSERT IGNORE INTO record_change (id, zone_id, created, type, fqdn, record_type, data) VALUES (?, ?, ?, ?, ?, ?, ?)"
|
||||
|
||||
/**
|
||||
* We have the same issue with changes as record sets, namely we may have to save millions of them
|
||||
@@ -82,7 +102,9 @@ class MySqlRecordChangeRepository
|
||||
change.zoneId,
|
||||
change.created.toEpochMilli,
|
||||
fromChangeType(change.changeType),
|
||||
toPB(change).toByteArray
|
||||
if(change.recordSet.name == change.zone.name) change.zone.name else change.recordSet.name + "." + change.zone.name,
|
||||
fromRecordType(change.recordSet.typ),
|
||||
toPB(change).toByteArray,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -94,33 +116,52 @@ class MySqlRecordChangeRepository
|
||||
}
|
||||
|
||||
def listRecordSetChanges(
|
||||
zoneId: String,
|
||||
zoneId: Option[String],
|
||||
startFrom: Option[Int],
|
||||
maxItems: Int
|
||||
maxItems: Int,
|
||||
fqdn: Option[String],
|
||||
recordType: Option[RecordType]
|
||||
): IO[ListRecordSetChangesResults] =
|
||||
monitor("repo.RecordChange.listRecordSetChanges") {
|
||||
IO {
|
||||
DB.readOnly { implicit s =>
|
||||
val changes = startFrom match {
|
||||
case Some(start) =>
|
||||
LIST_CHANGES_WITH_START
|
||||
.bindByName('zoneId -> zoneId, 'startFrom -> start, 'limit -> maxItems)
|
||||
.map(toRecordSetChange)
|
||||
.list()
|
||||
.apply()
|
||||
case None =>
|
||||
LIST_CHANGES_NO_START
|
||||
.bindByName('zoneId -> zoneId, 'limit -> maxItems)
|
||||
.map(toRecordSetChange)
|
||||
.list()
|
||||
.apply()
|
||||
val changes = if(startFrom.isDefined && fqdn.isDefined && recordType.isDefined){
|
||||
LIST_CHANGES_WITH_START_FQDN_TYPE
|
||||
.bindByName('fqdn -> fqdn.get, 'type -> fromRecordType(recordType.get), 'startFrom -> startFrom.get, 'limit -> (maxItems + 1))
|
||||
.map(toRecordSetChange)
|
||||
.list()
|
||||
.apply()
|
||||
} else if(fqdn.isDefined && recordType.isDefined){
|
||||
LIST_CHANGES_WITHOUT_START_FQDN_TYPE
|
||||
.bindByName('fqdn -> fqdn.get, 'type -> fromRecordType(recordType.get), 'limit -> (maxItems + 1))
|
||||
.map(toRecordSetChange)
|
||||
.list()
|
||||
.apply()
|
||||
} else if(startFrom.isDefined){
|
||||
LIST_CHANGES_WITH_START
|
||||
.bindByName('zoneId -> zoneId.get, 'startFrom -> startFrom.get, 'limit -> (maxItems + 1))
|
||||
.map(toRecordSetChange)
|
||||
.list()
|
||||
.apply()
|
||||
} else {
|
||||
LIST_CHANGES_NO_START
|
||||
.bindByName('zoneId -> zoneId.get, 'limit -> (maxItems + 1))
|
||||
.map(toRecordSetChange)
|
||||
.list()
|
||||
.apply()
|
||||
}
|
||||
|
||||
val maxQueries = changes.take(maxItems)
|
||||
val startValue = startFrom.getOrElse(0)
|
||||
val nextId = if (changes.size < maxItems) None else Some(startValue + maxItems)
|
||||
|
||||
// earlier maxItems was incremented, if the (maxItems + 1) size is not reached then pages are exhausted
|
||||
val nextId = changes match {
|
||||
case _ if changes.size <= maxItems | changes.isEmpty => None
|
||||
case _ => Some(startValue + maxItems)
|
||||
}
|
||||
|
||||
ListRecordSetChangesResults(
|
||||
changes,
|
||||
maxQueries,
|
||||
nextId,
|
||||
startFrom,
|
||||
maxItems
|
||||
|
@@ -76,7 +76,8 @@ class FrontendController @Inject() (
|
||||
}
|
||||
|
||||
def viewRecordSets(): Action[AnyContent] = userAction.async { implicit request =>
|
||||
Future(Ok(views.html.recordsets.recordSets(request.user.userName)))
|
||||
val canReview = request.user.isSuper || request.user.isSupport
|
||||
Future(Ok(views.html.recordsets.recordSets(request.user.userName, canReview)))
|
||||
}
|
||||
|
||||
def viewAllBatchChanges(): Action[AnyContent] = userAction.async { implicit request =>
|
||||
|
@@ -458,6 +458,16 @@ class VinylDNS @Inject() (
|
||||
// $COVERAGE-ON$
|
||||
}
|
||||
|
||||
def getCommonZoneDetails(id: String): Action[AnyContent] = userAction.async { implicit request =>
|
||||
// $COVERAGE-OFF$
|
||||
val vinyldnsRequest = new VinylDNSRequest("GET", s"$vinyldnsServiceBackend", s"zones/$id/details")
|
||||
executeRequest(vinyldnsRequest, request.user).map(response => {
|
||||
Status(response.status)(response.body)
|
||||
.withHeaders(cacheHeaders: _*)
|
||||
})
|
||||
// $COVERAGE-ON$
|
||||
}
|
||||
|
||||
def getZoneByName(name: String): Action[AnyContent] = userAction.async { implicit request =>
|
||||
val vinyldnsRequest =
|
||||
new VinylDNSRequest("GET", s"$vinyldnsServiceBackend", s"zones/name/$name")
|
||||
@@ -590,6 +600,25 @@ class VinylDNS @Inject() (
|
||||
// $COVERAGE-ON$
|
||||
}
|
||||
|
||||
def listRecordSetChangeHistory: Action[AnyContent] = userAction.async { implicit request =>
|
||||
// $COVERAGE-OFF$
|
||||
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",
|
||||
s"recordsetchange/history",
|
||||
parameters = queryParameters
|
||||
)
|
||||
executeRequest(vinyldnsRequest, request.user).map(response => {
|
||||
Status(response.status)(response.body)
|
||||
.withHeaders(cacheHeaders: _*)
|
||||
})
|
||||
// $COVERAGE-ON$
|
||||
}
|
||||
|
||||
def addZone(): Action[AnyContent] = userAction.async { implicit request =>
|
||||
// $COVERAGE-OFF$
|
||||
val json = request.body.asJson
|
||||
|
@@ -1,4 +1,4 @@
|
||||
@(rootAccountName: String)(implicit request: play.api.mvc.Request[Any], customLinks: models.CustomLinks, meta: models.Meta)
|
||||
@(rootAccountName: String, rootAccountCanReview: Boolean)(implicit request: play.api.mvc.Request[Any], customLinks: models.CustomLinks, meta: models.Meta)
|
||||
|
||||
@content = {
|
||||
<!-- PAGE CONTENT -->
|
||||
@@ -81,7 +81,6 @@
|
||||
<div class="btn-group">
|
||||
<button id="refresh-records-button" class="btn btn-default" ng-click="refreshRecords()"><span class="fa fa-refresh"></span> Refresh</button>
|
||||
<button id="create-record-button" class="btn btn-default" ng-if="canCreateRecords" ng-click="createRecord(defaultTtl)"><span class="fa fa-plus"></span> Create Record Set</button>
|
||||
<button id="zone-sync-button" class="btn btn-default mb-control" ng-if="zoneInfo.accessLevel=='Delete'" data-toggle="modal" data-target="#mb-sync"><span class="fa fa-exchange"></span> Sync Zone</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -167,6 +166,7 @@
|
||||
@if(meta.sharedDisplayEnabled) {
|
||||
<th>Owner Group Name</th>
|
||||
}
|
||||
<th ng-if="rootAccountCanReview || userCanAccessGroup">Record History</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -374,6 +374,9 @@
|
||||
title="Group with ID {{record.ownerGroupId}} no longer exists."><span class="fa fa-warning"></span> Group deleted</span>
|
||||
</td>
|
||||
}
|
||||
<td ng-if="rootAccountCanReview || (record.ownerGroupName && canAccessGroup(record.ownerGroupId))">
|
||||
<span><button class="btn btn-info btn-sm" ng-click="viewRecordHistory(record.fqdn, record.type)">View History</button></span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -402,6 +405,102 @@
|
||||
|
||||
</div>
|
||||
<!-- END PAGE CONTENT -->
|
||||
|
||||
<div class="modal fade in" id="record_history_modal">
|
||||
<div class="modal-dialog set-width">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<div class="modal-title">Record History</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- START SIMPLE DATATABLE -->
|
||||
<div class="panel panel-default" id="record_change_history">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">Record Change History</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-default" ng-click="refreshRecordChangeHistory(recordFqdn, recordType)"><span class="fa fa-refresh"></span> Refresh</button>
|
||||
</div>
|
||||
|
||||
<!-- PAGINATION -->
|
||||
<div class="dataTables_paginate">
|
||||
<ul class="pagination">
|
||||
<li class="paginate_button previous">
|
||||
<a ng-if="changeHistoryPrevPageEnabled()" ng-click="changeHistoryPrevPage()" class="paginate_button">Previous</a>
|
||||
</li>
|
||||
<li class="paginate_button next">
|
||||
<a ng-if="changeHistoryNextPageEnabled()" ng-click="changeHistoryNextPage()" class="paginate_button">Next</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- END PAGINATION -->
|
||||
|
||||
<table id="changeDataTable" class="table table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th class="col-md-5">Recordset Name</th>
|
||||
<th>Recordset Type</th>
|
||||
<th>Change Type</th>
|
||||
<th>User</th>
|
||||
<th>Status</th>
|
||||
<th>Additional Info</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="change in recordsetChanges track by $index">
|
||||
<td>{{change.created}}</td>
|
||||
<td class="wrap-long-text">{{change.recordSet.name}}</td>
|
||||
<td>{{change.recordSet.type}}</td>
|
||||
<td>{{change.changeType}}</td>
|
||||
<td>{{change.userName}}</td>
|
||||
<td>
|
||||
<span class="label label-{{ getRecordChangeStatusLabel(change.status) }}">{{ change.status }}</span>
|
||||
</td>
|
||||
<td class="col-md-3 wrap-long-text">
|
||||
{{change.systemMessage}}
|
||||
<div ng-if="change.status !='Failed'">
|
||||
<a ng-if="change.changeType =='Create'" ng-click="viewRecordInfo(change.recordSet)" class="force-cursor">View created recordset</a>
|
||||
<a ng-if="change.changeType =='Delete'" ng-click="viewRecordInfo(change.recordSet)" class="force-cursor">View deleted recordset</a>
|
||||
|
||||
<div><a ng-if="change.changeType =='Update'" ng-click="viewRecordInfo(change.recordSet)" class="force-cursor">View new recordset</a></div>
|
||||
<div><a ng-if="change.changeType =='Update'" ng-click="viewRecordInfo(change.updates)" class="force-cursor">View old recordset</a></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
|
||||
<!-- PAGINATION -->
|
||||
<div class="dataTables_paginate">
|
||||
<ul class="pagination">
|
||||
<li class="paginate_button previous">
|
||||
<a ng-if="changeHistoryPrevPageEnabled()" ng-click="changeHistoryPrevPage()" class="paginate_button">Previous</a>
|
||||
</li>
|
||||
<li class="paginate_button next">
|
||||
<a ng-if="changeHistoryNextPageEnabled()" ng-click="changeHistoryNextPage()" class="paginate_button">Next</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- END PAGINATION -->
|
||||
|
||||
</div>
|
||||
<div class="panel-footer"></div>
|
||||
</div>
|
||||
<!-- END SIMPLE DATATABLE -->
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<recordmodal></recordmodal>
|
||||
}
|
||||
|
||||
@plugins = {
|
||||
|
@@ -29,6 +29,7 @@ GET /api/recordsets @controllers.VinylDNS.listRecor
|
||||
GET /api/zones @controllers.VinylDNS.getZones
|
||||
GET /api/zones/backendids @controllers.VinylDNS.getBackendIds
|
||||
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/:id/changes @controllers.VinylDNS.getZoneChange(id: String)
|
||||
POST /api/zones @controllers.VinylDNS.addZone
|
||||
@@ -42,6 +43,7 @@ DELETE /api/zones/:zid/recordsets/:rid @controllers.VinylDNS.deleteRec
|
||||
PUT /api/zones/:zid/recordsets/:rid @controllers.VinylDNS.updateRecordSet(zid: String, rid:String)
|
||||
|
||||
GET /api/zones/:id/recordsetchanges @controllers.VinylDNS.listRecordSetChanges(id: String)
|
||||
GET /api/recordsetchange/history @controllers.VinylDNS.listRecordSetChangeHistory
|
||||
|
||||
GET /api/groups @controllers.VinylDNS.getGroups
|
||||
GET /api/groups/:gid @controllers.VinylDNS.getGroup(gid: String)
|
||||
|
@@ -563,6 +563,10 @@ input[type="file"] {
|
||||
}
|
||||
/* Ending of css override for cron library and it's associated elements used in zone sync scheduling */
|
||||
|
||||
.set-width {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
#set-dropdown-width {
|
||||
width: 4em;
|
||||
}
|
||||
|
@@ -237,6 +237,9 @@ angular.module('controller.manageZones', ['angular-cron-jobs'])
|
||||
|
||||
$scope.submitUpdateZone = function () {
|
||||
var zone = angular.copy($scope.updateZoneInfo);
|
||||
if(zone['recurrenceSchedule'] == ""){
|
||||
delete zone['recurrenceSchedule']
|
||||
}
|
||||
zone = zonesService.normalizeZoneDates(zone);
|
||||
zone = zonesService.setConnectionKeys(zone);
|
||||
zone = zonesService.checkBackendId(zone);
|
||||
|
@@ -29,6 +29,51 @@
|
||||
$scope.readRecordTypes = ['A', 'AAAA', 'CNAME', 'DS', 'MX', 'NS', 'PTR', "SOA", 'SRV', 'NAPTR', 'SSHFP', 'TXT'];
|
||||
$scope.selectedRecordTypes = [];
|
||||
$scope.groups = [];
|
||||
$scope.recordFqdn = undefined;
|
||||
$scope.recordType = undefined;
|
||||
$scope.recordsetChanges = {};
|
||||
$scope.currentRecord = {};
|
||||
$scope.zoneInfo = {};
|
||||
$scope.profile = {};
|
||||
$scope.recordTypes = ['A', 'AAAA', 'CNAME', 'DS', 'MX', 'NS', 'PTR', 'SRV', 'NAPTR', 'SSHFP', 'TXT', 'SOA'];
|
||||
$scope.sshfpAlgorithms = [{name: '(1) RSA', number: 1}, {name: '(2) DSA', number: 2}, {name: '(3) ECDSA', number: 3},
|
||||
{name: '(4) Ed25519', number: 4}];
|
||||
$scope.sshfpTypes = [{name: '(1) SHA-1', number: 1}, {name: '(2) SHA-256', number: 2}];
|
||||
$scope.dsAlgorithms = [{name: '(3) DSA', number: 3}, {name: '(5) RSASHA1', number: 5},
|
||||
{name: '(6) DSA_NSEC3_SHA1', number: 6}, {name: '(7) RSASHA1_NSEC3_SHA1' , number: 7},
|
||||
{name: '(8) RSASHA256', number: 8}, {name: '(10) RSASHA512' , number: 10},
|
||||
{name: '(12) ECC_GOST', number: 12}, {name: '(13) ECDSAP256SHA256' , number: 13},
|
||||
{name: '(14) ECDSAP384SHA384', number: 14}, {name: '(15) ED25519', number: 15},
|
||||
{name: '(16) ED448', number: 16},{name: '(253) PRIVATEDNS', number: 253},
|
||||
{name: '(254) PRIVATEOID', number: 254}]
|
||||
$scope.dsDigestTypes = [{name: '(1) SHA1', number: 1}, {name: '(2) SHA256', number: 2}, {name: '(3) GOSTR341194', number: 3}, {name: '(4) SHA384', number: 4}]
|
||||
$scope.isZoneAdmin = false;
|
||||
$scope.canReadZone = false;
|
||||
$scope.canCreateRecords = false;
|
||||
$scope.zoneId = undefined;
|
||||
$scope.recordModalState = {
|
||||
CREATE: 0,
|
||||
UPDATE: 1,
|
||||
DELETE: 2,
|
||||
CONFIRM_UPDATE: 3,
|
||||
CONFIRM_DELETE: 4,
|
||||
VIEW_DETAILS: 5
|
||||
};
|
||||
// read-only data for setting various classes/attributes in record modal
|
||||
$scope.recordModalParams = {
|
||||
readOnly: {
|
||||
class: "",
|
||||
readOnly: true
|
||||
},
|
||||
editable: {
|
||||
class: "record-edit",
|
||||
readOnly: false
|
||||
}
|
||||
};
|
||||
$scope.userCanAccessGroup = false;
|
||||
|
||||
// paging status for record changes
|
||||
var changePaging = pagingService.getNewPagingParams(100);
|
||||
|
||||
// paging status for recordsets
|
||||
var recordsPaging = pagingService.getNewPagingParams(100);
|
||||
@@ -68,6 +113,13 @@
|
||||
.append("<div>" + recordSet + "</div>")
|
||||
.appendTo(ul); };
|
||||
|
||||
$scope.viewRecordHistory = function(recordFqdn, recordType) {
|
||||
$scope.recordFqdn = recordFqdn;
|
||||
$scope.recordType = recordType;
|
||||
$scope.refreshRecordChangeHistory($scope.recordFqdn, $scope.recordType);
|
||||
$("#record_history_modal").modal("show");
|
||||
};
|
||||
|
||||
$scope.refreshRecords = function() {
|
||||
if($scope.query.includes("|")) {
|
||||
const queryRecord = $scope.query.split('|');
|
||||
@@ -80,6 +132,7 @@
|
||||
function success(response) {
|
||||
recordsPaging.next = response.data.nextId;
|
||||
updateRecordDisplay(response.data['recordSets']);
|
||||
getMembership();
|
||||
}
|
||||
return recordsService
|
||||
.listRecordSetData(recordsPaging.maxItems, undefined, recordName, recordType, $scope.nameSort, $scope.ownerGroupFilter)
|
||||
@@ -131,6 +184,11 @@
|
||||
newRecords.push(recordsService.toDisplayRecord(record, ''));
|
||||
});
|
||||
$scope.records = newRecords;
|
||||
for(var i = 0; i < $scope.records.length; i++) {
|
||||
if (!$scope.records[i].zoneShared){
|
||||
getZone($scope.records[i].zoneId, i);
|
||||
}
|
||||
}
|
||||
if ($scope.records.length > 0) {
|
||||
$("#ShowNoRec").modal("hide");
|
||||
$("td.dataTables_empty").hide();
|
||||
@@ -186,5 +244,142 @@
|
||||
handleError(error, 'recordsService::nextPage-failure');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.refreshRecordChangeHistory = function(recordFqdn, recordType) {
|
||||
changePaging = pagingService.resetPaging(changePaging);
|
||||
function success(response) {
|
||||
$scope.zoneId = response.data.zoneId;
|
||||
$scope.refreshZone();
|
||||
changePaging.next = response.data.nextId;
|
||||
updateChangeDisplay(response.data.recordSetChanges)
|
||||
}
|
||||
return recordsService
|
||||
.listRecordSetChangeHistory(changePaging.maxItems, undefined, recordFqdn, recordType)
|
||||
.then(success)
|
||||
.catch(function (error){
|
||||
handleError(error, 'recordsService::getRecordSetChangeHistory-failure');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Record change history paging
|
||||
*/
|
||||
|
||||
$scope.changeHistoryPrevPageEnabled = function() {
|
||||
return pagingService.prevPageEnabled(changePaging);
|
||||
};
|
||||
|
||||
$scope.changeHistoryNextPageEnabled = function() {
|
||||
return pagingService.nextPageEnabled(changePaging);
|
||||
};
|
||||
|
||||
$scope.changeHistoryPrevPage = function() {
|
||||
var startFrom = pagingService.getPrevStartFrom(changePaging);
|
||||
return recordsService
|
||||
.listRecordSetChangeHistory(changePaging.maxItems, startFrom, $scope.recordFqdn, $scope.recordType)
|
||||
.then(function(response) {
|
||||
changePaging = pagingService.prevPageUpdate(response.data.nextId, changePaging);
|
||||
updateChangeDisplay(response.data.recordSetChanges);
|
||||
})
|
||||
.catch(function (error) {
|
||||
handleError(error, 'recordsService::changePrevPage-failure');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.changeHistoryNextPage = function() {
|
||||
return recordsService
|
||||
.listRecordSetChangeHistory(changePaging.maxItems, changePaging.next, $scope.recordFqdn, $scope.recordType)
|
||||
.then(function(response) {
|
||||
var changes = response.data.recordSetChanges;
|
||||
changePaging = pagingService.nextPageUpdate(changes, response.data.nextId, changePaging);
|
||||
if(changes.length > 0){
|
||||
updateChangeDisplay(changes);
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
handleError(error, 'recordsService::changeNextPage-failure');
|
||||
});
|
||||
};
|
||||
|
||||
function updateChangeDisplay(changes) {
|
||||
var newChanges = [];
|
||||
angular.forEach(changes, function(change) {
|
||||
newChanges.push(change);
|
||||
});
|
||||
$scope.recordsetChanges = newChanges;
|
||||
}
|
||||
|
||||
$scope.getRecordChangeStatusLabel = function(status) {
|
||||
switch(status) {
|
||||
case 'Complete':
|
||||
return 'success';
|
||||
case 'Failed':
|
||||
return 'danger';
|
||||
default:
|
||||
return 'info';
|
||||
}
|
||||
};
|
||||
|
||||
$scope.viewRecordInfo = function(record) {
|
||||
$scope.currentRecord = recordsService.toDisplayRecord(record);
|
||||
$scope.recordModal = {
|
||||
action: $scope.recordModalState.VIEW_DETAILS,
|
||||
title: "Record Info",
|
||||
basics: $scope.recordModalParams.readOnly,
|
||||
details: $scope.recordModalParams.readOnly,
|
||||
sharedZone: $scope.zoneInfo.shared,
|
||||
sharedDisplayEnabled: $scope.sharedDisplayEnabled
|
||||
};
|
||||
$("#record_modal").modal("show");
|
||||
};
|
||||
|
||||
$scope.refreshZone = function() {
|
||||
function success(response) {
|
||||
$log.debug('recordsService::getZone-success');
|
||||
$scope.zoneInfo = response.data.zone;
|
||||
// Get current user's groups and determine if they're an admin of this zone
|
||||
getMembership()
|
||||
}
|
||||
return recordsService
|
||||
.getZone($scope.zoneId)
|
||||
.then(success)
|
||||
.catch(function (error){
|
||||
handleError(error, 'recordsService::getZone-catch');
|
||||
});
|
||||
};
|
||||
|
||||
function getZone(zoneId, index){
|
||||
recordsService
|
||||
.getCommonZoneDetails(zoneId)
|
||||
.then(
|
||||
function (results) {
|
||||
$scope.zoneDetails = results;
|
||||
$scope.records[index].ownerGroupId = results.data.zone.adminGroupId;
|
||||
$scope.records[index].ownerGroupName = results.data.zone.adminGroupName;
|
||||
if($scope.records[index].ownerGroupName && $scope.canAccessGroup($scope.records[index].ownerGroupId)){
|
||||
$scope.userCanAccessGroup = true;
|
||||
}
|
||||
})
|
||||
.catch(function (error){
|
||||
handleError(error, 'recordsService::getCommonZoneDetails-catch');
|
||||
});
|
||||
}
|
||||
|
||||
function getMembership(){
|
||||
groupsService
|
||||
.getGroupsStored()
|
||||
.then(
|
||||
function (results) {
|
||||
$scope.myGroups = results.groups;
|
||||
$scope.myGroupIds = results.groups.map(function(grp) {return grp['id']});
|
||||
})
|
||||
.catch(function (error){
|
||||
handleError(error, 'groupsService::getGroupsStored-failure');
|
||||
});
|
||||
}
|
||||
|
||||
$scope.canAccessGroup = function(groupId) {
|
||||
return $scope.myGroupIds !== undefined && $scope.myGroupIds.indexOf(groupId) > -1;
|
||||
};
|
||||
});
|
||||
})();
|
||||
|
@@ -100,6 +100,10 @@ angular.module('service.records', [])
|
||||
return $http.get("/api/zones/"+zid);
|
||||
};
|
||||
|
||||
this.getCommonZoneDetails = function (zid) {
|
||||
return $http.get("/api/zones/"+zid+"/details");
|
||||
};
|
||||
|
||||
this.syncZone = function (zid) {
|
||||
return $http.post("/api/zones/"+zid+"/sync", {}, {headers: utilityService.getCsrfHeader()});
|
||||
};
|
||||
@@ -108,7 +112,21 @@ angular.module('service.records', [])
|
||||
var url = '/api/zones/' + zid + '/recordsetchanges';
|
||||
var params = {
|
||||
"maxItems": maxItems,
|
||||
"startFrom": startFrom
|
||||
"startFrom": startFrom,
|
||||
"fqdn": undefined,
|
||||
"recordType": undefined
|
||||
};
|
||||
url = utilityService.urlBuilder(url, params);
|
||||
return $http.get(url);
|
||||
};
|
||||
|
||||
this.listRecordSetChangeHistory = function (maxItems, startFrom, fqdn, recordType) {
|
||||
var url = '/api/recordsetchange/history';
|
||||
var params = {
|
||||
"maxItems": maxItems,
|
||||
"startFrom": startFrom,
|
||||
"fqdn": fqdn,
|
||||
"recordType": recordType
|
||||
};
|
||||
url = utilityService.urlBuilder(url, params);
|
||||
return $http.get(url);
|
||||
|
@@ -238,6 +238,32 @@ class FrontendControllerSpec extends Specification with Mockito with TestApplica
|
||||
}
|
||||
}
|
||||
|
||||
"Get for '/recordsets'" should {
|
||||
"redirect to the login page when a user is not logged in" in new WithApplication(app) {
|
||||
val result = underTest.viewRecordSets()(FakeRequest(GET, "/recordsets"))
|
||||
status(result) must equalTo(SEE_OTHER)
|
||||
headers(result) must contain("Location" -> "/login?target=/recordsets")
|
||||
}
|
||||
"render the recordset view page when the user is logged in" in new WithApplication(app) {
|
||||
val result =
|
||||
underTest.viewRecordSets()(
|
||||
FakeRequest(GET, "/recordsets").withSession("username" -> "frodo").withCSRFToken
|
||||
)
|
||||
status(result) must beEqualTo(OK)
|
||||
contentType(result) must beSome.which(_ == "text/html")
|
||||
contentAsString(result) must contain("RecordSets | VinylDNS")
|
||||
}
|
||||
"redirect to the no access page when a user is locked out" in new WithApplication(app) {
|
||||
val result =
|
||||
lockedUserUnderTest.viewRecordSets()(
|
||||
FakeRequest(GET, "/recordsets")
|
||||
.withSession("username" -> "lockedFbaggins")
|
||||
.withCSRFToken
|
||||
)
|
||||
headers(result) must contain("Location" -> "/noaccess")
|
||||
}
|
||||
}
|
||||
|
||||
"Get for login" should {
|
||||
"with ldap enabled" should {
|
||||
"render the login page when the user is not logged in" in new WithApplication(app) {
|
||||
|
@@ -1650,6 +1650,35 @@ class VinylDNSSpec extends Specification with Mockito with TestApplicationData w
|
||||
}
|
||||
}
|
||||
|
||||
".getCommonZoneDetails" should {
|
||||
"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.getCommonZoneDetails(hobbitZoneId)(FakeRequest(GET, s"/api/zones/$hobbitZoneId/details"))
|
||||
|
||||
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.getCommonZoneDetails(hobbitZoneId)(
|
||||
FakeRequest(GET, s"/api/zones/$hobbitZoneId/details").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."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
".getZoneChange" should {
|
||||
|
||||
"return ok (200) if the zoneChanges is found" in new WithApplication(app) {
|
||||
@@ -1947,6 +1976,37 @@ class VinylDNSSpec extends Specification with Mockito with TestApplicationData w
|
||||
}
|
||||
}
|
||||
|
||||
".listRecordSetChangeHistory" should {
|
||||
"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.listRecordSetChangeHistory()(
|
||||
FakeRequest(GET, s"/api/recordsetchange/history")
|
||||
)
|
||||
|
||||
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.listRecordSetChangeHistory()(
|
||||
FakeRequest(GET, s"/api/recordsetchange/history").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."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
".addZone" should {
|
||||
"return unauthorized (401) if requesting user is not logged in" in new WithApplication(app) {
|
||||
val client = mock[WSClient]
|
||||
|
@@ -83,7 +83,7 @@ class Route53Backend(
|
||||
val found = result.getHostedZones.asScala.toList.headOption.map { hz =>
|
||||
val hzid = parseHostedZoneId(hz.getId)
|
||||
|
||||
// adds the hozted zone name and id to our cache if not present
|
||||
// adds the hosted zone name and id to our cache if not present
|
||||
zoneMap.putIfAbsent(hz.getName, hzid)
|
||||
hzid
|
||||
}
|
||||
|
Reference in New Issue
Block a user