forked from 2sys/phonograph
208 lines
7.7 KiB
Rust
208 lines
7.7 KiB
Rust
|
|
use std::fmt::Display;
|
||
|
|
|
||
|
|
use derive_builder::Builder;
|
||
|
|
use phono_backends::client::WorkspaceClient;
|
||
|
|
use phono_models::{
|
||
|
|
invite::Invite, invite_rel_permission::RelPermissionKind, service_cred::ServiceCred, user::User,
|
||
|
|
};
|
||
|
|
|
||
|
|
/// Permissions are not always granted to users, but may also be applied to
|
||
|
|
/// future users (via invites) or service credentials. This enum spans all
|
||
|
|
/// possible targets to provide a unified representation for
|
||
|
|
/// [`PermissionsEditor`].
|
||
|
|
#[derive(Clone, Debug)]
|
||
|
|
pub(crate) enum Grantee {
|
||
|
|
User(User),
|
||
|
|
Invite(Invite),
|
||
|
|
ServiceCred(ServiceCred),
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Display for Grantee {
|
||
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||
|
|
match self {
|
||
|
|
Self::User(user) => write!(f, "usr_{0}", user.id.simple()),
|
||
|
|
Self::Invite(invite) => write!(f, "invite_{0}", invite.id.simple()),
|
||
|
|
Self::ServiceCred(cred) => write!(f, "svc_{0}", cred.id.simple()),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, thiserror::Error)]
|
||
|
|
#[error("mismatched PermissionsTarget variant")]
|
||
|
|
pub(crate) struct InvalidVariantError;
|
||
|
|
|
||
|
|
impl TryFrom<Grantee> for User {
|
||
|
|
type Error = InvalidVariantError;
|
||
|
|
|
||
|
|
fn try_from(value: Grantee) -> Result<Self, Self::Error> {
|
||
|
|
if let Grantee::User(user) = value {
|
||
|
|
Ok(user)
|
||
|
|
} else {
|
||
|
|
Err(InvalidVariantError)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl TryFrom<Grantee> for Invite {
|
||
|
|
type Error = InvalidVariantError;
|
||
|
|
|
||
|
|
fn try_from(value: Grantee) -> Result<Self, Self::Error> {
|
||
|
|
if let Grantee::Invite(invite) = value {
|
||
|
|
Ok(invite)
|
||
|
|
} else {
|
||
|
|
Err(InvalidVariantError)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl TryFrom<Grantee> for ServiceCred {
|
||
|
|
type Error = InvalidVariantError;
|
||
|
|
|
||
|
|
fn try_from(value: Grantee) -> Result<Self, Self::Error> {
|
||
|
|
if let Grantee::ServiceCred(cred) = value {
|
||
|
|
Ok(cred)
|
||
|
|
} else {
|
||
|
|
Err(InvalidVariantError)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Builder, Debug)]
|
||
|
|
#[builder(
|
||
|
|
build_fn(error = "QueryError", vis = ""),
|
||
|
|
pattern = "owned",
|
||
|
|
vis = "pub(crate)"
|
||
|
|
)]
|
||
|
|
#[builder_struct_attr(doc = r"
|
||
|
|
A generalized mechanism for assigning relation permissions to a [`Grantee`].
|
||
|
|
It will make a best effort to resolve the delta between
|
||
|
|
`target.current_perms` and `desired_perms`.
|
||
|
|
|
||
|
|
Note that when grantee is not an invite, the permissions held by the
|
||
|
|
provided [`WorkspaceClient`] determine which actual Postgres roles may be
|
||
|
|
granted or revoked. For this reason, the requested permissions update may
|
||
|
|
only be partially applied, even if the [`PermissionsUpdateBuilder::execute`]
|
||
|
|
call succeeds.
|
||
|
|
|
||
|
|
The update operation does not double-check the authenticity of
|
||
|
|
`target.current_perms`, so it is technically valid to include only the
|
||
|
|
relevant subset of current relation permissions, so long as the difference
|
||
|
|
between `target.current_perms` and `desired_perms` remains as intended. For
|
||
|
|
example, if permissions are being granted only, the caller may leave
|
||
|
|
`target.current_perms` empty. This caller behavior is not recommended, but
|
||
|
|
it is permitted in the spirit of laziness.
|
||
|
|
")]
|
||
|
|
struct PermissionsUpdate<'a> {
|
||
|
|
target: Grantee,
|
||
|
|
desired_perms: HashSet<RelPermission>,
|
||
|
|
grantor_id: Uuid,
|
||
|
|
using_app_db: &'a mut AppDbClient,
|
||
|
|
using_workspace_client: &'a mut WorkspaceClient,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl<'a> PermissionsUpdateBuilder<'a> {
|
||
|
|
pub(crate) async fn execute(self) -> Result<(), AppError> {
|
||
|
|
let PermissionsUpdate {
|
||
|
|
target,
|
||
|
|
desired_perms: roles,
|
||
|
|
grantor_id,
|
||
|
|
using_app_db: app_db,
|
||
|
|
using_workspace_client: workspace_client,
|
||
|
|
} = self.build()?;
|
||
|
|
|
||
|
|
let perms_to_revoke: HashSet<_> = target.current_perms.difference(&roles).collect();
|
||
|
|
debug!(
|
||
|
|
"revoking {roles} from {target}",
|
||
|
|
roles = perms_to_revoke
|
||
|
|
.iter()
|
||
|
|
.map(|role| role.to_string())
|
||
|
|
.collect::<Vec<_>>()
|
||
|
|
.join(", "),
|
||
|
|
);
|
||
|
|
let perms_to_grant: HashSet<_> = roles.difference(&target.current_perms).collect();
|
||
|
|
debug!(
|
||
|
|
"granting {roles} to {target}",
|
||
|
|
roles = perms_to_grant
|
||
|
|
.iter()
|
||
|
|
.map(|role| role.to_string())
|
||
|
|
.collect::<Vec<_>>()
|
||
|
|
.join(", "),
|
||
|
|
);
|
||
|
|
|
||
|
|
match target.kind {
|
||
|
|
GranteeKind::Invite(invite) => {
|
||
|
|
let ids_to_delete: Vec<Uuid> = InviteRelPermission::belonging_to_invite(invite.id)
|
||
|
|
.fetch_all(app_db)
|
||
|
|
.await?
|
||
|
|
.into_iter()
|
||
|
|
.filter(|invite_perm| {
|
||
|
|
perms_to_revoke.iter().any(|perm| {
|
||
|
|
perm.rel_oid == invite_perm.rel_oid
|
||
|
|
&& perm.kind == invite_perm.permission
|
||
|
|
})
|
||
|
|
})
|
||
|
|
.map(|rel_invite| rel_invite.id)
|
||
|
|
.collect();
|
||
|
|
debug!("deleting {0} invite rel permissions", ids_to_delete.len());
|
||
|
|
InviteRelPermission::delete_by_ids(ids_to_delete)
|
||
|
|
.execute(app_db)
|
||
|
|
.await?;
|
||
|
|
for perm in perms_to_grant {
|
||
|
|
debug!("granting {perm}");
|
||
|
|
InviteRelPermission::upsert()
|
||
|
|
.invite_id(invite.id)
|
||
|
|
.rel_oid(perm.rel_oid)
|
||
|
|
.permission(perm.kind)
|
||
|
|
.granted_by(grantor_id)
|
||
|
|
.execute(app_db)
|
||
|
|
.await?;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
GranteeKind::ServiceCred(ServiceCred { id, .. })
|
||
|
|
| GranteeKind::User(User { id, .. }) => {
|
||
|
|
let target_rolname = match target.kind {
|
||
|
|
GranteeKind::ServiceCred(cred) => cred.rolname.clone(),
|
||
|
|
GranteeKind::User(_) => format!("{ROLE_PREFIX_USER}{id}", id = id.simple()),
|
||
|
|
GranteeKind::Invite(_) => unreachable!("outer match arm excludes variant"),
|
||
|
|
};
|
||
|
|
for perm in perms_to_revoke {
|
||
|
|
let rel = PgClass::with_oid(perm.rel_oid)
|
||
|
|
.fetch_one(workspace_client)
|
||
|
|
.await?;
|
||
|
|
let target_rolname_esc = escape_identifier(&target_rolname);
|
||
|
|
let perm_rolname = match perm.kind {
|
||
|
|
RelPermissionKind::Owner => get_owner_role(&rel),
|
||
|
|
RelPermissionKind::Reader => get_reader_role(&rel),
|
||
|
|
RelPermissionKind::Writer => get_writer_role(&rel),
|
||
|
|
}?;
|
||
|
|
let perm_rolname_esc = escape_identifier(&perm_rolname);
|
||
|
|
query(&format!(
|
||
|
|
"revoke {perm_rolname_esc} from {target_rolname_esc}"
|
||
|
|
))
|
||
|
|
.execute(workspace_client.get_conn())
|
||
|
|
.await?;
|
||
|
|
}
|
||
|
|
for perm in perms_to_grant {
|
||
|
|
let rel = PgClass::with_oid(perm.rel_oid)
|
||
|
|
.fetch_one(workspace_client)
|
||
|
|
.await?;
|
||
|
|
let target_rolname_esc = escape_identifier(&target_rolname);
|
||
|
|
let perm_rolname = match perm.kind {
|
||
|
|
RelPermissionKind::Owner => get_owner_role(&rel),
|
||
|
|
RelPermissionKind::Reader => get_reader_role(&rel),
|
||
|
|
RelPermissionKind::Writer => get_writer_role(&rel),
|
||
|
|
}?;
|
||
|
|
let perm_rolname_esc = escape_identifier(&perm_rolname);
|
||
|
|
query(&format!(
|
||
|
|
"grant {perm_rolname_esc} to {target_rolname_esc} with admin option"
|
||
|
|
))
|
||
|
|
.execute(workspace_client.get_conn())
|
||
|
|
.await?;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
}
|