cloudillo_action/
filter.rs1use std::collections::{HashMap, HashSet};
4use std::sync::Arc;
5
6use cloudillo_core::abac::can_view_item;
7use cloudillo_types::meta_adapter::{ActionView, ListActionOptions};
8
9use crate::{dsl::DslEngine, prelude::*};
10
11pub async fn filter_actions_by_visibility(
20 app: &App,
21 tn_id: TnId,
22 subject_id_tag: &str,
23 is_authenticated: bool,
24 tenant_id_tag: &str,
25 actions: Vec<ActionView>,
26) -> ClResult<Vec<ActionView>> {
27 if actions.is_empty() {
29 return Ok(actions);
30 }
31
32 let issuer_tags: HashSet<&str> = actions.iter().map(|a| a.issuer.id_tag.as_ref()).collect();
34
35 let relationships = load_relationships(app, tn_id, subject_id_tag, &issuer_tags).await?;
37
38 let subscribable_direct: Vec<&str> = actions
40 .iter()
41 .filter(|a| a.visibility.is_none() && is_subscribable(app, &a.typ))
42 .map(|a| a.action_id.as_ref())
43 .collect();
44
45 let subscribers_map = load_subscribers(app, tn_id, &subscribable_direct).await;
47
48 info!(
50 "filter_actions_by_visibility: subject={}, is_auth={}, tenant={}, action_count={}",
51 subject_id_tag,
52 is_authenticated,
53 tenant_id_tag,
54 actions.len()
55 );
56 let filtered = actions
57 .into_iter()
58 .filter(|action| {
59 let issuer_tag = action.issuer.id_tag.as_ref();
60 let (following, connected) =
61 relationships.get(issuer_tag).copied().unwrap_or((false, false));
62
63 let mut audience: Vec<&str> =
65 action.audience.as_ref().map(|a| vec![a.id_tag.as_ref()]).unwrap_or_default();
66
67 if action.visibility.is_none() {
69 if let Some(subs) = subscribers_map.get(action.action_id.as_ref()) {
70 if subs.contains(subject_id_tag) {
71 audience.push(subject_id_tag);
72 }
73 }
74 }
75
76 let allowed = can_view_item(
77 subject_id_tag,
78 is_authenticated,
79 issuer_tag,
80 tenant_id_tag,
81 action.visibility,
82 following,
83 connected,
84 Some(&audience),
85 );
86 if !allowed {
87 info!(
88 "FILTERED OUT action={}: subject={}, issuer={}, tenant={}, visibility={:?}, audience={:?}",
89 action.action_id, subject_id_tag, issuer_tag, tenant_id_tag, action.visibility, audience
90 );
91 }
92 allowed
93 })
94 .collect();
95
96 Ok(filtered)
97}
98
99fn is_subscribable(app: &App, action_type: &str) -> bool {
101 app.ext::<Arc<DslEngine>>()
102 .ok()
103 .and_then(|dsl| dsl.get_behavior(action_type))
104 .and_then(|b| b.subscribable)
105 .unwrap_or(false)
106}
107
108async fn load_subscribers(
112 app: &App,
113 tn_id: TnId,
114 action_ids: &[&str],
115) -> HashMap<String, HashSet<String>> {
116 let mut subscribers_map: HashMap<String, HashSet<String>> = HashMap::new();
117
118 for action_id in action_ids {
119 let subs_opts = ListActionOptions {
120 typ: Some(vec!["SUBS".into()]),
121 subject: Some((*action_id).to_string()),
122 status: Some(vec!["A".into()]),
123 ..Default::default()
124 };
125
126 if let Ok(subs) = app.meta_adapter.list_actions(tn_id, &subs_opts).await {
127 let issuer_tags: HashSet<String> =
128 subs.into_iter().map(|a| a.issuer.id_tag.to_string()).collect();
129 subscribers_map.insert((*action_id).to_string(), issuer_tags);
130 }
131 }
132
133 subscribers_map
134}
135
136async fn load_relationships(
141 app: &App,
142 tn_id: TnId,
143 subject_id_tag: &str,
144 target_id_tags: &HashSet<&str>,
145) -> ClResult<HashMap<String, (bool, bool)>> {
146 if subject_id_tag.is_empty() || target_id_tags.is_empty() {
148 return Ok(HashMap::new());
149 }
150
151 let targets: Vec<&str> = target_id_tags.iter().copied().collect();
153
154 app.meta_adapter.get_relationships(tn_id, &targets).await
156}
157
158