unleash-edge 19.2.1

Unleash edge is a proxy for Unleash. It can return both evaluated feature toggles as well as the raw data from Unleash's client API
use actix_web::{
    post,
    web::{self, Data, Json},
    HttpRequest,
};
use dashmap::DashMap;
use utoipa;

use crate::auth::token_validator::TokenValidator;
use crate::types::{
    EdgeJsonResult, EdgeToken, TokenStrings, TokenValidationStatus, ValidatedTokens,
};

#[utoipa::path(
    path = "/edge/validate",
    responses(
        (status = 200, description = "Return valid tokens from list of tokens passed in to validate", body = ValidatedTokens)
    ),
    request_body = TokenStrings
)]
#[post("/validate")]
pub async fn validate(
    token_cache: web::Data<DashMap<String, EdgeToken>>,
    req: HttpRequest,
    tokens: Json<TokenStrings>,
) -> EdgeJsonResult<ValidatedTokens> {
    let maybe_validator = req.app_data::<Data<TokenValidator>>();
    match maybe_validator {
        Some(validator) => {
            let known_tokens = validator
                .register_tokens(tokens.into_inner().tokens)
                .await?;
            Ok(Json(ValidatedTokens {
                tokens: known_tokens
                    .into_iter()
                    .filter(|t| t.status == TokenValidationStatus::Validated)
                    .collect(),
            }))
        }
        None => {
            let tokens_to_check = tokens.into_inner().tokens;
            let valid_tokens: Vec<EdgeToken> = tokens_to_check
                .iter()
                .filter_map(|t| token_cache.get(t).map(|e| e.value().clone()))
                .collect();
            Ok(Json(ValidatedTokens {
                tokens: valid_tokens,
            }))
        }
    }
}

pub fn configure_edge_api(cfg: &mut web::ServiceConfig) {
    cfg.service(validate);
}

#[cfg(test)]
mod tests {
    use std::sync::Arc;

    use actix_web::http::header::ContentType;
    use actix_web::web::Json;
    use actix_web::{test, web, App};
    use dashmap::DashMap;

    use crate::auth::token_validator::TokenValidator;
    use crate::edge_api::validate;
    use crate::types::{
        EdgeToken, TokenStrings, TokenType, TokenValidationStatus, ValidatedTokens,
    };

    #[tokio::test]
    pub async fn validating_incorrect_tokens_returns_empty_list() {
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
        let app = test::init_service(
            App::new()
                .app_data(web::Data::from(token_cache.clone()))
                .service(web::scope("/edge").service(validate)),
        )
        .await;
        let mut valid_token =
            EdgeToken::try_from("test-app:development.abcdefghijklmnopqrstu".to_string()).unwrap();
        valid_token.token_type = Some(TokenType::Client);
        valid_token.status = TokenValidationStatus::Validated;
        token_cache.insert(valid_token.token.clone(), valid_token.clone());
        let token_strings = TokenStrings {
            tokens: vec!["random_token:rqweqwew.qweqwjeqwkejlqwe".into()],
        };
        let req = test::TestRequest::post()
            .uri("/edge/validate")
            .insert_header(ContentType::json())
            .set_json(Json(token_strings))
            .to_request();
        let res: ValidatedTokens = test::call_and_read_body_json(&app, req).await;
        assert_eq!(res.tokens.len(), 0);
    }

    #[tokio::test]
    pub async fn validating_a_mix_of_tokens_only_returns_valid_tokens() {
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
        let app = test::init_service(
            App::new()
                .app_data(web::Data::from(token_cache.clone()))
                .service(web::scope("/edge").service(validate)),
        )
        .await;
        let mut valid_token =
            EdgeToken::try_from("test-app:development.abcdefghijklmnopqrstu".to_string()).unwrap();
        valid_token.token_type = Some(TokenType::Client);
        valid_token.status = TokenValidationStatus::Validated;
        token_cache.insert(valid_token.token.clone(), valid_token.clone());

        let token_strings = TokenStrings {
            tokens: vec![
                "test-app:development.abcdefghijklmnopqrstu".into(),
                "probablyaninvalidproject:development.some_crazy_secret".into(),
            ],
        };
        let req = test::TestRequest::post()
            .uri("/edge/validate")
            .insert_header(ContentType::json())
            .set_json(Json(token_strings))
            .to_request();
        let res: ValidatedTokens = test::call_and_read_body_json(&app, req).await;
        assert_eq!(res.tokens.len(), 1);
        assert!(res.tokens.iter().any(|t| t.token == valid_token.token));
    }

    #[tokio::test]
    pub async fn adding_a_token_validator_filters_so_only_validated_tokens_are_returned() {
        let token_cache: Arc<DashMap<String, EdgeToken>> = Arc::new(DashMap::default());
        let token_validator = TokenValidator {
            unleash_client: Arc::new(Default::default()),
            token_cache: token_cache.clone(),
            persistence: None,
        };
        let app = test::init_service(
            App::new()
                .app_data(web::Data::from(token_cache.clone()))
                .app_data(web::Data::new(token_validator))
                .service(web::scope("/edge").service(validate)),
        )
        .await;
        let mut valid_token =
            EdgeToken::try_from("test-app:development.abcdefghijklmnopqrstu".to_string()).unwrap();
        valid_token.token_type = Some(TokenType::Client);
        valid_token.status = TokenValidationStatus::Validated;
        let mut invalid_token = EdgeToken::try_from(
            "probablyaninvalidproject:development.some_crazy_secret".to_string(),
        )
        .unwrap();
        invalid_token.status = TokenValidationStatus::Invalid;
        invalid_token.token_type = Some(TokenType::Admin);
        token_cache.insert(valid_token.token.clone(), valid_token.clone());
        token_cache.insert(invalid_token.token.clone(), invalid_token.clone());
        let token_strings = TokenStrings {
            tokens: vec![
                "test-app:development.abcdefghijklmnopqrstu".into(),
                "probablyaninvalidproject:development.some_crazy_secret".into(),
            ],
        };
        let req = test::TestRequest::post()
            .uri("/edge/validate")
            .insert_header(ContentType::json())
            .set_json(Json(token_strings))
            .to_request();
        let res: ValidatedTokens = test::call_and_read_body_json(&app, req).await;
        assert_eq!(res.tokens.len(), 1);
        assert!(res.tokens.iter().any(|t| t.token == valid_token.token));
    }
}