Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WASM: Add JWT policy #30

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions compose/control-plane/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@
"policies": [],
"target_domain": "http://web.app:80",
"oidc_issuer": "http://keycloak:8080/auth/realms/ostia",
"hosts": [
"web",
"web.app"
],
"policies": [
{
"name": "jwt",
"configuration": {
"rules": [
"AA"
]
}
}
],
"proxy_rules": [
{
"pattern": "/",
Expand Down
1 change: 1 addition & 0 deletions src/oidc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ impl OIDCConfig {

let provider = JwtProvider {
issuer: self.issuer.clone(),
payload_in_metadata: "jwt_payload".to_string(),
from_headers: vec![JwtHeader {
name: "Authorization".to_string(),
value_prefix: "Bearer ".to_string(),
Expand Down
8 changes: 7 additions & 1 deletion src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,17 @@ pub struct MappingRules {
delta: u32,
}

#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct PoliciyConfig {
pub name: String,
configuration: serde_json::Value,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Service {
pub id: u32,
pub hosts: Vec<std::string::String>,
pub policies: Vec<std::string::String>,
pub policies: Vec<PoliciyConfig>,
pub target_domain: std::string::String,
pub proxy_rules: Vec<MappingRules>,
pub oidc_issuer: std::string::String,
Expand Down
63 changes: 63 additions & 0 deletions wasm_filter/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions wasm_filter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,10 @@ wasm-bindgen-macro = "0.2.60"
serde_json = "^1"
serde = { version = "^1", features = ["derive"] }

anyhow = "^1"

prost = { version = "^0", default-features = false, features = ["prost-derive"] }
prost-types = { version = "^0", default-features = false }

[lib]
crate-type = ["cdylib"]
9 changes: 8 additions & 1 deletion wasm_filter/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::cell::RefCell;
use std::collections::HashMap;

Expand Down Expand Up @@ -30,11 +31,17 @@ impl MappingRule {
}
}

#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct PoliciyConfig {
pub name: String,
configuration: serde_json::Value,
}

#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct Service {
pub id: u32,
pub hosts: Vec<String>,
pub policies: Vec<String>,
pub policies: Vec<PoliciyConfig>,
pub target_domain: String,
pub proxy_rules: Vec<MappingRule>,
}
Expand Down
151 changes: 151 additions & 0 deletions wasm_filter/src/jwt.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
use proxy_wasm::traits::*;
use proxy_wasm::types::*;

use prost::Message;

#[derive(Debug, Default)]
pub struct Rules {
path: String,
claim: String, // Move to liquid
claim_value: String, // Move to liquid
allow: bool,
}
impl Rules {
pub fn matches(&self, path: String, jwt_claim_value: &str) -> bool {
if self.path != path {
log::debug!(
"PATH failed match request_path='{}' with path='{}'",
self.path,
path
);
return false;
}
if self.claim_value.as_str() != jwt_claim_value {
log::debug!(
"Matches claim_value='{}' with jwt_value='{}' failed",
self.claim_value,
jwt_claim_value,
);
return false;
}

true
}
}

#[derive(Debug, Default)]
pub struct JWTConfig {
pub rules: Vec<Rules>,
}

#[derive(Debug, Default)]
pub struct JWT {
pub context_id: u32,
pub config: JWTConfig,
}

impl JWT {
pub fn new(context_id: u32) -> JWT {
JWT {
context_id,
config: JWTConfig {
..Default::default()
},
}
}

pub fn config(&mut self) {
self.config.rules = Vec::new();
self.config.rules.push(Rules {
path: "/headers".to_string(),
claim: "name".to_string(),
claim_value: "Jane Smith".to_string(),
allow: false,
});
}
pub fn get_jwt_claim(&self, claim: &str) -> Result<String, anyhow::Error> {
let key = vec![
"metadata",
"filter_metadata",
"envoy.filters.http.jwt_authn",
"jwt_payload",
claim,
];

let data = self.get_property(key);
if data.is_none() {
return Err(anyhow::Error::msg("Failed to get JWT payload"));
}
let tmp = data.clone().unwrap();

let ret = std::str::from_utf8(tmp.as_slice());
if ret.is_err() {
return Err(anyhow::Error::msg("Failed to get decode JWT payload"));
}
Ok(ret.unwrap().to_string())
}

pub fn get_jwt_token(&self) -> Result<Vec<u8>, anyhow::Error> {
let data = self.get_property(vec![
"metadata",
"filter_metadata",
"envoy.filters.http.jwt_authn",
"jwt_payload",
]);

if data.is_none() {
return Err(anyhow::Error::msg("Failed to get JWT payload"));
}
log::info!("Bytes --> {:?}", data.clone().unwrap().as_slice());
log::info!(
"STR --> {:?}",
std::str::from_utf8(data.clone().unwrap().as_slice())
);
let msg: prost_types::Struct =
Message::decode_length_delimited(data.clone().unwrap().as_slice())
.expect("cannot decode message");
log::info!("MSG--> {:?}", msg);
return Ok(data.unwrap());
}

fn get_path(&self) -> Option<std::string::String> {
return self.get_http_request_header(":path");
}
}

impl Context for JWT {}

impl HttpContext for JWT {
fn on_http_request_headers(&mut self, _: usize) -> Action {
// @TODO to be removed until HTTP_CONTEXT can have metadata attached
self.config();

let jwt_token = self.get_jwt_token();
if jwt_token.is_err() {
log::warn!("Error on JWT auth: '{:?}'", jwt_token);
self.send_http_response(403, vec![], Some(b"Access forbidden.\n"));
return Action::Pause;
}

let mut result = false;

for rule in &self.config.rules {
let claim_value = self.get_jwt_claim(rule.claim.as_str());
if claim_value.is_err() {
log::info!("Cannot retrieve {}", rule.claim.as_str());
continue;
}

if rule.matches(self.get_path().unwrap(), claim_value.unwrap().as_str()) {
result = true;
}
}

if result == true {
return Action::Continue;
}

self.send_http_response(403, vec![], Some(b"Access forbidden.\n"));
return Action::Pause;
}
}
12 changes: 11 additions & 1 deletion wasm_filter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use proxy_wasm::types::*;
use std::time::Duration;

mod config;
mod jwt;

const AUTH_BACKEND: &str = "httpbin";

Expand All @@ -28,7 +29,16 @@ impl Context for ConfigContext {}
impl RootContext for ConfigContext {
fn on_vm_start(&mut self, _: usize) -> bool {
let config = self.get_configuration();
config::import_config(std::str::from_utf8(&config.unwrap()).unwrap());

let service = config::import_config(std::str::from_utf8(&config.unwrap()).unwrap());
for policy in &service.policies {
if policy.name.as_str() == "jwt" {
let cb =
|context_id, _| -> Box<dyn HttpContext> { Box::new(jwt::JWT::new(context_id)) };
proxy_wasm::set_http_context(cb);
}
}

self.set_tick_period(Duration::from_secs(20));
true
}
Expand Down