1
0
Fork 0
forked from 2sys/phonograph
phonograph/phono-models/src/accessors/workspace.rs

139 lines
4.7 KiB
Rust
Raw Normal View History

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(())
}