1
0
Fork 0
mirror of https://github.com/zhaofengli/attic.git synced 2024-12-14 11:57:30 +00:00

Refactor token into a separate crate

This commit is contained in:
Zhaofeng Li 2023-01-08 00:57:22 -07:00
parent c89f5f0f3f
commit 77070b9895
9 changed files with 531 additions and 401 deletions

18
Cargo.lock generated
View file

@ -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"

View file

@ -4,4 +4,5 @@ members = [
"attic",
"client",
"server",
"token",
]

View file

@ -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"

View file

@ -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)((?P<bearer>bearer)|(?P<basic>basic))(?-i) (?P<rest>(.*))$").unwrap();
}
use attic_token::util::parse_authorization_header;
/// Auth state.
#[derive(Debug)]
@ -124,46 +116,3 @@ pub async fn apply_auth<B>(req: Request<B>, next: Next<B>) -> Response {
next.run(req).await
}
/// Extracts the JWT from an Authorization header.
fn parse_authorization_header(authorization: &str) -> Option<String> {
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(),
);
}
}

View file

@ -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.
///
/// <https://auth0.com/docs/security/tokens/json-web-tokens/create-namespaced-custom-claims>
///
/// 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<TokenClaims>);
/// 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<CacheNamePattern, CachePermission>,
}
/// 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<Self> {
let validation = JwtValidation::default();
jsonwebtoken::decode::<TokenClaims>(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<Utc>) -> 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<String> {
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::*;

19
token/Cargo.toml Normal file
View file

@ -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"

362
token/src/lib.rs Normal file
View file

@ -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.
///
/// <https://auth0.com/docs/security/tokens/json-web-tokens/create-namespaced-custom-claims>
///
/// 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<TokenClaims>);
/// 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<CacheNamePattern, CachePermission>,
}
/// 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<T> = std::result::Result<T, Error>;
/// 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<Self> {
let validation = JwtValidation::default();
jsonwebtoken::decode::<TokenClaims>(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<Utc>) -> 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<String> {
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
}

76
token/src/tests.rs Normal file
View file

@ -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());
}

52
token/src/util.rs Normal file
View file

@ -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)((?P<bearer>bearer)|(?P<basic>basic))(?-i) (?P<rest>(.*))$").unwrap();
}
/// Extracts the JWT from an Authorization header.
pub fn parse_authorization_header(authorization: &str) -> Option<String> {
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(),
);
}
}