use indexmap::IndexMap;
pub type Statements = IndexMap<String, Vec<String>>;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum Connector {
#[default]
And,
Or,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ActionRequest {
Actions(Vec<String>),
Rule {
actions: Vec<String>,
connector: Connector,
},
}
impl From<Vec<String>> for ActionRequest {
fn from(actions: Vec<String>) -> Self {
Self::Actions(actions)
}
}
pub type AuthorizeRequest = IndexMap<String, ActionRequest>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AuthorizeResponse {
Success,
Denied(String),
}
impl AuthorizeResponse {
#[must_use]
pub fn success(&self) -> bool {
matches!(self, Self::Success)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Role {
pub statements: Statements,
}
#[must_use]
pub fn role(statements: Statements) -> Role {
Role { statements }
}
impl Role {
#[must_use]
pub fn authorize(&self, request: &AuthorizeRequest) -> AuthorizeResponse {
self.authorize_with_connector(request, Connector::And)
}
#[must_use]
pub fn authorize_with_connector(
&self,
request: &AuthorizeRequest,
connector: Connector,
) -> AuthorizeResponse {
let mut entries: Vec<_> = request.iter().collect();
entries.sort_by_key(|(resource, _)| {
resource
.parse::<u32>()
.ok()
.filter(|index| *index != u32::MAX && index.to_string() == resource.as_str())
.unwrap_or(u32::MAX)
});
authorize(
|resource| self.statements.get(resource).map(Vec::as_slice),
entries.into_iter().map(|(resource, rule)| {
let (actions, connector) = match rule {
ActionRequest::Actions(actions) => (actions, Connector::And),
ActionRequest::Rule { actions, connector } => (actions, *connector),
};
(resource.as_str(), actions.as_slice(), connector)
}),
connector,
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AccessControl {
pub statements: Statements,
}
#[must_use]
pub fn create_access_control(statements: Statements) -> AccessControl {
AccessControl { statements }
}
impl AccessControl {
#[must_use]
pub fn new_role(&self, statements: Statements) -> Role {
role(statements)
}
}
pub(super) fn authorize<'a, 'b>(
grants: impl Fn(&str) -> Option<&'b [String]>,
request: impl IntoIterator<Item = (&'a str, &'a [String], Connector)>,
connector: Connector,
) -> AuthorizeResponse {
let mut authorized = false;
for (resource, actions, action_connector) in request {
let Some(allowed) = grants(resource) else {
if connector == Connector::And {
return AuthorizeResponse::Denied(format!(
"You are not allowed to access resource: {resource}"
));
}
continue;
};
let granted = !actions.is_empty()
&& match action_connector {
Connector::And => actions.iter().all(|action| allowed.contains(action)),
Connector::Or => actions.iter().any(|action| allowed.contains(action)),
};
if granted {
authorized = true;
if connector == Connector::Or {
return AuthorizeResponse::Success;
}
} else if connector == Connector::And {
return AuthorizeResponse::Denied(format!(
"unauthorized to access resource \"{resource}\""
));
}
}
if authorized {
AuthorizeResponse::Success
} else {
AuthorizeResponse::Denied("Not authorized".into())
}
}