Skip to content

Commit

Permalink
add validating loader;
Browse files Browse the repository at this point in the history
add policy validation methods;
refactor into smaller files
  • Loading branch information
jacobmichels committed Apr 29, 2024
1 parent 09fc7d3 commit ea3e4a8
Show file tree
Hide file tree
Showing 10 changed files with 235 additions and 83 deletions.
13 changes: 3 additions & 10 deletions src/loaders.rs → src/loader/fs.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
use std::{fs, path::PathBuf};

use color_eyre::{
eyre::{bail, Context},
Result,
};
use log::warn;
use std::{fs, path::PathBuf};

use crate::policy::LocalPolicy;

// This module holds functionality related to loading policies.
// This could be fetching them from the file system, environment, remote bucket, etc.
// Loaders implement the PolicyLoader trait.

pub trait PolicyLoader {
fn load_policies(&self) -> Result<Vec<LocalPolicy>>;
}
use super::PolicyLoader;

// Load local policies from the file system
pub struct FsPolicyLoader {
Expand Down Expand Up @@ -83,8 +78,6 @@ impl PolicyLoader for FsPolicyLoader {
.read_policy_files()
.wrap_err("failed to read policies")?;

// TODO: Validate these policies before returning them

Ok(policies)
}
}
17 changes: 17 additions & 0 deletions src/loader/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use color_eyre::Result;

use crate::policy::LocalPolicy;

mod fs;
mod validating;

pub use fs::FsPolicyLoader;
pub use validating::ValidatingPolicyLoader;

// This module holds functionality related to loading policies.
// This could be fetching them from the file system, environment, remote bucket, etc.
// Loaders implement the PolicyLoader trait.

pub trait PolicyLoader {
fn load_policies(&self) -> Result<Vec<LocalPolicy>>;
}
30 changes: 30 additions & 0 deletions src/loader/validating.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use color_eyre::Result;

// Loads policies from a wrapped PolicyLoader,
// ensuring they're valid before passing them up to the caller.

use crate::policy::LocalPolicy;

use super::PolicyLoader;

pub struct ValidatingPolicyLoader {
wrapped: Box<dyn PolicyLoader>,
}

impl ValidatingPolicyLoader {
pub fn new(wrapped: Box<dyn PolicyLoader>) -> ValidatingPolicyLoader {
ValidatingPolicyLoader { wrapped }
}
}

impl PolicyLoader for ValidatingPolicyLoader {
fn load_policies(&self) -> Result<Vec<LocalPolicy>> {
let raw_policies = self.wrapped.load_policies()?;

for policy in raw_policies.iter() {
policy.validate()?;
}

Ok(raw_policies)
}
}
10 changes: 5 additions & 5 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::cli::Cli;

mod cli;
mod fetchers;
mod loaders;
mod loader;
mod policy;
mod server;
mod tailscale;
Expand All @@ -32,10 +32,10 @@ fn init() -> Result<()> {
}

