Skip to content

Commit 1b53bae

Browse files
committed
Dynamo cache
1 parent 79c287e commit 1b53bae

File tree

3 files changed

+155
-4
lines changed

3 files changed

+155
-4
lines changed

Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ default = ["tracing_opentelemetry"]
1515

1616
# Feature set that should be used
1717
# This exists to avoid compatibility issues with otel version conflicts
18-
_docs = ["auth0", "grpc", "gzip", "redis-tls", "tracing_opentelemetry"]
18+
_docs = ["auth0", "cache-dynamodb", "grpc", "gzip", "redis-tls", "tracing_opentelemetry"]
1919

2020
auth0 = [
2121
"rand",
@@ -30,6 +30,8 @@ grpc = [ "_any_otel_version", "tonic"]
3030
gzip = ["reqwest/gzip"]
3131

3232
redis-tls = [ "redis", "redis/tls", "redis/tokio-native-tls-comp"]
33+
cache-dynamodb = [ "aws-sdk-dynamodb" ]
34+
3335
tracing_opentelemetry = ["tracing_opentelemetry_0_27"]
3436

3537
tracing_opentelemetry_0_21 = [
@@ -107,6 +109,7 @@ tonic = { version = "0.12", default-features = false, optional = true }
107109
tracing = { version = "0.1", optional = true }
108110
uuid = { version = ">=0.7.0, <2.0.0", features = ["serde", "v4"] }
109111
chacha20poly1305 = { version = "0.10.1", features = ["std"], optional = true }
112+
aws-sdk-dynamodb = { version = "1.63", optional = true }
110113

111114
reqwest-middleware = { version = "0.4.0", features = ["json", "multipart"] }
112115
http = "1.0.0"

src/auth0/cache/dynamodb.rs

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,148 @@
1+
use std::{error::Error, time::Duration};
12

3+
pub use aws_sdk_dynamodb;
4+
use aws_sdk_dynamodb::{
5+
client::Waiters,
6+
operation::describe_table::DescribeTableError,
7+
types::{AttributeDefinition, AttributeValue, TimeToLiveSpecification},
8+
};
9+
10+
use crate::auth0::Token;
11+
12+
use super::{Cache, CacheError};
13+
14+
#[derive(thiserror::Error, Debug)]
15+
pub enum DynamoDBCacheError {
16+
#[error("AWS error when interacting with dynamo cache: {0}")]
17+
Aws(Box<dyn Error>),
18+
#[error("Data in database is wrong. Key: {0}")]
19+
SchemaError(String),
20+
}
21+
22+
impl From<DynamoDBCacheError> for super::CacheError {
23+
fn from(val: DynamoDBCacheError) -> Self {
24+
CacheError(Box::new(val))
25+
}
26+
}
27+
28+
#[derive(Debug)]
29+
pub struct DynamoDBCache {
30+
table_name: String,
31+
client: aws_sdk_dynamodb::Client,
32+
}
33+
34+
impl DynamoDBCache {
35+
/// Construct a DynamoDBCache instance which uses a given table name and client
36+
///
37+
/// Note: this method doesn't currectly check whether a table with the given name exists during creation.
38+
pub fn new(client: aws_sdk_dynamodb::Client, table_name: String) -> Self {
39+
Self { client, table_name }
40+
}
41+
42+
pub async fn create_table_if_not_exists(&self) -> Result<(), DynamoDBCacheError> {
43+
match self
44+
.client
45+
.describe_table()
46+
.table_name(&self.table_name)
47+
.send()
48+
.await
49+
.map_err(|e| e.into_service_error())
50+
{
51+
Ok(_) => return Ok(()),
52+
Err(DescribeTableError::ResourceNotFoundException(_)) => (),
53+
Err(e) => return Err(DynamoDBCacheError::Aws(Box::new(e))),
54+
};
55+
56+
self.client
57+
.create_table()
58+
.table_name(self.table_name.clone())
59+
.attribute_definitions(
60+
AttributeDefinition::builder()
61+
.attribute_name("key".to_string())
62+
.attribute_type(aws_sdk_dynamodb::types::ScalarAttributeType::S)
63+
.build()
64+
// Unwraps here are fine, will be caught by tests
65+
.unwrap(),
66+
)
67+
.attribute_definitions(
68+
AttributeDefinition::builder()
69+
.attribute_name("expiration".to_string())
70+
.attribute_type(aws_sdk_dynamodb::types::ScalarAttributeType::N)
71+
.build()
72+
.unwrap(),
73+
)
74+
.send()
75+
.await
76+
.map_err(|e| DynamoDBCacheError::Aws(Box::new(e)))?;
77+
78+
self.client
79+
.wait_until_table_exists()
80+
.table_name(&self.table_name)
81+
.wait(Duration::from_secs(5))
82+
.await
83+
.map_err(|e| DynamoDBCacheError::Aws(Box::new(e)))?;
84+
85+
self.client
86+
.update_time_to_live()
87+
.table_name(self.table_name.clone())
88+
.time_to_live_specification(
89+
TimeToLiveSpecification::builder()
90+
.enabled(true)
91+
.attribute_name("expiration")
92+
.build()
93+
.unwrap(),
94+
)
95+
.send()
96+
.await
97+
.map_err(|e| DynamoDBCacheError::Aws(Box::new(e)))?;
98+
99+
Ok(())
100+
}
101+
}
102+
103+
#[async_trait::async_trait]
104+
impl Cache for DynamoDBCache {
105+
async fn get_token(&self, client_id: &str, aud: &str) -> Result<Option<Token>, CacheError> {
106+
let key = super::token_key(client_id, aud);
107+
let Some(attrs) = self
108+
.client
109+
.get_item()
110+
.table_name(&self.table_name)
111+
.key("key", AttributeValue::S(key.clone()))
112+
.send()
113+
.await
114+
.map_err(|e| DynamoDBCacheError::Aws(Box::new(e)))?
115+
.item
116+
else {
117+
return Ok(None);
118+
};
119+
120+
let token = attrs
121+
.get("token")
122+
.and_then(|t| t.as_s().ok())
123+
.ok_or(DynamoDBCacheError::SchemaError(key.clone()))?;
124+
125+
let token: Token = serde_json::from_str(token).unwrap();
126+
127+
Ok(Some(token))
128+
}
129+
130+
async fn put_token(&self, client_id: &str, aud: &str, token: &Token) -> Result<(), CacheError> {
131+
let key = super::token_key(client_id, aud);
132+
let encoded = serde_json::to_string(token).unwrap();
133+
self.client
134+
.put_item()
135+
.table_name(&self.table_name)
136+
.item("key", AttributeValue::S(key))
137+
.item("token", AttributeValue::S(encoded))
138+
.item(
139+
"expiration",
140+
AttributeValue::N(token.expire_date().timestamp().to_string()),
141+
)
142+
.send()
143+
.await
144+
.map_err(|e| DynamoDBCacheError::Aws(Box::new(e)))?;
145+
146+
Ok(())
147+
}
148+
}

src/auth0/cache/mod.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1+
pub use dynamodb::DynamoDBCache;
12
pub use inmemory::InMemoryCache;
23
pub use redis_impl::RedisCache;
34
use std::error::Error;
45

56
use crate::auth0::Token;
67

78
mod crypto;
8-
mod dynamodb;
9-
mod inmemory;
10-
mod redis_impl;
9+
pub mod dynamodb;
10+
pub mod inmemory;
11+
pub mod redis_impl;
1112

1213
const TOKEN_PREFIX: &str = "auth0rs_tokens";
1314
// The version of the token for backwards incompatible changes

0 commit comments

Comments
 (0)