Skip to content

Commit

Permalink
feat: adds a call for validating tokens (#34)
Browse files Browse the repository at this point in the history
Co-authored-by: Nuno Góis <github@nunogois.com>
  • Loading branch information
Christopher Kolstad and nunogois authored Feb 2, 2023
1 parent f224459 commit 0d037ec
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 31 deletions.
18 changes: 9 additions & 9 deletions server/src/data_sources/memory_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ impl EdgeProvider for MemoryProvider {}
impl FeaturesProvider for MemoryProvider {
fn get_client_features(&self, token: &EdgeToken) -> EdgeResult<ClientFeatures> {
self.data_store
.get(&token.secret)
.get(&token.token)
.map(|v| v.value().clone())
.ok_or_else(|| EdgeError::DataSourceError("Token not found".to_string()))
}
Expand All @@ -29,12 +29,12 @@ impl TokenProvider for MemoryProvider {
}

fn secret_is_valid(&self, secret: &str) -> EdgeResult<bool> {
Ok(self.get_known_tokens()?.iter().any(|t| t.secret == secret))
Ok(self.get_known_tokens()?.iter().any(|t| t.token == secret))
}

fn token_details(&self, secret: String) -> EdgeResult<Option<EdgeToken>> {
let tokens = self.get_known_tokens()?;
Ok(tokens.into_iter().find(|t| t.secret == secret))
Ok(tokens.into_iter().find(|t| t.token == secret))
}
}

Expand All @@ -50,13 +50,13 @@ mod test {

impl EdgeSink for MemoryProvider {
fn sink_features(&mut self, token: &EdgeToken, features: ClientFeatures) {
self.data_store.insert(token.secret.clone(), features);
self.data_store.insert(token.token.clone(), features);
}

fn sink_tokens(&mut self, tokens: Vec<EdgeToken>) {
let joined_tokens = tokens.iter().chain(self.token_store.iter());
let deduplicated: HashMap<String, EdgeToken> = joined_tokens
.map(|x| (x.secret.clone(), x.clone()))
.map(|x| (x.token.clone(), x.clone()))
.collect();
self.token_store = deduplicated.into_values().collect();
}
Expand All @@ -66,12 +66,12 @@ mod test {
fn memory_provider_correctly_deduplicates_tokens() {
let mut provider = MemoryProvider::default();
provider.sink_tokens(vec![EdgeToken {
secret: "some_secret".into(),
token: "some_secret".into(),
..EdgeToken::default()
}]);

provider.sink_tokens(vec![EdgeToken {
secret: "some_secret".into(),
token: "some_secret".into(),
..EdgeToken::default()
}]);

Expand All @@ -82,7 +82,7 @@ mod test {
fn memory_provider_correctly_determines_token_to_be_valid() {
let mut provider = MemoryProvider::default();
provider.sink_tokens(vec![EdgeToken {
secret: "some_secret".into(),
token: "some_secret".into(),
..EdgeToken::default()
}]);

Expand All @@ -93,7 +93,7 @@ mod test {
fn memory_provider_yields_correct_response_for_token() {
let mut provider = MemoryProvider::default();
let token = EdgeToken {
secret: "some-secret".into(),
token: "some-secret".into(),
..EdgeToken::default()
};

Expand Down
4 changes: 2 additions & 2 deletions server/src/data_sources/offline_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ impl TokenProvider for OfflineProvider {
}

fn secret_is_valid(&self, secret: &str) -> EdgeResult<bool> {
Ok(self.valid_tokens.iter().any(|t| t.secret == secret))
Ok(self.valid_tokens.iter().any(|t| t.token == secret))
}

fn token_details(&self, secret: String) -> EdgeResult<Option<EdgeToken>> {
Ok(self
.valid_tokens
.clone()
.into_iter()
.find(|t| t.secret == secret))
.find(|t| t.token == secret))
}
}

Expand Down
4 changes: 2 additions & 2 deletions server/src/data_sources/redis_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,11 @@ impl TokenProvider for RedisProvider {
}

fn secret_is_valid(&self, secret: &str) -> EdgeResult<bool> {
Ok(self.get_known_tokens()?.iter().any(|t| t.secret == secret))
Ok(self.get_known_tokens()?.iter().any(|t| t.token == secret))
}

fn token_details(&self, secret: String) -> EdgeResult<Option<EdgeToken>> {
let tokens = self.get_known_tokens()?;
Ok(tokens.into_iter().find(|t| t.secret == secret))
Ok(tokens.into_iter().find(|t| t.token == secret))
}
}
16 changes: 11 additions & 5 deletions server/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@ use awc::error::JsonPayloadError;
#[derive(Debug)]
pub enum EdgeError {
AuthorizationDenied,
ClientFeaturesFetchError,
ClientFeaturesParseError(JsonPayloadError),
DataSourceError(String),
EdgeTokenError,
EdgeTokenParseError,
InvalidBackupFile(String, String),
InvalidServerUrl(String),
JsonParseError(String),
NoFeaturesFile,
NoTokenProvider,
TokenParseError,
TlsError,
ClientFeaturesFetchError,
ClientFeaturesParseError(JsonPayloadError),
DataSourceError(String),
JsonParseError(String),
TokenParseError,
}

impl Error for EdgeError {}
Expand All @@ -41,6 +43,8 @@ impl Display for EdgeError {
write!(f, "Failed to parse client features: [{parse_error:#?}]")
}
EdgeError::InvalidServerUrl(msg) => write!(f, "Failed to parse server url: [{msg}]"),
EdgeError::EdgeTokenError => write!(f, "Edge token error"),
EdgeError::EdgeTokenParseError => write!(f, "Failed to parse token response"),
}
}
}
Expand All @@ -59,6 +63,8 @@ impl ResponseError for EdgeError {
EdgeError::InvalidServerUrl(_) => StatusCode::INTERNAL_SERVER_ERROR,
EdgeError::DataSourceError(_) => StatusCode::INTERNAL_SERVER_ERROR,
EdgeError::JsonParseError(_) => StatusCode::INTERNAL_SERVER_ERROR,
EdgeError::EdgeTokenError => StatusCode::BAD_REQUEST,
EdgeError::EdgeTokenParseError => StatusCode::BAD_REQUEST,
}
}

Expand Down
22 changes: 17 additions & 5 deletions server/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub type EdgeJsonResult<T> = Result<Json<T>, EdgeError>;
pub type EdgeResult<T> = Result<T, EdgeError>;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum TokenType {
Frontend,
Client,
Expand All @@ -32,12 +33,23 @@ pub enum ClientFeaturesResponse {
Updated(ClientFeatures, Option<EntityTag>),
}

#[derive(Clone, Debug)]
pub enum TokenStatus {
Invalid,
Valid(EdgeToken),
}

#[derive(Clone, Debug)]
pub struct ClientFeaturesRequest {
pub api_key: String,
pub etag: Option<EntityTag>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ValidateTokenRequest {
pub tokens: Vec<String>,
}

impl ClientFeaturesRequest {
pub fn new(api_key: String, etag: Option<String>) -> Self {
Self {
Expand All @@ -51,7 +63,7 @@ impl ClientFeaturesRequest {
#[cfg_attr(test, derive(Default))]
#[serde(rename_all = "camelCase")]
pub struct EdgeToken {
pub secret: String,
pub token: String,
#[serde(rename = "type")]
pub token_type: Option<TokenType>,
pub environment: Option<String>,
Expand All @@ -64,7 +76,7 @@ pub struct EdgeToken {
impl EdgeToken {
pub fn no_project_or_environment(s: &str) -> Self {
EdgeToken {
secret: s.into(),
token: s.into(),
token_type: None,
environment: None,
projects: vec![],
Expand Down Expand Up @@ -95,7 +107,7 @@ impl FromRequest for EdgeToken {
None => Err(EdgeError::AuthorizationDenied),
}
.and_then(|client_token| {
if token_provider.secret_is_valid(&client_token.secret)? {
if token_provider.secret_is_valid(&client_token.token)? {
Ok(client_token)
} else {
Err(EdgeError::AuthorizationDenied)
Expand Down Expand Up @@ -156,7 +168,7 @@ impl FromStr for EdgeToken {
environment: e_a_k.get(0).cloned(),
projects: token_projects,
token_type: None,
secret: s.into(),
token: s.into(),
expires_at: None,
seen_at: Some(Utc::now()),
alias: None,
Expand Down Expand Up @@ -276,7 +288,7 @@ mod tests {
let parsed_token = EdgeToken::from_str(token);
match parsed_token {
Ok(t) => {
assert_eq!(t.secret, token);
assert_eq!(t.token, token);
}
Err(e) => {
warn!("{}", e);
Expand Down
96 changes: 88 additions & 8 deletions server/src/unleash_client.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
use actix_web::http::header::{ContentType, EntityTag, IfNoneMatch};
use actix_web::http::StatusCode;
use awc::{Client, ClientRequest};
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use std::time::Duration;
use ulid::Ulid;
use unleash_types::client_features::ClientFeatures;
use url::Url;

use crate::types::{ClientFeaturesResponse, EdgeResult};
use crate::types::{
ClientFeaturesResponse, EdgeResult, EdgeToken, TokenStatus, ValidateTokenRequest,
};
use crate::urls::UnleashUrls;
use crate::{error::EdgeError, types::ClientFeaturesRequest};

Expand Down Expand Up @@ -35,6 +38,10 @@ pub fn new_awc_client(instance_id: String) -> Client {
.timeout(Duration::from_secs(5))
.finish()
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct EdgeTokens {
pub tokens: Vec<EdgeToken>,
}

impl UnleashClient {
pub fn from_url(server_url: Url) -> Self {
Expand Down Expand Up @@ -62,6 +69,10 @@ impl UnleashClient {
client_req
}
}
fn awc_validate_token_req(&self) -> ClientRequest {
self.backing_client
.post(self.urls.edge_validate_url.to_string())
}

pub async fn get_client_features(
&self,
Expand All @@ -88,12 +99,39 @@ impl UnleashClient {
Ok(ClientFeaturesResponse::Updated(features, etag))
}
}
pub async fn validate_token(&self, request: ValidateTokenRequest) -> EdgeResult<TokenStatus> {
let mut result = self
.awc_validate_token_req()
.send_body(serde_json::to_string(&request).unwrap())
.await
.map_err(|_| EdgeError::EdgeTokenError)?;
match result.status() {
StatusCode::FORBIDDEN => Ok(TokenStatus::Invalid),
StatusCode::OK => {
let token_response = result
.json::<EdgeTokens>()
.await
.map_err(|_| EdgeError::EdgeTokenParseError)?;
match token_response.tokens.len() {
0 => Ok(TokenStatus::Invalid),
_ => {
let validated_token = token_response.tokens.get(0).unwrap();
Ok(TokenStatus::Valid(validated_token.clone()))
}
}
}
_ => Err(EdgeError::EdgeTokenError),
}
}
}

#[cfg(test)]
mod tests {
use crate::{
types::{ClientFeaturesRequest, ClientFeaturesResponse},
types::{
ClientFeaturesRequest, ClientFeaturesResponse, EdgeToken, TokenStatus,
ValidateTokenRequest,
},
unleash_client::UnleashClient,
};
use actix_http::HttpService;
Expand All @@ -103,6 +141,11 @@ mod tests {
use actix_web::{dev::AppConfig, http::header::EntityTag, web, App, HttpResponse};
use std::str::FromStr;
use unleash_types::client_features::{ClientFeature, ClientFeatures};

use super::EdgeTokens;

const TEST_TOKEN: &str = "[]:development.08bce4267a3b1aa";

fn two_client_features() -> ClientFeatures {
ClientFeatures {
version: 2,
Expand All @@ -125,14 +168,28 @@ mod tests {
async fn return_client_features() -> HttpResponse {
HttpResponse::Ok().json(two_client_features())
}
async fn return_validate_tokens() -> HttpResponse {
HttpResponse::Ok().json(EdgeTokens {
tokens: vec![EdgeToken {
token: TEST_TOKEN.into(),
..Default::default()
}],
})
}

async fn test_features_server() -> TestServer {
test_server(move || {
HttpService::new(map_config(
App::new().wrap(Etag::default()).service(
web::resource("/api/client/features")
.route(web::get().to(return_client_features)),
),
App::new()
.wrap(Etag::default())
.service(
web::resource("/api/client/features")
.route(web::get().to(return_client_features)),
)
.service(
web::resource("/edge/validate")
.route(web::post().to(return_validate_tokens)),
),
|_| AppConfig::default(),
))
.tcp()
Expand Down Expand Up @@ -170,13 +227,12 @@ mod tests {

#[actix_web::test]
async fn client_handles_304() {
let api_key = "*:development.a113e11e04133c367f5fa7c731f9293c492322cf9d6060812cfe3fea";
let srv = test_features_server().await;
let tag = expected_etag(two_client_features());
let client = UnleashClient::new(srv.url("/").as_str(), None).unwrap();
let client_features_result = client
.get_client_features(ClientFeaturesRequest::new(
api_key.to_string(),
TEST_TOKEN.to_string(),
Some(tag.clone()),
))
.await;
Expand All @@ -190,6 +246,30 @@ mod tests {
}
}

#[actix_web::test]
async fn can_validate_token() {
let srv = test_features_server().await;
let client = UnleashClient::new(srv.url("/").as_str(), None).unwrap();
let validate_result = client
.validate_token(ValidateTokenRequest {
tokens: vec![TEST_TOKEN.to_string()],
})
.await;
match validate_result {
Ok(token_status) => match token_status {
TokenStatus::Valid(data) => {
assert_eq!(data.token, TEST_TOKEN.to_string());
}
TokenStatus::Invalid => {
panic!("Expected my token to be valid, but got an invalid status instead");
}
},
Err(e) => {
panic!("Error validating token: {e}");
}
}
}

#[test]
pub fn can_parse_entity_tag() {
let etag = EntityTag::from_str("W/\"b5e6-DPC/1RShRw1J/jtxvRtTo1jf4+o\"").unwrap();
Expand Down

0 comments on commit 0d037ec

Please sign in to comment.