async fn run(c: Cli) -> Result<()> {
// this function is where I can set up dependencies.
// this function is where I set up dependencies.

// TODO: add another PolicyLoader that wraps a PolicyLoader and validates the policies before returning them
let fs_loader = loaders::FsPolicyLoader::new(c.policies_dir);
let fs_loader = loader::FsPolicyLoader::new(c.policies_dir);
let validating_loader = loader::ValidatingPolicyLoader::new(Box::new(fs_loader));

let reqwest_fetcher = fetchers::ReqwestJWKSFetcher::new();

Expand All @@ -47,7 +47,7 @@ async fn run(c: Cli) -> Result<()> {

debug!("dependencies initialized, starting app");
server::start(
Box::new(fs_loader),
Box::new(validating_loader),
Box::new(reqwest_fetcher),
Box::new(tailscale),
c.port,
Expand Down
60 changes: 0 additions & 60 deletions src/policy.rs

This file was deleted.

38 changes: 38 additions & 0 deletions src/policy/algorithm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use color_eyre::{eyre::bail, Report, Result};
use jsonwebtoken::Algorithm;
use serde::Deserialize;

#[derive(Debug, Clone, Deserialize)]
pub struct AcceptedAlgorithm(Algorithm);

impl AcceptedAlgorithm {
pub fn new(alg: Algorithm) -> Result<AcceptedAlgorithm> {
if !ACCEPTABLE_ALGORITHMS.contains(&alg) {
bail!("algorithm is not supported: {:?}", alg);
}

Ok(AcceptedAlgorithm(alg))
}

pub fn wrapped(&self) -> Algorithm {
self.0.clone()
}
}

pub const ACCEPTABLE_ALGORITHMS: [Algorithm; 3] =
[Algorithm::RS256, Algorithm::RS384, Algorithm::RS512];

impl TryFrom<String> for AcceptedAlgorithm {
type Error = Report;
fn try_from(value: String) -> Result<Self, Self::Error> {
let alg: Result<Algorithm> = match value.to_lowercase().as_str() {
"rs256" => Ok(Algorithm::RS256),
"rs384" => Ok(Algorithm::RS384),
"rs512" => Ok(Algorithm::RS512),
_ => {
bail!("unsupported algorithm")
}
};
AcceptedAlgorithm::new(alg?)
}
}
101 changes: 101 additions & 0 deletions src/policy/local_policy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
use std::collections::{HashMap, HashSet};

use color_eyre::{
eyre::{bail, Context},
Result,
};
use jsonwebtoken::jwk::JwkSet;
use reqwest::Url;
use serde::Deserialize;

use super::{algorithm::AcceptedAlgorithm, policy_with_jwks::PolicyWithJWKS};

// A policy without its JWKS loaded
#[derive(Deserialize, Debug)]
pub struct LocalPolicy {
pub issuer: HashSet<String>,
pub subject: String,
pub algorithm: String,
pub jwks_url: String,
pub allowed_scopes: HashMap<String, String>,
}

impl LocalPolicy {
pub fn attach_jwks(self, jwks: JwkSet) -> Result<PolicyWithJWKS> {
let alg: AcceptedAlgorithm = self.algorithm.try_into()?;

Ok(PolicyWithJWKS {
issuer: self.issuer,
jwks,
algorithm: alg,
allowed_scopes: self.allowed_scopes,
subject: self.subject,
})
}

pub fn validate(&self) -> Result<()> {
self.validate_iss()?;
self.validate_alg()?;
self.validate_sub()?;
self.validate_jwks_url()?;
self.validate_allowed_scopes()?;

Ok(())
}

fn validate_iss(&self) -> Result<()> {
if self.issuer.is_empty() {
bail!("no issuer set")
}

for iss in self.issuer.iter() {
if iss.is_empty() {
bail!("issuer has empty entry")
}
}

Ok(())
}

fn validate_sub(&self) -> Result<()> {
if self.subject.is_empty() {
bail!("subject is empty")
}

Ok(())
}

fn validate_alg(&self) -> Result<()> {
let alg: Result<AcceptedAlgorithm> = self.algorithm.clone().try_into();
if let Err(e) = alg {
bail!("algorithm is invalid: {}", e);
}

Ok(())
}

fn validate_jwks_url(&self) -> Result<()> {
if self.jwks_url.is_empty() {
bail!("jwks_url is empty");
}

Url::parse(&self.jwks_url).wrap_err("jwks_url is not a valid url")?;
Ok(())
}

fn validate_allowed_scopes(&self) -> Result<()> {
if self.allowed_scopes.is_empty() {
bail!("no scopes are allowed, nothing can satisfy this policy");
}

for (key, value) in self.allowed_scopes.iter() {
if key.is_empty() {
bail!("allowed scope with empty key");
} else if value.is_empty() {
bail!("allowed scope {} has empty value", key);
}
}

Ok(())
}
}
7 changes: 7 additions & 0 deletions src/policy/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
mod algorithm;
mod local_policy;
mod policy_with_jwks;

pub use algorithm::ACCEPTABLE_ALGORITHMS;
pub use local_policy::LocalPolicy;
pub use policy_with_jwks::PolicyWithJWKS;
29 changes: 29 additions & 0 deletions src/policy/policy_with_jwks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use std::collections::{HashMap, HashSet};

use jsonwebtoken::jwk::JwkSet;
use serde::Deserialize;

use super::algorithm::AcceptedAlgorithm;

// A policy without its JWKS loaded
#[derive(Deserialize, Debug, Clone)]
pub struct PolicyWithJWKS {
pub issuer: HashSet<String>,
pub subject: String,
pub algorithm: AcceptedAlgorithm,
pub jwks: JwkSet,
pub allowed_scopes: HashMap<String, String>, // TODO: I don't like using strings to represent scopes, but don't want to update this app when new scopes are made available. Any way to look at the Tailscale API to get available scopes??
}

impl PolicyWithJWKS {
pub fn check_scope_allowed(&self, scope_name: &str, scope_value: &str) -> bool {
match self.allowed_scopes.get(scope_name) {
Some(allowed_value) => {
(allowed_value == "read" && scope_value == "read")
|| (allowed_value == "write" && scope_value == "write")
|| (allowed_value == "write" && scope_value == "read")
}
None => false,
}
}
}
Loading

0 comments on commit ea3e4a8

Please sign in to comment.