-
Notifications
You must be signed in to change notification settings - Fork 0
feat(flags): Do token validation and extract distinct id #41
Changes from 2 commits
a6d7309
d0e9bc0
327074c
838dd2c
ad04232
756c62c
2ca642a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,3 +5,4 @@ pub mod router; | |
pub mod server; | ||
pub mod v0_endpoint; | ||
pub mod v0_request; | ||
pub mod team; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
use std::sync::Arc; | ||
|
||
use crate::{api::FlagError, redis::Client}; | ||
|
||
use serde::{Deserialize, Serialize}; | ||
use tracing::instrument; | ||
|
||
|
||
// TRICKY: I'm still not sure where the :1: is coming from. | ||
// The Django prefix is `posthog` only. | ||
// It's from here: https://docs.djangoproject.com/en/4.2/topics/cache/#cache-versioning | ||
// F&!£%% on the bright side we don't use this functionality yet. | ||
// Will rely on integration tests to catch this. | ||
const TEAM_TOKEN_CACHE_PREFIX: &str = "posthog:1:team_token:"; | ||
|
||
// TODO: Check what happens if json has extra stuff, does serde ignore it? Yes | ||
// Make sure we don't serialize and store team data in redis. Let main decide endpoint control this... | ||
// and track misses. Revisit if this becomes an issue. | ||
// because otherwise very annoying to keep this in sync with main django which has a lot of extra fields we need here. | ||
// will lead to inconsistent behaviour. | ||
// This is turning out to be very annoying, because we have django key prefixes to be mindful of as well. | ||
// Wonder if it would be better to make these caches independent? This generates that new problem of CRUD happening in Django, | ||
// which needs to update this cache immediately, so they can't really ever be independent. | ||
// True for both team cache and flags cache. Hmm. Just I guess need to add tests around the key prefixes... | ||
#[derive(Debug, Deserialize, Serialize)] | ||
pub struct Team { | ||
pub id: i64, | ||
pub name: String, | ||
pub api_token: String, | ||
} | ||
|
||
impl Team { | ||
/// Validates a token, and returns a team if it exists. | ||
/// | ||
|
||
#[instrument(skip_all)] | ||
pub async fn from_redis( | ||
client: Arc<dyn Client + Send + Sync>, | ||
token: String, | ||
) -> Result<Team, FlagError> { | ||
|
||
// TODO: Instead of failing here, i.e. if not in redis, fallback to pg | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: since that dataset is relatively small, an in-process LRU cache would be very useful. We do have it in ingestion's There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oof tricky, how do you handle invalidations in this case? Team config options / api tokens would get updated out of sync via django. Or is the TTL here so low that it doesn't make a difference in practice 👀 . Either way, good idea with the low ttl, will look into this! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The plugin-server cache has a 2 minute TTL, so tokens are still accepted 2 minutes after rotation, which is good enough in my book. Negative lookups (token is not valid) are cached for 5 minutes too, as it's less probable. |
||
let serialized_team = client | ||
.get( | ||
format!("{TEAM_TOKEN_CACHE_PREFIX}{}", token) | ||
) | ||
.await | ||
.map_err(|e| { | ||
tracing::error!("failed to fetch data: {}", e); | ||
// TODO: Can be other errors if serde_pickle destructuring fails? | ||
FlagError::TokenValidationError | ||
})?; | ||
|
||
let team: Team = serde_json::from_str(&serialized_team).map_err(|e| { | ||
tracing::error!("failed to parse data to team: {}", e); | ||
// TODO: Internal error, shouldn't send back to client | ||
FlagError::RequestParsingError(e) | ||
})?; | ||
|
||
Ok(team) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use std::sync::Arc; | ||
use anyhow::Error; | ||
|
||
use crate::redis::RedisClient; | ||
use rand::{distributions::Alphanumeric, Rng}; | ||
|
||
use super::*; | ||
|
||
fn random_string(prefix: &str, length: usize) -> String { | ||
let suffix: String = rand::thread_rng() | ||
.sample_iter(Alphanumeric) | ||
.take(length) | ||
.map(char::from) | ||
.collect(); | ||
format!("{}{}", prefix, suffix) | ||
} | ||
|
||
async fn insert_new_team_in_redis(client: Arc<RedisClient>) -> Result<Team, Error> { | ||
let id = rand::thread_rng().gen_range(0..10_000_000); | ||
let token = random_string("phc_", 12); | ||
let team = Team { | ||
id: id, | ||
name: "team".to_string(), | ||
api_token: token, | ||
}; | ||
|
||
let serialized_team = serde_json::to_string(&team)?; | ||
client | ||
.set( | ||
format!("{TEAM_TOKEN_CACHE_PREFIX}{}", team.api_token.clone()), | ||
serialized_team, | ||
) | ||
.await?; | ||
|
||
Ok(team) | ||
} | ||
|
||
#[tokio::test] | ||
async fn test_fetch_team_from_redis() { | ||
let client = RedisClient::new("redis://localhost:6379/".to_string()) | ||
.expect("Failed to create redis client"); | ||
let client = Arc::new(client); | ||
|
||
let team = insert_new_team_in_redis(client.clone()).await.unwrap(); | ||
|
||
let target_token = team.api_token; | ||
|
||
let team_from_redis = Team::from_redis(client.clone(), target_token.clone()).await.unwrap(); | ||
assert_eq!( | ||
team_from_redis.api_token, target_token | ||
); | ||
assert_eq!( | ||
team_from_redis.id, team.id | ||
); | ||
} | ||
|
||
#[tokio::test] | ||
async fn test_fetch_invalid_team_from_redis() { | ||
let client = RedisClient::new("redis://localhost:6379/".to_string()) | ||
.expect("Failed to create redis client"); | ||
let client = Arc::new(client); | ||
|
||
match Team::from_redis(client.clone(), "banana".to_string()).await { | ||
Err(FlagError::TokenValidationError) => (), | ||
_ => panic!("Expected TokenValidationError"), | ||
}; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO: because rust exposes all string handling, double check if something very bad will happen if I have a utf-16 character in the team name
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
eh will come back to this, will remove team name from consideration for now, will handle this on string matching properties.