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

Wildcard search (#636)

* Add wildcard support

* Add wildcard search for zones

* Update documentation

* Fix for AWSAuthenticator to support asterisks
This commit is contained in:
Paul Cleary 2019-05-23 10:56:52 -04:00 committed by GitHub
parent 95fe56e070
commit e03690271a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 136 additions and 23 deletions

2
.gitignore vendored
View File

@ -25,3 +25,5 @@ release.version
.ensime_cache .ensime_cache
package-lock.json package-lock.json
*trustStore.jks *trustStore.jks
.bloop
.metals

View File

@ -214,7 +214,7 @@ def test_list_recordsets_with_record_name_filter_all(rs_fixture):
client = rs_fixture.client client = rs_fixture.client
ok_zone = rs_fixture.test_context ok_zone = rs_fixture.test_context
list_results = client.list_recordsets(ok_zone['id'], record_name_filter="list", status=200) list_results = client.list_recordsets(ok_zone['id'], record_name_filter="*list*", status=200)
rs_fixture.check_recordsets_page_accuracy(list_results, size=10, offset=0) rs_fixture.check_recordsets_page_accuracy(list_results, size=10, offset=0)
@ -227,7 +227,7 @@ def test_list_recordsets_with_record_name_filter_and_page_size(rs_fixture):
ok_zone = rs_fixture.test_context ok_zone = rs_fixture.test_context
#page of 4 items #page of 4 items
list_results = client.list_recordsets(ok_zone['id'], max_items=4, record_name_filter="CNAME", status=200) list_results = client.list_recordsets(ok_zone['id'], max_items=4, record_name_filter="*CNAME*", status=200)
assert_that(list_results['recordSets'], has_length(4)) assert_that(list_results['recordSets'], has_length(4))
list_results_records = list_results['recordSets']; list_results_records = list_results['recordSets'];
@ -235,7 +235,7 @@ def test_list_recordsets_with_record_name_filter_and_page_size(rs_fixture):
assert_that(list_results_records[i]['name'], contains_string('CNAME')) assert_that(list_results_records[i]['name'], contains_string('CNAME'))
#page of 5 items but excess max items #page of 5 items but excess max items
list_results = client.list_recordsets(ok_zone['id'], max_items=7, record_name_filter="CNAME", status=200) list_results = client.list_recordsets(ok_zone['id'], max_items=7, record_name_filter="*CNAME*", status=200)
assert_that(list_results['recordSets'], has_length(5)) assert_that(list_results['recordSets'], has_length(5))
list_results_records = list_results['recordSets']; list_results_records = list_results['recordSets'];
@ -252,12 +252,12 @@ def test_list_recordsets_with_record_name_filter_and_chaining_pages_with_nextId(
ok_zone = rs_fixture.test_context ok_zone = rs_fixture.test_context
#page of 2 items #page of 2 items
list_results = client.list_recordsets(ok_zone['id'], max_items=2, record_name_filter="CNAME", status=200) list_results = client.list_recordsets(ok_zone['id'], max_items=2, record_name_filter="*CNAME*", status=200)
assert_that(list_results['recordSets'], has_length(2)) assert_that(list_results['recordSets'], has_length(2))
start_key = list_results['nextId'] start_key = list_results['nextId']
#page of 2 items #page of 2 items
list_results = client.list_recordsets(ok_zone['id'], start_from=start_key, max_items=2, record_name_filter="CNAME", status=200) list_results = client.list_recordsets(ok_zone['id'], start_from=start_key, max_items=2, record_name_filter="*CNAME*", status=200)
assert_that(list_results['recordSets'], has_length(2)) assert_that(list_results['recordSets'], has_length(2))
list_results_records = list_results['recordSets']; list_results_records = list_results['recordSets'];
@ -272,7 +272,7 @@ def test_list_recordsets_with_record_name_filter_one(rs_fixture):
client = rs_fixture.client client = rs_fixture.client
ok_zone = rs_fixture.test_context ok_zone = rs_fixture.test_context
list_results = client.list_recordsets(ok_zone['id'], record_name_filter="8", status=200) list_results = client.list_recordsets(ok_zone['id'], record_name_filter="*8*", status=200)
rs_fixture.check_recordsets_page_accuracy(list_results, size=1, offset=8) rs_fixture.check_recordsets_page_accuracy(list_results, size=1, offset=8)
@ -283,7 +283,7 @@ def test_list_recordsets_with_record_name_filter_none(rs_fixture):
client = rs_fixture.client client = rs_fixture.client
ok_zone = rs_fixture.test_context ok_zone = rs_fixture.test_context
list_results = client.list_recordsets(ok_zone['id'], record_name_filter="Dummy", status=200) list_results = client.list_recordsets(ok_zone['id'], record_name_filter="*Dummy*", status=200)
rs_fixture.check_recordsets_page_accuracy(list_results, size=0, offset=0) rs_fixture.check_recordsets_page_accuracy(list_results, size=0, offset=0)

View File

@ -199,7 +199,7 @@ def test_list_zones_with_search_first_page(list_zones_context):
""" """
Test that the first page of listing zones returns correctly when a name filter is provided Test that the first page of listing zones returns correctly when a name filter is provided
""" """
result = list_zones_context.client.list_zones(name_filter='searched', max_items=2, status=200) result = list_zones_context.client.list_zones(name_filter='*searched*', max_items=2, status=200)
zones = result['zones'] zones = result['zones']
assert_that(zones, has_length(2)) assert_that(zones, has_length(2))
@ -208,7 +208,7 @@ def test_list_zones_with_search_first_page(list_zones_context):
assert_that(result['nextId'], is_('list-zones-test-searched-2.')) assert_that(result['nextId'], is_('list-zones-test-searched-2.'))
assert_that(result['maxItems'], is_(2)) assert_that(result['maxItems'], is_(2))
assert_that(result['nameFilter'], is_('searched')) assert_that(result['nameFilter'], is_('*searched*'))
assert_that(result, is_not(has_key('startFrom'))) assert_that(result, is_not(has_key('startFrom')))
@ -231,7 +231,7 @@ def test_list_zones_with_search_last_page(list_zones_context):
""" """
Test that the second page of listing zones returns correctly when a name filter is provided Test that the second page of listing zones returns correctly when a name filter is provided
""" """
result = list_zones_context.client.list_zones(name_filter='searched', start_from="list-zones-test-searched-2.", max_items=2, status=200) result = list_zones_context.client.list_zones(name_filter='*searched*', start_from="list-zones-test-searched-2.", max_items=2, status=200)
zones = result['zones'] zones = result['zones']
assert_that(zones, has_length(1)) assert_that(zones, has_length(1))
@ -239,5 +239,5 @@ def test_list_zones_with_search_last_page(list_zones_context):
assert_that(result, is_not(has_key('nextId'))) assert_that(result, is_not(has_key('nextId')))
assert_that(result['maxItems'], is_(2)) assert_that(result['maxItems'], is_(2))
assert_that(result['nameFilter'], is_('searched')) assert_that(result['nameFilter'], is_('*searched*'))
assert_that(result['startFrom'], is_('list-zones-test-searched-2.')) assert_that(result['startFrom'], is_('list-zones-test-searched-2.'))

View File

@ -239,6 +239,7 @@ class Aws4Authenticator {
. .
// and doesn't encode '~' at all // and doesn't encode '~' at all
replaceAllLiterally("%7E", "~") replaceAllLiterally("%7E", "~")
.replaceAllLiterally("*", "%2A") // aws encodes the asterisk specially
private def hexString(bs: Array[Byte]) = private def hexString(bs: Array[Byte]) =
bs.foldLeft("")((out, b) => f"$out%s${b & 0x0ff}%02x") bs.foldLeft("")((out, b) => f"$out%s${b & 0x0ff}%02x")

View File

@ -16,7 +16,7 @@ Retrieves a list of RecordSets from the zone
name | type | required? | description | name | type | required? | description |
------------ | ------------- | ----------- | :---------- | ------------ | ------------- | ----------- | :---------- |
recordNameFilter | string | no | One or more characters contained in the name of the record set to search for. For example `vinyl`. This is a contains search only, no wildcards or regular expressions are supported | recordNameFilter | string | no | Characters that are part of the record name to search for. The wildcard character `*` is supported, for example `www*`. Omit the wildcard when searching for an exact record name. |
startFrom | *any* | no | In order to advance through pages of results, the startFrom is set to the `nextId` that is returned on the previous response. It is up to the client to maintain previous pages if the client wishes to advance forward and backward. If not specified, will return the first page of results | startFrom | *any* | no | In order to advance through pages of results, the startFrom is set to the `nextId` that is returned on the previous response. It is up to the client to maintain previous pages if the client wishes to advance forward and backward. If not specified, will return the first page of results |
maxItems | integer | no | The number of items to return in the page. Valid values are 1 to 100. Defaults to 100 if not provided. | maxItems | integer | no | The number of items to return in the page. Valid values are 1 to 100. Defaults to 100 if not provided. |

View File

@ -16,7 +16,7 @@ Retrieves the list of zones a user has access to. The zone name is only sorted
name | type | required? | description | name | type | required? | description |
------------ | ------------- | ----------- | :---------- | ------------ | ------------- | ----------- | :---------- |
nameFilter | string | no | One or more characters contained in the name of the zone to search for. For example `www-`. This is a contains search only, no wildcards or regular expressions are supported | nameFilter | string | no | Characters that are part of the zone name to search for. The wildcard character `*` is supported, for example `www*`. Omit the wildcard character when searching for an exact zone name. |
startFrom | *any* | no | In order to advance through pages of results, the startFrom is set to the `nextId` that is returned on the previous response. It is up to the client to maintain previous pages if the client wishes to advance forward and backward. If not specified, will return the first page of results | startFrom | *any* | no | In order to advance through pages of results, the startFrom is set to the `nextId` that is returned on the previous response. It is up to the client to maintain previous pages if the client wishes to advance forward and backward. If not specified, will return the first page of results |
maxItems | int | no | The number of items to return in the page. Valid values are 1 - 100. Defaults to 100 if not provided. | maxItems | int | no | The number of items to return in the page. Valid values are 1 - 100. Defaults to 100 if not provided. |

View File

@ -7,7 +7,7 @@ position: 7
# Getting Help # Getting Help
- Gitter community: - Gitter community:
<https://gitter.im/vinyldns> <https://gitter.im/vinyldns/vinyldns>
- Contact the VinylDNS Core Team: - Contact the VinylDNS Core Team:
vinyldns-core@googlegroups.com vinyldns-core@googlegroups.com

View File

@ -341,10 +341,7 @@ class MySqlRecordSetRepositoryIntegrationSpec
found.recordSets should contain theSameElementsInOrderAs existing.slice(2, 4) found.recordSets should contain theSameElementsInOrderAs existing.slice(2, 4)
} }
"return the record sets after startFrom respecting maxItems and filter" in { "return the record sets after startFrom respecting maxItems and filter" in {
// load some deterministic names so we can filter and respect max items
val recordNames = List("aaa", "bbb", "ccc", "ddd", "eeez", "fffz", "ggg", "hhhz", "iii", "jjj") val recordNames = List("aaa", "bbb", "ccc", "ddd", "eeez", "fffz", "ggg", "hhhz", "iii", "jjj")
// our search will be filtered by records with "z"
val expectedNames = recordNames.filter(_.contains("z")) val expectedNames = recordNames.filter(_.contains("z"))
val newRecordSets = val newRecordSets =
@ -359,9 +356,58 @@ class MySqlRecordSetRepositoryIntegrationSpec
val changes = newRecordSets.map(makeTestAddChange(_, okZone)) val changes = newRecordSets.map(makeTestAddChange(_, okZone))
insert(changes) insert(changes)
// start after the second, pulling 3 records that have "z"
val startFrom = Some(newRecordSets(1).name) val startFrom = Some(newRecordSets(1).name)
val found = repo.listRecordSets(okZone.id, startFrom, Some(3), Some("z")).unsafeRunSync() val found = repo.listRecordSets(okZone.id, startFrom, Some(3), Some("*z*")).unsafeRunSync()
found.recordSets.map(_.name) should contain theSameElementsInOrderAs expectedNames
}
"return record sets using starts with wildcard" in {
val recordNames = List("aaa", "aab", "ccc")
val expectedNames = recordNames.filter(_.startsWith("aa"))
val newRecordSets =
for {
n <- recordNames
} yield
aaaa.copy(
zoneId = okZone.id,
name = n,
id = UUID.randomUUID().toString)
val changes = newRecordSets.map(makeTestAddChange(_, okZone))
insert(changes)
val found = repo.listRecordSets(okZone.id, None, Some(3), Some("aa*")).unsafeRunSync()
found.recordSets.map(_.name) should contain theSameElementsInOrderAs expectedNames
}
"return record sets using ends with wildcard" in {
val recordNames = List("aaa", "aab", "ccb")
val expectedNames = recordNames.filter(_.endsWith("b"))
val newRecordSets =
for {
n <- recordNames
} yield aaaa.copy(zoneId = okZone.id, name = n, id = UUID.randomUUID().toString)
val changes = newRecordSets.map(makeTestAddChange(_, okZone))
insert(changes)
val found = repo.listRecordSets(okZone.id, None, Some(3), Some("*b")).unsafeRunSync()
found.recordSets.map(_.name) should contain theSameElementsInOrderAs expectedNames
}
"return record sets exact match with no wildcards" in {
// load some deterministic names so we can filter and respect max items
val recordNames = List("aaa", "aab", "ccb")
val expectedNames = List("aaa")
val newRecordSets =
for {
n <- recordNames
} yield aaaa.copy(zoneId = okZone.id, name = n, id = UUID.randomUUID().toString)
val changes = newRecordSets.map(makeTestAddChange(_, okZone))
insert(changes)
val found = repo.listRecordSets(okZone.id, None, Some(3), Some("aaa")).unsafeRunSync()
found.recordSets.map(_.name) should contain theSameElementsInOrderAs expectedNames found.recordSets.map(_.name) should contain theSameElementsInOrderAs expectedNames
} }
"pages through the list properly" in { "pages through the list properly" in {

View File

@ -384,7 +384,7 @@ class MySqlZoneRepositoryIntegrationSpec
val f = val f =
for { for {
_ <- saveZones(testZones) _ <- saveZones(testZones)
retrieved <- repo.listZones(superUserAuth, zoneNameFilter = Some("system")) retrieved <- repo.listZones(superUserAuth, zoneNameFilter = Some("system*"))
} yield retrieved } yield retrieved
f.unsafeRunSync().zones should contain theSameElementsAs expectedZones f.unsafeRunSync().zones should contain theSameElementsAs expectedZones
@ -405,7 +405,69 @@ class MySqlZoneRepositoryIntegrationSpec
val f = val f =
for { for {
_ <- saveZones(testZones) _ <- saveZones(testZones)
retrieved <- repo.listZones(auth, zoneNameFilter = Some("system")) retrieved <- repo.listZones(auth, zoneNameFilter = Some("system*"))
} yield retrieved
f.unsafeRunSync().zones should contain theSameElementsInOrderAs expectedZones
}
"support starts with wildcard" in {
val testZones = Seq(
testZone("system-test", adminGroupId = "foo"),
testZone("system-temp", adminGroupId = "foo"),
testZone("system-nomatch", adminGroupId = "bar")
)
val expectedZones = Seq(testZones(0), testZones(1)).sortBy(_.name)
val auth = AuthPrincipal(dummyUser, Seq("foo"))
val f =
for {
_ <- saveZones(testZones)
retrieved <- repo.listZones(auth, zoneNameFilter = Some("system*"))
} yield retrieved
f.unsafeRunSync().zones should contain theSameElementsInOrderAs expectedZones
}
"support ends with wildcard" in {
val testZones = Seq(
testZone("system-test", adminGroupId = "foo"),
testZone("system-temp", adminGroupId = "foo"),
testZone("system-nomatch", adminGroupId = "bar")
)
val expectedZones = Seq(testZones(0))
val auth = AuthPrincipal(dummyUser, Seq("foo"))
val f =
for {
_ <- saveZones(testZones)
retrieved <- repo.listZones(auth, zoneNameFilter = Some("*test"))
} yield retrieved
f.unsafeRunSync().zones should contain theSameElementsInOrderAs expectedZones
}
"support contains wildcard" in {
val testZones = Seq(
testZone("system-jokerswild", adminGroupId = "foo"),
testZone("system-wildcard", adminGroupId = "foo"),
testZone("system-nomatch", adminGroupId = "bar")
)
val expectedZones = Seq(testZones(0), testZones(1))
val auth = AuthPrincipal(dummyUser, Seq("foo"))
val f =
for {
_ <- saveZones(testZones)
retrieved <- repo.listZones(auth, zoneNameFilter = Some("*wild*"))
} yield retrieved } yield retrieved
f.unsafeRunSync().zones should contain theSameElementsInOrderAs expectedZones f.unsafeRunSync().zones should contain theSameElementsInOrderAs expectedZones

View File

@ -191,7 +191,7 @@ class MySqlRecordSetRepository extends RecordSetRepository with Monitored {
val params = (Some('zoneId -> zoneId) ++ val params = (Some('zoneId -> zoneId) ++
startFrom.map(n => 'startFrom -> n) ++ startFrom.map(n => 'startFrom -> n) ++
recordNameFilter.map(f => 'nameFilter -> s"%$f%") ++ recordNameFilter.map(f => 'nameFilter -> f.replace('*', '%')) ++
maxItems.map(m => 'maxItems -> m)).toSeq maxItems.map(m => 'maxItems -> m)).toSeq
val query = "SELECT data FROM recordset WHERE zone_id = {zoneId} " + opts val query = "SELECT data FROM recordset WHERE zone_id = {zoneId} " + opts

View File

@ -228,7 +228,7 @@ class MySqlZoneRepository extends ZoneRepository with ProtobufConversions with M
sb.append(withAccessorCheck) sb.append(withAccessorCheck)
val filters = List( val filters = List(
zoneNameFilter.map(flt => s"z.name LIKE '%$flt%'"), zoneNameFilter.map(flt => s"z.name LIKE '${flt.replace('*', '%')}'"),
startFrom.map(os => s"z.name > '$os'") startFrom.map(os => s"z.name > '$os'")
).flatten ).flatten

View File

@ -12,3 +12,5 @@ package-lock.json
release.version release.version
private private
public/gentelella public/gentelella
.bloop
.metals