Skip to content

Commit 930d3dd

Browse files
author
Bennett Hardwick
committed
Add a direct API for building queries
1 parent 1e39c11 commit 930d3dd

File tree

3 files changed

+251
-20
lines changed

3 files changed

+251
-20
lines changed

src/encrypted_table/mod.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ use crate::{
1414
use aws_sdk_dynamodb::types::{AttributeValue, Delete, Put, TransactWriteItem};
1515
use cipherstash_client::{
1616
config::{console_config::ConsoleConfig, zero_kms_config::ZeroKMSConfig},
17-
credentials::{auto_refresh::AutoRefresh, service_credentials::ServiceCredentials},
17+
credentials::{
18+
auto_refresh::AutoRefresh,
19+
service_credentials::{ServiceCredentials, ServiceToken},
20+
Credentials,
21+
},
1822
encryption::Encryption,
1923
zero_kms::ZeroKMS,
2024
};
@@ -46,9 +50,9 @@ pub struct EncryptedTable<D = Dynamo> {
4650
}
4751

4852
impl<D> EncryptedTable<D> {
49-
// option here to generate query params
50-
51-
// option here to seal
53+
pub fn cipher(&self) -> &Encryption<impl Credentials<Token = ServiceToken>> {
54+
self.cipher.as_ref()
55+
}
5256
}
5357

5458
impl EncryptedTable<Headless> {
@@ -209,11 +213,11 @@ impl DynamoRecordPatch {
209213
}
210214

211215
impl<D> EncryptedTable<D> {
212-
pub fn query<S>(&self) -> QueryBuilder<S, D>
216+
pub fn query<S>(&self) -> QueryBuilder<S, &Self>
213217
where
214218
S: Searchable,
215219
{
216-
QueryBuilder::new(self)
220+
QueryBuilder::with_backend(self)
217221
}
218222

219223
pub async fn unseal_all(

src/encrypted_table/query.rs

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ use super::{Dynamo, EncryptedTable, QueryError};
2323
* D = Database
2424
*
2525
*/
26-
pub struct QueryBuilder<'t, S, D = Dynamo> {
26+
pub struct QueryBuilder<S, B = ()> {
2727
parts: Vec<(String, SingleIndex, Plaintext)>,
28-
table: &'t EncryptedTable<D>,
29-
__table: PhantomData<S>,
28+
backend: B,
29+
__searchable: PhantomData<S>,
3030
}
3131

3232
pub struct PreparedQuery {
@@ -94,15 +94,22 @@ impl PreparedQuery {
9494
}
9595
}
9696

97-
impl<'t, S, D> QueryBuilder<'t, S, D>
98-
where
99-
S: Searchable,
100-
{
101-
pub fn new(table: &'t EncryptedTable<D>) -> Self {
97+
impl<S> QueryBuilder<S> {
98+
pub fn new() -> Self {
10299
Self {
103100
parts: vec![],
104-
table,
105-
__table: Default::default(),
101+
backend: Default::default(),
102+
__searchable: Default::default(),
103+
}
104+
}
105+
}
106+
107+
impl<S, B> QueryBuilder<S, B> {
108+
pub fn with_backend(backend: B) -> Self {
109+
Self {
110+
parts: vec![],
111+
backend,
112+
__searchable: Default::default(),
106113
}
107114
}
108115

@@ -117,8 +124,13 @@ where
117124
.push((name.into(), SingleIndex::Prefix, plaintext.into()));
118125
self
119126
}
127+
}
120128

121-
fn build(self) -> Result<PreparedQuery, QueryError> {
129+
impl<S, B> QueryBuilder<S, B>
130+
where
131+
S: Searchable,
132+
{
133+
pub fn build(self) -> Result<PreparedQuery, QueryError> {
122134
let items_len = self.parts.len();
123135

124136
// this is the simplest way to brute force the index names but relies on some gross
@@ -184,12 +196,12 @@ where
184196
}
185197
}
186198

187-
impl<'t, S> QueryBuilder<'t, S, Dynamo>
199+
impl<S> QueryBuilder<S, &EncryptedTable<Dynamo>>
188200
where
189201
S: Searchable + Identifiable,
190202
{
191203
pub async fn load<T: Decryptable>(self) -> Result<Vec<T>, QueryError> {
192-
let table = self.table;
204+
let table = self.backend;
193205
let query = self.build()?;
194206

195207
let items = query.send(table).await?;
@@ -199,7 +211,7 @@ where
199211
}
200212
}
201213

202-
impl<'t, S> QueryBuilder<'t, S, Dynamo>
214+
impl<S> QueryBuilder<S, &EncryptedTable<Dynamo>>
203215
where
204216
S: Searchable + Decryptable + Identifiable,
205217
{

tests/query_builder_direct.rs

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
use cipherstash_dynamodb::{
2+
Decryptable, Encryptable, EncryptedTable, Identifiable, QueryBuilder, Searchable,
3+
};
4+
use itertools::Itertools;
5+
use serial_test::serial;
6+
use std::future::Future;
7+
8+
mod common;
9+
10+
#[derive(
11+
Identifiable, Encryptable, Decryptable, Searchable, Debug, PartialEq, Ord, PartialOrd, Eq,
12+
)]
13+
#[cipherstash(sort_key_prefix = "user")]
14+
pub struct User {
15+
#[cipherstash(query = "exact", compound = "email#name")]
16+
#[cipherstash(query = "exact")]
17+
#[partition_key]
18+
pub email: String,
19+
20+
#[cipherstash(query = "prefix", compound = "email#name")]
21+
#[cipherstash(query = "prefix")]
22+
pub name: String,
23+
24+
#[cipherstash(plaintext)]
25+
pub tag: String,
26+
27+
#[cipherstash(skip)]
28+
pub temp: bool,
29+
}
30+
31+
impl User {
32+
pub fn new(email: impl Into<String>, name: impl Into<String>, tag: impl Into<String>) -> Self {
33+
Self {
34+
name: name.into(),
35+
email: email.into(),
36+
tag: tag.into(),
37+
temp: false,
38+
}
39+
}
40+
}
41+
42+
async fn run_test<F: Future<Output = ()>>(
43+
mut f: impl FnMut(aws_sdk_dynamodb::Client, String) -> F,
44+
) {
45+
let config = aws_config::from_env()
46+
.endpoint_url("http://localhost:8000")
47+
.load()
48+
.await;
49+
50+
let client = aws_sdk_dynamodb::Client::new(&config);
51+
52+
let table_name = "test-users-direct-query-builder";
53+
54+
common::create_table(&client, table_name).await;
55+
56+
let table = EncryptedTable::init(client.clone(), table_name)
57+
.await
58+
.expect("Failed to init table");
59+
60+
table
61+
.put(User::new("[email protected]", "Dan Draper", "blue"))
62+
.await
63+
.expect("Failed to insert Dan");
64+
65+
table
66+
.put(User::new("[email protected]", "Jane Smith", "red"))
67+
.await
68+
.expect("Failed to insert Jane");
69+
70+
table
71+
.put(User::new("[email protected]", "Daniel Johnson", "green"))
72+
.await
73+
.expect("Failed to insert Daniel");
74+
75+
f(client, table_name.to_string()).await;
76+
}
77+
78+
#[tokio::test]
79+
#[serial]
80+
async fn test_query_single_exact() {
81+
run_test(|client, name| async move {
82+
let table = EncryptedTable::init_headless()
83+
.await
84+
.expect("failed to init table");
85+
86+
let query = QueryBuilder::<User>::new()
87+
.eq("email", "[email protected]")
88+
.build()
89+
.expect("failed to build query");
90+
91+
let term = query
92+
.encrypt(table.cipher())
93+
.await
94+
.expect("failed to encrypt query");
95+
96+
let query = client
97+
.query()
98+
.table_name(name)
99+
.index_name("TermIndex")
100+
.key_condition_expression("term = :term")
101+
.expression_attribute_values(":term", term);
102+
103+
let result = query.send().await.expect("failed to send");
104+
105+
let items = result.items.unwrap();
106+
107+
let res: Vec<User> = table
108+
.decrypt_all(items)
109+
.await
110+
.expect("failed to decrypt")
111+
.into_iter()
112+
.sorted()
113+
.collect_vec();
114+
115+
assert_eq!(
116+
res,
117+
vec![User::new("[email protected]", "Dan Draper", "blue")]
118+
);
119+
})
120+
.await;
121+
}
122+
123+
#[tokio::test]
124+
#[serial]
125+
async fn test_query_single_prefix() {
126+
run_test(|client, name| async move {
127+
let table = EncryptedTable::init_headless()
128+
.await
129+
.expect("failed to init table");
130+
131+
let query = QueryBuilder::<User>::new()
132+
.starts_with("name", "Dan")
133+
.build()
134+
.expect("failed to build query");
135+
136+
let term = query
137+
.encrypt(table.cipher())
138+
.await
139+
.expect("failed to encrypt query");
140+
141+
let query = client
142+
.query()
143+
.table_name(name)
144+
.index_name("TermIndex")
145+
.key_condition_expression("term = :term")
146+
.expression_attribute_values(":term", term);
147+
148+
let result = query.send().await.expect("failed to send");
149+
150+
let items = result.items.unwrap();
151+
152+
let res: Vec<User> = table
153+
.decrypt_all(items)
154+
.await
155+
.expect("failed to decrypt")
156+
.into_iter()
157+
.sorted()
158+
.collect_vec();
159+
160+
assert_eq!(
161+
res,
162+
vec![
163+
User::new("[email protected]", "Dan Draper", "blue"),
164+
User::new("[email protected]", "Daniel Johnson", "green")
165+
]
166+
);
167+
})
168+
.await;
169+
}
170+
171+
#[tokio::test]
172+
#[serial]
173+
async fn test_query_compound() {
174+
run_test(|client, name| async move {
175+
let table = EncryptedTable::init_headless()
176+
.await
177+
.expect("failed to init table");
178+
179+
let query = QueryBuilder::<User>::new()
180+
.starts_with("name", "Dan")
181+
.eq("email", "[email protected]")
182+
.build()
183+
.expect("failed to build query");
184+
185+
let term = query
186+
.encrypt(table.cipher())
187+
.await
188+
.expect("failed to encrypt query");
189+
190+
let query = client
191+
.query()
192+
.table_name(name)
193+
.index_name("TermIndex")
194+
.key_condition_expression("term = :term")
195+
.expression_attribute_values(":term", term);
196+
197+
let result = query.send().await.expect("failed to send");
198+
199+
let items = result.items.unwrap();
200+
201+
let res: Vec<User> = table
202+
.decrypt_all(items)
203+
.await
204+
.expect("failed to decrypt")
205+
.into_iter()
206+
.sorted()
207+
.collect_vec();
208+
209+
assert_eq!(
210+
res,
211+
vec![User::new("[email protected]", "Dan Draper", "blue")]
212+
);
213+
})
214+
.await;
215+
}

0 commit comments

Comments
 (0)