mirror of
https://github.com/VinylDNS/vinyldns
synced 2025-09-04 00:05:12 +00:00
Merge branch 'master' into fix_change_failure_metrics
This commit is contained in:
@@ -256,7 +256,7 @@ class MembershipService(
|
|||||||
_ <- isGroupChangePresent(result).toResult
|
_ <- isGroupChangePresent(result).toResult
|
||||||
_ <- canSeeGroup(result.get.newGroup.id, authPrincipal).toResult
|
_ <- canSeeGroup(result.get.newGroup.id, authPrincipal).toResult
|
||||||
allUserIds = getGroupUserIds(Seq(result.get))
|
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)
|
groupChangeMessage <- determineGroupDifference(Seq(result.get), allUserMap)
|
||||||
groupChanges = (groupChangeMessage, Seq(result.get)).zipped.map{ (a, b) => b.copy(groupChangeMessage = Some(a)) }
|
groupChanges = (groupChangeMessage, Seq(result.get)).zipped.map{ (a, b) => b.copy(groupChangeMessage = Some(a)) }
|
||||||
userIds = Seq(result.get).map(_.userId).toSet
|
userIds = Seq(result.get).map(_.userId).toSet
|
||||||
@@ -276,7 +276,7 @@ class MembershipService(
|
|||||||
.getGroupChanges(groupId, startFrom, maxItems)
|
.getGroupChanges(groupId, startFrom, maxItems)
|
||||||
.toResult[ListGroupChangesResults]
|
.toResult[ListGroupChangesResults]
|
||||||
allUserIds = getGroupUserIds(result.changes)
|
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)
|
groupChangeMessage <- determineGroupDifference(result.changes, allUserMap)
|
||||||
groupChanges = (groupChangeMessage, result.changes).zipped.map{ (a, b) => b.copy(groupChangeMessage = Some(a)) }
|
groupChanges = (groupChangeMessage, result.changes).zipped.map{ (a, b) => b.copy(groupChangeMessage = Some(a)) }
|
||||||
userIds = result.changes.map(_.userId).toSet
|
userIds = result.changes.map(_.userId).toSet
|
||||||
@@ -316,7 +316,8 @@ class MembershipService(
|
|||||||
sb.append(s"Group email changed to '${change.newGroup.email}'. ")
|
sb.append(s"Group email changed to '${change.newGroup.email}'. ")
|
||||||
}
|
}
|
||||||
if (change.oldGroup.get.description != change.newGroup.description) {
|
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)
|
val adminAddDifference = change.newGroup.adminUserIds.diff(change.oldGroup.get.adminUserIds)
|
||||||
if (adminAddDifference.nonEmpty) {
|
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(
|
case class ListFailedRecordSetChangesResponse(
|
||||||
failedRecordSetChanges: List[RecordSetChange] = Nil,
|
failedRecordSetChanges: List[RecordSetChange] = Nil,
|
||||||
nextId: Int,
|
nextId: Int,
|
||||||
|
@@ -576,20 +576,38 @@ class RecordSetService(
|
|||||||
} yield change
|
} yield change
|
||||||
|
|
||||||
def listRecordSetChanges(
|
def listRecordSetChanges(
|
||||||
zoneId: String,
|
zoneId: Option[String] = None,
|
||||||
startFrom: Option[Int] = None,
|
startFrom: Option[Int] = None,
|
||||||
maxItems: Int = 100,
|
maxItems: Int = 100,
|
||||||
|
fqdn: Option[String] = None,
|
||||||
|
recordType: Option[RecordType] = None,
|
||||||
authPrincipal: AuthPrincipal
|
authPrincipal: AuthPrincipal
|
||||||
): Result[ListRecordSetChangesResponse] =
|
): Result[ListRecordSetChangesResponse] =
|
||||||
for {
|
for {
|
||||||
zone <- getZone(zoneId)
|
zone <- getZone(zoneId.get)
|
||||||
_ <- canSeeZone(authPrincipal, zone).toResult
|
_ <- canSeeZone(authPrincipal, zone).toResult
|
||||||
recordSetChangesResults <- recordChangeRepository
|
recordSetChangesResults <- recordChangeRepository
|
||||||
.listRecordSetChanges(zone.id, startFrom, maxItems)
|
.listRecordSetChanges(Some(zone.id), startFrom, maxItems, fqdn, recordType)
|
||||||
.toResult[ListRecordSetChangesResults]
|
.toResult[ListRecordSetChangesResults]
|
||||||
recordSetChangesInfo <- buildRecordSetChangeInfo(recordSetChangesResults.items)
|
recordSetChangesInfo <- buildRecordSetChangeInfo(recordSetChangesResults.items)
|
||||||
} yield ListRecordSetChangesResponse(zoneId, recordSetChangesResults, recordSetChangesInfo)
|
} 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 {
|
||||||
|
recordSetChangesResults <- recordChangeRepository
|
||||||
|
.listRecordSetChanges(zoneId, startFrom, maxItems, fqdn, recordType)
|
||||||
|
.toResult[ListRecordSetChangesResults]
|
||||||
|
recordSetChangesInfo <- buildRecordSetChangeInfo(recordSetChangesResults.items)
|
||||||
|
_ <- 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(
|
def listFailedRecordSetChanges(
|
||||||
authPrincipal: AuthPrincipal,
|
authPrincipal: AuthPrincipal,
|
||||||
|
@@ -101,12 +101,23 @@ trait RecordSetServiceAlgebra {
|
|||||||
): Result[RecordSetChange]
|
): Result[RecordSetChange]
|
||||||
|
|
||||||
def listRecordSetChanges(
|
def listRecordSetChanges(
|
||||||
zoneId: String,
|
zoneId: Option[String],
|
||||||
startFrom: Option[Int],
|
startFrom: Option[Int],
|
||||||
maxItems: Int,
|
maxItems: Int,
|
||||||
|
fqdn: Option[String],
|
||||||
|
recordType: Option[RecordType],
|
||||||
authPrincipal: AuthPrincipal
|
authPrincipal: AuthPrincipal
|
||||||
): Result[ListRecordSetChangesResponse]
|
): Result[ListRecordSetChangesResponse]
|
||||||
|
|
||||||
|
def listRecordSetChangeHistory(
|
||||||
|
zoneId: Option[String],
|
||||||
|
startFrom: Option[Int],
|
||||||
|
maxItems: Int,
|
||||||
|
fqdn: Option[String],
|
||||||
|
recordType: Option[RecordType],
|
||||||
|
authPrincipal: AuthPrincipal
|
||||||
|
): Result[ListRecordSetHistoryResponse]
|
||||||
|
|
||||||
def listFailedRecordSetChanges(
|
def listFailedRecordSetChanges(
|
||||||
authPrincipal: AuthPrincipal,
|
authPrincipal: AuthPrincipal,
|
||||||
startFrom: Int,
|
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(
|
case class ZoneSummaryInfo(
|
||||||
name: String,
|
name: String,
|
||||||
email: String,
|
email: String,
|
||||||
|
@@ -153,6 +153,12 @@ class ZoneService(
|
|||||||
accessLevel = getZoneAccess(auth, zone)
|
accessLevel = getZoneAccess(auth, zone)
|
||||||
} yield ZoneInfo(zone, aclInfo, groupName, accessLevel)
|
} 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] =
|
def getZoneByName(zoneName: String, auth: AuthPrincipal): Result[ZoneInfo] =
|
||||||
for {
|
for {
|
||||||
zone <- getZoneByNameOrFail(ensureTrailingDot(zoneName))
|
zone <- getZoneByNameOrFail(ensureTrailingDot(zoneName))
|
||||||
|
@@ -35,6 +35,8 @@ trait ZoneServiceAlgebra {
|
|||||||
|
|
||||||
def getZone(zoneId: String, auth: AuthPrincipal): Result[ZoneInfo]
|
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 getZoneByName(zoneName: String, auth: AuthPrincipal): Result[ZoneInfo]
|
||||||
|
|
||||||
def listZones(
|
def listZones(
|
||||||
|
@@ -223,8 +223,8 @@ class RecordSetRoute(
|
|||||||
} ~
|
} ~
|
||||||
path("zones" / Segment / "recordsetchanges") { zoneId =>
|
path("zones" / Segment / "recordsetchanges") { zoneId =>
|
||||||
(get & monitor("Endpoint.listRecordSetChanges")) {
|
(get & monitor("Endpoint.listRecordSetChanges")) {
|
||||||
parameters("startFrom".as[Int].?, "maxItems".as[Int].?(DEFAULT_MAX_ITEMS)) {
|
parameters("startFrom".as[Int].?, "maxItems".as[Int].?(DEFAULT_MAX_ITEMS), "fqdn".as[String].?, "recordType".as[String].?) {
|
||||||
(startFrom: Option[Int], maxItems: Int) =>
|
(startFrom: Option[Int], maxItems: Int, fqdn: Option[String], _: Option[String]) =>
|
||||||
handleRejections(invalidQueryHandler) {
|
handleRejections(invalidQueryHandler) {
|
||||||
validate(
|
validate(
|
||||||
check = 0 < maxItems && maxItems <= DEFAULT_MAX_ITEMS,
|
check = 0 < maxItems && maxItems <= DEFAULT_MAX_ITEMS,
|
||||||
@@ -233,7 +233,34 @@ class RecordSetRoute(
|
|||||||
) {
|
) {
|
||||||
authenticateAndExecute(
|
authenticateAndExecute(
|
||||||
recordSetService
|
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 =>
|
) { changes =>
|
||||||
complete(StatusCodes.OK, changes)
|
complete(StatusCodes.OK, changes)
|
||||||
}
|
}
|
||||||
|
@@ -29,6 +29,7 @@ import vinyldns.core.domain.zone._
|
|||||||
import scala.concurrent.duration._
|
import scala.concurrent.duration._
|
||||||
|
|
||||||
case class GetZoneResponse(zone: ZoneInfo)
|
case class GetZoneResponse(zone: ZoneInfo)
|
||||||
|
case class GetZoneDetailsResponse(zone: ZoneDetails)
|
||||||
case class ZoneRejected(zone: Zone, errors: List[String])
|
case class ZoneRejected(zone: Zone, errors: List[String])
|
||||||
|
|
||||||
class ZoneRoute(
|
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 =>
|
path("zones" / Segment / "sync") { id =>
|
||||||
(post & monitor("Endpoint.syncZone")) {
|
(post & monitor("Endpoint.syncZone")) {
|
||||||
authenticateAndExecute(zoneService.syncZone(id, _)) { chg =>
|
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"))
|
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):
|
def test_list_recordset_changes_no_authorization(shared_zone_test_context):
|
||||||
"""
|
"""
|
||||||
Test that recordset changes without authorization fails
|
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_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"))
|
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["shared"], is_(True))
|
||||||
assert_that(retrieved["accessLevel"], is_("Read"))
|
assert_that(retrieved["accessLevel"], is_("Read"))
|
||||||
|
|
||||||
|
|
||||||
def test_get_zone_private_by_id_fails_without_access(shared_zone_test_context):
|
def test_get_zone_private_by_id_fails_without_access(shared_zone_test_context):
|
||||||
"""
|
"""
|
||||||
Test get an existing zone by id without access
|
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)
|
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
|
@pytest.mark.serial
|
||||||
def test_get_zone_by_id_includes_acl_display_name(shared_zone_test_context):
|
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.close()
|
||||||
self.session_not_found_ok.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()
|
session = session or requests.Session()
|
||||||
retry = Retry(
|
retry = Retry(
|
||||||
total=retries,
|
total=retries,
|
||||||
@@ -79,7 +80,8 @@ class VinylDNSClient(object):
|
|||||||
session.mount("https://", adapter)
|
session.mount("https://", adapter)
|
||||||
return session
|
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
|
# pull out status or None
|
||||||
status_code = kwargs.pop("status", None)
|
status_code = kwargs.pop("status", None)
|
||||||
@@ -100,13 +102,15 @@ class VinylDNSClient(object):
|
|||||||
for k, v in query.items())
|
for k, v in query.items())
|
||||||
|
|
||||||
if sign_request:
|
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:
|
else:
|
||||||
signed_headers = headers or {}
|
signed_headers = headers or {}
|
||||||
signed_body = body_string
|
signed_body = body_string
|
||||||
|
|
||||||
if not_found_ok:
|
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:
|
else:
|
||||||
response = self.session.request(method, url, data=signed_body, headers=signed_headers, **kwargs)
|
response = self.session.request(method, url, data=signed_body, headers=signed_headers, **kwargs)
|
||||||
|
|
||||||
@@ -388,6 +392,17 @@ class VinylDNSClient(object):
|
|||||||
|
|
||||||
return data
|
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):
|
def get_zone_by_name(self, zone_name, **kwargs):
|
||||||
"""
|
"""
|
||||||
Gets a zone for the given zone name
|
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)
|
response, data = self.make_request(url, "GET", self.headers, not_found_ok=True, **kwargs)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def list_zones(self, name_filter=None, start_from=None, max_items=None, 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
|
Gets a list of zones that currently exist
|
||||||
:return: a list of zones
|
: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)
|
response, data = self.make_request(url, "GET", self.headers, None, not_found_ok=True, **kwargs)
|
||||||
return data
|
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
|
Retrieves all recordsets in a zone
|
||||||
:param zone_id: the zone to retrieve
|
:param zone_id: the zone to retrieve
|
||||||
@@ -618,7 +656,8 @@ class VinylDNSClient(object):
|
|||||||
_, data = self.make_request(url, "POST", self.headers, **kwargs)
|
_, data = self.make_request(url, "POST", self.headers, **kwargs)
|
||||||
return data
|
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
|
Gets list of user's batch change summaries
|
||||||
:return: the content of the response
|
:return: the content of the response
|
||||||
@@ -660,7 +699,8 @@ class VinylDNSClient(object):
|
|||||||
:return: the content of the response
|
:return: the content of the response
|
||||||
"""
|
"""
|
||||||
url = urljoin(self.index_url, "/zones/{0}/acl/rules".format(zone_id))
|
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
|
return data
|
||||||
|
|
||||||
@@ -804,7 +844,8 @@ class VinylDNSClient(object):
|
|||||||
while change["status"] != expected_status and retries > 0:
|
while change["status"] != expected_status and retries > 0:
|
||||||
time.sleep(RETRY_WAIT)
|
time.sleep(RETRY_WAIT)
|
||||||
retries -= 1
|
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:
|
if type(latest_change) != str:
|
||||||
change = latest_change
|
change = latest_change
|
||||||
|
|
||||||
|
@@ -184,6 +184,9 @@ class RecordSetServiceSpec
|
|||||||
doReturn(IO.pure(ListUsersResults(Seq(), None)))
|
doReturn(IO.pure(ListUsersResults(Seq(), None)))
|
||||||
.when(mockUserRepo)
|
.when(mockUserRepo)
|
||||||
.getUsers(Set.empty, None, None)
|
.getUsers(Set.empty, None, None)
|
||||||
|
doReturn(IO.pure(Some(okGroup)))
|
||||||
|
.when(mockGroupRepo)
|
||||||
|
.getGroup(okGroup.id)
|
||||||
|
|
||||||
val result: RecordSetChange =
|
val result: RecordSetChange =
|
||||||
underTest.addRecordSet(record, okAuth).map(_.asInstanceOf[RecordSetChange]).value.unsafeRunSync().toOption.get
|
underTest.addRecordSet(record, okAuth).map(_.asInstanceOf[RecordSetChange]).value.unsafeRunSync().toOption.get
|
||||||
@@ -468,7 +471,7 @@ class RecordSetServiceSpec
|
|||||||
|
|
||||||
result.recordSet.ownerGroupId shouldBe Some(okGroup.id)
|
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))
|
val record = aaaa.copy(ownerGroupId = Some(dummyGroup.id))
|
||||||
|
|
||||||
doReturn(IO.pure(List()))
|
doReturn(IO.pure(List()))
|
||||||
@@ -583,6 +586,9 @@ class RecordSetServiceSpec
|
|||||||
doReturn(IO.pure(ListUsersResults(listOfDummyUsers.toSeq, None)))
|
doReturn(IO.pure(ListUsersResults(listOfDummyUsers.toSeq, None)))
|
||||||
.when(mockUserRepo)
|
.when(mockUserRepo)
|
||||||
.getUsers(dummyGroup.memberIds, None, None)
|
.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
|
// passes as all three properties within dotted hosts config (allowed zones, users and record types) are satisfied
|
||||||
val result: RecordSetChange =
|
val result: RecordSetChange =
|
||||||
@@ -787,6 +793,9 @@ class RecordSetServiceSpec
|
|||||||
doReturn(IO.pure(ListUsersResults(listOfDummyUsers.toSeq, None)))
|
doReturn(IO.pure(ListUsersResults(listOfDummyUsers.toSeq, None)))
|
||||||
.when(mockUserRepo)
|
.when(mockUserRepo)
|
||||||
.getUsers(dummyGroup.memberIds, None, None)
|
.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
|
// 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
|
val result = underTest.addRecordSet(record, abcAuth).value.unsafeRunSync().swap.toOption.get
|
||||||
@@ -1932,13 +1941,13 @@ class RecordSetServiceSpec
|
|||||||
|
|
||||||
doReturn(IO.pure(ListRecordSetChangesResults(completeRecordSetChanges)))
|
doReturn(IO.pure(ListRecordSetChangesResults(completeRecordSetChanges)))
|
||||||
.when(mockRecordChangeRepo)
|
.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)))
|
doReturn(IO.pure(ListUsersResults(Seq(okUser), None)))
|
||||||
.when(mockUserRepo)
|
.when(mockUserRepo)
|
||||||
.getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
|
.getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
|
||||||
|
|
||||||
val result: ListRecordSetChangesResponse =
|
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 =
|
val changesWithName =
|
||||||
completeRecordSetChanges.map(change => RecordSetChangeInfo(change, Some("ok")))
|
completeRecordSetChanges.map(change => RecordSetChangeInfo(change, Some("ok")))
|
||||||
val expectedResults = ListRecordSetChangesResponse(
|
val expectedResults = ListRecordSetChangesResponse(
|
||||||
@@ -1951,13 +1960,42 @@ class RecordSetServiceSpec
|
|||||||
result shouldBe expectedResults
|
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 {
|
"return a zone with no changes if no changes exist" in {
|
||||||
doReturn(IO.pure(ListRecordSetChangesResults(items = Nil)))
|
doReturn(IO.pure(ListRecordSetChangesResults(items = Nil)))
|
||||||
.when(mockRecordChangeRepo)
|
.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 =
|
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(
|
val expectedResults = ListRecordSetChangesResponse(
|
||||||
zoneId = okZone.id,
|
zoneId = okZone.id,
|
||||||
recordSetChanges = List(),
|
recordSetChanges = List(),
|
||||||
@@ -2023,7 +2061,7 @@ class RecordSetServiceSpec
|
|||||||
|
|
||||||
"return a NotAuthorizedError" in {
|
"return a NotAuthorizedError" in {
|
||||||
val error =
|
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]
|
error shouldBe a[NotAuthorizedError]
|
||||||
}
|
}
|
||||||
@@ -2034,10 +2072,13 @@ class RecordSetServiceSpec
|
|||||||
|
|
||||||
doReturn(IO.pure(ListRecordSetChangesResults(List(rsChange2, rsChange1))))
|
doReturn(IO.pure(ListRecordSetChangesResults(List(rsChange2, rsChange1))))
|
||||||
.when(mockRecordChangeRepo)
|
.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 =
|
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 =
|
val changesWithName =
|
||||||
List(RecordSetChangeInfo(rsChange2, Some("ok")), RecordSetChangeInfo(rsChange1, Some("ok")))
|
List(RecordSetChangeInfo(rsChange2, Some("ok")), RecordSetChangeInfo(rsChange1, Some("ok")))
|
||||||
val expectedResults = ListRecordSetChangesResponse(
|
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 {
|
"ListZones" should {
|
||||||
"not fail with no zones returned" in {
|
"not fail with no zones returned" in {
|
||||||
doReturn(IO.pure(ListZonesResults(List())))
|
doReturn(IO.pure(ListZonesResults(List())))
|
||||||
|
@@ -556,7 +556,8 @@ class ZoneSyncHandlerSpec
|
|||||||
|
|
||||||
verify(recordChangeRepo).save(any[DB], captor.capture())
|
verify(recordChangeRepo).save(any[DB], captor.capture())
|
||||||
val req = captor.getValue
|
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 {
|
"save the record changes to the recordSetCacheRepo" in {
|
||||||
@@ -566,7 +567,8 @@ class ZoneSyncHandlerSpec
|
|||||||
|
|
||||||
verify(recordSetCacheRepo).save(any[DB], captor.capture())
|
verify(recordSetCacheRepo).save(any[DB], captor.capture())
|
||||||
val req = captor.getValue
|
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())
|
verify(recordSetRepo).apply(any[DB], captor.capture())
|
||||||
val req = captor.getValue
|
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 {
|
"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 org.scalatest.wordspec.AnyWordSpec
|
||||||
import vinyldns.api.Interfaces._
|
import vinyldns.api.Interfaces._
|
||||||
import vinyldns.api.config.LimitsConfig
|
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.api.domain.zone._
|
||||||
import vinyldns.core.TestMembershipData.okAuth
|
import vinyldns.core.TestMembershipData.okAuth
|
||||||
import vinyldns.core.domain.Fqdn
|
import vinyldns.core.domain.Fqdn
|
||||||
@@ -409,6 +409,16 @@ class RecordSetRoutingSpec
|
|||||||
maxItems = 100
|
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 =
|
private val failedChangesWithUserName =
|
||||||
List(rsChange1.copy(status = RecordSetChangeStatus.Failed) , rsChange2.copy(status = RecordSetChangeStatus.Failed))
|
List(rsChange1.copy(status = RecordSetChangeStatus.Failed) , rsChange2.copy(status = RecordSetChangeStatus.Failed))
|
||||||
private val listFailedRecordSetChangeResponse = ListFailedRecordSetChangesResponse(
|
private val listFailedRecordSetChangeResponse = ListFailedRecordSetChangesResponse(
|
||||||
@@ -736,18 +746,35 @@ class RecordSetRoutingSpec
|
|||||||
}.toResult
|
}.toResult
|
||||||
|
|
||||||
def listRecordSetChanges(
|
def listRecordSetChanges(
|
||||||
zoneId: String,
|
zoneId: Option[String],
|
||||||
startFrom: Option[Int],
|
startFrom: Option[Int],
|
||||||
maxItems: Int,
|
maxItems: Int,
|
||||||
|
fqdn: Option[String],
|
||||||
|
recordType: Option[RecordType],
|
||||||
authPrincipal: AuthPrincipal
|
authPrincipal: AuthPrincipal
|
||||||
): Result[ListRecordSetChangesResponse] = {
|
): Result[ListRecordSetChangesResponse] = {
|
||||||
zoneId match {
|
zoneId match {
|
||||||
case zoneNotFound.id => Left(ZoneNotFoundError(s"$zoneId"))
|
case Some(zoneNotFound.id) => Left(ZoneNotFoundError(s"$zoneId"))
|
||||||
case notAuthorizedZone.id => Left(NotAuthorizedError("no way"))
|
case Some(notAuthorizedZone.id) => Left(NotAuthorizedError("no way"))
|
||||||
case _ => Right(listRecordSetChangesResponse)
|
case _ => Right(listRecordSetChangesResponse)
|
||||||
}
|
}
|
||||||
}.toResult
|
}.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
|
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 {
|
"GET failed record set changes" should {
|
||||||
"return the failed record set changes" in {
|
"return the failed record set changes" in {
|
||||||
val rsChangeFailed1 = rsChange1.copy(status = RecordSetChangeStatus.Failed)
|
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 ok = Zone("ok.", "ok@test.com", acl = zoneAcl, adminGroupId = "test")
|
||||||
private val aclAsInfo = ZoneACLInfo(zoneAcl.rules.map(ACLRuleInfo(_, Some("name"))))
|
private val aclAsInfo = ZoneACLInfo(zoneAcl.rules.map(ACLRuleInfo(_, Some("name"))))
|
||||||
private val okAsZoneInfo = ZoneInfo(ok, aclAsInfo, okGroup.name, AccessLevel.Read)
|
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 badRegex = Zone("ok.", "bad-regex@test.com", adminGroupId = "test")
|
||||||
private val trailingDot = Zone("trailing.dot", "trailing-dot@test.com")
|
private val trailingDot = Zone("trailing.dot", "trailing-dot@test.com")
|
||||||
private val connectionOk = Zone(
|
private val connectionOk = Zone(
|
||||||
@@ -249,6 +250,15 @@ class ZoneRoutingSpec
|
|||||||
outcome.toResult
|
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] = {
|
def getZoneByName(zoneName: String, auth: AuthPrincipal): Result[ZoneInfo] = {
|
||||||
val outcome = zoneName match {
|
val outcome = zoneName match {
|
||||||
case notFound.name => Left(ZoneNotFoundError(s"$zoneName"))
|
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 {
|
"GET zone by name " should {
|
||||||
"return the zone is retrieved" in {
|
"return the zone is retrieved" in {
|
||||||
Get(s"/zones/name/${ok.name}") ~> zoneRoute ~> check {
|
Get(s"/zones/name/${ok.name}") ~> zoneRoute ~> check {
|
||||||
|
@@ -50,6 +50,11 @@ case class ChangeSet(
|
|||||||
status: ChangeSetStatus
|
status: ChangeSetStatus
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
def withRecordSetChange(recordSetChanges: Seq[RecordSetChange]): ChangeSet =
|
||||||
|
copy(
|
||||||
|
changes = recordSetChanges
|
||||||
|
)
|
||||||
|
|
||||||
def complete(change: RecordSetChange): ChangeSet = {
|
def complete(change: RecordSetChange): ChangeSet = {
|
||||||
val updatedChanges = this.changes.filterNot(_.id == change.id) :+ change
|
val updatedChanges = this.changes.filterNot(_.id == change.id) :+ change
|
||||||
if (isFinished)
|
if (isFinished)
|
||||||
|
@@ -18,6 +18,7 @@ package vinyldns.core.domain.record
|
|||||||
|
|
||||||
import cats.effect._
|
import cats.effect._
|
||||||
import scalikejdbc.DB
|
import scalikejdbc.DB
|
||||||
|
import vinyldns.core.domain.record.RecordType.RecordType
|
||||||
import vinyldns.core.repository.Repository
|
import vinyldns.core.repository.Repository
|
||||||
|
|
||||||
trait RecordChangeRepository extends Repository {
|
trait RecordChangeRepository extends Repository {
|
||||||
@@ -25,9 +26,11 @@ trait RecordChangeRepository extends Repository {
|
|||||||
def save(db: DB, changeSet: ChangeSet): IO[ChangeSet]
|
def save(db: DB, changeSet: ChangeSet): IO[ChangeSet]
|
||||||
|
|
||||||
def listRecordSetChanges(
|
def listRecordSetChanges(
|
||||||
zoneId: String,
|
zoneId: Option[String],
|
||||||
startFrom: Option[Int] = None,
|
startFrom: Option[Int] = None,
|
||||||
maxItems: Int = 100
|
maxItems: Int = 100,
|
||||||
|
fqdn: Option[String] = None,
|
||||||
|
recordType: Option[RecordType] = None
|
||||||
): IO[ListRecordSetChangesResults]
|
): IO[ListRecordSetChangesResults]
|
||||||
|
|
||||||
def getRecordSetChange(zoneId: String, changeId: String): IO[Option[RecordSetChange]]
|
def getRecordSetChange(zoneId: String, changeId: String): IO[Option[RecordSetChange]]
|
||||||
|
@@ -15,12 +15,13 @@ section: "operator_menu"
|
|||||||
- [Queue Configuration](#queue-configuration)
|
- [Queue Configuration](#queue-configuration)
|
||||||
- [Database Configuration](#database-configuration)
|
- [Database Configuration](#database-configuration)
|
||||||
- [Cryptography](#cryptography-settings)
|
- [Cryptography](#cryptography-settings)
|
||||||
|
- [Zone Connections](#zone-connections)
|
||||||
- [Additional Configuration Settings](#additional-configuration-settings)
|
- [Additional Configuration Settings](#additional-configuration-settings)
|
||||||
- [Full Example Config](#full-example-config)
|
- [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
|
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
|
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
|
It is important to note that the `api` and `portal` have _different_ configuration. We will review the configuration for
|
||||||
each separately.
|
each separately.
|
||||||
@@ -271,7 +272,7 @@ vinyldns {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Default Zone Connections
|
## Zone Connections
|
||||||
|
|
||||||
VinylDNS has three ways of indicating 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
|
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.
|
default value of 10000 will be used if not provided.
|
||||||
|
|
||||||
|
### Global Zone Connections Configuration:
|
||||||
```yaml
|
```yaml
|
||||||
vinyldns {
|
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
|
## Additional Configuration Settings
|
||||||
|
|
||||||
### Approved Name Servers
|
### Approved Name Servers
|
||||||
|
@@ -22,10 +22,12 @@ import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}
|
|||||||
import org.scalatest.matchers.should.Matchers
|
import org.scalatest.matchers.should.Matchers
|
||||||
import org.scalatest.wordspec.AnyWordSpec
|
import org.scalatest.wordspec.AnyWordSpec
|
||||||
import scalikejdbc._
|
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.core.domain.zone.Zone
|
||||||
import vinyldns.mysql.TestMySqlInstance
|
import vinyldns.mysql.TestMySqlInstance
|
||||||
import vinyldns.mysql.TransactionProvider
|
import vinyldns.mysql.TransactionProvider
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
class MySqlRecordChangeRepositoryIntegrationSpec
|
class MySqlRecordChangeRepositoryIntegrationSpec
|
||||||
extends AnyWordSpec
|
extends AnyWordSpec
|
||||||
with Matchers
|
with Matchers
|
||||||
@@ -60,6 +62,15 @@ class MySqlRecordChangeRepositoryIntegrationSpec
|
|||||||
newRecordSets.map(makeTestAddChange(_, zone)).toList
|
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] = {
|
def generateFailedInserts(zone: Zone, count: Int): List[RecordSetChange] = {
|
||||||
val newRecordSets =
|
val newRecordSets =
|
||||||
for {
|
for {
|
||||||
@@ -102,7 +113,7 @@ class MySqlRecordChangeRepositoryIntegrationSpec
|
|||||||
repo.save(db, ChangeSet(inserts))
|
repo.save(db, ChangeSet(inserts))
|
||||||
}
|
}
|
||||||
saveRecChange.attempt.unsafeRunSync() shouldBe right
|
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.nextId shouldBe defined
|
||||||
result.maxItems shouldBe 5
|
result.maxItems shouldBe 5
|
||||||
(result.items should have).length(5)
|
(result.items should have).length(5)
|
||||||
@@ -121,21 +132,48 @@ class MySqlRecordChangeRepositoryIntegrationSpec
|
|||||||
repo.save(db, ChangeSet(timeSpaced))
|
repo.save(db, ChangeSet(timeSpaced))
|
||||||
}
|
}
|
||||||
saveRecChange.attempt.unsafeRunSync() shouldBe right
|
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.nextId shouldBe Some(2)
|
||||||
page1.maxItems shouldBe 2
|
page1.maxItems shouldBe 2
|
||||||
(page1.items should contain).theSameElementsInOrderAs(expectedOrder.take(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.nextId shouldBe Some(4)
|
||||||
page2.maxItems shouldBe 2
|
page2.maxItems shouldBe 2
|
||||||
(page2.items should contain).theSameElementsInOrderAs(expectedOrder.slice(2, 4))
|
(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.nextId shouldBe None
|
||||||
page3.maxItems shouldBe 2
|
page3.maxItems shouldBe 2
|
||||||
page3.items should contain theSameElementsAs expectedOrder.slice(4, 5)
|
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 {
|
"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 cats.effect._
|
||||||
import scalikejdbc._
|
import scalikejdbc._
|
||||||
import vinyldns.core.domain.record.RecordSetChangeType.RecordSetChangeType
|
import vinyldns.core.domain.record.RecordSetChangeType.RecordSetChangeType
|
||||||
|
import vinyldns.core.domain.record.RecordType.RecordType
|
||||||
import vinyldns.core.domain.record._
|
import vinyldns.core.domain.record._
|
||||||
import vinyldns.core.protobuf.ProtobufConversions
|
import vinyldns.core.protobuf.ProtobufConversions
|
||||||
import vinyldns.core.route.Monitored
|
import vinyldns.core.route.Monitored
|
||||||
|
import vinyldns.mysql.repository.MySqlRecordSetRepository.fromRecordType
|
||||||
import vinyldns.proto.VinylDNSProto
|
import vinyldns.proto.VinylDNSProto
|
||||||
|
|
||||||
class MySqlRecordChangeRepository
|
class MySqlRecordChangeRepository
|
||||||
@@ -39,6 +41,24 @@ class MySqlRecordChangeRepository
|
|||||||
| LIMIT {limit} OFFSET {startFrom}
|
| LIMIT {limit} OFFSET {startFrom}
|
||||||
""".stripMargin
|
""".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 =
|
private val LIST_RECORD_CHANGES =
|
||||||
sql"""
|
sql"""
|
||||||
|SELECT data
|
|SELECT data
|
||||||
@@ -63,7 +83,7 @@ class MySqlRecordChangeRepository
|
|||||||
""".stripMargin
|
""".stripMargin
|
||||||
|
|
||||||
private val INSERT_CHANGES =
|
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
|
* 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.zoneId,
|
||||||
change.created.toEpochMilli,
|
change.created.toEpochMilli,
|
||||||
fromChangeType(change.changeType),
|
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(
|
def listRecordSetChanges(
|
||||||
zoneId: String,
|
zoneId: Option[String],
|
||||||
startFrom: Option[Int],
|
startFrom: Option[Int],
|
||||||
maxItems: Int
|
maxItems: Int,
|
||||||
|
fqdn: Option[String],
|
||||||
|
recordType: Option[RecordType]
|
||||||
): IO[ListRecordSetChangesResults] =
|
): IO[ListRecordSetChangesResults] =
|
||||||
monitor("repo.RecordChange.listRecordSetChanges") {
|
monitor("repo.RecordChange.listRecordSetChanges") {
|
||||||
IO {
|
IO {
|
||||||
DB.readOnly { implicit s =>
|
DB.readOnly { implicit s =>
|
||||||
val changes = startFrom match {
|
val changes = if(startFrom.isDefined && fqdn.isDefined && recordType.isDefined){
|
||||||
case Some(start) =>
|
LIST_CHANGES_WITH_START_FQDN_TYPE
|
||||||
LIST_CHANGES_WITH_START
|
.bindByName('fqdn -> fqdn.get, 'type -> fromRecordType(recordType.get), 'startFrom -> startFrom.get, 'limit -> (maxItems + 1))
|
||||||
.bindByName('zoneId -> zoneId, 'startFrom -> start, 'limit -> maxItems)
|
|
||||||
.map(toRecordSetChange)
|
.map(toRecordSetChange)
|
||||||
.list()
|
.list()
|
||||||
.apply()
|
.apply()
|
||||||
case None =>
|
} 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
|
LIST_CHANGES_NO_START
|
||||||
.bindByName('zoneId -> zoneId, 'limit -> maxItems)
|
.bindByName('zoneId -> zoneId.get, 'limit -> (maxItems + 1))
|
||||||
.map(toRecordSetChange)
|
.map(toRecordSetChange)
|
||||||
.list()
|
.list()
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val maxQueries = changes.take(maxItems)
|
||||||
val startValue = startFrom.getOrElse(0)
|
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(
|
ListRecordSetChangesResults(
|
||||||
changes,
|
maxQueries,
|
||||||
nextId,
|
nextId,
|
||||||
startFrom,
|
startFrom,
|
||||||
maxItems
|
maxItems
|
||||||
|
@@ -76,7 +76,8 @@ class FrontendController @Inject() (
|
|||||||
}
|
}
|
||||||
|
|
||||||
def viewRecordSets(): Action[AnyContent] = userAction.async { implicit request =>
|
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 =>
|
def viewAllBatchChanges(): Action[AnyContent] = userAction.async { implicit request =>
|
||||||
|
@@ -458,6 +458,16 @@ class VinylDNS @Inject() (
|
|||||||
// $COVERAGE-ON$
|
// $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 =>
|
def getZoneByName(name: String): Action[AnyContent] = userAction.async { implicit request =>
|
||||||
val vinyldnsRequest =
|
val vinyldnsRequest =
|
||||||
new VinylDNSRequest("GET", s"$vinyldnsServiceBackend", s"zones/name/$name")
|
new VinylDNSRequest("GET", s"$vinyldnsServiceBackend", s"zones/name/$name")
|
||||||
@@ -590,6 +600,25 @@ class VinylDNS @Inject() (
|
|||||||
// $COVERAGE-ON$
|
// $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 =>
|
def addZone(): Action[AnyContent] = userAction.async { implicit request =>
|
||||||
// $COVERAGE-OFF$
|
// $COVERAGE-OFF$
|
||||||
val json = request.body.asJson
|
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 = {
|
@content = {
|
||||||
<!-- PAGE CONTENT -->
|
<!-- PAGE CONTENT -->
|
||||||
@@ -81,7 +81,6 @@
|
|||||||
<div class="btn-group">
|
<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="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="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>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -167,6 +166,7 @@
|
|||||||
@if(meta.sharedDisplayEnabled) {
|
@if(meta.sharedDisplayEnabled) {
|
||||||
<th>Owner Group Name</th>
|
<th>Owner Group Name</th>
|
||||||
}
|
}
|
||||||
|
<th ng-if="rootAccountCanReview || userCanAccessGroup">Record History</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -374,6 +374,9 @@
|
|||||||
title="Group with ID {{record.ownerGroupId}} no longer exists."><span class="fa fa-warning"></span> Group deleted</span>
|
title="Group with ID {{record.ownerGroupId}} no longer exists."><span class="fa fa-warning"></span> Group deleted</span>
|
||||||
</td>
|
</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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -402,6 +405,102 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
<!-- END PAGE CONTENT -->
|
<!-- 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 = {
|
@plugins = {
|
||||||
|
@@ -29,6 +29,7 @@ GET /api/recordsets @controllers.VinylDNS.listRecor
|
|||||||
GET /api/zones @controllers.VinylDNS.getZones
|
GET /api/zones @controllers.VinylDNS.getZones
|
||||||
GET /api/zones/backendids @controllers.VinylDNS.getBackendIds
|
GET /api/zones/backendids @controllers.VinylDNS.getBackendIds
|
||||||
GET /api/zones/:id @controllers.VinylDNS.getZone(id: String)
|
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/name/:name @controllers.VinylDNS.getZoneByName(name: String)
|
||||||
GET /api/zones/:id/changes @controllers.VinylDNS.getZoneChange(id: String)
|
GET /api/zones/:id/changes @controllers.VinylDNS.getZoneChange(id: String)
|
||||||
POST /api/zones @controllers.VinylDNS.addZone
|
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)
|
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/zones/:id/recordsetchanges @controllers.VinylDNS.listRecordSetChanges(id: String)
|
||||||
|
GET /api/recordsetchange/history @controllers.VinylDNS.listRecordSetChangeHistory
|
||||||
|
|
||||||
GET /api/groups @controllers.VinylDNS.getGroups
|
GET /api/groups @controllers.VinylDNS.getGroups
|
||||||
GET /api/groups/:gid @controllers.VinylDNS.getGroup(gid: String)
|
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 */
|
/* Ending of css override for cron library and it's associated elements used in zone sync scheduling */
|
||||||
|
|
||||||
|
.set-width {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
#set-dropdown-width {
|
#set-dropdown-width {
|
||||||
width: 4em;
|
width: 4em;
|
||||||
}
|
}
|
||||||
|
@@ -237,6 +237,9 @@ angular.module('controller.manageZones', ['angular-cron-jobs'])
|
|||||||
|
|
||||||
$scope.submitUpdateZone = function () {
|
$scope.submitUpdateZone = function () {
|
||||||
var zone = angular.copy($scope.updateZoneInfo);
|
var zone = angular.copy($scope.updateZoneInfo);
|
||||||
|
if(zone['recurrenceSchedule'] == ""){
|
||||||
|
delete zone['recurrenceSchedule']
|
||||||
|
}
|
||||||
zone = zonesService.normalizeZoneDates(zone);
|
zone = zonesService.normalizeZoneDates(zone);
|
||||||
zone = zonesService.setConnectionKeys(zone);
|
zone = zonesService.setConnectionKeys(zone);
|
||||||
zone = zonesService.checkBackendId(zone);
|
zone = zonesService.checkBackendId(zone);
|
||||||
|
@@ -29,6 +29,51 @@
|
|||||||
$scope.readRecordTypes = ['A', 'AAAA', 'CNAME', 'DS', 'MX', 'NS', 'PTR', "SOA", 'SRV', 'NAPTR', 'SSHFP', 'TXT'];
|
$scope.readRecordTypes = ['A', 'AAAA', 'CNAME', 'DS', 'MX', 'NS', 'PTR', "SOA", 'SRV', 'NAPTR', 'SSHFP', 'TXT'];
|
||||||
$scope.selectedRecordTypes = [];
|
$scope.selectedRecordTypes = [];
|
||||||
$scope.groups = [];
|
$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
|
// paging status for recordsets
|
||||||
var recordsPaging = pagingService.getNewPagingParams(100);
|
var recordsPaging = pagingService.getNewPagingParams(100);
|
||||||
@@ -68,6 +113,13 @@
|
|||||||
.append("<div>" + recordSet + "</div>")
|
.append("<div>" + recordSet + "</div>")
|
||||||
.appendTo(ul); };
|
.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() {
|
$scope.refreshRecords = function() {
|
||||||
if($scope.query.includes("|")) {
|
if($scope.query.includes("|")) {
|
||||||
const queryRecord = $scope.query.split('|');
|
const queryRecord = $scope.query.split('|');
|
||||||
@@ -80,6 +132,7 @@
|
|||||||
function success(response) {
|
function success(response) {
|
||||||
recordsPaging.next = response.data.nextId;
|
recordsPaging.next = response.data.nextId;
|
||||||
updateRecordDisplay(response.data['recordSets']);
|
updateRecordDisplay(response.data['recordSets']);
|
||||||
|
getMembership();
|
||||||
}
|
}
|
||||||
return recordsService
|
return recordsService
|
||||||
.listRecordSetData(recordsPaging.maxItems, undefined, recordName, recordType, $scope.nameSort, $scope.ownerGroupFilter)
|
.listRecordSetData(recordsPaging.maxItems, undefined, recordName, recordType, $scope.nameSort, $scope.ownerGroupFilter)
|
||||||
@@ -131,6 +184,11 @@
|
|||||||
newRecords.push(recordsService.toDisplayRecord(record, ''));
|
newRecords.push(recordsService.toDisplayRecord(record, ''));
|
||||||
});
|
});
|
||||||
$scope.records = newRecords;
|
$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) {
|
if ($scope.records.length > 0) {
|
||||||
$("#ShowNoRec").modal("hide");
|
$("#ShowNoRec").modal("hide");
|
||||||
$("td.dataTables_empty").hide();
|
$("td.dataTables_empty").hide();
|
||||||
@@ -186,5 +244,142 @@
|
|||||||
handleError(error, 'recordsService::nextPage-failure');
|
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);
|
return $http.get("/api/zones/"+zid);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.getCommonZoneDetails = function (zid) {
|
||||||
|
return $http.get("/api/zones/"+zid+"/details");
|
||||||
|
};
|
||||||
|
|
||||||
this.syncZone = function (zid) {
|
this.syncZone = function (zid) {
|
||||||
return $http.post("/api/zones/"+zid+"/sync", {}, {headers: utilityService.getCsrfHeader()});
|
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 url = '/api/zones/' + zid + '/recordsetchanges';
|
||||||
var params = {
|
var params = {
|
||||||
"maxItems": maxItems,
|
"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);
|
url = utilityService.urlBuilder(url, params);
|
||||||
return $http.get(url);
|
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 {
|
"Get for login" should {
|
||||||
"with ldap enabled" should {
|
"with ldap enabled" should {
|
||||||
"render the login page when the user is not logged in" in new WithApplication(app) {
|
"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 {
|
".getZoneChange" should {
|
||||||
|
|
||||||
"return ok (200) if the zoneChanges is found" in new WithApplication(app) {
|
"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 {
|
".addZone" should {
|
||||||
"return unauthorized (401) if requesting user is not logged in" in new WithApplication(app) {
|
"return unauthorized (401) if requesting user is not logged in" in new WithApplication(app) {
|
||||||
val client = mock[WSClient]
|
val client = mock[WSClient]
|
||||||
|
@@ -83,7 +83,7 @@ class Route53Backend(
|
|||||||
val found = result.getHostedZones.asScala.toList.headOption.map { hz =>
|
val found = result.getHostedZones.asScala.toList.headOption.map { hz =>
|
||||||
val hzid = parseHostedZoneId(hz.getId)
|
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)
|
zoneMap.putIfAbsent(hz.getName, hzid)
|
||||||
hzid
|
hzid
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user