diff --git a/Cargo.lock b/Cargo.lock index 1589b24..0203889 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -199,6 +199,7 @@ dependencies = [ "async-compression", "async-trait", "attic", + "attic-token", "aws-config", "aws-sdk-s3", "axum", @@ -218,7 +219,6 @@ dependencies = [ "humantime-serde", "itoa", "jsonwebtoken", - "lazy_static", "maybe-owned", "rand", "regex", @@ -240,6 +240,22 @@ dependencies = [ "xdg", ] +[[package]] +name = "attic-token" +version = "0.1.0" +dependencies = [ + "attic", + "base64 0.20.0", + "chrono", + "displaydoc", + "jsonwebtoken", + "lazy_static", + "regex", + "serde", + "serde_with", + "tracing", +] + [[package]] name = "atty" version = "0.2.14" diff --git a/Cargo.toml b/Cargo.toml index ed6c96b..fd756c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,4 +4,5 @@ members = [ "attic", "client", "server", + "token", ] diff --git a/server/Cargo.toml b/server/Cargo.toml index 8b2c5cb..4923c44 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -20,6 +20,7 @@ doc = false [dependencies] attic = { path = "../attic", default-features = false } +attic-token = { path = "../token" } anyhow = "1.0.68" async-trait = "0.1.60" @@ -41,7 +42,6 @@ humantime = "2.1.0" humantime-serde = "1.1.1" itoa = "1.0.5" jsonwebtoken = "8.2.0" -lazy_static = "1.4.0" maybe-owned = "0.3.4" rand = "0.8.5" regex = "1.7.0" diff --git a/server/src/access/http.rs b/server/src/access/http.rs index 3345bd2..391f6f9 100644 --- a/server/src/access/http.rs +++ b/server/src/access/http.rs @@ -1,10 +1,6 @@ //! HTTP middlewares for access control. -use std::str; - use axum::{http::Request, middleware::Next, response::Response}; -use lazy_static::lazy_static; -use regex::Regex; use sea_orm::DatabaseConnection; use tokio::sync::OnceCell; @@ -13,11 +9,7 @@ use crate::database::{entity::cache::CacheModel, AtticDatabase}; use crate::error::ServerResult; use crate::{RequestState, State}; use attic::cache::CacheName; - -lazy_static! { - static ref AUTHORIZATION_REGEX: Regex = - Regex::new(r"^(?i)((?Pbearer)|(?Pbasic))(?-i) (?P(.*))$").unwrap(); -} +use attic_token::util::parse_authorization_header; /// Auth state. #[derive(Debug)] @@ -124,46 +116,3 @@ pub async fn apply_auth(req: Request, next: Next) -> Response { next.run(req).await } - -/// Extracts the JWT from an Authorization header. -fn parse_authorization_header(authorization: &str) -> Option { - let captures = AUTHORIZATION_REGEX.captures(authorization)?; - let rest = captures.name("rest").unwrap().as_str(); - - if captures.name("bearer").is_some() { - // Bearer token - Some(rest.to_string()) - } else { - // Basic auth - let bytes = base64::decode(rest).ok()?; - - let user_pass = str::from_utf8(&bytes).ok()?; - let colon = user_pass.find(':')?; - let pass = &user_pass[colon + 1..]; - - Some(pass.to_string()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_authorization_header() { - assert_eq!( - "somepass", - parse_authorization_header("Basic c29tZXVzZXI6c29tZXBhc3M=").unwrap(), - ); - - assert_eq!( - "somepass", - parse_authorization_header("baSIC c29tZXVzZXI6c29tZXBhc3M=").unwrap(), - ); - - assert_eq!( - "some-token", - parse_authorization_header("bearer some-token").unwrap(), - ); - } -} diff --git a/server/src/access/mod.rs b/server/src/access/mod.rs index 0db3da0..ec8e2a8 100644 --- a/server/src/access/mod.rs +++ b/server/src/access/mod.rs @@ -1,352 +1,7 @@ //! Access control. //! -//! Access control in Attic is simple and stateless [0] - The server validates -//! the JWT against a trusted public key and allows access based on the -//! `x-attic-access` claim. -//! -//! One primary goal of the Attic Server is easy scalability. It's designed -//! to be deployed to serverless platforms like AWS Lambda and have fast -//! cold-start times. Instances are created and destoyed rapidly in response -//! to requests. -//! -//! [0] We may revisit this later :) -//! -//! ## Cache discovery -//! -//! If the JWT grants any permission at all to the requested cache name, -//! then the bearer is able to discover the presence of the cache, meaning -//! that NoSuchCache or Forbidden can be returned depending on the scenario. -//! Otherwise, the user will get a generic 401 response (Unauthorized) -//! regardless of the request (or whether the cache exists or not). -//! -//! ## Supplying the token -//! -//! The JWT can be supplied to the server in one of two ways: -//! -//! - As a normal Bearer token. -//! - As the password in Basic Auth (used by Nix). The username is ignored. -//! -//! To add the token to Nix, use the following format in `~/.config/nix/netrc`: -//! -//! ```text -//! machine attic.server.tld password eyJhb... -//! ``` -//! -//! ## Example token -//! -//! ```json -//! { -//! "sub": "meow", -//! "exp": 4102324986, -//! "https://jwt.attic.rs/v1": { -//! "caches": { -//! "cache-rw": { -//! "w": 1, -//! "r": 1 -//! }, -//! "cache-ro": { -//! "r": 1 -//! }, -//! "team-*": { -//! "w": 1, -//! "r": 1, -//! "cc": 1 -//! } -//! } -//! } -//! } -//! ``` +//! See [attic_token] for more details. pub mod http; -#[cfg(test)] -mod tests; - -use std::collections::HashMap; - -use chrono::{DateTime, Utc}; -use displaydoc::Display; -pub use jsonwebtoken::{ - Algorithm as JwtAlgorithm, DecodingKey as JwtDecodingKey, EncodingKey as JwtEncodingKey, - Header as JwtHeader, Validation as JwtValidation, -}; -use serde::{Deserialize, Serialize}; -use serde_with::{serde_as, BoolFromInt}; - -use crate::error::ServerResult; -use attic::cache::{CacheName, CacheNamePattern}; - -/// Custom claim namespace for the AtticAccess information. -/// -/// Custom claim namespaces are required by platforms like Auth0, and -/// custom claims without one will be silently dropped. -/// -/// -/// -/// Also change the `#[serde(rename)]` below if you change this. -pub const CLAIM_NAMESPACE: &str = "https://jwt.attic.rs/v1"; - -macro_rules! require_permission_function { - ($name:ident, $descr:literal, $member:ident) => { - pub fn $name(&self) -> ServerResult<()> { - if !self.$member { - tracing::debug!("Client has no {} permission", $descr); - if self.can_discover() { - Err(Error::PermissionDenied.into()) - } else { - Err(Error::NoDiscoveryPermission.into()) - } - } else { - Ok(()) - } - } - }; -} - -/// A validated JSON Web Token. -#[derive(Debug)] -pub struct Token(jsonwebtoken::TokenData); - -/// Claims of a JSON Web Token. -#[derive(Debug, Serialize, Deserialize)] -struct TokenClaims { - /// Subject. - sub: String, - - /// Expiration timestamp. - exp: usize, - - /// Attic namespace. - #[serde(rename = "https://jwt.attic.rs/v1")] - attic_ns: AtticAccess, -} - -/// Permissions granted to a client. -/// -/// This is the content of the `attic-access` claim in JWTs. -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct AtticAccess { - /// Cache permissions. - /// - /// Keys here may include wildcards. - caches: HashMap, -} - -/// Permission to a single cache. -#[serde_as] -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CachePermission { - /// Can pull objects from the cache. - #[serde(default = "CachePermission::permission_default")] - #[serde(skip_serializing_if = "is_false")] - #[serde(rename = "r")] - #[serde_as(as = "BoolFromInt")] - pub pull: bool, - - /// Can push objects to the cache. - #[serde(default = "CachePermission::permission_default")] - #[serde(skip_serializing_if = "is_false")] - #[serde(rename = "w")] - #[serde_as(as = "BoolFromInt")] - pub push: bool, - - /// Can delete objects from the cache. - #[serde(default = "CachePermission::permission_default")] - #[serde(skip_serializing_if = "is_false")] - #[serde(rename = "d")] - #[serde_as(as = "BoolFromInt")] - pub delete: bool, - - /// Can create the cache itself. - #[serde(default = "CachePermission::permission_default")] - #[serde(skip_serializing_if = "is_false")] - #[serde(rename = "cc")] - #[serde_as(as = "BoolFromInt")] - pub create_cache: bool, - - /// Can reconfigure the cache. - #[serde(default = "CachePermission::permission_default")] - #[serde(skip_serializing_if = "is_false")] - #[serde(rename = "cr")] - #[serde_as(as = "BoolFromInt")] - pub configure_cache: bool, - - /// Can configure retention/quota settings. - #[serde(default = "CachePermission::permission_default")] - #[serde(skip_serializing_if = "is_false")] - #[serde(rename = "cq")] - #[serde_as(as = "BoolFromInt")] - pub configure_cache_retention: bool, - - /// Can destroy the cache itself. - #[serde(default = "CachePermission::permission_default")] - #[serde(skip_serializing_if = "is_false")] - #[serde(rename = "cd")] - #[serde_as(as = "BoolFromInt")] - pub destroy_cache: bool, -} - -/// An access error. -#[derive(Debug, Display)] -#[ignore_extra_doc_attributes] -pub enum Error { - /// User has no permission to this cache. - NoDiscoveryPermission, - - /// User does not have permission to complete this action. - /// - /// This implies that there is some permission granted to the - /// user, so the user is authorized to discover the cache. - PermissionDenied, - - /// JWT error: {0} - TokenError(jsonwebtoken::errors::Error), -} - -impl Token { - /// Verifies and decodes a token. - pub fn from_jwt(token: &str, key: &JwtDecodingKey) -> ServerResult { - let validation = JwtValidation::default(); - jsonwebtoken::decode::(token, key, &validation) - .map_err(|e| Error::TokenError(e).into()) - .map(Token) - } - - /// Creates a new token with an expiration timestamp. - pub fn new(sub: String, exp: &DateTime) -> Self { - let claims = TokenClaims { - sub, - exp: exp.timestamp() as usize, - attic_ns: Default::default(), - }; - - Self(jsonwebtoken::TokenData { - header: JwtHeader::new(JwtAlgorithm::HS256), - claims, - }) - } - - /// Encodes the token. - pub fn encode(&self, key: &JwtEncodingKey) -> ServerResult { - jsonwebtoken::encode(&self.0.header, &self.0.claims, key) - .map_err(|e| Error::TokenError(e).into()) - } - - /// Returns the subject of the token. - pub fn sub(&self) -> &str { - self.0.claims.sub.as_str() - } - - /// Returns the claims as a serializable value. - pub fn opaque_claims(&self) -> &impl Serialize { - &self.0.claims - } - - /// Returns a mutable reference to a permission entry. - pub fn get_or_insert_permission_mut( - &mut self, - pattern: CacheNamePattern, - ) -> &mut CachePermission { - use std::collections::hash_map::Entry; - - let access = self.attic_access_mut(); - match access.caches.entry(pattern) { - Entry::Occupied(v) => v.into_mut(), - Entry::Vacant(v) => v.insert(CachePermission::default()), - } - } - - /// Returns explicit permission granted for a cache. - pub fn get_permission_for_cache(&self, cache: &CacheName) -> CachePermission { - let access = self.attic_access(); - - let pattern_key = cache.to_pattern(); - if let Some(direct_match) = access.caches.get(&pattern_key) { - return direct_match.clone(); - } - - for (pattern, permission) in access.caches.iter() { - if pattern.matches(cache) { - return permission.clone(); - } - } - - CachePermission::default() - } - - fn attic_access(&self) -> &AtticAccess { - &self.0.claims.attic_ns - } - - fn attic_access_mut(&mut self) -> &mut AtticAccess { - &mut self.0.claims.attic_ns - } -} - -impl CachePermission { - /// Adds implicit grants for public caches. - pub fn add_public_permissions(&mut self) { - self.pull = true; - } - - /// Returns whether the user is allowed to discover this cache. - /// - /// This permission is implied when any permission is explicitly - /// granted. - pub const fn can_discover(&self) -> bool { - self.push - || self.pull - || self.delete - || self.create_cache - || self.configure_cache - || self.destroy_cache - || self.configure_cache_retention - } - - pub fn require_discover(&self) -> ServerResult<()> { - if !self.can_discover() { - Err(Error::NoDiscoveryPermission.into()) - } else { - Ok(()) - } - } - - require_permission_function!(require_pull, "pull", pull); - require_permission_function!(require_push, "push", push); - require_permission_function!(require_delete, "delete", delete); - require_permission_function!(require_create_cache, "create cache", create_cache); - require_permission_function!( - require_configure_cache, - "reconfigure cache", - configure_cache - ); - require_permission_function!( - require_configure_cache_retention, - "configure cache retention", - configure_cache_retention - ); - require_permission_function!(require_destroy_cache, "destroy cache", destroy_cache); - - fn permission_default() -> bool { - false - } -} - -impl Default for CachePermission { - fn default() -> Self { - Self { - pull: false, - push: false, - delete: false, - create_cache: false, - configure_cache: false, - configure_cache_retention: false, - destroy_cache: false, - } - } -} - -// bruh -fn is_false(b: &bool) -> bool { - !b -} +pub use attic_token::*; diff --git a/token/Cargo.toml b/token/Cargo.toml new file mode 100644 index 0000000..5aa284e --- /dev/null +++ b/token/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "attic-token" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +attic = { path = "../attic", default-features = false } + +base64 = "0.20.0" +chrono = "0.4.23" +displaydoc = "0.2.3" +jsonwebtoken = "8.2.0" +lazy_static = "1.4.0" +regex = "1.7.0" +serde = "1.0.151" +serde_with = "2.1.0" +tracing = "0.1.37" diff --git a/token/src/lib.rs b/token/src/lib.rs new file mode 100644 index 0000000..fd46dbc --- /dev/null +++ b/token/src/lib.rs @@ -0,0 +1,362 @@ +//! Access control. +//! +//! Access control in Attic is simple and stateless [0] - The server validates +//! the JWT against a trusted public key and allows access based on the +//! `x-attic-access` claim. +//! +//! One primary goal of the Attic Server is easy scalability. It's designed +//! to be deployed to serverless platforms like AWS Lambda and have fast +//! cold-start times. Instances are created and destoyed rapidly in response +//! to requests. +//! +//! [0] We may revisit this later :) +//! +//! ## Opaqueness +//! +//! The token format is unstable and claims beyond the standard ones defined +//! in RFC 7519 should never be interpreted by the client. The token might not +//! even be a valid JWT, in which case the client must not throw an error. +//! +//! ## Cache discovery +//! +//! If the JWT grants any permission at all to the requested cache name, +//! then the bearer is able to discover the presence of the cache, meaning +//! that NoSuchCache or Forbidden can be returned depending on the scenario. +//! Otherwise, the user will get a generic 401 response (Unauthorized) +//! regardless of the request (or whether the cache exists or not). +//! +//! ## Supplying the token +//! +//! The JWT can be supplied to the server in one of two ways: +//! +//! - As a normal Bearer token. +//! - As the password in Basic Auth (used by Nix). The username is ignored. +//! +//! To add the token to Nix, use the following format in `~/.config/nix/netrc`: +//! +//! ```text +//! machine attic.server.tld password eyJhb... +//! ``` +//! +//! ## Example token +//! +//! ```json +//! { +//! "sub": "meow", +//! "exp": 4102324986, +//! "https://jwt.attic.rs/v1": { +//! "caches": { +//! "cache-rw": { +//! "w": 1, +//! "r": 1 +//! }, +//! "cache-ro": { +//! "r": 1 +//! }, +//! "team-*": { +//! "w": 1, +//! "r": 1, +//! "cc": 1 +//! } +//! } +//! } +//! } +//! ``` + +pub mod util; + +#[cfg(test)] +mod tests; + +use std::collections::HashMap; +use std::error::Error as StdError; + +use chrono::{DateTime, Utc}; +use displaydoc::Display; +pub use jsonwebtoken::{ + Algorithm as JwtAlgorithm, DecodingKey as JwtDecodingKey, EncodingKey as JwtEncodingKey, + Header as JwtHeader, Validation as JwtValidation, +}; +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, BoolFromInt}; + +use attic::cache::{CacheName, CacheNamePattern}; + +/// Custom claim namespace for the AtticAccess information. +/// +/// Custom claim namespaces are required by platforms like Auth0, and +/// custom claims without one will be silently dropped. +/// +/// +/// +/// Also change the `#[serde(rename)]` below if you change this. +pub const CLAIM_NAMESPACE: &str = "https://jwt.attic.rs/v1"; + +macro_rules! require_permission_function { + ($name:ident, $descr:literal, $member:ident) => { + pub fn $name(&self) -> Result<()> { + if !self.$member { + tracing::debug!("Client has no {} permission", $descr); + if self.can_discover() { + Err(Error::PermissionDenied) + } else { + Err(Error::NoDiscoveryPermission) + } + } else { + Ok(()) + } + } + }; +} + +/// A validated JSON Web Token. +#[derive(Debug)] +pub struct Token(jsonwebtoken::TokenData); + +/// Claims of a JSON Web Token. +#[derive(Debug, Serialize, Deserialize)] +struct TokenClaims { + /// Subject. + sub: String, + + /// Expiration timestamp. + exp: usize, + + /// Attic namespace. + #[serde(rename = "https://jwt.attic.rs/v1")] + attic_ns: AtticAccess, +} + +/// Permissions granted to a client. +/// +/// This is the content of the `attic-access` claim in JWTs. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct AtticAccess { + /// Cache permissions. + /// + /// Keys here may include wildcards. + caches: HashMap, +} + +/// Permission to a single cache. +#[serde_as] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CachePermission { + /// Can pull objects from the cache. + #[serde(default = "CachePermission::permission_default")] + #[serde(skip_serializing_if = "is_false")] + #[serde(rename = "r")] + #[serde_as(as = "BoolFromInt")] + pub pull: bool, + + /// Can push objects to the cache. + #[serde(default = "CachePermission::permission_default")] + #[serde(skip_serializing_if = "is_false")] + #[serde(rename = "w")] + #[serde_as(as = "BoolFromInt")] + pub push: bool, + + /// Can delete objects from the cache. + #[serde(default = "CachePermission::permission_default")] + #[serde(skip_serializing_if = "is_false")] + #[serde(rename = "d")] + #[serde_as(as = "BoolFromInt")] + pub delete: bool, + + /// Can create the cache itself. + #[serde(default = "CachePermission::permission_default")] + #[serde(skip_serializing_if = "is_false")] + #[serde(rename = "cc")] + #[serde_as(as = "BoolFromInt")] + pub create_cache: bool, + + /// Can reconfigure the cache. + #[serde(default = "CachePermission::permission_default")] + #[serde(skip_serializing_if = "is_false")] + #[serde(rename = "cr")] + #[serde_as(as = "BoolFromInt")] + pub configure_cache: bool, + + /// Can configure retention/quota settings. + #[serde(default = "CachePermission::permission_default")] + #[serde(skip_serializing_if = "is_false")] + #[serde(rename = "cq")] + #[serde_as(as = "BoolFromInt")] + pub configure_cache_retention: bool, + + /// Can destroy the cache itself. + #[serde(default = "CachePermission::permission_default")] + #[serde(skip_serializing_if = "is_false")] + #[serde(rename = "cd")] + #[serde_as(as = "BoolFromInt")] + pub destroy_cache: bool, +} + +pub type Result = std::result::Result; + +/// An access error. +#[derive(Debug, Display)] +#[ignore_extra_doc_attributes] +pub enum Error { + /// User has no permission to this cache. + NoDiscoveryPermission, + + /// User does not have permission to complete this action. + /// + /// This implies that there is some permission granted to the + /// user, so the user is authorized to discover the cache. + PermissionDenied, + + /// JWT error: {0} + TokenError(jsonwebtoken::errors::Error), +} + +impl Token { + /// Verifies and decodes a token. + pub fn from_jwt(token: &str, key: &JwtDecodingKey) -> Result { + let validation = JwtValidation::default(); + jsonwebtoken::decode::(token, key, &validation) + .map_err(|e| Error::TokenError(e)) + .map(Token) + } + + /// Creates a new token with an expiration timestamp. + pub fn new(sub: String, exp: &DateTime) -> Self { + let claims = TokenClaims { + sub, + exp: exp.timestamp() as usize, + attic_ns: Default::default(), + }; + + Self(jsonwebtoken::TokenData { + header: JwtHeader::new(JwtAlgorithm::HS256), + claims, + }) + } + + /// Encodes the token. + pub fn encode(&self, key: &JwtEncodingKey) -> Result { + jsonwebtoken::encode(&self.0.header, &self.0.claims, key) + .map_err(|e| Error::TokenError(e)) + } + + /// Returns the subject of the token. + pub fn sub(&self) -> &str { + self.0.claims.sub.as_str() + } + + /// Returns the claims as a serializable value. + pub fn opaque_claims(&self) -> &impl Serialize { + &self.0.claims + } + + /// Returns a mutable reference to a permission entry. + pub fn get_or_insert_permission_mut( + &mut self, + pattern: CacheNamePattern, + ) -> &mut CachePermission { + use std::collections::hash_map::Entry; + + let access = self.attic_access_mut(); + match access.caches.entry(pattern) { + Entry::Occupied(v) => v.into_mut(), + Entry::Vacant(v) => v.insert(CachePermission::default()), + } + } + + /// Returns explicit permission granted for a cache. + pub fn get_permission_for_cache(&self, cache: &CacheName) -> CachePermission { + let access = self.attic_access(); + + let pattern_key = cache.to_pattern(); + if let Some(direct_match) = access.caches.get(&pattern_key) { + return direct_match.clone(); + } + + for (pattern, permission) in access.caches.iter() { + if pattern.matches(cache) { + return permission.clone(); + } + } + + CachePermission::default() + } + + fn attic_access(&self) -> &AtticAccess { + &self.0.claims.attic_ns + } + + fn attic_access_mut(&mut self) -> &mut AtticAccess { + &mut self.0.claims.attic_ns + } +} + +impl CachePermission { + /// Adds implicit grants for public caches. + pub fn add_public_permissions(&mut self) { + self.pull = true; + } + + /// Returns whether the user is allowed to discover this cache. + /// + /// This permission is implied when any permission is explicitly + /// granted. + pub const fn can_discover(&self) -> bool { + self.push + || self.pull + || self.delete + || self.create_cache + || self.configure_cache + || self.destroy_cache + || self.configure_cache_retention + } + + pub fn require_discover(&self) -> Result<()> { + if !self.can_discover() { + Err(Error::NoDiscoveryPermission) + } else { + Ok(()) + } + } + + require_permission_function!(require_pull, "pull", pull); + require_permission_function!(require_push, "push", push); + require_permission_function!(require_delete, "delete", delete); + require_permission_function!(require_create_cache, "create cache", create_cache); + require_permission_function!( + require_configure_cache, + "reconfigure cache", + configure_cache + ); + require_permission_function!( + require_configure_cache_retention, + "configure cache retention", + configure_cache_retention + ); + require_permission_function!(require_destroy_cache, "destroy cache", destroy_cache); + + fn permission_default() -> bool { + false + } +} + +impl Default for CachePermission { + fn default() -> Self { + Self { + pull: false, + push: false, + delete: false, + create_cache: false, + configure_cache: false, + configure_cache_retention: false, + destroy_cache: false, + } + } +} + +impl StdError for Error {} + +// bruh +fn is_false(b: &bool) -> bool { + !b +} diff --git a/token/src/tests.rs b/token/src/tests.rs new file mode 100644 index 0000000..fc4b9c5 --- /dev/null +++ b/token/src/tests.rs @@ -0,0 +1,76 @@ +use super::*; + +use attic::cache::CacheName; + +macro_rules! cache { + ($n:expr) => { + CacheName::new($n.to_string()).unwrap() + }; +} + +#[test] +fn test_basic() { + // "very secure secret" + let base64_secret = "dmVyeSBzZWN1cmUgc2VjcmV0"; + + let dec_key = + JwtDecodingKey::from_base64_secret(base64_secret).expect("Could not import decoding key"); + + /* + { + "sub": "meow", + "exp": 4102324986, + "https://jwt.attic.rs/v1": { + "caches": { + "cache-rw": {"r":1,"w":1}, + "cache-ro": {"r":1}, + "team-*": {"r":1,"w":1,"cc":1} + } + } + } + */ + + let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJtZW93IiwiZXhwIjo0MTAyMzI0OTg2LCJodHRwczovL2p3dC5hdHRpYy5ycy92MSI6eyJjYWNoZXMiOnsiY2FjaGUtcnciOnsiciI6MSwidyI6MX0sImNhY2hlLXJvIjp7InIiOjF9LCJ0ZWFtLSoiOnsiciI6MSwidyI6MSwiY2MiOjF9fX19.UlsIM9bQHr9SXGAcSQcoVPo9No8Zhh6Y5xfX8vCmKmA"; + + let decoded = Token::from_jwt(token, &dec_key).unwrap(); + + let perm_rw = decoded.get_permission_for_cache(&cache! { "cache-rw" }); + + assert!(perm_rw.pull); + assert!(perm_rw.push); + assert!(!perm_rw.delete); + assert!(!perm_rw.create_cache); + + assert!(perm_rw.require_pull().is_ok()); + assert!(perm_rw.require_push().is_ok()); + assert!(perm_rw.require_delete().is_err()); + assert!(perm_rw.require_create_cache().is_err()); + + let perm_ro = decoded.get_permission_for_cache(&cache! { "cache-ro" }); + + assert!(perm_ro.pull); + assert!(!perm_ro.push); + assert!(!perm_ro.delete); + assert!(!perm_ro.create_cache); + + assert!(perm_ro.require_pull().is_ok()); + assert!(perm_ro.require_push().is_err()); + assert!(perm_ro.require_delete().is_err()); + assert!(perm_ro.require_create_cache().is_err()); + + let perm_team = decoded.get_permission_for_cache(&cache! { "team-xyz" }); + + assert!(perm_team.pull); + assert!(perm_team.push); + assert!(!perm_team.delete); + assert!(perm_team.create_cache); + + assert!(perm_team.require_pull().is_ok()); + assert!(perm_team.require_push().is_ok()); + assert!(perm_team.require_delete().is_err()); + assert!(perm_team.require_create_cache().is_ok()); + + assert!(!decoded + .get_permission_for_cache(&cache! { "forbidden-cache" }) + .can_discover()); +} diff --git a/token/src/util.rs b/token/src/util.rs new file mode 100644 index 0000000..c933347 --- /dev/null +++ b/token/src/util.rs @@ -0,0 +1,52 @@ +use std::str; + +use lazy_static::lazy_static; +use regex::Regex; + +lazy_static! { + static ref AUTHORIZATION_REGEX: Regex = + Regex::new(r"^(?i)((?Pbearer)|(?Pbasic))(?-i) (?P(.*))$").unwrap(); +} + +/// Extracts the JWT from an Authorization header. +pub fn parse_authorization_header(authorization: &str) -> Option { + let captures = AUTHORIZATION_REGEX.captures(authorization)?; + let rest = captures.name("rest").unwrap().as_str(); + + if captures.name("bearer").is_some() { + // Bearer token + Some(rest.to_string()) + } else { + // Basic auth + let bytes = base64::decode(rest).ok()?; + + let user_pass = str::from_utf8(&bytes).ok()?; + let colon = user_pass.find(':')?; + let pass = &user_pass[colon + 1..]; + + Some(pass.to_string()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_authorization_header() { + assert_eq!( + "somepass", + parse_authorization_header("Basic c29tZXVzZXI6c29tZXBhc3M=").unwrap(), + ); + + assert_eq!( + "somepass", + parse_authorization_header("baSIC c29tZXVzZXI6c29tZXBhc3M=").unwrap(), + ); + + assert_eq!( + "some-token", + parse_authorization_header("bearer some-token").unwrap(), + ); + } +}