use reduct_base::error::ReductError;
use reduct_base::forbidden;
use reduct_base::msg::token_api::Token;
pub trait Policy {
fn validate(&self, token: Result<Token, ReductError>) -> Result<(), ReductError>;
}
pub struct AnonymousPolicy {}
impl Policy for AnonymousPolicy {
fn validate(&self, _: Result<Token, ReductError>) -> Result<(), ReductError> {
Ok(())
}
}
pub struct AuthenticatedPolicy {}
impl Policy for AuthenticatedPolicy {
fn validate(&self, token: Result<Token, ReductError>) -> Result<(), ReductError> {
token?;
Ok(())
}
}
pub struct FullAccessPolicy {}
impl Policy for FullAccessPolicy {
fn validate(&self, token: Result<Token, ReductError>) -> Result<(), ReductError> {
let token = token?;
if token.permissions.unwrap_or_default().full_access {
Ok(())
} else {
Err(forbidden!(
"Token '{}' doesn't have full access",
token.name
))
}
}
}
pub struct ReadAccessPolicy<'a> {
pub(crate) bucket: &'a str,
}
impl Policy for ReadAccessPolicy<'_> {
fn validate(&self, token: Result<Token, ReductError>) -> Result<(), ReductError> {
let token = token?;
let permissions = &token.permissions.unwrap_or_default();
if permissions.full_access {
return Ok(());
}
if check_bucket_permissions(&permissions.read, self.bucket) {
return Ok(());
}
Err(forbidden!(
"Token '{}' doesn't have read access to bucket '{}'",
token.name,
self.bucket
))
}
}
pub struct WriteAccessPolicy<'a> {
pub(crate) bucket: &'a str,
}
impl Policy for WriteAccessPolicy<'_> {
fn validate(&self, token: Result<Token, ReductError>) -> Result<(), ReductError> {
let token = token?;
let permissions = &token.permissions.unwrap_or_default();
if permissions.full_access {
return Ok(());
}
if check_bucket_permissions(&permissions.write, self.bucket) {
return Ok(());
}
Err(forbidden!(
"Token '{}' doesn't have write access to bucket '{}'",
token.name,
self.bucket
))
}
}
fn check_bucket_permissions(token_list: &Vec<String>, bucket: &str) -> bool {
for token_bucket in token_list {
if token_bucket == bucket {
return true;
}
if bucket.starts_with('$') {
continue;
}
let wildcard_bucket = token_bucket.ends_with('*');
if wildcard_bucket && bucket.starts_with(&token_bucket[..token_bucket.len() - 1]) {
return true;
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use reduct_base::msg::token_api::Permissions;
#[test]
fn test_anonymous_policy() {
let policy = AnonymousPolicy {};
assert!(policy.validate(Ok(Token::default())).is_ok());
assert!(policy.validate(Err(forbidden!("Invalid token"))).is_ok());
}
#[test]
fn test_authenticated_policy() {
let policy = AuthenticatedPolicy {};
assert!(policy.validate(Ok(Token::default())).is_ok());
assert_eq!(
policy.validate(Err(forbidden!("Invalid token"))),
Err(forbidden!("Invalid token"))
);
}
#[test]
fn test_full_access_policy() {
let policy = FullAccessPolicy {};
let mut token = Token {
name: "test_token".to_string(),
permissions: Some(Permissions {
full_access: true,
read: vec![],
write: vec![],
}),
..Default::default()
};
assert!(policy.validate(Ok(token.clone())).is_ok());
token.permissions.as_mut().unwrap().full_access = false;
assert_eq!(
policy.validate(Ok(token)),
Err(forbidden!("Token 'test_token' doesn't have full access"))
);
assert_eq!(
policy.validate(Err(forbidden!("Invalid token"))),
Err(forbidden!("Invalid token"))
);
}
#[test]
fn test_read_access_policy() {
let policy = ReadAccessPolicy { bucket: "bucket" };
let mut token = Token {
name: "test_token".to_string(),
permissions: Some(Permissions {
full_access: true,
read: vec![],
write: vec![],
}),
..Default::default()
};
assert!(policy.validate(Ok(token.clone())).is_ok());
token.permissions.as_mut().unwrap().full_access = false;
token.permissions.as_mut().unwrap().read = vec!["bucket".to_string()];
assert!(policy.validate(Ok(token.clone())).is_ok());
token.permissions.as_mut().unwrap().read = vec!["bucket2".to_string()];
assert_eq!(
policy.validate(Ok(token.clone())),
Err(forbidden!(
"Token 'test_token' doesn't have read access to bucket 'bucket'"
))
);
token.permissions.as_mut().unwrap().read = vec!["bucket*".to_string()];
assert!(policy.validate(Ok(token.clone())).is_ok());
token.permissions.as_mut().unwrap().read = vec!["*".to_string()];
assert!(policy.validate(Ok(token)).is_ok());
assert_eq!(
policy.validate(Err(forbidden!("Invalid token"))),
Err(forbidden!("Invalid token"))
);
}
#[test]
fn test_read_access_policy_excludes_audit_bucket_from_wildcard() {
let policy = ReadAccessPolicy { bucket: "$audit" };
let token = Token {
name: "test_token".to_string(),
permissions: Some(Permissions {
full_access: false,
read: vec!["*".to_string()],
write: vec![],
}),
..Default::default()
};
assert_eq!(
policy.validate(Ok(token)),
Err(forbidden!(
"Token 'test_token' doesn't have read access to bucket '$audit'"
))
);
}
#[test]
fn test_read_access_policy_allows_explicit_audit_bucket_permission() {
let policy = ReadAccessPolicy { bucket: "$audit" };
let token = Token {
name: "test_token".to_string(),
permissions: Some(Permissions {
full_access: false,
read: vec!["$audit".to_string()],
write: vec![],
}),
..Default::default()
};
assert!(policy.validate(Ok(token)).is_ok());
}
#[test]
fn test_read_access_policy_excludes_any_system_bucket_from_wildcard() {
let policy = ReadAccessPolicy { bucket: "$system" };
let token = Token {
name: "test_token".to_string(),
permissions: Some(Permissions {
full_access: false,
read: vec!["*".to_string()],
write: vec![],
}),
..Default::default()
};
assert_eq!(
policy.validate(Ok(token)),
Err(forbidden!(
"Token 'test_token' doesn't have read access to bucket '$system'"
))
);
}
#[test]
fn test_write_access_policy() {
let policy = WriteAccessPolicy { bucket: "bucket" };
let mut token = Token {
name: "test_token".to_string(),
permissions: Some(Permissions {
full_access: true,
read: vec![],
write: vec![],
}),
..Default::default()
};
assert!(policy.validate(Ok(token.clone())).is_ok());
token.permissions.as_mut().unwrap().full_access = false;
token.permissions.as_mut().unwrap().write = vec!["bucket".to_string()];
assert!(policy.validate(Ok(token.clone())).is_ok());
token.permissions.as_mut().unwrap().write = vec!["bucket2".to_string()];
assert_eq!(
policy.validate(Ok(token.clone())),
Err(forbidden!(
"Token 'test_token' doesn't have write access to bucket 'bucket'"
))
);
token.permissions.as_mut().unwrap().write = vec!["bucket*".to_string()];
assert!(policy.validate(Ok(token.clone())).is_ok());
token.permissions.as_mut().unwrap().write = vec!["*".to_string()];
assert!(policy.validate(Ok(token)).is_ok());
assert_eq!(
policy.validate(Err(forbidden!("Invalid token"))),
Err(forbidden!("Invalid token"))
);
}
#[test]
fn test_write_access_policy_excludes_audit_bucket_from_wildcard() {
let policy = WriteAccessPolicy { bucket: "$audit" };
let token = Token {
name: "test_token".to_string(),
permissions: Some(Permissions {
full_access: false,
read: vec![],
write: vec!["*".to_string()],
}),
..Default::default()
};
assert_eq!(
policy.validate(Ok(token)),
Err(forbidden!(
"Token 'test_token' doesn't have write access to bucket '$audit'"
))
);
}
#[test]
fn test_write_access_policy_allows_explicit_audit_bucket_permission() {
let policy = WriteAccessPolicy { bucket: "$audit" };
let token = Token {
name: "test_token".to_string(),
permissions: Some(Permissions {
full_access: false,
read: vec![],
write: vec!["$audit".to_string()],
}),
..Default::default()
};
assert!(policy.validate(Ok(token)).is_ok());
}
#[test]
fn test_write_access_policy_excludes_any_system_bucket_from_wildcard() {
let policy = WriteAccessPolicy { bucket: "$system" };
let token = Token {
name: "test_token".to_string(),
permissions: Some(Permissions {
full_access: false,
read: vec![],
write: vec!["*".to_string()],
}),
..Default::default()
};
assert_eq!(
policy.validate(Ok(token)),
Err(forbidden!(
"Token 'test_token' doesn't have write access to bucket '$system'"
))
);
}
}