//! Who may do what in a team.
use slave_framework::database::Executor;
use crate::base::auth::AuthUser;
use crate::base::error::{AppError, AppResult};
use crate::modules::team::Membership;
/// Everything in the team, including its members and owners.
pub const OWNER: &str = "owner";
/// Everything in the team except managing owners.
pub const ADMIN: &str = "admin";
/// Only the modules granted to them.
pub const STAFF: &str = "staff";
/// The parts of a team that staff can be given. Add yours here; check them
/// with [`require_access`]. Managing members is for owners and admins only.
pub const MODULES: &[&str] = &["catalog", "orders", "customers", "theme", "settings"];
/// What the caller may do in one team.
#[derive(Debug, Clone)]
pub struct Access {
/// `owner`, `admin`, `staff`, or `super_admin` for the platform team
/// acting on a team they are not a member of.
pub role: String,
/// The modules they may use (all of them for every role but `staff`).
pub modules: Vec<String>,
}
impl Access {
pub fn can_use(&self, module: &str) -> bool {
self.modules.iter().any(|m| m == module)
}
/// Owners, admins and super admins manage members.
pub fn manages_members(&self) -> bool {
self.role != STAFF
}
/// Only owners and super admins touch owners.
pub fn manages_owners(&self) -> bool {
self.role == OWNER || self.role == crate::base::auth::SUPER_ADMIN_ROLE
}
}
/// The caller's access to team `team_id`, failing with `Forbidden` unless
/// they are a member (or a super admin) and, if `module` is given, may use it.
pub async fn require_access(
db: &impl Executor,
user: &AuthUser,
team_id: i64,
module: Option<&str>,
) -> AppResult<Access> {
let membership = find_membership(db, team_id, user.id).await?;
let access = match membership {
Some(membership) => access_of(&membership),
None if user.is_super_admin() => Access {
role: crate::base::auth::SUPER_ADMIN_ROLE.to_owned(),
modules: all_modules(),
},
None => return Err(AppError::Forbidden),
};
match module {
Some(module) if !access.can_use(module) => Err(AppError::Forbidden),
_ => Ok(access),
}
}
/// User `user_id`'s membership of team `team_id`, if any.
pub(crate) async fn find_membership(
db: &impl Executor,
team_id: i64,
user_id: i64,
) -> AppResult<Option<Membership>> {
let membership: Option<Membership> = db
.query(format!(
"SELECT {} FROM type::record('membership', [$team_id, $user_id])",
Membership::FIELDS
))
.bind(("team_id", team_id))
.bind(("user_id", user_id))
.await?
.take(0)?;
Ok(membership)
}
pub(crate) fn access_of(membership: &Membership) -> Access {
let modules = if membership.role == STAFF {
split_modules(&membership.modules)
} else {
all_modules()
};
Access {
role: membership.role.clone(),
modules,
}
}
pub(crate) fn split_modules(modules: &str) -> Vec<String> {
modules
.split(',')
.map(str::trim)
.filter(|m| !m.is_empty())
.map(str::to_owned)
.collect()
}
fn all_modules() -> Vec<String> {
MODULES.iter().map(|m| (*m).to_owned()).collect()
}
/// Checks a role name and, for staff, the module names.
pub(crate) fn validate_role(role: &str, modules: &[String]) -> AppResult<()> {
if ![OWNER, ADMIN, STAFF].contains(&role) {
return Err(AppError::Validation(format!(
"role must be `{OWNER}`, `{ADMIN}` or `{STAFF}`, not `{role}`"
)));
}
if let Some(unknown) = modules.iter().find(|m| !MODULES.contains(&m.as_str())) {
return Err(AppError::Validation(format!(
"unknown module `{unknown}`; choose from {}",
MODULES.join(", ")
)));
}
Ok(())
}