2
0
mirror of https://github.com/VinylDNS/vinyldns synced 2025-08-22 02:02:14 +00:00

Record type filter for shared zones (#479)

This commit is contained in:
Britney Wright 2019-02-19 12:00:34 -05:00 committed by GitHub
parent c5c5bccfa9
commit 593fe45b52
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 250 additions and 82 deletions

View File

@ -188,6 +188,9 @@ vinyldns {
"fd69:27cc:fe91:0:0:0:ffff:0"
]
}
# types of unowned records that users can access in shared zones
shared-approved-types = ["A", "AAAA", "CNAME", "PTR"]
}
akka {

View File

@ -1874,28 +1874,44 @@ def test_create_in_shared_zone_without_owner_group_id_succeeds(shared_zone_test_
delete_result = dummy_client.delete_recordset(create_rs['zoneId'], create_rs['id'], status=202)
shared_client.wait_until_recordset_change_status(delete_result, 'Complete')
def test_create_in_shared_zone_by_unassociated_user_succeeds(shared_zone_test_context):
def test_create_in_shared_zone_by_unassociated_user_succeeds_if_record_type_is_approved(shared_zone_test_context):
"""
Test that creating a record in a shared zone by an unassociated user succeeds
Test that creating a record in a shared zone by a user with no write permissions succeeds if the record type is approved
"""
dummy_client = shared_zone_test_context.dummy_vinyldns_client
shared_client = shared_zone_test_context.shared_zone_vinyldns_client
client = shared_zone_test_context.dummy_vinyldns_client
zone = shared_zone_test_context.shared_zone
group = shared_zone_test_context.dummy_group
record_json = get_recordset_json(zone, 'test_shared_approved_record_type', 'A', [{'address': '1.1.1.1'}])
record_json['ownerGroupId'] = group['id']
create_rs = None
record_json = get_recordset_json(zone, 'test_shared_bad_user', 'A', [{'address': '1.1.1.1'}], ownergroup_id=group['id'])
try:
create_response = dummy_client.create_recordset(record_json, status=202)
create_rs = shared_client.wait_until_recordset_change_status(create_response, 'Complete')['recordSet']
create_response = client.create_recordset(record_json, status=202)
create_rs = client.wait_until_recordset_change_status(create_response, 'Complete')['recordSet']
assert_that(create_rs['ownerGroupId'], is_(group['id']))
finally:
if create_rs:
delete_result = dummy_client.delete_recordset(create_rs['zoneId'], create_rs['id'], status=202)
shared_client.wait_until_recordset_change_status(delete_result, 'Complete')
delete_result = client.delete_recordset(zone['id'], create_rs['id'], status=202)
client.wait_until_recordset_change_status(delete_result, 'Complete')
def test_create_in_shared_zone_by_unassociated_user_fails_if_record_type_is_not_approved(shared_zone_test_context):
"""
Test that creating a record in a shared zone by a user with no write permissions fails if the record type is not approved
"""
client = shared_zone_test_context.dummy_vinyldns_client
zone = shared_zone_test_context.shared_zone
group = shared_zone_test_context.dummy_group
record_json = get_recordset_json(zone, 'test_shared_not_approved_record_type', 'MX', [{'preference': 3, 'exchange': 'mx'}])
record_json['ownerGroupId'] = group['id']
error = client.create_recordset(record_json, status=403)
assert_that(error, is_('User dummy does not have access to create test-shared-not-approved-record-type.shared.'))
def test_create_with_not_found_owner_group_fails(shared_zone_test_context):
"""

View File

@ -662,53 +662,6 @@ def test_no_delete_access_non_test_zone(shared_zone_test_context):
client.delete_recordset(zone_id, record_delete['id'], status=403)
def test_delete_for_user_not_in_record_owner_group_in_shared_zone_fails(shared_zone_test_context):
"""
Test that a user cannot delete a record in a shared zone if not part of record owner group
"""
dummy_client = shared_zone_test_context.dummy_vinyldns_client
shared_client = shared_zone_test_context.shared_zone_vinyldns_client
shared_zone = shared_zone_test_context.shared_zone
result_rs = None
record_json = get_recordset_json(shared_zone, 'test_shared_del_nonog', 'A', [{'address': '1.1.1.1'}], ownergroup_id = shared_zone_test_context.shared_record_group['id'])
try:
create_rs = shared_client.create_recordset(record_json, status=202)
result_rs = shared_client.wait_until_recordset_change_status(create_rs, 'Complete')['recordSet']
error = dummy_client.delete_recordset(shared_zone['id'], result_rs['id'], status=403)
assert_that(error, is_('User dummy does not have access to delete test-shared-del-nonog.shared.'))
finally:
if result_rs:
delete_rs = shared_client.delete_recordset(result_rs['zoneId'], result_rs['id'], status=202)
shared_client.wait_until_recordset_change_status(delete_rs, 'Complete')
def test_delete_for_user_in_record_owner_group_in_non_shared_zone_fails(shared_zone_test_context):
"""
Test that a user in record owner group cannot delete a record in a non-shared zone
"""
ok_client = shared_zone_test_context.ok_vinyldns_client
shared_client = shared_zone_test_context.shared_zone_vinyldns_client
ok_zone = shared_zone_test_context.ok_zone
result_rs = None
record_json = get_recordset_json(ok_zone, 'test_non_shared_del_og', 'A', [{'address': '1.1.1.1'}], ownergroup_id = shared_zone_test_context.shared_record_group['id'])
try:
create_rs = ok_client.create_recordset(record_json, status=202)
result_rs = ok_client.wait_until_recordset_change_status(create_rs, 'Complete')['recordSet']
error = shared_client.delete_recordset(ok_zone['id'], result_rs['id'], status=403)
assert_that(error, is_('User sharedZoneUser does not have access to delete test-non-shared-del-og.ok.'))
finally:
if result_rs:
delete_rs = ok_client.delete_recordset(result_rs['zoneId'], result_rs['id'], status=202)
ok_client.wait_until_recordset_change_status(delete_rs, 'Complete')
def test_delete_for_user_in_record_owner_group_in_shared_zone_succeeds(shared_zone_test_context):
"""
Test that a user in record owner group can delete a record in a shared zone
@ -740,3 +693,90 @@ def test_delete_for_zone_admin_in_shared_zone_succeeds(shared_zone_test_context)
delete_rs = shared_client.delete_recordset(result_rs['zoneId'], result_rs['id'], status=202)
shared_client.wait_until_recordset_change_status(delete_rs, 'Complete')
def test_delete_for_unowned_record_with_approved_record_type_in_shared_zone_succeeds(shared_zone_test_context):
"""
Test that a user not associated with a unowned record can delete it in a shared zone
"""
shared_client = shared_zone_test_context.shared_zone_vinyldns_client
shared_zone = shared_zone_test_context.shared_zone
ok_client = shared_zone_test_context.ok_vinyldns_client
record_json = get_recordset_json(shared_zone, 'test_shared_approved_record_type', 'A', [{'address': '1.1.1.1'}])
create_rs = shared_client.create_recordset(record_json, status=202)
result_rs = shared_client.wait_until_recordset_change_status(create_rs, 'Complete')['recordSet']
delete_rs = ok_client.delete_recordset(result_rs['zoneId'], result_rs['id'], status=202)
ok_client.wait_until_recordset_change_status(delete_rs, 'Complete')
def test_delete_for_user_not_in_record_owner_group_in_shared_zone_fails(shared_zone_test_context):
"""
Test that a user cannot delete a record in a shared zone if not part of record owner group
"""
dummy_client = shared_zone_test_context.dummy_vinyldns_client
shared_client = shared_zone_test_context.shared_zone_vinyldns_client
shared_zone = shared_zone_test_context.shared_zone
result_rs = None
record_json = get_recordset_json(shared_zone, 'test_shared_del_nonog', 'A', [{'address': '1.1.1.1'}], ownergroup_id = shared_zone_test_context.shared_record_group['id'])
try:
create_rs = shared_client.create_recordset(record_json, status=202)
result_rs = shared_client.wait_until_recordset_change_status(create_rs, 'Complete')['recordSet']
error = dummy_client.delete_recordset(shared_zone['id'], result_rs['id'], status=403)
assert_that(error, is_('User dummy does not have access to delete test-shared-del-nonog.shared.'))
finally:
if result_rs:
delete_rs = shared_client.delete_recordset(result_rs['zoneId'], result_rs['id'], status=202)
shared_client.wait_until_recordset_change_status(delete_rs, 'Complete')
def test_delete_for_user_not_in_unowned_record_in_shared_zone_fails_if_record_type_is_not_approved(shared_zone_test_context):
"""
Test that a user cannot delete a record in a shared zone if the record is unowned and the record type is not approved
"""
dummy_client = shared_zone_test_context.dummy_vinyldns_client
shared_client = shared_zone_test_context.shared_zone_vinyldns_client
shared_zone = shared_zone_test_context.shared_zone
result_rs = None
record_json = get_recordset_json(shared_zone, 'test_shared_del_not_approved_record_type', 'MX', [{'preference': 3, 'exchange': 'mx'}])
try:
create_rs = shared_client.create_recordset(record_json, status=202)
result_rs = shared_client.wait_until_recordset_change_status(create_rs, 'Complete')['recordSet']
error = dummy_client.delete_recordset(shared_zone['id'], result_rs['id'], status=403)
assert_that(error, is_('User dummy does not have access to delete test-shared-del-not-approved-record-type.shared.'))
finally:
if result_rs:
delete_rs = shared_client.delete_recordset(result_rs['zoneId'], result_rs['id'], status=202)
shared_client.wait_until_recordset_change_status(delete_rs, 'Complete')
def test_delete_for_user_in_record_owner_group_in_non_shared_zone_fails(shared_zone_test_context):
"""
Test that a user in record owner group cannot delete a record in a non-shared zone
"""
ok_client = shared_zone_test_context.ok_vinyldns_client
shared_client = shared_zone_test_context.shared_zone_vinyldns_client
ok_zone = shared_zone_test_context.ok_zone
result_rs = None
record_json = get_recordset_json(ok_zone, 'test_non_shared_del_og', 'A', [{'address': '1.1.1.1'}], ownergroup_id = shared_zone_test_context.shared_record_group['id'])
try:
create_rs = ok_client.create_recordset(record_json, status=202)
result_rs = ok_client.wait_until_recordset_change_status(create_rs, 'Complete')['recordSet']
error = shared_client.delete_recordset(ok_zone['id'], result_rs['id'], status=403)
assert_that(error, is_('User sharedZoneUser does not have access to delete test-non-shared-del-og.ok.'))
finally:
if result_rs:
delete_rs = ok_client.delete_recordset(result_rs['zoneId'], result_rs['id'], status=202)
ok_client.wait_until_recordset_change_status(delete_rs, 'Complete')

View File

@ -158,22 +158,47 @@ def test_get_recordset_from_shared_zone(shared_zone_test_context):
delete_result = client.delete_recordset(retrieved_rs['zoneId'], retrieved_rs['id'], status=202)
client.wait_until_recordset_change_status(delete_result, 'Complete')
def test_get_unowned_recordset_from_shared_zone(shared_zone_test_context):
def test_get_unowned_recordset_from_shared_zone_succeeds_if_record_type_approved(shared_zone_test_context):
"""
Test getting an unowned recordset with no admin rights succeeds
Test getting an unowned recordset with no admin rights succeeds if the record type is approved
"""
client = shared_zone_test_context.shared_zone_vinyldns_client
ok_client = shared_zone_test_context.ok_vinyldns_client
result_rs = None
try:
new_rs = get_recordset_json(shared_zone_test_context.shared_zone,
"test_get_unowned_recordset_approved_type", "A", [{"address": "1.2.3.4"}])
result = client.create_recordset(new_rs, status=202)
result_rs = client.wait_until_recordset_change_status(result, 'Complete')['recordSet']
# Get the recordset we just made and verify
retrieved = ok_client.get_recordset(result_rs['zoneId'], result_rs['id'], status=200)
retrieved_rs = retrieved['recordSet']
verify_recordset(retrieved_rs, new_rs)
finally:
if result_rs:
delete_result = ok_client.delete_recordset(result_rs['zoneId'], result_rs['id'], status=202)
ok_client.wait_until_recordset_change_status(delete_result, 'Complete')
def test_get_unowned_recordset_from_shared_zone_fails_if_record_type_not_approved(shared_zone_test_context):
"""
Test getting an unowned recordset with no admin rights fails if the record type is not approved
"""
client = shared_zone_test_context.shared_zone_vinyldns_client
result_rs = None
try:
new_rs = get_recordset_json(shared_zone_test_context.shared_zone,
"test_get_unowned_recordset", "TXT", [{'text':'should-not-work'}])
"test_get_unowned_recordset", "MX", [{'preference': 3, 'exchange': 'mx'}])
result = client.create_recordset(new_rs, status=202)
result_rs = client.wait_until_recordset_change_status(result, 'Complete')['recordSet']
# Get the recordset we just made and verify
ok_client = shared_zone_test_context.ok_vinyldns_client
ok_client.get_recordset(result_rs['zoneId'], result_rs['id'], status=200)
error = ok_client.get_recordset(result_rs['zoneId'], result_rs['id'], status=403)
assert_that(error, is_("User ok does not have access to view test-get-unowned-recordset.shared."))
finally:
if result_rs:

View File

@ -2032,11 +2032,11 @@ def test_update_owner_group_from_user_in_record_owner_group_for_shared_zone_pass
ok_client = shared_zone_test_context.ok_vinyldns_client
shared_record_group = shared_zone_test_context.shared_record_group
shared_client = shared_zone_test_context.shared_zone_vinyldns_client
zone = shared_zone_test_context.shared_zone
shared_zone = shared_zone_test_context.shared_zone
update_rs = None
try:
record_json = get_recordset_json(zone, 'test_shared_success', 'A', [{'address': '1.1.1.1'}])
record_json = get_recordset_json(shared_zone, 'test_shared_success', 'A', [{'address': '1.1.1.1'}])
record_json['ownerGroupId'] = shared_record_group['id']
create_response = shared_client.create_recordset(record_json, status=202)
update = shared_client.wait_until_recordset_change_status(create_response, 'Complete')['recordSet']
@ -2051,7 +2051,7 @@ def test_update_owner_group_from_user_in_record_owner_group_for_shared_zone_pass
finally:
if update_rs:
delete_result = shared_client.delete_recordset(zone['id'], update_rs['id'], status=202)
delete_result = shared_client.delete_recordset(shared_zone['id'], update_rs['id'], status=202)
shared_client.wait_until_recordset_change_status(delete_result, 'Complete')
@ -2084,10 +2084,35 @@ def test_update_owner_group_from_admin_in_shared_zone_passes(shared_zone_test_co
delete_result = shared_client.delete_recordset(zone['id'], update_rs['id'], status=202)
shared_client.wait_until_recordset_change_status(delete_result, 'Complete')
def test_update_from_unassociated_user_in_shared_zone_succeeds(shared_zone_test_context):
def test_update_from_unassociated_user_in_shared_zone_passes_when_record_type_is_approved(shared_zone_test_context):
"""
Test that an unassociated user updating record without existing owner group ID in shared zone succeeds
Test that updating with a user that does not have write access succeeds in a shared zone if the record type is approved
"""
ok_client = shared_zone_test_context.ok_vinyldns_client
shared_client = shared_zone_test_context.shared_zone_vinyldns_client
zone = shared_zone_test_context.shared_zone
update_rs = None
try:
record_json = get_recordset_json(zone, 'test_shared_approved_record_type', 'A', [{'address': '1.1.1.1'}])
create_response = shared_client.create_recordset(record_json, status=202)
create_rs = shared_client.wait_until_recordset_change_status(create_response, 'Complete')['recordSet']
assert_that(create_rs, is_not(has_key('ownerGroupId')))
update = create_rs
update['ttl'] = update['ttl'] + 100
update_response = ok_client.update_recordset(update, status=202)
update_rs = shared_client.wait_until_recordset_change_status(update_response, 'Complete')['recordSet']
finally:
if update_rs:
delete_result = shared_client.delete_recordset(zone['id'], update_rs['id'], status=202)
shared_client.wait_until_recordset_change_status(delete_result, 'Complete')
def test_update_from_unassociated_user_in_shared_zone_fails(shared_zone_test_context):
"""
Test that updating with a user that does not have write access fails in a shared zone
"""
ok_client = shared_zone_test_context.ok_vinyldns_client
@ -2096,16 +2121,15 @@ def test_update_from_unassociated_user_in_shared_zone_succeeds(shared_zone_test_
create_rs = None
try:
record_json = get_recordset_json(zone, 'test_shared_success', 'A', [{'address': '1.1.1.1'}])
record_json = get_recordset_json(zone, 'test_shared_unapproved_record_type', 'MX', [{'preference': 3, 'exchange': 'mx'}])
create_response = shared_client.create_recordset(record_json, status=202)
create_rs = shared_client.wait_until_recordset_change_status(create_response, 'Complete')['recordSet']
assert_that(create_rs, is_not(has_key('ownerGroupId')))
update = create_rs
update['ttl'] = update['ttl'] + 100
update_response = ok_client.update_recordset(update, status=202)
update_rs = shared_client.wait_until_recordset_change_status(update_response, 'Complete')
assert_that(update_rs, is_not(has_key('ownerGroupId')))
error = ok_client.update_recordset(update, status=403)
assert_that(error, is_('User ok does not have access to update test-shared-unapproved-record-type.shared.'))
finally:
if create_rs:
@ -2127,7 +2151,7 @@ def test_update_from_acl_for_shared_zone_passes(shared_zone_test_context):
try:
add_shared_zone_acl_rules(shared_zone_test_context, [acl_rule])
record_json = get_recordset_json(zone, 'test_shared_success', 'A', [{'address': '1.1.1.1'}])
record_json = get_recordset_json(zone, 'test_shared_acl', 'A', [{'address': '1.1.1.1'}])
create_response = shared_client.create_recordset(record_json, status=202)
update = shared_client.wait_until_recordset_change_status(create_response, 'Complete')['recordSet']
assert_that(update, is_not(has_key('ownerGroupId')))

View File

@ -53,6 +53,8 @@ vinyldns {
]
}
# types of unowned records that users can access in shared zones
shared-approved-types = ["A", "AAAA", "CNAME", "PTR"]
dynamodb.repositories {
record-set {

View File

@ -116,4 +116,7 @@ vinyldns {
"fd69:27cc:fe91:0:0:0:ffff:0"
]
}
# types of unowned records that users can access in shared zones
shared-approved-types = ["A", "AAAA", "CNAME", "PTR"]
}

View File

@ -23,6 +23,9 @@ import com.typesafe.config.{Config, ConfigFactory}
import pureconfig.module.catseffect.loadConfigF
import vinyldns.api.crypto.Crypto
import com.comcast.ip4s._
import net.ceedubs.ficus.Ficus._
import net.ceedubs.ficus.readers.EnumerationReader._
import vinyldns.core.domain.record.RecordType
import scala.collection.JavaConverters._
import scala.util.matching.Regex
@ -63,6 +66,9 @@ object VinylDNSConfig {
lazy val highValueIpList: List[IpAddress] =
getOptionalStringList("high-value-domains.ip-list").flatMap(ip => IpAddress(ip))
lazy val sharedApprovedTypes: Set[RecordType.Value] =
vinyldnsConfig.as[Option[Set[RecordType.Value]]]("shared-approved-types").getOrElse(Set())
lazy val defaultZoneConnection: ZoneConnection = {
val connectionConfig = VinylDNSConfig.vinyldnsConfig.getConfig("defaultZoneConnection")
val name = connectionConfig.getString("name")

View File

@ -17,6 +17,7 @@
package vinyldns.api.domain
import vinyldns.api.Interfaces.ensuring
import vinyldns.api.VinylDNSConfig
import vinyldns.api.domain.zone._
import vinyldns.core.domain.auth.AuthPrincipal
import vinyldns.core.domain.record.RecordType
@ -184,11 +185,18 @@ object AccessValidations extends AccessValidationAlgebra {
case testUser if testUser.isTestUser && !zone.isTest => AccessLevel.NoAccess
case admin if admin.canEditAll || admin.isGroupMember(zone.adminGroupId) =>
AccessLevel.Delete
case recordOwner if zone.shared && recordOwnerGroupId.forall(recordOwner.isGroupMember) =>
case recordOwner if zone.shared && sharedRecordAccess(recordOwner, recordType, recordOwnerGroupId) =>
AccessLevel.Delete
case supportUser if supportUser.canReadAll =>
val aclAccess = getAccessFromAcl(auth, recordName, recordType, zone)
if (aclAccess == AccessLevel.NoAccess) AccessLevel.Read else aclAccess
case _ => getAccessFromAcl(auth, recordName, recordType, zone)
}
def sharedRecordAccess(
auth: AuthPrincipal,
recordType: RecordType,
recordOwnerGroupId: Option[String]): Boolean =
recordOwnerGroupId.exists(auth.isGroupMember) ||
(recordOwnerGroupId.isEmpty && VinylDNSConfig.sharedApprovedTypes.contains(recordType))
}

View File

@ -37,6 +37,9 @@ vinyldns {
]
}
# types of unowned records that users can access in shared zones
shared-approved-types = ["A", "AAAA", "CNAME", "PTR"]
# used for testing only
string-list-test = ["test"]

View File

@ -490,18 +490,28 @@ class AccessValidationsSpec
result shouldBe AccessLevel.Delete
}
"return AccessLevel.Delete if the record is unowned and the zone is shared" in {
val recordOwnerAuth = AuthPrincipal(dummyUser.copy(isSupport = true), Seq())
"return AccessLevel.Delete if the zone is shared and the record is unowned and an approved record type" in {
val result =
accessValidationTest.getAccessLevel(
recordOwnerAuth,
okAuth,
sharedZoneRecordNoOwnerGroup.name,
sharedZoneRecordNoOwnerGroup.typ,
RecordType.AAAA,
sharedZone,
sharedZoneRecordNoOwnerGroup.ownerGroupId)
None)
result shouldBe AccessLevel.Delete
}
"return AccessLevel.NoAccess if the zone is shared and the record is unowned but not an approved record type" in {
val result =
accessValidationTest.getAccessLevel(
okAuth,
sharedZoneRecordNotApprovedRecordType.name,
RecordType.MX,
sharedZone,
None)
result shouldBe AccessLevel.NoAccess
}
"return the result of getAccessLevel if the user is a record owner but zone is not shared" in {
val result =
accessValidationTest.getAccessLevel(

View File

@ -654,17 +654,32 @@ class RecordSetServiceSpec
result shouldBe a[NotAuthorizedError]
}
"fail when the account is not authorized for the record" in {
doReturn(IO.pure(Some(sharedZoneRecordNotFoundOwnerGroup)))
"return the unowned record in a shared zone when the record has an approved record type" in {
doReturn(IO.pure(Some(sharedZoneRecordNoOwnerGroup)))
.when(mockRecordRepo)
.getRecordSet(sharedZone.id, sharedZoneRecordNotFoundOwnerGroup.id)
doReturn(IO.pure(None)).when(mockGroupRepo).getGroup(any[String])
val expectedRecordSetInfo = RecordSetInfo(sharedZoneRecordNoOwnerGroup, None)
val result: RecordSetInfo =
rightResultOf(
underTest.getRecordSet(sharedZoneRecordNoOwnerGroup.id, sharedZone.id, sharedAuth).value)
result shouldBe expectedRecordSetInfo
}
"fail when the unowned record in a shared zone is not an approved record type and user is unassociated with it" in {
doReturn(IO.pure(Some(sharedZoneRecordNotApprovedRecordType)))
.when(mockRecordRepo)
.getRecordSet(sharedZone.id, sharedZoneRecordNotApprovedRecordType.id)
doReturn(IO.pure(None)).when(mockGroupRepo).getGroup(any[String])
val result =
leftResultOf(
underTest
.getRecordSet(sharedZoneRecordNotFoundOwnerGroup.id, sharedZone.id, okAuth)
.getRecordSet(sharedZoneRecordNotApprovedRecordType.id, sharedZone.id, okAuth)
.value)
result shouldBe a[NotAuthorizedError]
}

View File

@ -183,6 +183,17 @@ object TestRecordSetData {
val sharedZoneRecordNotFoundOwnerGroup: RecordSet =
sharedZoneRecord.copy(name = "records", ownerGroupId = Some("not-in-backend"))
val sharedZoneRecordNotApprovedRecordType: RecordSet =
RecordSet(
sharedZone.id,
"mxsharedrecord",
RecordType.MX,
200,
RecordSetStatus.Pending,
DateTime.now,
None,
List(MXData(3, "mx")))
/* RECORDSET CHANGES */
def makeTestAddChange(

View File

@ -6,6 +6,7 @@ bower_components/
node_modules/
public/javascripts/
public/stylesheets/
public/test_frameworks/
public/custom/views.vinyl.js
package-lock.json
release.version

View File

@ -48,7 +48,8 @@ object Dependencies {
"org.typelevel" %% "cats-effect" % catsEffectV,
"com.47deg" %% "github4s" % "0.18.6",
"com.comcast" %% "ip4s-core" % ip4sV,
"com.comcast" %% "ip4s-cats" % ip4sV
"com.comcast" %% "ip4s-cats" % ip4sV,
"com.iheart" %% "ficus" % "1.4.3"
)
lazy val coreDependencies = Seq(