use crate::prelude::*;
use cloudillo_types::meta_adapter::FileView;
use cloudillo_types::types::{AccessLevel, TokenScope};
pub struct FileAccessResult {
pub file_view: FileView,
pub access_level: AccessLevel,
pub read_only: bool,
}
pub enum FileAccessError {
NotFound,
AccessDenied,
InternalError(String),
}
pub struct FileAccessCtx<'a> {
pub user_id_tag: &'a str,
pub tenant_id_tag: &'a str,
pub user_roles: &'a [Box<str>],
}
pub async fn walk_parent_chain_for_share(
app: &App,
tn_id: TnId,
file_id: &str,
user_id_tag: &str,
) -> Option<AccessLevel> {
let mut current_id = file_id.to_string();
let mut depth = 0;
while depth < 20 {
match app.meta_adapter.read_file(tn_id, ¤t_id).await {
Ok(Some(file)) => {
if let Some(ref parent_id) = file.parent_id {
if let Ok(Some(perm)) = app
.meta_adapter
.check_share_access(tn_id, 'F', parent_id, 'U', user_id_tag)
.await
{
return Some(AccessLevel::from_perm_char(perm));
}
current_id = parent_id.to_string();
depth += 1;
} else {
break;
}
}
_ => break,
}
}
None
}
pub async fn check_share_for_file(
app: &App,
tn_id: TnId,
file_id: &str,
user_id_tag: &str,
) -> Option<AccessLevel> {
if let Ok(Some(perm)) =
app.meta_adapter.check_share_access(tn_id, 'F', file_id, 'U', user_id_tag).await
{
return Some(AccessLevel::from_perm_char(perm));
}
walk_parent_chain_for_share(app, tn_id, file_id, user_id_tag).await
}
pub async fn get_access_level(
app: &App,
tn_id: TnId,
file_id: &str,
owner_id_tag: &str,
ctx: &FileAccessCtx<'_>,
inherited_share: Option<AccessLevel>,
) -> AccessLevel {
if ctx.user_id_tag == owner_id_tag {
return AccessLevel::Write;
}
if let Ok(Some(perm)) = app
.meta_adapter
.check_share_access(tn_id, 'F', file_id, 'U', ctx.user_id_tag)
.await
{
return AccessLevel::from_perm_char(perm);
}
if let Some(level) = inherited_share {
return level;
}
if let Some(level) = walk_parent_chain_for_share(app, tn_id, file_id, ctx.user_id_tag).await {
return level;
}
if owner_id_tag == ctx.tenant_id_tag && !ctx.user_roles.is_empty() {
if ctx
.user_roles
.iter()
.any(|r| matches!(r.as_ref(), "leader" | "moderator" | "contributor"))
{
return AccessLevel::Write;
}
return AccessLevel::Read;
}
let action_key = format!("FSHR:{}:{}", file_id, ctx.user_id_tag);
match app.meta_adapter.get_action_by_key(tn_id, &action_key).await {
Ok(Some(action)) => {
if action.typ.as_ref() == "FSHR" {
match action.sub_typ.as_ref().map(AsRef::as_ref) {
Some("WRITE") => AccessLevel::Write,
Some("COMMENT") => AccessLevel::Comment,
_ => AccessLevel::Read,
}
} else {
AccessLevel::None
}
}
Ok(None) | Err(_) => AccessLevel::None,
}
}
pub async fn get_access_level_with_scope(
app: &App,
tn_id: TnId,
file_id: &str,
owner_id_tag: &str,
ctx: &FileAccessCtx<'_>,
scope: Option<&str>,
root_id: Option<&str>,
) -> AccessLevel {
if let Some(scope_str) = scope {
if let Some(token_scope) = TokenScope::parse(scope_str) {
match &token_scope {
TokenScope::File { file_id: scope_file_id, access } => {
if scope_file_id == file_id {
return *access;
}
if let Some(root) = root_id
&& scope_file_id.as_str() == root
{
return *access;
}
if let Ok(Some(perm)) = app
.meta_adapter
.check_share_access(tn_id, 'F', scope_file_id, 'F', file_id)
.await
{
return (*access).min(AccessLevel::from_perm_char(perm));
}
return AccessLevel::None;
}
TokenScope::ApkgPublish => {
return AccessLevel::None;
}
}
}
return AccessLevel::None;
}
get_access_level(app, tn_id, file_id, owner_id_tag, ctx, None).await
}
pub async fn check_file_access_with_scope(
app: &App,
tn_id: TnId,
file_id: &str,
ctx: &FileAccessCtx<'_>,
scope: Option<&str>,
via: Option<&str>,
) -> Result<FileAccessResult, FileAccessError> {
use tracing::debug;
let file_view = match app.meta_adapter.read_file(tn_id, file_id).await {
Ok(Some(f)) => f,
Ok(None) => return Err(FileAccessError::NotFound),
Err(e) => return Err(FileAccessError::InternalError(e.to_string())),
};
let owner_id_tag = file_view
.owner
.as_ref()
.and_then(|p| if p.id_tag.is_empty() { None } else { Some(p.id_tag.as_ref()) })
.unwrap_or(ctx.tenant_id_tag);
debug!(file_id = file_id, user = ctx.user_id_tag, owner = owner_id_tag, scope = ?scope, "Checking file access");
let mut access_level = get_access_level_with_scope(
app,
tn_id,
file_id,
owner_id_tag,
ctx,
scope,
file_view.root_id.as_deref(),
)
.await;
if access_level == AccessLevel::None && file_view.visibility == Some('P') {
access_level = AccessLevel::Read;
}
if let Some(via_file_id) = via
&& scope.is_none()
&& access_level != AccessLevel::None
{
match app.meta_adapter.check_share_access(tn_id, 'F', via_file_id, 'F', file_id).await {
Ok(Some(perm)) => {
access_level = access_level.min(AccessLevel::from_perm_char(perm));
}
Ok(None) | Err(_) => {
access_level = AccessLevel::None;
}
}
}
if access_level == AccessLevel::None {
return Err(FileAccessError::AccessDenied);
}
let read_only = access_level != AccessLevel::Write && access_level != AccessLevel::Admin;
Ok(FileAccessResult { file_view, access_level, read_only })
}
pub enum ScopeCheck {
NoScope,
Allowed(AccessLevel),
Denied,
}
pub fn check_scope_allows_file(
scope: Option<&str>,
file_id: &str,
root_id: Option<&str>,
) -> ScopeCheck {
let Some(scope_str) = scope else { return ScopeCheck::NoScope };
let Some(token_scope) = TokenScope::parse(scope_str) else { return ScopeCheck::Denied };
match &token_scope {
TokenScope::File { file_id: scope_file_id, access } => {
if scope_file_id == file_id {
return ScopeCheck::Allowed(*access);
}
if let Some(root) = root_id
&& scope_file_id.as_str() == root
{
return ScopeCheck::Allowed(*access);
}
ScopeCheck::Denied
}
TokenScope::ApkgPublish => ScopeCheck::Denied,
}
}
pub fn check_scope_allows_create(scope: Option<&str>, root_id: Option<&str>) -> Result<(), Error> {
let Some(scope_str) = scope else { return Ok(()) };
let Some(token_scope) = TokenScope::parse(scope_str) else {
return Err(Error::PermissionDenied);
};
match &token_scope {
TokenScope::File { file_id: scope_file_id, access } => {
if *access != AccessLevel::Write {
return Err(Error::PermissionDenied);
}
match root_id {
Some(root) if root == scope_file_id => Ok(()),
_ => Err(Error::PermissionDenied),
}
}
TokenScope::ApkgPublish => Ok(()), }
}