implement table reader/writer/owner roles
This commit is contained in:
parent
fb6b0f0ed8
commit
44ccb2791c
5 changed files with 141 additions and 18 deletions
14
README.md
14
README.md
|
|
@ -48,11 +48,22 @@ database should always be confirmed.
|
|||
|
||||
#### Accessing the `phono` schema
|
||||
|
||||
`GRANT USAGE ON <schema> to <role>;`
|
||||
`GRANT USAGE ON <schema> TO <role>;`
|
||||
|
||||
This permission is granted when initially creating the workspace, as well as
|
||||
when accepting an invitation to a table.
|
||||
|
||||
### Creating, updating, and deleting columns
|
||||
|
||||
`GRANT <table owner role> TO <role>;`
|
||||
|
||||
This permission is granted when initially creating the table, as well as when
|
||||
accepting an invitation to a table, if the invitation includes "owner"
|
||||
permissions.
|
||||
|
||||
**This permission is only used via the web UI and must not be granted to service
|
||||
credentials, lest users alter table structure in unsupported ways.**
|
||||
|
||||
#### Reading table data
|
||||
|
||||
`GRANT SELECT ON <table> TO <role>;`
|
||||
|
|
@ -92,7 +103,6 @@ is added; this is simplified by maintaining a single "writer" role per table.
|
|||
### Actions Facilitated by Root
|
||||
|
||||
- Creating tables
|
||||
- Creating, updating, and deleting columns
|
||||
|
||||
### Service Credentials
|
||||
|
||||
|
|
|
|||
|
|
@ -10,10 +10,18 @@ use crate::errors::AppError;
|
|||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub(crate) struct PresentationForm {
|
||||
pub(crate) presentation_tag: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub(crate) dropdown_option_colors: Vec<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub(crate) dropdown_option_values: Vec<String>,
|
||||
pub(crate) text_input_mode: Option<String>,
|
||||
pub(crate) timestamp_format: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub(crate) text_input_mode: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub(crate) timestamp_format: String,
|
||||
}
|
||||
|
||||
impl TryFrom<PresentationForm> for Presentation {
|
||||
|
|
@ -35,18 +43,14 @@ impl TryFrom<PresentationForm> for Presentation {
|
|||
.collect(),
|
||||
},
|
||||
Presentation::Text { .. } => Presentation::Text {
|
||||
input_mode: form_value
|
||||
.text_input_mode
|
||||
.clone()
|
||||
.map(|value| TextInputMode::try_from(value.as_str()))
|
||||
.transpose()?
|
||||
.unwrap_or_default(),
|
||||
input_mode: TextInputMode::try_from(form_value.text_input_mode.as_str())?,
|
||||
},
|
||||
Presentation::Timestamp { .. } => Presentation::Timestamp {
|
||||
format: form_value
|
||||
.timestamp_format
|
||||
.clone()
|
||||
.unwrap_or(RFC_3339_S.to_owned()),
|
||||
format: if form_value.timestamp_format.is_empty() {
|
||||
RFC_3339_S.to_owned()
|
||||
} else {
|
||||
form_value.timestamp_format
|
||||
},
|
||||
},
|
||||
Presentation::Uuid { .. } => Presentation::Uuid {},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ use crate::{
|
|||
presentation_form::PresentationForm,
|
||||
user::CurrentUser,
|
||||
workspace_pooler::{RoleAssignment, WorkspacePooler},
|
||||
workspace_utils::{get_reader_role, get_writer_role},
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
@ -86,6 +87,14 @@ pub(super) async fn post(
|
|||
))
|
||||
.execute(workspace_client.get_conn())
|
||||
.await?;
|
||||
query(&format!(
|
||||
"grant insert ({col}), update ({col}) on table {ident} to {writer_role}",
|
||||
col = escape_identifier(&form.name),
|
||||
ident = class.get_identifier(),
|
||||
writer_role = escape_identifier(&get_writer_role(class.clone())?),
|
||||
))
|
||||
.execute(workspace_client.get_conn())
|
||||
.await?;
|
||||
|
||||
Field::insert()
|
||||
.portal_id(portal.id)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use axum::{
|
|||
extract::{Path, State},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use interim_pgtypes::{escape_identifier, pg_class::PgClass};
|
||||
use interim_pgtypes::escape_identifier;
|
||||
use serde::Deserialize;
|
||||
use sqlx::query;
|
||||
use uuid::Uuid;
|
||||
|
|
@ -14,6 +14,9 @@ use crate::{
|
|||
settings::Settings,
|
||||
user::CurrentUser,
|
||||
workspace_pooler::{RoleAssignment, WorkspacePooler},
|
||||
workspace_utils::{
|
||||
TABLE_OWNER_ROLE_PREFIX, TABLE_READER_ROLE_PREFIX, TABLE_WRITER_ROLE_PREFIX,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
@ -55,9 +58,14 @@ pub(super) async fn post(
|
|||
user_id = user.id.simple()
|
||||
);
|
||||
let rolname_uuid = Uuid::new_v4().simple();
|
||||
let rolname_table_reader = format!("table_reader_{rolname_uuid}");
|
||||
let rolname_table_writer = format!("table_writer_{rolname_uuid}");
|
||||
for rolname in [&rolname_table_reader, &rolname_table_writer] {
|
||||
let rolname_table_owner = format!("{TABLE_OWNER_ROLE_PREFIX}{rolname_uuid}");
|
||||
let rolname_table_reader = format!("{TABLE_READER_ROLE_PREFIX}{rolname_uuid}");
|
||||
let rolname_table_writer = format!("{TABLE_WRITER_ROLE_PREFIX}{rolname_uuid}");
|
||||
for rolname in [
|
||||
&rolname_table_owner,
|
||||
&rolname_table_reader,
|
||||
&rolname_table_writer,
|
||||
] {
|
||||
query(&format!("create role {0}", escape_identifier(rolname)))
|
||||
.execute(root_client.get_conn())
|
||||
.await?;
|
||||
|
|
@ -69,6 +77,7 @@ pub(super) async fn post(
|
|||
.execute(root_client.get_conn())
|
||||
.await?;
|
||||
}
|
||||
|
||||
query(&format!(
|
||||
r#"
|
||||
create table {0}.{1} (
|
||||
|
|
@ -82,6 +91,14 @@ create table {0}.{1} (
|
|||
))
|
||||
.execute(root_client.get_conn())
|
||||
.await?;
|
||||
query(&format!(
|
||||
"alter table {nsp}.{ident} owner to {owner}",
|
||||
nsp = escape_identifier(&settings.phono_table_namespace),
|
||||
ident = escape_identifier(&table_name),
|
||||
owner = escape_identifier(&rolname_table_owner),
|
||||
))
|
||||
.execute(root_client.get_conn())
|
||||
.await?;
|
||||
query(&format!(
|
||||
"grant select on {0}.{1} to {2}",
|
||||
escape_identifier(&settings.phono_table_namespace),
|
||||
|
|
@ -90,6 +107,14 @@ create table {0}.{1} (
|
|||
))
|
||||
.execute(root_client.get_conn())
|
||||
.await?;
|
||||
query(&format!(
|
||||
"grant delete, truncate on {0}.{1} to {2}",
|
||||
escape_identifier(&settings.phono_table_namespace),
|
||||
escape_identifier(&table_name),
|
||||
escape_identifier(&rolname_table_writer),
|
||||
))
|
||||
.execute(root_client.get_conn())
|
||||
.await?;
|
||||
|
||||
Ok(navigator.workspace_page(workspace_id).redirect_to())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,23 @@
|
|||
//! the [`interim_models::workspace`] module, which is also used extensively
|
||||
//! across the server code.
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use interim_models::{client::AppDbClient, portal::Portal};
|
||||
use interim_pgtypes::{
|
||||
client::WorkspaceClient,
|
||||
pg_acl::{PgAclItem, PgPrivilegeType},
|
||||
pg_class::{PgClass, PgRelKind},
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::errors::AppError;
|
||||
|
||||
pub(crate) const TABLE_OWNER_ROLE_PREFIX: &str = "table_owner_";
|
||||
pub(crate) const TABLE_READER_ROLE_PREFIX: &str = "table_reader_";
|
||||
pub(crate) const TABLE_WRITER_ROLE_PREFIX: &str = "table_writer_";
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct RelationPortalSet {
|
||||
pub(crate) rel: PgClass,
|
||||
|
|
@ -45,3 +55,68 @@ pub(crate) async fn fetch_all_accessible_portals(
|
|||
|
||||
Ok(portal_sets)
|
||||
}
|
||||
|
||||
// TODO: custom error type
|
||||
// TODO: make params and result references
|
||||
fn get_table_role(
|
||||
relacl: Option<Vec<PgAclItem>>,
|
||||
required_privileges: HashSet<PgPrivilegeType>,
|
||||
disallowed_privileges: HashSet<PgPrivilegeType>,
|
||||
role_prefix: &str,
|
||||
) -> Result<String, AppError> {
|
||||
let mut roles: Vec<String> = vec![];
|
||||
for acl_item in relacl.ok_or(anyhow!("acl not present on class"))? {
|
||||
if acl_item.grantee.starts_with(role_prefix) {
|
||||
let privileges_set: HashSet<PgPrivilegeType> = acl_item
|
||||
.privileges
|
||||
.iter()
|
||||
.map(|privilege| privilege.privilege)
|
||||
.collect();
|
||||
assert!(
|
||||
privileges_set.intersection(&required_privileges).count()
|
||||
== required_privileges.len()
|
||||
);
|
||||
assert!(privileges_set.intersection(&disallowed_privileges).count() == 0);
|
||||
roles.push(acl_item.grantee)
|
||||
}
|
||||
}
|
||||
assert!(roles.len() == 1);
|
||||
Ok(roles
|
||||
.first()
|
||||
.expect("already asserted that `roles` has len 1")
|
||||
.clone())
|
||||
}
|
||||
|
||||
/// Returns the name of the "table_reader" role created by Phonograph for a
|
||||
/// particular workspace table. The role is assessed based on its name and the
|
||||
/// table permissions directly granted to it. Returns an error if no matching
|
||||
/// role is found, and panics if a role is found with excess permissions
|
||||
/// granted to it directly.
|
||||
pub(crate) fn get_reader_role(rel: PgClass) -> Result<String, AppError> {
|
||||
get_table_role(
|
||||
rel.relacl,
|
||||
[PgPrivilegeType::Select].into(),
|
||||
[
|
||||
PgPrivilegeType::Insert,
|
||||
PgPrivilegeType::Update,
|
||||
PgPrivilegeType::Delete,
|
||||
PgPrivilegeType::Truncate,
|
||||
]
|
||||
.into(),
|
||||
TABLE_READER_ROLE_PREFIX,
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the name of the "table_writer" role created by Phonograph for a
|
||||
/// particular workspace table. The role is assessed based on its name and the
|
||||
/// table permissions directly granted to it. Returns an error if no matching
|
||||
/// role is found, and panics if a role is found with excess permissions
|
||||
/// granted to it directly.
|
||||
pub(crate) fn get_writer_role(rel: PgClass) -> Result<String, AppError> {
|
||||
get_table_role(
|
||||
rel.relacl,
|
||||
[PgPrivilegeType::Delete, PgPrivilegeType::Truncate].into(),
|
||||
[PgPrivilegeType::Select].into(),
|
||||
TABLE_WRITER_ROLE_PREFIX,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue