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
|
#### 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
|
This permission is granted when initially creating the workspace, as well as
|
||||||
when accepting an invitation to a table.
|
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
|
#### Reading table data
|
||||||
|
|
||||||
`GRANT SELECT ON <table> TO <role>;`
|
`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
|
### Actions Facilitated by Root
|
||||||
|
|
||||||
- Creating tables
|
- Creating tables
|
||||||
- Creating, updating, and deleting columns
|
|
||||||
|
|
||||||
### Service Credentials
|
### Service Credentials
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,18 @@ use crate::errors::AppError;
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
pub(crate) struct PresentationForm {
|
pub(crate) struct PresentationForm {
|
||||||
pub(crate) presentation_tag: String,
|
pub(crate) presentation_tag: String,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
pub(crate) dropdown_option_colors: Vec<String>,
|
pub(crate) dropdown_option_colors: Vec<String>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
pub(crate) dropdown_option_values: Vec<String>,
|
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 {
|
impl TryFrom<PresentationForm> for Presentation {
|
||||||
|
|
@ -35,18 +43,14 @@ impl TryFrom<PresentationForm> for Presentation {
|
||||||
.collect(),
|
.collect(),
|
||||||
},
|
},
|
||||||
Presentation::Text { .. } => Presentation::Text {
|
Presentation::Text { .. } => Presentation::Text {
|
||||||
input_mode: form_value
|
input_mode: TextInputMode::try_from(form_value.text_input_mode.as_str())?,
|
||||||
.text_input_mode
|
|
||||||
.clone()
|
|
||||||
.map(|value| TextInputMode::try_from(value.as_str()))
|
|
||||||
.transpose()?
|
|
||||||
.unwrap_or_default(),
|
|
||||||
},
|
},
|
||||||
Presentation::Timestamp { .. } => Presentation::Timestamp {
|
Presentation::Timestamp { .. } => Presentation::Timestamp {
|
||||||
format: form_value
|
format: if form_value.timestamp_format.is_empty() {
|
||||||
.timestamp_format
|
RFC_3339_S.to_owned()
|
||||||
.clone()
|
} else {
|
||||||
.unwrap_or(RFC_3339_S.to_owned()),
|
form_value.timestamp_format
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Presentation::Uuid { .. } => Presentation::Uuid {},
|
Presentation::Uuid { .. } => Presentation::Uuid {},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ use crate::{
|
||||||
presentation_form::PresentationForm,
|
presentation_form::PresentationForm,
|
||||||
user::CurrentUser,
|
user::CurrentUser,
|
||||||
workspace_pooler::{RoleAssignment, WorkspacePooler},
|
workspace_pooler::{RoleAssignment, WorkspacePooler},
|
||||||
|
workspace_utils::{get_reader_role, get_writer_role},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
|
@ -86,6 +87,14 @@ pub(super) async fn post(
|
||||||
))
|
))
|
||||||
.execute(workspace_client.get_conn())
|
.execute(workspace_client.get_conn())
|
||||||
.await?;
|
.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()
|
Field::insert()
|
||||||
.portal_id(portal.id)
|
.portal_id(portal.id)
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
};
|
};
|
||||||
use interim_pgtypes::{escape_identifier, pg_class::PgClass};
|
use interim_pgtypes::escape_identifier;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use sqlx::query;
|
use sqlx::query;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
@ -14,6 +14,9 @@ use crate::{
|
||||||
settings::Settings,
|
settings::Settings,
|
||||||
user::CurrentUser,
|
user::CurrentUser,
|
||||||
workspace_pooler::{RoleAssignment, WorkspacePooler},
|
workspace_pooler::{RoleAssignment, WorkspacePooler},
|
||||||
|
workspace_utils::{
|
||||||
|
TABLE_OWNER_ROLE_PREFIX, TABLE_READER_ROLE_PREFIX, TABLE_WRITER_ROLE_PREFIX,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
|
@ -55,9 +58,14 @@ pub(super) async fn post(
|
||||||
user_id = user.id.simple()
|
user_id = user.id.simple()
|
||||||
);
|
);
|
||||||
let rolname_uuid = Uuid::new_v4().simple();
|
let rolname_uuid = Uuid::new_v4().simple();
|
||||||
let rolname_table_reader = format!("table_reader_{rolname_uuid}");
|
let rolname_table_owner = format!("{TABLE_OWNER_ROLE_PREFIX}{rolname_uuid}");
|
||||||
let rolname_table_writer = format!("table_writer_{rolname_uuid}");
|
let rolname_table_reader = format!("{TABLE_READER_ROLE_PREFIX}{rolname_uuid}");
|
||||||
for rolname in [&rolname_table_reader, &rolname_table_writer] {
|
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)))
|
query(&format!("create role {0}", escape_identifier(rolname)))
|
||||||
.execute(root_client.get_conn())
|
.execute(root_client.get_conn())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
@ -69,6 +77,7 @@ pub(super) async fn post(
|
||||||
.execute(root_client.get_conn())
|
.execute(root_client.get_conn())
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
query(&format!(
|
query(&format!(
|
||||||
r#"
|
r#"
|
||||||
create table {0}.{1} (
|
create table {0}.{1} (
|
||||||
|
|
@ -82,6 +91,14 @@ create table {0}.{1} (
|
||||||
))
|
))
|
||||||
.execute(root_client.get_conn())
|
.execute(root_client.get_conn())
|
||||||
.await?;
|
.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!(
|
query(&format!(
|
||||||
"grant select on {0}.{1} to {2}",
|
"grant select on {0}.{1} to {2}",
|
||||||
escape_identifier(&settings.phono_table_namespace),
|
escape_identifier(&settings.phono_table_namespace),
|
||||||
|
|
@ -90,6 +107,14 @@ create table {0}.{1} (
|
||||||
))
|
))
|
||||||
.execute(root_client.get_conn())
|
.execute(root_client.get_conn())
|
||||||
.await?;
|
.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())
|
Ok(navigator.workspace_page(workspace_id).redirect_to())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,23 @@
|
||||||
//! the [`interim_models::workspace`] module, which is also used extensively
|
//! the [`interim_models::workspace`] module, which is also used extensively
|
||||||
//! across the server code.
|
//! across the server code.
|
||||||
|
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use anyhow::anyhow;
|
||||||
use interim_models::{client::AppDbClient, portal::Portal};
|
use interim_models::{client::AppDbClient, portal::Portal};
|
||||||
use interim_pgtypes::{
|
use interim_pgtypes::{
|
||||||
client::WorkspaceClient,
|
client::WorkspaceClient,
|
||||||
|
pg_acl::{PgAclItem, PgPrivilegeType},
|
||||||
pg_class::{PgClass, PgRelKind},
|
pg_class::{PgClass, PgRelKind},
|
||||||
};
|
};
|
||||||
use uuid::Uuid;
|
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)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct RelationPortalSet {
|
pub(crate) struct RelationPortalSet {
|
||||||
pub(crate) rel: PgClass,
|
pub(crate) rel: PgClass,
|
||||||
|
|
@ -45,3 +55,68 @@ pub(crate) async fn fetch_all_accessible_portals(
|
||||||
|
|
||||||
Ok(portal_sets)
|
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