use crate::prelude::*;
use cloudillo_types::auth_adapter::AuthCtx;
use cloudillo_types::types::AccessLevel;
use crate::file_access::{self, FileAccessCtx, FileAccessResult};
pub async fn require_unscoped_file_access(
app: &App,
tn_id: TnId,
file_id: &str,
auth: &AuthCtx,
tenant_id_tag: &str,
) -> ClResult<FileAccessResult> {
if auth.scope.is_some() {
warn!("Scoped token attempted to access share entries");
return Err(Error::PermissionDenied);
}
let ctx = FileAccessCtx { user_id_tag: &auth.id_tag, tenant_id_tag, user_roles: &auth.roles };
match file_access::check_file_access_with_scope(app, tn_id, file_id, &ctx, None, None).await {
Err(file_access::FileAccessError::NotFound) => Err(Error::NotFound),
Err(file_access::FileAccessError::AccessDenied) => Err(Error::PermissionDenied),
Err(file_access::FileAccessError::InternalError(msg)) => Err(Error::Internal(msg)),
Ok(access) => Ok(access),
}
}
fn is_share_manager(
access: AccessLevel,
subject: &str,
tenant_id_tag: &str,
owner_id_tag: Option<&str>,
creator_id_tag: Option<&str>,
leader_over_tenant_row: bool,
) -> bool {
if access == AccessLevel::None {
return false;
}
if leader_over_tenant_row || access.can_manage_shares() || owner_id_tag == Some(subject) {
return true;
}
let tenant_owned = owner_id_tag == Some(tenant_id_tag);
tenant_owned && creator_id_tag == Some(subject)
}
fn is_share_reader(access: AccessLevel) -> bool {
access.can_write()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ShareStanding {
None,
Reader,
Manager,
}
fn classify_standing(
access: AccessLevel,
subject: &str,
tenant_id_tag: &str,
owner_id_tag: Option<&str>,
creator_id_tag: Option<&str>,
leader_over_tenant_row: bool,
) -> ShareStanding {
if is_share_manager(
access,
subject,
tenant_id_tag,
owner_id_tag,
creator_id_tag,
leader_over_tenant_row,
) {
ShareStanding::Manager
} else if is_share_reader(access) {
ShareStanding::Reader
} else {
ShareStanding::None
}
}
fn grant_ceiling(access: AccessLevel, is_owner: bool, leader_over_tenant_row: bool) -> AccessLevel {
if leader_over_tenant_row || is_owner { AccessLevel::Admin } else { access }
}
pub struct ShareAuthority {
pub access: FileAccessResult,
pub standing: ShareStanding,
pub grant_ceiling: AccessLevel,
}
pub async fn share_standing(
app: &App,
tn_id: TnId,
file_id: &str,
auth: &AuthCtx,
tenant_id_tag: &str,
) -> ClResult<ShareAuthority> {
let access = require_unscoped_file_access(app, tn_id, file_id, auth, tenant_id_tag).await?;
let owner_id_tag =
effective_owner(access.file_view.owner.as_ref().map(|p| p.id_tag.as_ref()), tenant_id_tag);
let creator_id_tag =
non_empty_id_tag(access.file_view.creator.as_ref().map(|p| p.id_tag.as_ref()));
let leader_over_tenant_row =
crate::roles::is_leader(&auth.roles) && owner_id_tag == tenant_id_tag;
let standing = classify_standing(
access.access_level,
&auth.id_tag,
tenant_id_tag,
Some(owner_id_tag),
creator_id_tag,
leader_over_tenant_row,
);
let ceiling =
grant_ceiling(access.access_level, owner_id_tag == &*auth.id_tag, leader_over_tenant_row);
Ok(ShareAuthority { access, standing, grant_ceiling: ceiling })
}
pub async fn share_standing_for_actor(
app: &App,
tn_id: TnId,
file_id: &str,
actor_id_tag: &str,
tenant_id_tag: &str,
) -> ClResult<ShareAuthority> {
let roles: Box<[Box<str>]> = if actor_id_tag == tenant_id_tag {
crate::roles::parse_roles(&crate::roles::expand_roles_preserving_extras(&["leader".into()]))
} else {
match app.meta_adapter.read_profile_roles(tn_id, actor_id_tag).await? {
Some(highest) => {
crate::roles::parse_roles(&crate::roles::expand_roles_preserving_extras(&highest))
}
None => Box::new([]),
}
};
let auth = AuthCtx { tn_id, id_tag: actor_id_tag.into(), roles, scope: None, anonymous: false };
share_standing(app, tn_id, file_id, &auth, tenant_id_tag).await
}
pub fn ensure_grant_within(
granted: AccessLevel,
own: AccessLevel,
subject: &str,
file_id: &str,
) -> ClResult<()> {
if granted <= own {
return Ok(());
}
warn!(
subject = %subject,
file_id = %file_id,
granted = %granted.as_str(),
own = %own.as_str(),
"Share grant denied - a manager may not hand out more access than they hold"
);
Err(Error::PermissionDenied)
}
pub fn ensure_standing(
standing: ShareStanding,
min: ShareStanding,
subject: &str,
file_id: &str,
) -> ClResult<()> {
if standing >= min {
return Ok(());
}
let reason = match min {
ShareStanding::Manager => "Share management denied - owner/creator/leader/admin required",
ShareStanding::Reader => "Share listing denied - write access or share management required",
ShareStanding::None => "Share access denied - no minimum standing was required",
};
warn!(subject = %subject, file_id = %file_id, "{}", reason);
Err(Error::PermissionDenied)
}
pub async fn require_share_manager(
app: &App,
tn_id: TnId,
file_id: &str,
auth: &AuthCtx,
tenant_id_tag: &str,
) -> ClResult<ShareAuthority> {
let authority = share_standing(app, tn_id, file_id, auth, tenant_id_tag).await?;
ensure_standing(authority.standing, ShareStanding::Manager, &auth.id_tag, file_id)?;
Ok(authority)
}
pub async fn require_share_reader(
app: &App,
tn_id: TnId,
file_id: &str,
auth: &AuthCtx,
tenant_id_tag: &str,
) -> ClResult<ShareAuthority> {
let authority = share_standing(app, tn_id, file_id, auth, tenant_id_tag).await?;
ensure_standing(authority.standing, ShareStanding::Reader, &auth.id_tag, file_id)?;
Ok(authority)
}
fn non_empty_id_tag(id_tag: Option<&str>) -> Option<&str> {
id_tag.filter(|s| !s.is_empty())
}
fn effective_owner<'a>(owner: Option<&'a str>, tenant_id_tag: &'a str) -> &'a str {
non_empty_id_tag(owner).unwrap_or(tenant_id_tag)
}
#[cfg(test)]
mod tests {
use super::*;
const TENANT: &str = "community.example.com";
const MEMBER: &str = "alice.example.com";
const OTHER: &str = "bob.example.com";
const W: AccessLevel = AccessLevel::Write;
const A: AccessLevel = AccessLevel::Admin;
#[test]
fn personal_tenant_owner_manages_shares() {
assert!(is_share_manager(W, TENANT, TENANT, Some(TENANT), Some(TENANT), false));
}
#[test]
fn creator_of_tenant_owned_file_manages_shares() {
assert!(is_share_manager(W, MEMBER, TENANT, Some(TENANT), Some(MEMBER), false));
}
#[test]
fn non_creator_member_cannot_manage_shares() {
assert!(!is_share_manager(W, MEMBER, TENANT, Some(TENANT), Some(OTHER), false));
}
#[test]
fn leader_manages_shares() {
assert!(is_share_manager(W, MEMBER, TENANT, Some(TENANT), Some(OTHER), true));
}
#[test]
fn leader_does_not_reach_a_foreign_owned_row() {
for access in [AccessLevel::Read, W] {
assert!(!is_share_manager(access, MEMBER, TENANT, Some(OTHER), Some(OTHER), false));
assert_eq!(grant_ceiling(access, false, false), access);
}
assert!(is_share_manager(A, MEMBER, TENANT, Some(OTHER), Some(OTHER), false));
}
#[test]
fn leader_over_a_tenant_owned_row_keeps_manager_standing_and_an_admin_ceiling() {
assert_eq!(
classify_standing(AccessLevel::Read, MEMBER, TENANT, Some(TENANT), Some(OTHER), true),
ShareStanding::Manager
);
assert_eq!(grant_ceiling(AccessLevel::Read, false, true), AccessLevel::Admin);
}
#[test]
fn creator_of_placed_foreign_file_cannot_manage_shares() {
assert!(!is_share_manager(W, MEMBER, TENANT, Some(OTHER), Some(MEMBER), false));
}
#[test]
fn explicit_admin_grant_manages_shares() {
assert!(!is_share_manager(W, OTHER, TENANT, Some(TENANT), Some(MEMBER), false));
assert!(is_share_manager(A, OTHER, TENANT, Some(TENANT), Some(MEMBER), false));
}
#[test]
fn plain_write_grantee_cannot_manage_shares() {
assert!(!is_share_manager(W, OTHER, TENANT, Some(MEMBER), Some(MEMBER), false));
}
#[test]
fn missing_owner_and_creator_deny() {
assert!(!is_share_manager(W, MEMBER, TENANT, None, None, false));
}
#[test]
fn no_access_denies_before_any_standing_rule() {
assert!(!is_share_manager(
AccessLevel::None,
MEMBER,
TENANT,
Some(TENANT),
Some(MEMBER),
true
));
}
#[test]
fn read_access_creator_is_still_a_share_manager() {
assert!(is_share_manager(
AccessLevel::Read,
MEMBER,
TENANT,
Some(TENANT),
Some(MEMBER),
false
));
}
#[test]
fn manager_standing_implies_reader() {
for (access, subject, owner, creator, leader) in [
(W, TENANT, Some(TENANT), Some(TENANT), false),
(W, MEMBER, Some(TENANT), Some(MEMBER), false),
(W, MEMBER, Some(TENANT), Some(OTHER), true),
(A, OTHER, Some(TENANT), Some(MEMBER), false),
(AccessLevel::Read, MEMBER, Some(TENANT), Some(MEMBER), false),
] {
assert!(is_share_manager(access, subject, TENANT, owner, creator, leader));
let standing = classify_standing(access, subject, TENANT, owner, creator, leader);
assert_eq!(standing, ShareStanding::Manager);
assert!(standing >= ShareStanding::Reader);
}
let grantee = classify_standing(W, OTHER, TENANT, Some(MEMBER), Some(MEMBER), false);
assert_eq!(grantee, ShareStanding::Reader);
let outsider =
classify_standing(AccessLevel::Read, OTHER, TENANT, Some(MEMBER), Some(MEMBER), false);
assert_eq!(outsider, ShareStanding::None);
}
#[test]
fn admin_access_alone_confers_manager_standing_and_an_admin_ceiling() {
let standing = classify_standing(A, OTHER, TENANT, Some(MEMBER), Some(MEMBER), false);
assert_eq!(standing, ShareStanding::Manager);
assert_eq!(grant_ceiling(A, false, false), AccessLevel::Admin);
}
#[test]
fn share_reader_needs_write_access() {
assert!(is_share_reader(AccessLevel::Write));
assert!(is_share_reader(AccessLevel::Admin));
assert!(!is_share_reader(AccessLevel::Read));
assert!(!is_share_reader(AccessLevel::Comment));
}
#[test]
fn ensure_standing_enforces_the_minimum() {
let ok = |standing, min| ensure_standing(standing, min, MEMBER, "f1~test").is_ok();
assert!(ok(ShareStanding::Manager, ShareStanding::Reader));
assert!(ok(ShareStanding::Manager, ShareStanding::Manager));
assert!(ok(ShareStanding::Reader, ShareStanding::Reader));
assert!(!ok(ShareStanding::Reader, ShareStanding::Manager));
assert!(!ok(ShareStanding::None, ShareStanding::Reader));
assert!(!ok(ShareStanding::None, ShareStanding::Manager));
assert!(matches!(
ensure_standing(ShareStanding::None, ShareStanding::Reader, MEMBER, "f1~test"),
Err(Error::PermissionDenied)
));
}
#[test]
fn a_manager_cannot_grant_beyond_their_own_access() {
let ok = |granted, own| ensure_grant_within(granted, own, MEMBER, "f1~test").is_ok();
assert!(!ok(AccessLevel::Write, AccessLevel::Read));
assert!(!ok(AccessLevel::Comment, AccessLevel::Read));
assert!(ok(AccessLevel::Read, AccessLevel::Read));
assert!(ok(W, W));
assert!(ok(AccessLevel::Comment, W));
assert!(ok(AccessLevel::Read, W));
assert_eq!(AccessLevel::from_perm_char('A'), AccessLevel::Admin);
assert!(!ok(AccessLevel::from_perm_char('A'), W));
assert!(ok(AccessLevel::from_perm_char('A'), AccessLevel::Admin));
assert!(matches!(
ensure_grant_within(W, AccessLevel::Read, MEMBER, "f1~test"),
Err(Error::PermissionDenied)
));
}
#[test]
fn grant_ceiling_is_admin_only_for_ownership_derived_standing() {
assert_eq!(grant_ceiling(W, true, false), AccessLevel::Admin);
assert_eq!(grant_ceiling(W, false, true), AccessLevel::Admin);
assert_eq!(grant_ceiling(A, false, false), AccessLevel::Admin);
assert_eq!(grant_ceiling(AccessLevel::Read, false, false), AccessLevel::Read);
assert_eq!(grant_ceiling(W, false, false), W);
let creator = grant_ceiling(AccessLevel::Read, false, false);
assert!(ensure_grant_within(AccessLevel::Read, creator, MEMBER, "f1~test").is_ok());
assert!(ensure_grant_within(W, creator, MEMBER, "f1~test").is_err());
let owner = grant_ceiling(W, true, false);
assert!(
ensure_grant_within(AccessLevel::from_perm_char('A'), owner, MEMBER, "f1~test").is_ok()
);
}
#[test]
fn non_empty_id_tag_normalizes_blank() {
assert_eq!(non_empty_id_tag(Some("")), None);
assert_eq!(non_empty_id_tag(Some(MEMBER)), Some(MEMBER));
assert_eq!(non_empty_id_tag(None), None);
}
#[test]
fn a_row_with_no_explicit_owner_belongs_to_the_tenant() {
assert_eq!(effective_owner(None, TENANT), TENANT);
assert_eq!(effective_owner(Some(""), TENANT), TENANT);
assert_eq!(effective_owner(Some(MEMBER), TENANT), MEMBER);
let owner = effective_owner(None, TENANT);
assert!(is_share_manager(W, MEMBER, TENANT, Some(owner), Some(MEMBER), false));
let leader_over_tenant_row = owner == TENANT;
assert!(is_share_manager(
AccessLevel::Read,
OTHER,
TENANT,
Some(owner),
Some(MEMBER),
leader_over_tenant_row
));
assert!(!is_share_manager(W, MEMBER, TENANT, None, Some(MEMBER), false));
}
}