forked from 2sys/phonograph
138 lines
4.7 KiB
Rust
138 lines
4.7 KiB
Rust
use derive_builder::Builder;
|
|
use phono_backends::{
|
|
client::WorkspaceClient, pg_acl::PgPrivilegeType, pg_database::PgDatabase,
|
|
rolnames::ROLE_PREFIX_USER,
|
|
};
|
|
use tracing::{Instrument as _, debug, info_span};
|
|
use uuid::Uuid;
|
|
|
|
use crate::{
|
|
client::AppDbClient,
|
|
errors::{AccessError, QueryResult},
|
|
workspace::Workspace,
|
|
workspace_user_perm::WorkspaceMembership,
|
|
};
|
|
|
|
use super::{Accessor, Actor};
|
|
|
|
/// Utility for fetching a [`Workspace`], with authorization.
|
|
#[derive(Builder, Debug)]
|
|
#[builder(
|
|
// Build fn should only be called internally, via `fetch_optional()`.
|
|
build_fn(private, error = "AccessError"),
|
|
// Callers interact primarily with the generated builder struct.
|
|
name = "WorkspaceAccessor",
|
|
vis = "pub",
|
|
// "Owned" pattern circumvents the `Clone` trait bound on fields.
|
|
pattern = "owned",
|
|
)]
|
|
struct GeneratedWorkspaceAccessor<'a> {
|
|
/// Required. ID of the workspace to be accessed.
|
|
id: Uuid,
|
|
|
|
/// Required. Identity against which to evaluate authorization checks.
|
|
as_actor: Actor,
|
|
|
|
/// Required. Client for the backing database. Providing a client authorized
|
|
/// as the root Phonograph user will not compromise the integrity of the
|
|
/// authorization checks.
|
|
using_workspace_client: &'a mut WorkspaceClient,
|
|
|
|
/// Required. Client for the application database.
|
|
using_app_db: &'a mut AppDbClient,
|
|
}
|
|
|
|
impl<'a> Accessor<Workspace> for WorkspaceAccessor<'a> {
|
|
async fn fetch_one(self) -> Result<Workspace, AccessError> {
|
|
let spec = self.build()?;
|
|
async {
|
|
debug!("accessing workspace");
|
|
let workspace = if let Some(value) = Workspace::with_id(spec.id)
|
|
.fetch_optional(spec.using_app_db)
|
|
.await?
|
|
{
|
|
value
|
|
} else {
|
|
debug!("workspace access denied: workspace not found");
|
|
clear_workspace_membership(spec.id, spec.as_actor, spec.using_app_db).await?;
|
|
return Err(AccessError::NotFound);
|
|
};
|
|
debug!("workspace found");
|
|
|
|
let actor_rolname = match spec.as_actor {
|
|
Actor::Bypass => None,
|
|
Actor::User(user_id) => Some(format!(
|
|
"{ROLE_PREFIX_USER}{user_id}",
|
|
user_id = user_id.simple()
|
|
)),
|
|
};
|
|
|
|
if let Some(actor_rolname) = actor_rolname {
|
|
// Verify database CONNECT permissions.
|
|
let pg_db = PgDatabase::current()
|
|
.fetch_one(spec.using_workspace_client)
|
|
.await?;
|
|
if !pg_db.datacl.unwrap_or_default().iter().any(|acl| {
|
|
// Currently database connect permissions are always granted
|
|
// directly, though this may change in the future.
|
|
// TODO: Generalize to inherited roles
|
|
acl.grantee == actor_rolname
|
|
&& acl
|
|
.privileges
|
|
.iter()
|
|
.any(|privilege| privilege.privilege == PgPrivilegeType::Connect)
|
|
}) {
|
|
debug!("workspace access denied: actor lacks postgres connect privilege");
|
|
clear_workspace_membership(spec.id, spec.as_actor, spec.using_app_db).await?;
|
|
return Err(AccessError::NotFound);
|
|
}
|
|
}
|
|
|
|
debug!("workspace access approved");
|
|
cache_workspace_membership(spec.id, spec.as_actor, spec.using_app_db).await?;
|
|
Ok(workspace)
|
|
}
|
|
.instrument(info_span!(
|
|
"WorkspaceAccessor::fetch_one()",
|
|
workspace_id = spec.id.to_string(),
|
|
actor = spec.as_actor.to_string(),
|
|
))
|
|
.await
|
|
}
|
|
}
|
|
|
|
async fn clear_workspace_membership(
|
|
workspace_id: Uuid,
|
|
actor: Actor,
|
|
app_db: &mut AppDbClient,
|
|
) -> QueryResult<()> {
|
|
if let Actor::User(user_id) = actor {
|
|
WorkspaceMembership::delete()
|
|
.user_id(user_id)
|
|
.workspace_id(workspace_id)
|
|
.execute(app_db)
|
|
.await?;
|
|
debug!("cleared workspace membership cache entry if present");
|
|
} else {
|
|
debug!("no action for workspace membership cache: actor is \"{actor}\"");
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
async fn cache_workspace_membership(
|
|
workspace_id: Uuid,
|
|
actor: Actor,
|
|
app_db: &mut AppDbClient,
|
|
) -> QueryResult<()> {
|
|
if let Actor::User(user_id) = actor {
|
|
WorkspaceMembership::upsert()
|
|
.user_id(user_id)
|
|
.workspace_id(workspace_id)
|
|
.execute(app_db)
|
|
.await?;
|
|
debug!("cached workspace membership");
|
|
} else {
|
|
debug!("no action for workspace membership cache: actor is \"{actor}\"");
|
|
}
|
|
Ok(())
|
|
}
|