1use crate::prelude::*;
28use cloudillo_types::auth_adapter::AuthCtx;
29use cloudillo_types::types::AccessLevel;
30
31use crate::file_access::{self, FileAccessCtx, FileAccessResult};
32
33pub async fn require_unscoped_file_access(
39 app: &App,
40 tn_id: TnId,
41 file_id: &str,
42 auth: &AuthCtx,
43 tenant_id_tag: &str,
44) -> ClResult<FileAccessResult> {
45 if auth.scope.is_some() {
46 warn!("Scoped token attempted to access share entries");
47 return Err(Error::PermissionDenied);
48 }
49
50 let ctx = FileAccessCtx { user_id_tag: &auth.id_tag, tenant_id_tag, user_roles: &auth.roles };
51 match file_access::check_file_access_with_scope(app, tn_id, file_id, &ctx, None, None).await {
53 Err(file_access::FileAccessError::NotFound) => Err(Error::NotFound),
54 Err(file_access::FileAccessError::AccessDenied) => Err(Error::PermissionDenied),
55 Err(file_access::FileAccessError::InternalError(msg)) => Err(Error::Internal(msg)),
56 Ok(access) => Ok(access),
57 }
58}
59
60fn is_share_manager(
76 access: AccessLevel,
77 subject: &str,
78 tenant_id_tag: &str,
79 owner_id_tag: Option<&str>,
80 creator_id_tag: Option<&str>,
81 leader_over_tenant_row: bool,
82) -> bool {
83 if access == AccessLevel::None {
85 return false;
86 }
87 if leader_over_tenant_row || access.can_manage_shares() || owner_id_tag == Some(subject) {
91 return true;
92 }
93 let tenant_owned = owner_id_tag == Some(tenant_id_tag);
94 tenant_owned && creator_id_tag == Some(subject)
95}
96
97fn is_share_reader(access: AccessLevel) -> bool {
103 access.can_write()
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
108pub enum ShareStanding {
109 None,
110 Reader,
111 Manager,
112}
113
114fn classify_standing(
116 access: AccessLevel,
117 subject: &str,
118 tenant_id_tag: &str,
119 owner_id_tag: Option<&str>,
120 creator_id_tag: Option<&str>,
121 leader_over_tenant_row: bool,
122) -> ShareStanding {
123 if is_share_manager(
124 access,
125 subject,
126 tenant_id_tag,
127 owner_id_tag,
128 creator_id_tag,
129 leader_over_tenant_row,
130 ) {
131 ShareStanding::Manager
132 } else if is_share_reader(access) {
133 ShareStanding::Reader
134 } else {
135 ShareStanding::None
136 }
137}
138
139fn grant_ceiling(access: AccessLevel, is_owner: bool, leader_over_tenant_row: bool) -> AccessLevel {
151 if leader_over_tenant_row || is_owner { AccessLevel::Admin } else { access }
152}
153
154pub struct ShareAuthority {
156 pub access: FileAccessResult,
158 pub standing: ShareStanding,
159 pub grant_ceiling: AccessLevel,
162}
163
164pub async fn share_standing(
168 app: &App,
169 tn_id: TnId,
170 file_id: &str,
171 auth: &AuthCtx,
172 tenant_id_tag: &str,
173) -> ClResult<ShareAuthority> {
174 let access = require_unscoped_file_access(app, tn_id, file_id, auth, tenant_id_tag).await?;
175
176 let owner_id_tag =
177 effective_owner(access.file_view.owner.as_ref().map(|p| p.id_tag.as_ref()), tenant_id_tag);
178 let creator_id_tag =
179 non_empty_id_tag(access.file_view.creator.as_ref().map(|p| p.id_tag.as_ref()));
180 let leader_over_tenant_row =
183 crate::roles::is_leader(&auth.roles) && owner_id_tag == tenant_id_tag;
184
185 let standing = classify_standing(
188 access.access_level,
189 &auth.id_tag,
190 tenant_id_tag,
191 Some(owner_id_tag),
192 creator_id_tag,
193 leader_over_tenant_row,
194 );
195
196 let ceiling =
197 grant_ceiling(access.access_level, owner_id_tag == &*auth.id_tag, leader_over_tenant_row);
198
199 Ok(ShareAuthority { access, standing, grant_ceiling: ceiling })
200}
201
202pub async fn share_standing_for_actor(
213 app: &App,
214 tn_id: TnId,
215 file_id: &str,
216 actor_id_tag: &str,
217 tenant_id_tag: &str,
218) -> ClResult<ShareAuthority> {
219 let roles: Box<[Box<str>]> = if actor_id_tag == tenant_id_tag {
220 crate::roles::parse_roles(&crate::roles::expand_roles_preserving_extras(&["leader".into()]))
221 } else {
222 match app.meta_adapter.read_profile_roles(tn_id, actor_id_tag).await? {
223 Some(highest) => {
224 crate::roles::parse_roles(&crate::roles::expand_roles_preserving_extras(&highest))
225 }
226 None => Box::new([]),
227 }
228 };
229
230 let auth = AuthCtx { tn_id, id_tag: actor_id_tag.into(), roles, scope: None, anonymous: false };
231 share_standing(app, tn_id, file_id, &auth, tenant_id_tag).await
232}
233
234pub fn ensure_grant_within(
241 granted: AccessLevel,
242 own: AccessLevel,
243 subject: &str,
244 file_id: &str,
245) -> ClResult<()> {
246 if granted <= own {
247 return Ok(());
248 }
249 warn!(
250 subject = %subject,
251 file_id = %file_id,
252 granted = %granted.as_str(),
253 own = %own.as_str(),
254 "Share grant denied - a manager may not hand out more access than they hold"
255 );
256 Err(Error::PermissionDenied)
257}
258
259pub fn ensure_standing(
264 standing: ShareStanding,
265 min: ShareStanding,
266 subject: &str,
267 file_id: &str,
268) -> ClResult<()> {
269 if standing >= min {
270 return Ok(());
271 }
272 let reason = match min {
273 ShareStanding::Manager => "Share management denied - owner/creator/leader/admin required",
274 ShareStanding::Reader => "Share listing denied - write access or share management required",
275 ShareStanding::None => "Share access denied - no minimum standing was required",
278 };
279 warn!(subject = %subject, file_id = %file_id, "{}", reason);
280 Err(Error::PermissionDenied)
281}
282
283pub async fn require_share_manager(
290 app: &App,
291 tn_id: TnId,
292 file_id: &str,
293 auth: &AuthCtx,
294 tenant_id_tag: &str,
295) -> ClResult<ShareAuthority> {
296 let authority = share_standing(app, tn_id, file_id, auth, tenant_id_tag).await?;
297 ensure_standing(authority.standing, ShareStanding::Manager, &auth.id_tag, file_id)?;
298 Ok(authority)
299}
300
301pub async fn require_share_reader(
308 app: &App,
309 tn_id: TnId,
310 file_id: &str,
311 auth: &AuthCtx,
312 tenant_id_tag: &str,
313) -> ClResult<ShareAuthority> {
314 let authority = share_standing(app, tn_id, file_id, auth, tenant_id_tag).await?;
315 ensure_standing(authority.standing, ShareStanding::Reader, &auth.id_tag, file_id)?;
316 Ok(authority)
317}
318
319fn non_empty_id_tag(id_tag: Option<&str>) -> Option<&str> {
321 id_tag.filter(|s| !s.is_empty())
322}
323
324fn effective_owner<'a>(owner: Option<&'a str>, tenant_id_tag: &'a str) -> &'a str {
329 non_empty_id_tag(owner).unwrap_or(tenant_id_tag)
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335
336 const TENANT: &str = "community.example.com";
337 const MEMBER: &str = "alice.example.com";
338 const OTHER: &str = "bob.example.com";
339
340 const W: AccessLevel = AccessLevel::Write;
341 const A: AccessLevel = AccessLevel::Admin;
342
343 #[test]
344 fn personal_tenant_owner_manages_shares() {
345 assert!(is_share_manager(W, TENANT, TENANT, Some(TENANT), Some(TENANT), false));
347 }
348
349 #[test]
350 fn creator_of_tenant_owned_file_manages_shares() {
351 assert!(is_share_manager(W, MEMBER, TENANT, Some(TENANT), Some(MEMBER), false));
354 }
355
356 #[test]
357 fn non_creator_member_cannot_manage_shares() {
358 assert!(!is_share_manager(W, MEMBER, TENANT, Some(TENANT), Some(OTHER), false));
359 }
360
361 #[test]
362 fn leader_manages_shares() {
363 assert!(is_share_manager(W, MEMBER, TENANT, Some(TENANT), Some(OTHER), true));
364 }
365
366 #[test]
367 fn leader_does_not_reach_a_foreign_owned_row() {
368 for access in [AccessLevel::Read, W] {
371 assert!(!is_share_manager(access, MEMBER, TENANT, Some(OTHER), Some(OTHER), false));
372 assert_eq!(grant_ceiling(access, false, false), access);
374 }
375 assert!(is_share_manager(A, MEMBER, TENANT, Some(OTHER), Some(OTHER), false));
378 }
379
380 #[test]
381 fn leader_over_a_tenant_owned_row_keeps_manager_standing_and_an_admin_ceiling() {
382 assert_eq!(
385 classify_standing(AccessLevel::Read, MEMBER, TENANT, Some(TENANT), Some(OTHER), true),
386 ShareStanding::Manager
387 );
388 assert_eq!(grant_ceiling(AccessLevel::Read, false, true), AccessLevel::Admin);
389 }
390
391 #[test]
392 fn creator_of_placed_foreign_file_cannot_manage_shares() {
393 assert!(!is_share_manager(W, MEMBER, TENANT, Some(OTHER), Some(MEMBER), false));
396 }
397
398 #[test]
399 fn explicit_admin_grant_manages_shares() {
400 assert!(!is_share_manager(W, OTHER, TENANT, Some(TENANT), Some(MEMBER), false));
403 assert!(is_share_manager(A, OTHER, TENANT, Some(TENANT), Some(MEMBER), false));
404 }
405
406 #[test]
407 fn plain_write_grantee_cannot_manage_shares() {
408 assert!(!is_share_manager(W, OTHER, TENANT, Some(MEMBER), Some(MEMBER), false));
411 }
412
413 #[test]
414 fn missing_owner_and_creator_deny() {
415 assert!(!is_share_manager(W, MEMBER, TENANT, None, None, false));
416 }
417
418 #[test]
419 fn no_access_denies_before_any_standing_rule() {
420 assert!(!is_share_manager(
423 AccessLevel::None,
424 MEMBER,
425 TENANT,
426 Some(TENANT),
427 Some(MEMBER),
428 true
429 ));
430 }
431
432 #[test]
433 fn read_access_creator_is_still_a_share_manager() {
434 assert!(is_share_manager(
437 AccessLevel::Read,
438 MEMBER,
439 TENANT,
440 Some(TENANT),
441 Some(MEMBER),
442 false
443 ));
444 }
445
446 #[test]
447 fn manager_standing_implies_reader() {
448 for (access, subject, owner, creator, leader) in [
451 (W, TENANT, Some(TENANT), Some(TENANT), false),
452 (W, MEMBER, Some(TENANT), Some(MEMBER), false),
453 (W, MEMBER, Some(TENANT), Some(OTHER), true),
454 (A, OTHER, Some(TENANT), Some(MEMBER), false),
455 (AccessLevel::Read, MEMBER, Some(TENANT), Some(MEMBER), false),
456 ] {
457 assert!(is_share_manager(access, subject, TENANT, owner, creator, leader));
458 let standing = classify_standing(access, subject, TENANT, owner, creator, leader);
459 assert_eq!(standing, ShareStanding::Manager);
460 assert!(standing >= ShareStanding::Reader);
461 }
462
463 let grantee = classify_standing(W, OTHER, TENANT, Some(MEMBER), Some(MEMBER), false);
465 assert_eq!(grantee, ShareStanding::Reader);
466
467 let outsider =
469 classify_standing(AccessLevel::Read, OTHER, TENANT, Some(MEMBER), Some(MEMBER), false);
470 assert_eq!(outsider, ShareStanding::None);
471 }
472
473 #[test]
474 fn admin_access_alone_confers_manager_standing_and_an_admin_ceiling() {
475 let standing = classify_standing(A, OTHER, TENANT, Some(MEMBER), Some(MEMBER), false);
478 assert_eq!(standing, ShareStanding::Manager);
479 assert_eq!(grant_ceiling(A, false, false), AccessLevel::Admin);
481 }
482
483 #[test]
484 fn share_reader_needs_write_access() {
485 assert!(is_share_reader(AccessLevel::Write));
486 assert!(is_share_reader(AccessLevel::Admin));
487 assert!(!is_share_reader(AccessLevel::Read));
489 assert!(!is_share_reader(AccessLevel::Comment));
490 }
491
492 #[test]
493 fn ensure_standing_enforces_the_minimum() {
494 let ok = |standing, min| ensure_standing(standing, min, MEMBER, "f1~test").is_ok();
495
496 assert!(ok(ShareStanding::Manager, ShareStanding::Reader));
498 assert!(ok(ShareStanding::Manager, ShareStanding::Manager));
499 assert!(ok(ShareStanding::Reader, ShareStanding::Reader));
501 assert!(!ok(ShareStanding::Reader, ShareStanding::Manager));
502 assert!(!ok(ShareStanding::None, ShareStanding::Reader));
504 assert!(!ok(ShareStanding::None, ShareStanding::Manager));
505
506 assert!(matches!(
507 ensure_standing(ShareStanding::None, ShareStanding::Reader, MEMBER, "f1~test"),
508 Err(Error::PermissionDenied)
509 ));
510 }
511
512 #[test]
513 fn a_manager_cannot_grant_beyond_their_own_access() {
514 let ok = |granted, own| ensure_grant_within(granted, own, MEMBER, "f1~test").is_ok();
515
516 assert!(!ok(AccessLevel::Write, AccessLevel::Read));
519 assert!(!ok(AccessLevel::Comment, AccessLevel::Read));
520 assert!(ok(AccessLevel::Read, AccessLevel::Read));
522 assert!(ok(W, W));
523 assert!(ok(AccessLevel::Comment, W));
524 assert!(ok(AccessLevel::Read, W));
525 assert_eq!(AccessLevel::from_perm_char('A'), AccessLevel::Admin);
527 assert!(!ok(AccessLevel::from_perm_char('A'), W));
528 assert!(ok(AccessLevel::from_perm_char('A'), AccessLevel::Admin));
529
530 assert!(matches!(
531 ensure_grant_within(W, AccessLevel::Read, MEMBER, "f1~test"),
532 Err(Error::PermissionDenied)
533 ));
534 }
535
536 #[test]
537 fn grant_ceiling_is_admin_only_for_ownership_derived_standing() {
538 assert_eq!(grant_ceiling(W, true, false), AccessLevel::Admin);
541 assert_eq!(grant_ceiling(W, false, true), AccessLevel::Admin);
544 assert_eq!(grant_ceiling(A, false, false), AccessLevel::Admin);
546
547 assert_eq!(grant_ceiling(AccessLevel::Read, false, false), AccessLevel::Read);
551 assert_eq!(grant_ceiling(W, false, false), W);
552
553 let creator = grant_ceiling(AccessLevel::Read, false, false);
555 assert!(ensure_grant_within(AccessLevel::Read, creator, MEMBER, "f1~test").is_ok());
556 assert!(ensure_grant_within(W, creator, MEMBER, "f1~test").is_err());
557 let owner = grant_ceiling(W, true, false);
558 assert!(
559 ensure_grant_within(AccessLevel::from_perm_char('A'), owner, MEMBER, "f1~test").is_ok()
560 );
561 }
562
563 #[test]
564 fn non_empty_id_tag_normalizes_blank() {
565 assert_eq!(non_empty_id_tag(Some("")), None);
566 assert_eq!(non_empty_id_tag(Some(MEMBER)), Some(MEMBER));
567 assert_eq!(non_empty_id_tag(None), None);
568 }
569
570 #[test]
571 fn a_row_with_no_explicit_owner_belongs_to_the_tenant() {
572 assert_eq!(effective_owner(None, TENANT), TENANT);
576 assert_eq!(effective_owner(Some(""), TENANT), TENANT);
577 assert_eq!(effective_owner(Some(MEMBER), TENANT), MEMBER);
578
579 let owner = effective_owner(None, TENANT);
581 assert!(is_share_manager(W, MEMBER, TENANT, Some(owner), Some(MEMBER), false));
582 let leader_over_tenant_row = owner == TENANT;
584 assert!(is_share_manager(
585 AccessLevel::Read,
586 OTHER,
587 TENANT,
588 Some(owner),
589 Some(MEMBER),
590 leader_over_tenant_row
591 ));
592 assert!(!is_share_manager(W, MEMBER, TENANT, None, Some(MEMBER), false));
594 }
595}
596
597