use std::collections::{BTreeMap, BTreeSet};
use std::marker::PhantomData;
use super::filter::FilterExpr;
use super::surface::RoleGrant;
#[derive(Clone, Debug)]
pub struct ReadPermission {
pub(crate) columns: Option<BTreeSet<String>>,
pub(crate) all_columns: bool,
pub(crate) row_filter: Option<FilterExpr>,
pub(crate) limit: Option<u64>,
pub(crate) aggregations: bool,
}
pub fn read() -> ReadPermission {
ReadPermission {
columns: Some(BTreeSet::new()),
all_columns: false,
row_filter: None,
limit: None,
aggregations: false,
}
}
impl ReadPermission {
pub fn all_columns(mut self) -> Self {
self.all_columns = true;
self.columns = None;
self
}
pub fn columns<I: IntoIterator<Item = impl Into<String>>>(mut self, i: I) -> Self {
self.all_columns = false;
self.columns = Some(i.into_iter().map(Into::into).collect());
self
}
pub fn rows(mut self, predicate: FilterExpr) -> Self {
self.row_filter = Some(predicate);
self
}
pub fn limit(mut self, n: u64) -> Self {
self.limit = Some(n);
self
}
pub fn aggregations(mut self) -> Self {
self.aggregations = true;
self
}
#[cfg_attr(not(feature = "graphql"), allow(dead_code))]
pub(crate) fn allows_column(&self, name: &str) -> bool {
if self.all_columns {
return true;
}
self.columns
.as_ref()
.is_some_and(|cols| cols.contains(name))
}
#[allow(dead_code)]
pub(crate) fn allowed_columns_for<'a>(
&self,
schema_columns: impl Iterator<Item = &'a str>,
) -> BTreeSet<String> {
if self.all_columns {
schema_columns.map(str::to_string).collect()
} else {
self.columns.clone().unwrap_or_default()
}
}
pub fn to_role_grant(&self) -> RoleGrant {
role_grant_from_read_permission(self)
}
}
pub fn role_grant_from_read_permission(perm: &ReadPermission) -> RoleGrant {
let mut g = if perm.all_columns {
RoleGrant::all_columns()
} else {
RoleGrant::columns(perm.columns.clone().unwrap_or_default())
};
if perm.aggregations {
g = g.with_aggregations();
}
if let Some(predicate) = &perm.row_filter {
g = g.rows(predicate.clone());
}
if let Some(limit) = perm.limit {
g = g.limit(limit);
}
g
}
pub fn role_grants_from_model_role_perms<'a>(
role: &str,
entries: impl IntoIterator<Item = (&'a (String, String), &'a ReadPermission)>,
) -> BTreeMap<String, RoleGrant> {
let mut out = BTreeMap::new();
for ((model, r), perm) in entries {
if r == role {
out.insert(model.clone(), role_grant_from_read_permission(perm));
}
}
out
}
pub struct ModelPermissions<M> {
pub(crate) entries: Vec<(String, ReadPermission)>,
_marker: PhantomData<M>,
}
impl<M> Default for ModelPermissions<M> {
fn default() -> Self {
Self::new()
}
}
impl<M> ModelPermissions<M> {
pub fn new() -> Self {
Self {
entries: Vec::new(),
_marker: PhantomData,
}
}
pub fn grant(mut self, role: &str, perm: ReadPermission) -> Self {
self.entries.push((role.to_string(), perm));
self
}
pub fn surface_grants(&self) -> BTreeMap<String, RoleGrant> {
self.entries
.iter()
.map(|(role, permission)| (role.clone(), permission.to_role_grant()))
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a12_all_columns_maps() {
let g = role_grant_from_read_permission(&read().all_columns());
assert!(g.all_columns);
assert!(g.columns.is_empty());
assert!(!g.aggregations);
}
#[test]
fn a12_column_list_and_aggregations() {
let g =
role_grant_from_read_permission(&read().columns(["order_id", "status"]).aggregations());
assert!(!g.all_columns);
assert!(g.columns.contains("order_id"));
assert!(g.columns.contains("status"));
assert!(!g.columns.contains("meta"));
assert!(g.aggregations);
assert!(g.allows_column("order_id"));
assert!(!g.allows_column("meta"));
}
#[test]
fn a12_role_grants_from_model_role_perms_filters_role() {
let user = read().columns(["a"]);
let admin = read().all_columns().aggregations();
let key_u = ("M".to_string(), "user".to_string());
let key_a = ("M".to_string(), "admin".to_string());
let map = role_grants_from_model_role_perms("user", [(&key_u, &user), (&key_a, &admin)]);
assert_eq!(map.len(), 1);
assert!(map["M"].columns.contains("a"));
assert!(!map["M"].all_columns);
}
}