use crate::wildcard;
use crate::{Effect, Policy, AuthRequest, Result};
use uuid::Uuid;
pub trait PolicyManager {
fn create(&mut self, policy: Policy) -> Result<Uuid>;
fn update(&mut self, policy: &Policy) -> Result<()>;
fn delete(&mut self, id: &Uuid) -> Result<()>;
fn get(&self, id: &Uuid) -> Result<&Policy>;
fn list(&self) -> Result<Vec<Policy>>;
fn attach(&mut self, principal: &str, id: &Uuid) -> Result<()>;
fn detach(&mut self, principal: &str, id: &Uuid) -> Result<()>;
fn get_policies_for_principal(&self, principal: &str) -> Result<Option<Vec<Policy>>>;
}
pub struct Engine<T: PolicyManager> {
pub manager: T,
}
impl<T: PolicyManager> Engine<T> {
pub fn new(manager: T) -> Self {
Engine { manager: manager }
}
pub fn is_allowed(&mut self, req: &AuthRequest) -> Result<bool> {
let policies = self.manager.get_policies_for_principal(&req.principal)?;
if policies.is_none() {
return Ok(false);
}
let policies = policies.unwrap();
let mut allowed = false;
for p in policies.iter() {
for stmt in p.statements.iter() {
if !stmt
.actions
.iter()
.any(|action| wildcard::matches(action, &req.action))
{
continue;
}
if !stmt
.resources
.iter()
.any(|resource| wildcard::matches(resource, &req.resource))
{
continue;
}
match stmt.effect {
Effect::Allow => {
allowed = true;
}
Effect::Deny => {
return Ok(false);
}
}
}
}
Ok(allowed)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::managers::MemoryManager;
use test::Bencher;
#[test]
fn test_engine_is_allowed() {
let mut engine = Engine::new(MemoryManager::new());
let jsp = r#"
{
"name": "Account policy",
"statements": [
{
"sid": "Grant account list access",
"effect": "allow",
"actions": ["account:list"],
"resources": ["resource:account:*"]
},
{
"sid": "Deny root account access",
"effect": "deny",
"actions": ["account:list"],
"resources": ["resource:account:123"]
},
{
"sid": "Grant all read access on specific account",
"effect": "allow",
"actions": ["account:describe:*"],
"resources": ["resource:account:789"]
}
]
}
"#;
let policy: Policy = serde_json::from_str(jsp).unwrap();
let id = engine.manager.create(policy).unwrap();
let principal = "user:test-user";
engine.manager.attach(principal, &id).unwrap();
#[rustfmt::skip]
let cases = vec![
( "user:test-user", "account:list", "resource:account:567", true,), ( "user:test-user", "account:list", "resource:account:789", true,), ( "user:test-user-2", "account:list", "resource:account:789", false,), ( "user:test-user", "account:list", "resource:account:123", false,), ( "user:test-user", "account:describe:limits", "resource:account:123", false,), ( "user:test-user", "account:describe:limits", "resource:account:789", true,), ];
for x in cases {
let (principal, action, resource, expected) = x;
let req = AuthRequest {
principal: principal.to_string(),
action: action.to_string(),
resource: resource.to_string(),
};
let actual = engine.is_allowed(&req).unwrap();
assert_eq!(expected, actual, "req: {:?}", req);
}
}
#[bench]
fn bench_is_allowed(b: &mut Bencher) {
let mut engine = Engine::new(MemoryManager::new());
let jsp = r#"
{
"name": "Account policy",
"statements": [
{
"sid": "Grant account list access",
"effect": "allow",
"actions": ["account:list"],
"resources": ["resource:account:*"]
},
{
"sid": "Deny root account access",
"effect": "deny",
"actions": ["account:list"],
"resources": ["resource:account:123"]
},
{
"sid": "Grant all read access on specific account",
"effect": "allow",
"actions": ["account:describe:*"],
"resources": ["resource:account:789"]
}
]
}
"#;
let policy: Policy = serde_json::from_str(jsp).unwrap();
let id = engine.manager.create(policy).unwrap();
let principal = "user:test-user";
engine.manager.attach(principal, &id).unwrap();
let (principal, action, resource) = ( "user:test-user", "account:describe:limits", "resource:account:789",); let req = AuthRequest {
principal: principal.to_string(),
action: action.to_string(),
resource: resource.to_string(),
};
b.iter(|| engine.is_allowed(&req));
}
}