Skip to content

Commit 3a6e35f

Browse files
improved permission tracking in searches
1 parent 56cc1b8 commit 3a6e35f

4 files changed

Lines changed: 409 additions & 39 deletions

File tree

Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
use pangolin_core::permission::{Permission, PermissionScope, Action};
2+
use pangolin_core::model::{Catalog, Namespace, Asset};
3+
use pangolin_core::user::UserRole;
4+
use uuid::Uuid;
5+
6+
/// Check if a user has access to a catalog based on their permissions
7+
///
8+
/// Checks for Read or Discoverable actions on:
9+
/// - Exact catalog scope
10+
/// - Tenant-wide scope
11+
pub fn has_catalog_access(
12+
catalog_id: Uuid,
13+
permissions: &[Permission],
14+
required_actions: &[Action],
15+
) -> bool {
16+
permissions.iter().any(|perm| {
17+
// Check if permission scope covers this catalog
18+
let scope_matches = matches!(
19+
&perm.scope,
20+
PermissionScope::Catalog { catalog_id: cid } if *cid == catalog_id
21+
) || matches!(&perm.scope, PermissionScope::Tenant);
22+
23+
// Check if permission has any of the required actions
24+
let has_action = required_actions.iter().any(|action| {
25+
perm.actions.iter().any(|a| a.implies(action))
26+
});
27+
28+
scope_matches && has_action
29+
})
30+
}
31+
32+
/// Check if a user has access to a namespace based on their permissions
33+
///
34+
/// Checks for Read or Discoverable actions on:
35+
/// - Exact namespace scope
36+
/// - Parent catalog scope
37+
/// - Tenant-wide scope
38+
pub fn has_namespace_access(
39+
catalog_id: Uuid,
40+
namespace: &str,
41+
permissions: &[Permission],
42+
required_actions: &[Action],
43+
) -> bool {
44+
permissions.iter().any(|perm| {
45+
// Check if permission scope covers this namespace
46+
let scope_matches = match &perm.scope {
47+
PermissionScope::Namespace { catalog_id: cid, namespace: ns } => {
48+
*cid == catalog_id && ns == namespace
49+
},
50+
PermissionScope::Catalog { catalog_id: cid } => *cid == catalog_id,
51+
PermissionScope::Tenant => true,
52+
_ => false,
53+
};
54+
55+
// Check if permission has any of the required actions
56+
let has_action = required_actions.iter().any(|action| {
57+
perm.actions.iter().any(|a| a.implies(action))
58+
});
59+
60+
scope_matches && has_action
61+
})
62+
}
63+
64+
/// Check if a user has access to an asset based on their permissions
65+
///
66+
/// Checks for Read or Discoverable actions on:
67+
/// - Exact asset scope
68+
/// - Parent namespace scope
69+
/// - Parent catalog scope
70+
/// - Tenant-wide scope
71+
pub fn has_asset_access(
72+
catalog_id: Uuid,
73+
namespace: &str,
74+
asset_id: Uuid,
75+
permissions: &[Permission],
76+
required_actions: &[Action],
77+
) -> bool {
78+
permissions.iter().any(|perm| {
79+
// Check if permission scope covers this asset
80+
let scope_matches = match &perm.scope {
81+
PermissionScope::Asset { catalog_id: cid, namespace: ns, asset_id: aid } => {
82+
*cid == catalog_id && ns == namespace && *aid == asset_id
83+
},
84+
PermissionScope::Namespace { catalog_id: cid, namespace: ns } => {
85+
*cid == catalog_id && ns == namespace
86+
},
87+
PermissionScope::Catalog { catalog_id: cid } => *cid == catalog_id,
88+
PermissionScope::Tenant => true,
89+
_ => false,
90+
};
91+
92+
// Check if permission has any of the required actions
93+
let has_action = required_actions.iter().any(|action| {
94+
perm.actions.iter().any(|a| a.implies(action))
95+
});
96+
97+
scope_matches && has_action
98+
})
99+
}
100+
101+
/// Filter catalogs based on user permissions
102+
///
103+
/// Returns only catalogs the user has Read or Discoverable access to.
104+
/// Root and TenantAdmin users bypass filtering.
105+
pub fn filter_catalogs(
106+
catalogs: Vec<Catalog>,
107+
permissions: &[Permission],
108+
user_role: UserRole,
109+
) -> Vec<Catalog> {
110+
// Root and TenantAdmin see everything
111+
if matches!(user_role, UserRole::Root | UserRole::TenantAdmin) {
112+
return catalogs;
113+
}
114+
115+
let required_actions = vec![Action::Read, Action::ManageDiscovery];
116+
117+
catalogs.into_iter()
118+
.filter(|catalog| has_catalog_access(catalog.id, permissions, &required_actions))
119+
.collect()
120+
}
121+
122+
/// Filter namespaces based on user permissions
123+
///
124+
/// Returns only namespaces the user has Read or Discoverable access to.
125+
/// Root and TenantAdmin users bypass filtering.
126+
pub fn filter_namespaces(
127+
namespaces: Vec<(Namespace, String)>,
128+
permissions: &[Permission],
129+
user_role: UserRole,
130+
catalog_id_map: &std::collections::HashMap<String, Uuid>,
131+
) -> Vec<(Namespace, String)> {
132+
// Root and TenantAdmin see everything
133+
if matches!(user_role, UserRole::Root | UserRole::TenantAdmin) {
134+
return namespaces;
135+
}
136+
137+
let required_actions = vec![Action::Read, Action::ManageDiscovery];
138+
139+
namespaces.into_iter()
140+
.filter(|(namespace, catalog_name)| {
141+
// Get catalog ID from the map
142+
if let Some(&catalog_id) = catalog_id_map.get(catalog_name) {
143+
let namespace_str = namespace.name.join(".");
144+
has_namespace_access(catalog_id, &namespace_str, permissions, &required_actions)
145+
} else {
146+
false
147+
}
148+
})
149+
.collect()
150+
}
151+
152+
/// Filter assets based on user permissions
153+
///
154+
/// Returns only assets the user has Read or Discoverable access to.
155+
/// Root and TenantAdmin users bypass filtering.
156+
pub fn filter_assets(
157+
assets: Vec<(Asset, Option<pangolin_core::business_metadata::BusinessMetadata>, String, Vec<String>)>,
158+
permissions: &[Permission],
159+
user_role: UserRole,
160+
catalog_id_map: &std::collections::HashMap<String, Uuid>,
161+
) -> Vec<(Asset, Option<pangolin_core::business_metadata::BusinessMetadata>, String, Vec<String>)> {
162+
// Root and TenantAdmin see everything
163+
if matches!(user_role, UserRole::Root | UserRole::TenantAdmin) {
164+
return assets;
165+
}
166+
167+
let required_actions = vec![Action::Read, Action::ManageDiscovery];
168+
169+
assets.into_iter()
170+
.filter(|(asset, _metadata, catalog_name, namespace)| {
171+
// Get catalog ID from the map
172+
if let Some(&catalog_id) = catalog_id_map.get(catalog_name) {
173+
let namespace_str = namespace.join(".");
174+
has_asset_access(catalog_id, &namespace_str, asset.id, permissions, &required_actions)
175+
} else {
176+
false
177+
}
178+
})
179+
.collect()
180+
}
181+
182+
#[cfg(test)]
183+
mod tests {
184+
use super::*;
185+
use std::collections::HashSet;
186+
use chrono::Utc;
187+
188+
#[test]
189+
fn test_has_catalog_access_with_catalog_permission() {
190+
let catalog_id = Uuid::new_v4();
191+
let mut actions = HashSet::new();
192+
actions.insert(Action::Read);
193+
194+
let permission = Permission {
195+
id: Uuid::new_v4(),
196+
user_id: Uuid::new_v4(),
197+
scope: PermissionScope::Catalog { catalog_id },
198+
actions,
199+
granted_by: Uuid::new_v4(),
200+
granted_at: Utc::now(),
201+
};
202+
203+
assert!(has_catalog_access(catalog_id, &[permission], &[Action::Read]));
204+
}
205+
206+
#[test]
207+
fn test_has_catalog_access_with_tenant_permission() {
208+
let catalog_id = Uuid::new_v4();
209+
let mut actions = HashSet::new();
210+
actions.insert(Action::Read);
211+
212+
let permission = Permission {
213+
id: Uuid::new_v4(),
214+
user_id: Uuid::new_v4(),
215+
scope: PermissionScope::Tenant,
216+
actions,
217+
granted_by: Uuid::new_v4(),
218+
granted_at: Utc::now(),
219+
};
220+
221+
assert!(has_catalog_access(catalog_id, &[permission], &[Action::Read]));
222+
}
223+
224+
#[test]
225+
fn test_has_catalog_access_without_permission() {
226+
let catalog_id = Uuid::new_v4();
227+
let other_catalog_id = Uuid::new_v4();
228+
let mut actions = HashSet::new();
229+
actions.insert(Action::Read);
230+
231+
let permission = Permission {
232+
id: Uuid::new_v4(),
233+
user_id: Uuid::new_v4(),
234+
scope: PermissionScope::Catalog { catalog_id: other_catalog_id },
235+
actions,
236+
granted_by: Uuid::new_v4(),
237+
granted_at: Utc::now(),
238+
};
239+
240+
assert!(!has_catalog_access(catalog_id, &[permission], &[Action::Read]));
241+
}
242+
243+
#[test]
244+
fn test_filter_catalogs_as_root() {
245+
let catalogs = vec![
246+
Catalog {
247+
id: Uuid::new_v4(),
248+
name: "catalog1".to_string(),
249+
catalog_type: pangolin_core::model::CatalogType::Local,
250+
warehouse_name: None,
251+
storage_location: None,
252+
federated_config: None,
253+
properties: std::collections::HashMap::new(),
254+
}
255+
];
256+
257+
let filtered = filter_catalogs(catalogs.clone(), &[], UserRole::Root);
258+
assert_eq!(filtered.len(), catalogs.len());
259+
}
260+
261+
#[test]
262+
fn test_filter_catalogs_as_tenant_user_with_permission() {
263+
let catalog_id = Uuid::new_v4();
264+
let catalogs = vec![
265+
Catalog {
266+
id: catalog_id,
267+
name: "catalog1".to_string(),
268+
catalog_type: pangolin_core::model::CatalogType::Local,
269+
warehouse_name: None,
270+
storage_location: None,
271+
federated_config: None,
272+
properties: std::collections::HashMap::new(),
273+
}
274+
];
275+
276+
let mut actions = HashSet::new();
277+
actions.insert(Action::Read);
278+
279+
let permission = Permission {
280+
id: Uuid::new_v4(),
281+
user_id: Uuid::new_v4(),
282+
scope: PermissionScope::Catalog { catalog_id },
283+
actions,
284+
granted_by: Uuid::new_v4(),
285+
granted_at: Utc::now(),
286+
};
287+
288+
let filtered = filter_catalogs(catalogs.clone(), &[permission], UserRole::TenantUser);
289+
assert_eq!(filtered.len(), 1);
290+
}
291+
292+
#[test]
293+
fn test_filter_catalogs_as_tenant_user_without_permission() {
294+
let catalog_id = Uuid::new_v4();
295+
let catalogs = vec![
296+
Catalog {
297+
id: catalog_id,
298+
name: "catalog1".to_string(),
299+
catalog_type: pangolin_core::model::CatalogType::Local,
300+
warehouse_name: None,
301+
storage_location: None,
302+
federated_config: None,
303+
properties: std::collections::HashMap::new(),
304+
}
305+
];
306+
307+
let filtered = filter_catalogs(catalogs, &[], UserRole::TenantUser);
308+
assert_eq!(filtered.len(), 0);
309+
}
310+
}

pangolin/pangolin_api/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ pub mod user_handlers;
2121
pub mod oauth_handlers;
2222
pub mod auth_middleware;
2323
pub mod authz;
24+
pub mod authz_utils; // Permission filtering utilities
2425
pub mod business_metadata_handlers;
2526
pub mod conflict_detector;
2627
pub mod merge_handlers;

0 commit comments

Comments
 (0)