Skip to content

Commit 57663a2

Browse files
authored
feat: Support Authorizers which cannot list projects (lakekeeper#1481)
1 parent f8fa500 commit 57663a2

4 files changed

Lines changed: 114 additions & 7 deletions

File tree

crates/authz-openfga/src/authorizer.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,22 @@ impl Authorizer for OpenFGAAuthorizer {
290290
.map_err(Into::into)
291291
}
292292

293+
async fn are_allowed_project_actions_impl(
294+
&self,
295+
metadata: &RequestMetadata,
296+
projects_with_actions: &[(&ProjectId, Self::ProjectAction)],
297+
) -> std::result::Result<Vec<bool>, AuthorizationBackendUnavailable> {
298+
let items: Vec<_> = projects_with_actions
299+
.iter()
300+
.map(|(project, a)| CheckRequestTupleKey {
301+
user: metadata.actor().to_openfga(),
302+
relation: a.to_string(),
303+
object: project.to_openfga(),
304+
})
305+
.collect();
306+
self.batch_check(items).await.map_err(Into::into)
307+
}
308+
293309
async fn is_allowed_warehouse_action_impl(
294310
&self,
295311
metadata: &RequestMetadata,

crates/lakekeeper/src/api/management/v1/project.rs

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -211,18 +211,49 @@ pub trait Service<C: CatalogStore, A: Authorizer, S: SecretStore> {
211211
) -> Result<ListProjectsResponse> {
212212
// ------------------- AuthZ -------------------
213213
let authorizer = context.v1_state.authz;
214-
let projects = authorizer.list_projects(&request_metadata).await?;
214+
let authz_projects_response = authorizer.list_projects(&request_metadata).await?;
215215

216216
// ------------------- Business Logic -------------------
217-
let project_id_filter = match projects {
218-
AuthZListProjectsResponse::All => None,
217+
let authz_list_unsupported =
218+
authz_projects_response == AuthZListProjectsResponse::Unsupported;
219+
let project_id_filter = match authz_projects_response {
220+
AuthZListProjectsResponse::All | AuthZListProjectsResponse::Unsupported => None,
219221
AuthZListProjectsResponse::Projects(projects) => Some(projects),
220222
};
221223
let mut trx = C::Transaction::begin_read(context.v1_state.catalog).await?;
222-
223224
let projects = C::list_projects(project_id_filter, trx.transaction()).await?;
224225
trx.commit().await?;
225226

227+
let projects = if authz_list_unsupported {
228+
tracing::debug!(
229+
"Authorization backend does not support listing projects, filtering per project."
230+
);
231+
let decisions = authorizer
232+
.are_allowed_project_actions_vec(
233+
&request_metadata,
234+
&projects
235+
.iter()
236+
.map(|p| (&p.project_id, CatalogProjectAction::CanGetMetadata))
237+
.collect::<Vec<_>>(),
238+
)
239+
.await?;
240+
projects
241+
.into_iter()
242+
.zip(decisions.into_inner())
243+
.filter_map(
244+
|(project, is_allowed)| {
245+
if is_allowed {
246+
Some(project)
247+
} else {
248+
None
249+
}
250+
},
251+
)
252+
.collect()
253+
} else {
254+
projects
255+
};
256+
226257
Ok(ListProjectsResponse {
227258
projects: projects
228259
.into_iter()

crates/lakekeeper/src/service/authz/mod.rs

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,10 +241,14 @@ where
241241
async fn bootstrap(&self, metadata: &RequestMetadata, is_operator: bool) -> Result<()>;
242242

243243
/// Return Err only for internal errors.
244+
/// If unsupported is returned, Lakekeeper will run checks for every project individually using
245+
/// `are_allowed_project_actions`.
244246
async fn list_projects_impl(
245247
&self,
246-
metadata: &RequestMetadata,
247-
) -> std::result::Result<ListProjectsResponse, AuthorizationBackendUnavailable>;
248+
_metadata: &RequestMetadata,
249+
) -> std::result::Result<ListProjectsResponse, AuthorizationBackendUnavailable> {
250+
Ok(ListProjectsResponse::Unsupported)
251+
}
248252

249253
/// Search users
250254
async fn can_search_users_impl(&self, metadata: &RequestMetadata) -> Result<bool>;
@@ -323,6 +327,29 @@ where
323327
action: Self::ProjectAction,
324328
) -> std::result::Result<bool, AuthorizationBackendUnavailable>;
325329

330+
async fn are_allowed_project_actions_impl(
331+
&self,
332+
metadata: &RequestMetadata,
333+
projects_with_actions: &[(&ProjectId, Self::ProjectAction)],
334+
) -> std::result::Result<Vec<bool>, AuthorizationBackendUnavailable> {
335+
let n_inputs = projects_with_actions.len();
336+
let futures: Vec<_> = projects_with_actions
337+
.iter()
338+
.map(|(project, a)| async move {
339+
self.is_allowed_project_action(metadata, project, *a)
340+
.await
341+
.map(MustUse::into_inner)
342+
})
343+
.collect();
344+
let results = try_join_all(futures).await?;
345+
debug_assert_eq!(
346+
results.len(),
347+
n_inputs,
348+
"are_allowed_project_actions_impl to return as many results as provided inputs"
349+
);
350+
Ok(results)
351+
}
352+
326353
/// Return Ok(true) if the action is allowed, otherwise return Ok(false).
327354
/// Return Err for internal errors.
328355
async fn is_allowed_warehouse_action_impl(

crates/lakekeeper/src/service/authz/project.rs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@ where
1818

1919
impl ProjectAction for CatalogProjectAction {}
2020

21-
#[derive(Debug, Clone, PartialEq)]
21+
#[derive(Debug, Clone, PartialEq, Eq)]
2222
pub enum ListProjectsResponse {
2323
/// List of projects that the user is allowed to see.
2424
Projects(HashSet<ProjectId>),
2525
/// The user is allowed to see all projects.
2626
All,
27+
/// Unsupported by the authorization backend.
28+
Unsupported,
2729
}
2830

2931
// --------------------------- Errors ---------------------------
@@ -96,6 +98,37 @@ pub trait AuthZProjectOps: Authorizer {
9698
}
9799
}
98100

101+
async fn are_allowed_project_actions_vec<A: Into<Self::ProjectAction> + Send + Copy + Sync>(
102+
&self,
103+
metadata: &RequestMetadata,
104+
projects_with_actions: &[(&ProjectId, A)],
105+
) -> Result<MustUse<Vec<bool>>, AuthorizationBackendUnavailable> {
106+
if metadata.has_admin_privileges() {
107+
Ok(vec![true; projects_with_actions.len()])
108+
} else {
109+
let converted: Vec<(&ProjectId, Self::ProjectAction)> = projects_with_actions
110+
.iter()
111+
.map(|(id, action)| (*id, (*action).into()))
112+
.collect();
113+
let decisions = self.are_allowed_project_actions_impl(metadata, &converted)
114+
.await;
115+
116+
#[cfg(debug_assertions)]
117+
{
118+
if let Ok(ref decisions) = decisions {
119+
assert_eq!(
120+
decisions.len(),
121+
projects_with_actions.len(),
122+
"The number of decisions returned by are_allowed_project_actions_impl does not match the number of project-action pairs provided."
123+
);
124+
}
125+
}
126+
127+
decisions
128+
}
129+
.map(MustUse::from)
130+
}
131+
99132
async fn is_allowed_project_action(
100133
&self,
101134
metadata: &RequestMetadata,

0 commit comments

Comments
 (0)