ability to grant permissions to service creds
This commit is contained in:
parent
97b5ccc064
commit
b12127d220
8 changed files with 305 additions and 40 deletions
|
|
@ -90,7 +90,7 @@ impl<'a> NavigatorPage for WorkspacePage<'a> {
|
||||||
format!(
|
format!(
|
||||||
"{root_path}/w/{workspace_id}/{suffix}",
|
"{root_path}/w/{workspace_id}/{suffix}",
|
||||||
root_path = self.root_path,
|
root_path = self.root_path,
|
||||||
workspace_id = self.workspace_id,
|
workspace_id = self.workspace_id.simple(),
|
||||||
suffix = self.suffix.unwrap_or_default(),
|
suffix = self.suffix.unwrap_or_default(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -168,7 +168,7 @@ impl<'a> NavigatorPage for RelPage<'a> {
|
||||||
root_path = self.root_path,
|
root_path = self.root_path,
|
||||||
workspace_id = self.workspace_id.simple(),
|
workspace_id = self.workspace_id.simple(),
|
||||||
rel_oid = self.rel_oid.0,
|
rel_oid = self.rel_oid.0,
|
||||||
suffix = self.suffix.clone().unwrap_or_default(),
|
suffix = self.suffix.unwrap_or_default(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,11 @@ use sqlx::{postgres::types::Oid, prelude::FromRow, query_as};
|
||||||
|
|
||||||
use crate::errors::AppError;
|
use crate::errors::AppError;
|
||||||
|
|
||||||
pub(crate) const ROLE_PREFIX_USER: &str = "usr_";
|
|
||||||
pub(crate) const ROLE_PREFIX_SERVICE_CRED: &str = "svc_";
|
pub(crate) const ROLE_PREFIX_SERVICE_CRED: &str = "svc_";
|
||||||
pub(crate) const ROLE_PREFIX_TABLE_OWNER: &str = "tbo_";
|
pub(crate) const ROLE_PREFIX_TABLE_OWNER: &str = "tbo_";
|
||||||
pub(crate) const ROLE_PREFIX_TABLE_READER: &str = "tbr_";
|
pub(crate) const ROLE_PREFIX_TABLE_READER: &str = "tbr_";
|
||||||
pub(crate) const ROLE_PREFIX_TABLE_WRITER: &str = "tbw_";
|
pub(crate) const ROLE_PREFIX_TABLE_WRITER: &str = "tbw_";
|
||||||
|
pub(crate) const ROLE_PREFIX_USER: &str = "usr_";
|
||||||
pub(crate) const SERVICE_CRED_SUFFIX_LEN: usize = 8;
|
pub(crate) const SERVICE_CRED_SUFFIX_LEN: usize = 8;
|
||||||
pub(crate) const SERVICE_CRED_CONN_LIMIT: usize = 4;
|
pub(crate) const SERVICE_CRED_CONN_LIMIT: usize = 4;
|
||||||
|
|
||||||
|
|
@ -29,6 +29,7 @@ fn get_table_role(
|
||||||
role_prefix: &str,
|
role_prefix: &str,
|
||||||
) -> Result<String, AppError> {
|
) -> Result<String, AppError> {
|
||||||
let mut roles: Vec<String> = vec![];
|
let mut roles: Vec<String> = vec![];
|
||||||
|
dbg!(&relacl);
|
||||||
for acl_item in relacl.ok_or(anyhow!("acl not present on class"))? {
|
for acl_item in relacl.ok_or(anyhow!("acl not present on class"))? {
|
||||||
if acl_item.grantee.starts_with(role_prefix) {
|
if acl_item.grantee.starts_with(role_prefix) {
|
||||||
let privileges_set: HashSet<PgPrivilegeType> = acl_item
|
let privileges_set: HashSet<PgPrivilegeType> = acl_item
|
||||||
|
|
@ -56,9 +57,9 @@ fn get_table_role(
|
||||||
/// table permissions directly granted to it. Returns an error if no matching
|
/// 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
|
/// role is found, and panics if a role is found with excess permissions
|
||||||
/// granted to it directly.
|
/// granted to it directly.
|
||||||
pub(crate) fn get_reader_role(rel: PgClass) -> Result<String, AppError> {
|
pub(crate) fn get_reader_role(rel: &PgClass) -> Result<String, AppError> {
|
||||||
get_table_role(
|
get_table_role(
|
||||||
rel.relacl,
|
rel.relacl.clone(),
|
||||||
[PgPrivilegeType::Select].into(),
|
[PgPrivilegeType::Select].into(),
|
||||||
[
|
[
|
||||||
PgPrivilegeType::Insert,
|
PgPrivilegeType::Insert,
|
||||||
|
|
@ -76,9 +77,9 @@ pub(crate) fn get_reader_role(rel: PgClass) -> Result<String, AppError> {
|
||||||
/// table permissions directly granted to it. Returns an error if no matching
|
/// 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
|
/// role is found, and panics if a role is found with excess permissions
|
||||||
/// granted to it directly.
|
/// granted to it directly.
|
||||||
pub(crate) fn get_writer_role(rel: PgClass) -> Result<String, AppError> {
|
pub(crate) fn get_writer_role(rel: &PgClass) -> Result<String, AppError> {
|
||||||
get_table_role(
|
get_table_role(
|
||||||
rel.relacl,
|
rel.relacl.clone(),
|
||||||
[PgPrivilegeType::Delete, PgPrivilegeType::Truncate].into(),
|
[PgPrivilegeType::Delete, PgPrivilegeType::Truncate].into(),
|
||||||
[PgPrivilegeType::Select].into(),
|
[PgPrivilegeType::Select].into(),
|
||||||
ROLE_PREFIX_TABLE_WRITER,
|
ROLE_PREFIX_TABLE_WRITER,
|
||||||
|
|
@ -101,7 +102,7 @@ impl RoleDisplay {
|
||||||
pub async fn from_rolname(
|
pub async fn from_rolname(
|
||||||
rolname: &str,
|
rolname: &str,
|
||||||
client: &mut WorkspaceClient,
|
client: &mut WorkspaceClient,
|
||||||
) -> sqlx::Result<Option<RoleDisplay>> {
|
) -> sqlx::Result<Option<Self>> {
|
||||||
if rolname.starts_with(ROLE_PREFIX_TABLE_OWNER)
|
if rolname.starts_with(ROLE_PREFIX_TABLE_OWNER)
|
||||||
|| rolname.starts_with(ROLE_PREFIX_TABLE_READER)
|
|| rolname.starts_with(ROLE_PREFIX_TABLE_READER)
|
||||||
|| rolname.starts_with(ROLE_PREFIX_TABLE_WRITER)
|
|| rolname.starts_with(ROLE_PREFIX_TABLE_WRITER)
|
||||||
|
|
@ -123,6 +124,7 @@ impl RoleDisplay {
|
||||||
from pg_class
|
from pg_class
|
||||||
)
|
)
|
||||||
where grantee = $1::regrole::oid
|
where grantee = $1::regrole::oid
|
||||||
|
group by oid
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(rolname)
|
.bind(rolname)
|
||||||
|
|
@ -131,24 +133,24 @@ impl RoleDisplay {
|
||||||
assert!(rels.len() <= 1);
|
assert!(rels.len() <= 1);
|
||||||
Ok(rels.pop().map(|rel| {
|
Ok(rels.pop().map(|rel| {
|
||||||
if rolname.starts_with(ROLE_PREFIX_TABLE_OWNER) {
|
if rolname.starts_with(ROLE_PREFIX_TABLE_OWNER) {
|
||||||
RoleDisplay::TableOwner {
|
Self::TableOwner {
|
||||||
oid: rel.oid,
|
oid: rel.oid,
|
||||||
relname: rel.relname,
|
relname: rel.relname,
|
||||||
}
|
}
|
||||||
} else if rolname.starts_with(ROLE_PREFIX_TABLE_READER) {
|
} else if rolname.starts_with(ROLE_PREFIX_TABLE_READER) {
|
||||||
RoleDisplay::TableReader {
|
Self::TableReader {
|
||||||
oid: rel.oid,
|
oid: rel.oid,
|
||||||
relname: rel.relname,
|
relname: rel.relname,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
RoleDisplay::TableWriter {
|
Self::TableWriter {
|
||||||
oid: rel.oid,
|
oid: rel.oid,
|
||||||
relname: rel.relname,
|
relname: rel.relname,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
Ok(Some(RoleDisplay::Unknown {
|
Ok(Some(Self::Unknown {
|
||||||
rolname: rolname.to_owned(),
|
rolname: rolname.to_owned(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ pub(super) async fn post(
|
||||||
"grant insert ({col}), update ({col}) on table {ident} to {writer_role}",
|
"grant insert ({col}), update ({col}) on table {ident} to {writer_role}",
|
||||||
col = escape_identifier(&form.name),
|
col = escape_identifier(&form.name),
|
||||||
ident = class.get_identifier(),
|
ident = class.get_identifier(),
|
||||||
writer_role = escape_identifier(&get_writer_role(class.clone())?),
|
writer_role = escape_identifier(&get_writer_role(&class)?),
|
||||||
))
|
))
|
||||||
.execute(workspace_client.get_conn())
|
.execute(workspace_client.get_conn())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ mod add_service_credential_handler;
|
||||||
mod add_table_handler;
|
mod add_table_handler;
|
||||||
mod nav_handler;
|
mod nav_handler;
|
||||||
mod service_credentials_handler;
|
mod service_credentials_handler;
|
||||||
|
mod update_service_cred_permissions_handler;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
struct PathParams {
|
struct PathParams {
|
||||||
|
|
@ -43,9 +44,13 @@ pub(super) fn new_router() -> Router<App> {
|
||||||
.route("/{workspace_id}/add-table", post(add_table_handler::post))
|
.route("/{workspace_id}/add-table", post(add_table_handler::post))
|
||||||
.route_with_tsr("/{workspace_id}/nav/", get(nav_handler::get))
|
.route_with_tsr("/{workspace_id}/nav/", get(nav_handler::get))
|
||||||
.route_with_tsr(
|
.route_with_tsr(
|
||||||
"/{workspace_id}/service-credentials",
|
"/{workspace_id}/service-credentials/",
|
||||||
get(service_credentials_handler::get),
|
get(service_credentials_handler::get),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/{workspace_id}/service-credentials/{service_cred_id}/update-permissions",
|
||||||
|
post(update_service_cred_permissions_handler::post),
|
||||||
|
)
|
||||||
.nest(
|
.nest(
|
||||||
"/{workspace_id}/r/{rel_oid}",
|
"/{workspace_id}/r/{rel_oid}",
|
||||||
relations_single::new_router(),
|
relations_single::new_router(),
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use anyhow::anyhow;
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use axum::{
|
use axum::{
|
||||||
debug_handler,
|
debug_handler,
|
||||||
|
|
@ -6,7 +7,7 @@ use axum::{
|
||||||
};
|
};
|
||||||
use futures::{lock::Mutex, prelude::*, stream};
|
use futures::{lock::Mutex, prelude::*, stream};
|
||||||
use interim_models::{service_cred::ServiceCred, workspace::Workspace};
|
use interim_models::{service_cred::ServiceCred, workspace::Workspace};
|
||||||
use interim_pgtypes::pg_role::RoleTree;
|
use interim_pgtypes::{pg_class::PgClass, pg_role::RoleTree};
|
||||||
use redact::Secret;
|
use redact::Secret;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
@ -21,6 +22,7 @@ use crate::{
|
||||||
user::CurrentUser,
|
user::CurrentUser,
|
||||||
workspace_nav::WorkspaceNav,
|
workspace_nav::WorkspaceNav,
|
||||||
workspace_pooler::{RoleAssignment, WorkspacePooler},
|
workspace_pooler::{RoleAssignment, WorkspacePooler},
|
||||||
|
workspace_utils::PHONO_TABLE_NAMESPACE,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
|
@ -60,6 +62,50 @@ pub(super) async fn get(
|
||||||
conn_string_redacted: String,
|
conn_string_redacted: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ServiceCredInfo {
|
||||||
|
fn is_reader_of(&self, relname: &str) -> bool {
|
||||||
|
self.member_of.iter().any(|role_display| {
|
||||||
|
if let RoleDisplay::TableReader {
|
||||||
|
relname: role_relname,
|
||||||
|
..
|
||||||
|
} = role_display
|
||||||
|
{
|
||||||
|
role_relname == relname
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_writer_of(&self, relname: &str) -> bool {
|
||||||
|
self.member_of.iter().any(|role_display| {
|
||||||
|
if let RoleDisplay::TableWriter {
|
||||||
|
relname: role_relname,
|
||||||
|
..
|
||||||
|
} = role_display
|
||||||
|
{
|
||||||
|
role_relname == relname
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_owner_of(&self, relname: &str) -> bool {
|
||||||
|
self.member_of.iter().any(|role_display| {
|
||||||
|
if let RoleDisplay::TableOwner {
|
||||||
|
relname: role_relname,
|
||||||
|
..
|
||||||
|
} = role_display
|
||||||
|
{
|
||||||
|
role_relname == relname
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let service_cred_info = stream::iter(
|
let service_cred_info = stream::iter(
|
||||||
ServiceCred::belonging_to_user(user.id)
|
ServiceCred::belonging_to_user(user.id)
|
||||||
.fetch_all(&mut app_db)
|
.fetch_all(&mut app_db)
|
||||||
|
|
@ -67,28 +113,31 @@ pub(super) async fn get(
|
||||||
)
|
)
|
||||||
.then(async |cred| {
|
.then(async |cred| {
|
||||||
let member_of: Vec<RoleDisplay> = stream::iter({
|
let member_of: Vec<RoleDisplay> = stream::iter({
|
||||||
|
// Guard must be assigned to a local variable,
|
||||||
|
// lest the mutex become deadlocked.
|
||||||
let mut locked_client = workspace_client.lock().await;
|
let mut locked_client = workspace_client.lock().await;
|
||||||
let tree = RoleTree::granted_to_rolname(&cred.rolname)
|
RoleTree::granted_to_rolname(&cred.rolname)
|
||||||
.fetch_tree(&mut locked_client)
|
.fetch_tree(&mut locked_client)
|
||||||
.await?;
|
.await?
|
||||||
tree.unwrap().flatten_inherited()
|
.ok_or(anyhow!("listing roles for service cred: role tree is None"))?
|
||||||
|
.flatten_inherited()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|role| role.rolname != cred.rolname)
|
||||||
})
|
})
|
||||||
.then(async |role| {
|
.then(async |role| {
|
||||||
tracing::debug!("111");
|
|
||||||
let mut locked_client = workspace_client.lock().await;
|
let mut locked_client = workspace_client.lock().await;
|
||||||
RoleDisplay::from_rolname(&role.rolname, &mut locked_client).await
|
RoleDisplay::from_rolname(&role.rolname, &mut locked_client).await
|
||||||
})
|
})
|
||||||
.collect::<Vec<Result<_, _>>>()
|
.collect::<Vec<Result<_, _>>>()
|
||||||
.await
|
.await
|
||||||
.into_iter()
|
.into_iter()
|
||||||
// [`futures::stream::StreamExt::collect`] can only collect to types
|
// [`futures::stream::StreamExt::collect`]
|
||||||
// that implement [`Default`], so we must do result handling with the
|
// can only collect to types that implement [`Default`],
|
||||||
// sync version of `collect()`.
|
// so we must do result handling with the sync version of `collect()`.
|
||||||
.collect::<Result<Vec<_>, _>>()?
|
.collect::<Result<Vec<_>, _>>()?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flatten()
|
.flatten()
|
||||||
.collect();
|
.collect();
|
||||||
tracing::debug!("222");
|
|
||||||
let conn_string = cluster.conn_str_for_db(
|
let conn_string = cluster.conn_str_for_db(
|
||||||
&workspace.db_name,
|
&workspace.db_name,
|
||||||
Some((
|
Some((
|
||||||
|
|
@ -105,26 +154,33 @@ pub(super) async fn get(
|
||||||
})
|
})
|
||||||
.collect::<Vec<Result<ServiceCredInfo, AppError>>>()
|
.collect::<Vec<Result<ServiceCredInfo, AppError>>>()
|
||||||
.await
|
.await
|
||||||
// [`futures::stream::StreamExt::collect`] can only collect to types that
|
|
||||||
// implement [`Default`], so we must do result handling with the sync
|
|
||||||
// version of `collect()`.
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect::<Result<Vec<ServiceCredInfo>, AppError>>()?;
|
.collect::<Result<Vec<ServiceCredInfo>, AppError>>()?;
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "workspaces_single/service_credentials.html")]
|
#[template(path = "workspaces_single/service_credentials.html")]
|
||||||
struct ResponseTemplate {
|
struct ResponseTemplate {
|
||||||
|
all_rels: Vec<PgClass>,
|
||||||
service_cred_info: Vec<ServiceCredInfo>,
|
service_cred_info: Vec<ServiceCredInfo>,
|
||||||
settings: Settings,
|
settings: Settings,
|
||||||
workspace_nav: WorkspaceNav,
|
workspace_nav: WorkspaceNav,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut locked_client = workspace_client
|
||||||
|
.try_lock()
|
||||||
|
.ok_or(anyhow!("mutex should be unlocked"))?;
|
||||||
Ok(Html(
|
Ok(Html(
|
||||||
ResponseTemplate {
|
ResponseTemplate {
|
||||||
|
all_rels: PgClass::belonging_to_namespace(PHONO_TABLE_NAMESPACE)
|
||||||
|
.fetch_all(&mut locked_client)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.filter(|rel| rel.relkind == 'r' as i8)
|
||||||
|
.collect(),
|
||||||
workspace_nav: WorkspaceNav::builder()
|
workspace_nav: WorkspaceNav::builder()
|
||||||
.navigator(navigator)
|
.navigator(navigator)
|
||||||
.workspace(workspace.clone())
|
.workspace(workspace.clone())
|
||||||
.populate_rels(&mut app_db, &mut *workspace_client.lock().await)
|
.populate_rels(&mut app_db, &mut locked_client)
|
||||||
.await?
|
.await?
|
||||||
.build()?,
|
.build()?,
|
||||||
service_cred_info,
|
service_cred_info,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
debug_handler,
|
||||||
|
extract::{Path, State},
|
||||||
|
response::IntoResponse,
|
||||||
|
};
|
||||||
|
use axum_extra::extract::Form;
|
||||||
|
use futures::{lock::Mutex, prelude::*, stream};
|
||||||
|
use interim_models::service_cred::ServiceCred;
|
||||||
|
use interim_pgtypes::{escape_identifier, pg_class::PgClass};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use sqlx::query;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app::{App, AppDbConn},
|
||||||
|
errors::AppError,
|
||||||
|
navigator::{Navigator, NavigatorPage},
|
||||||
|
roles::{get_reader_role, get_writer_role},
|
||||||
|
user::CurrentUser,
|
||||||
|
workspace_pooler::{RoleAssignment, WorkspacePooler},
|
||||||
|
workspace_utils::PHONO_TABLE_NAMESPACE,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
pub(super) struct PathParams {
|
||||||
|
workspace_id: Uuid,
|
||||||
|
service_cred_id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// HTTP POST handler for assigning
|
||||||
|
#[debug_handler(state = App)]
|
||||||
|
pub(super) async fn post(
|
||||||
|
State(mut pooler): State<WorkspacePooler>,
|
||||||
|
AppDbConn(mut app_db): AppDbConn,
|
||||||
|
CurrentUser(user): CurrentUser,
|
||||||
|
navigator: Navigator,
|
||||||
|
Path(PathParams {
|
||||||
|
workspace_id,
|
||||||
|
service_cred_id,
|
||||||
|
}): Path<PathParams>,
|
||||||
|
Form(form): Form<HashMap<String, Vec<String>>>,
|
||||||
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
|
// FIXME: csrf, auth
|
||||||
|
|
||||||
|
let workspace_client = Mutex::new(
|
||||||
|
pooler
|
||||||
|
.acquire_for(workspace_id, RoleAssignment::User(user.id))
|
||||||
|
.await?,
|
||||||
|
);
|
||||||
|
|
||||||
|
let all_rels = {
|
||||||
|
let mut locked_client = workspace_client.lock().await;
|
||||||
|
PgClass::belonging_to_namespace(PHONO_TABLE_NAMESPACE)
|
||||||
|
.fetch_all(&mut locked_client)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.filter(|rel| rel.relkind == 'r' as i8)
|
||||||
|
};
|
||||||
|
|
||||||
|
let cred = ServiceCred::with_id(service_cred_id)
|
||||||
|
.fetch_one(&mut app_db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
stream::iter(all_rels)
|
||||||
|
.then(async |rel| -> Result<(), AppError> {
|
||||||
|
let rolname_reader = get_reader_role(&rel)?;
|
||||||
|
let rolname_writer = get_writer_role(&rel)?;
|
||||||
|
|
||||||
|
let roles_to_grant: HashSet<_> = form
|
||||||
|
.get(&rel.oid.0.to_string())
|
||||||
|
.unwrap_or(&vec![])
|
||||||
|
.iter()
|
||||||
|
.flat_map(|value| match value.as_str() {
|
||||||
|
"reader" => Some(&rolname_reader),
|
||||||
|
"writer" => Some(&rolname_writer),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let all_roles = HashSet::from([&rolname_reader, &rolname_writer]);
|
||||||
|
let roles_to_revoke: HashSet<_> = all_roles.difference(&roles_to_grant).collect();
|
||||||
|
|
||||||
|
if !roles_to_revoke.is_empty() {
|
||||||
|
let mut locked_client = workspace_client.lock().await;
|
||||||
|
let roles_to_revoke_snippet = roles_to_revoke
|
||||||
|
.into_iter()
|
||||||
|
.map(|value| escape_identifier(value))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ");
|
||||||
|
query(&format!(
|
||||||
|
"revoke {roles_to_revoke_snippet} from {rolname_esc}",
|
||||||
|
rolname_esc = escape_identifier(&cred.rolname),
|
||||||
|
))
|
||||||
|
.execute(locked_client.get_conn())
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !roles_to_grant.is_empty() {
|
||||||
|
let mut locked_client = workspace_client.lock().await;
|
||||||
|
let roles_to_grant_snippet = roles_to_grant
|
||||||
|
.into_iter()
|
||||||
|
.map(|value| escape_identifier(value))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ");
|
||||||
|
query(&format!(
|
||||||
|
"grant {roles_to_grant_snippet} to {rolname_esc}",
|
||||||
|
rolname_esc = escape_identifier(&cred.rolname),
|
||||||
|
))
|
||||||
|
.execute(locked_client.get_conn())
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Result<Vec<_>, AppError>>()?;
|
||||||
|
|
||||||
|
Ok(navigator
|
||||||
|
.workspace_page()
|
||||||
|
.workspace_id(workspace_id)
|
||||||
|
.suffix("service-credentials/")
|
||||||
|
.build()?
|
||||||
|
.redirect_to())
|
||||||
|
}
|
||||||
|
|
@ -24,9 +24,9 @@
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Connection String</th>
|
<th scope="col">Connection String</th>
|
||||||
<th>Permissions</th>
|
<th scope="col">Permissions</th>
|
||||||
<th>Actions</th>
|
<th scope="col">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
@ -45,6 +45,70 @@
|
||||||
</slot>
|
</slot>
|
||||||
</copy-source>
|
</copy-source>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
{% for role_display in cred.member_of %}
|
||||||
|
{{ role_display | safe }}
|
||||||
|
{% endfor %}
|
||||||
|
<button
|
||||||
|
popovertarget="permissions-editor-{{ cred.service_cred.rolname }}"
|
||||||
|
popovertargetaction="toggle"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<dialog
|
||||||
|
class="dialog padded--lg"
|
||||||
|
id="permissions-editor-{{ cred.service_cred.rolname }}"
|
||||||
|
popover="auto"
|
||||||
|
>
|
||||||
|
<form action="{{ cred.service_cred.id.simple() }}/update-permissions" method="post">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Table</th>
|
||||||
|
<th scope="col">Reader</th>
|
||||||
|
<th scope="col">Writer</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for rel in all_rels %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ rel.relname }}</td>
|
||||||
|
<td style="text-align: center;">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="{{ rel.oid.0 }}"
|
||||||
|
value="reader"
|
||||||
|
{%- if cred.is_reader_of(rel.relname) %}
|
||||||
|
checked="true"
|
||||||
|
{%- endif %}
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
<td style="text-align: center;">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="{{ rel.oid.0 }}"
|
||||||
|
value="writer"
|
||||||
|
{%- if cred.is_writer_of(rel.relname) %}
|
||||||
|
checked="true"
|
||||||
|
{%- endif %}
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<button
|
||||||
|
class="button--primary"
|
||||||
|
style="margin-top: 16px;"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
</td>
|
||||||
|
<td></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
||||||
|
|
@ -247,3 +247,14 @@ button, input[type="submit"] {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dialog:popover-open {
|
||||||
|
@include globals.rounded;
|
||||||
|
|
||||||
|
background: #fff;
|
||||||
|
border: globals.$popover-border;
|
||||||
|
box-shadow: globals.$popover-shadow;
|
||||||
|
display: block;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue