use anyhow::{Context, Result};
use sqlx::postgres::PgConnection;
use sqlx::postgres::types::Oid;
use std::collections::BTreeMap;
use tracing::info;
use super::dedup_preserving_order;
use super::exclusion::{Converted, Excluded, ExclusionReason, is_system_schema};
use super::oid_index::OidIndex;
use super::shared::{SharedCatalog, class};
use crate::catalog::DependsOn;
use crate::catalog::id::DbObjectId;
use crate::catalog::policy::{Policy, PolicyCommand};
#[derive(Debug, Clone)]
pub struct RawPolicy {
pub oid: Oid,
pub name: String,
pub table_oid: Oid,
pub table_namespace: Oid,
pub table_name: String,
pub command: String,
pub permissive: bool,
pub roles: Vec<String>,
pub using_expr: Option<String>,
pub with_check_expr: Option<String>,
}
#[derive(Debug, Clone)]
pub struct RawPolicyColumnDependency {
pub policy_oid: Oid,
pub relation_namespace: Oid,
pub relation_name: String,
pub relkind: String,
pub column_name: String,
}
#[derive(Debug, Clone)]
pub struct RawPolicyObjectDependency {
pub policy_oid: Oid,
pub relation_namespace: Option<Oid>,
pub relation_name: Option<String>,
pub relkind: Option<String>,
pub function_oid: Option<Oid>,
pub function_namespace: Option<Oid>,
pub function_name: Option<String>,
pub function_args: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct RawPolicies {
pub policies: Vec<RawPolicy>,
pub column_dependencies: Vec<RawPolicyColumnDependency>,
pub object_dependencies: Vec<RawPolicyObjectDependency>,
}
pub async fn fetch(conn: &mut PgConnection) -> Result<RawPolicies> {
info!("Fetching RLS policies...");
let policies = fetch_policies(&mut *conn).await?;
let (column_dependencies, object_dependencies) = if policies.is_empty() {
(Vec::new(), Vec::new())
} else {
info!("Fetching policy dependencies...");
(
fetch_column_dependencies(&mut *conn).await?,
fetch_object_dependencies(&mut *conn).await?,
)
};
Ok(RawPolicies {
policies,
column_dependencies,
object_dependencies,
})
}
#[allow(dead_code)]
pub async fn load(conn: &mut PgConnection, shared: &SharedCatalog) -> Result<Vec<Policy>> {
Ok(load_with_exclusions(conn, shared)
.await?
.log_and_take_objects("policy"))
}
pub async fn load_with_exclusions(
conn: &mut PgConnection,
shared: &SharedCatalog,
) -> Result<Converted<Policy>> {
let raw = fetch(conn).await?;
let mut converted = convert(&raw, shared)?;
let index = OidIndex::from_pairs(
class::PG_POLICY,
converted
.objects
.iter()
.map(|(oid, policy)| (*oid, policy.id())),
)?;
let comments = index.object_comments(&shared.descriptions, class::PG_POLICY);
for (_, policy) in &mut converted.objects {
policy.comment = comments.get(&policy.id()).map(|text| text.to_string());
}
converted.index = index;
Ok(converted.map(|(_, policy)| policy))
}
pub fn convert(raw: &RawPolicies, shared: &SharedCatalog) -> Result<Converted<(Oid, Policy)>> {
let mut kept: BTreeMap<u32, usize> = BTreeMap::new();
let mut converted: Converted<(Oid, Policy)> = Converted::new();
for row in &raw.policies {
let schema = shared
.namespaces
.name(row.table_namespace)
.with_context(|| format!("policy {} has no namespace entry", row.name))?;
if is_system_schema(schema) {
converted.excluded.push(Excluded::new(
row.oid,
"policy",
schema,
&row.name,
ExclusionReason::SystemSchema,
));
continue;
}
if let Some(extension) = shared.extensions.owner_of_relation_subobject(row.table_oid) {
converted.excluded.push(Excluded::new(
row.oid,
"policy",
schema,
&row.name,
ExclusionReason::ExtensionOwned {
extension: extension.to_string(),
},
));
continue;
}
kept.insert(row.oid.0, converted.objects.len());
converted.objects.push((
row.oid,
Policy {
schema: schema.to_string(),
table_name: row.table_name.clone(),
name: row.name.clone(),
command: command(&row.command),
permissive: row.permissive,
roles: row.roles.clone(),
using_expr: row.using_expr.clone(),
with_check_expr: row.with_check_expr.clone(),
comment: None,
depends_on: vec![DbObjectId::Table {
schema: schema.to_string(),
name: row.table_name.clone(),
}],
},
));
}
for row in &raw.column_dependencies {
let Some(&idx) = kept.get(&row.policy_oid.0) else {
continue;
};
let Some(schema) = shared.namespaces.name(row.relation_namespace) else {
continue;
};
if is_system_schema(schema) {
continue;
}
let (_, policy) = &mut converted.objects[idx];
policy.depends_on.push(DbObjectId::Column {
schema: schema.to_string(),
table: row.relation_name.clone(),
column: row.column_name.clone(),
});
if let Some(dep) = relation_dependency(&row.relkind, schema, &row.relation_name, policy) {
policy.depends_on.push(dep);
}
}
for row in &raw.object_dependencies {
let Some(&idx) = kept.get(&row.policy_oid.0) else {
continue;
};
let (_, policy) = &mut converted.objects[idx];
if let (Some(namespace), Some(name), Some(relkind)) =
(row.relation_namespace, &row.relation_name, &row.relkind)
{
let Some(schema) = shared.namespaces.name(namespace) else {
continue;
};
if is_system_schema(schema) {
continue;
}
if let Some(dep) = relation_dependency(relkind, schema, name, policy) {
policy.depends_on.push(dep);
}
continue;
}
if let (Some(oid), Some(namespace), Some(name), Some(args)) = (
row.function_oid,
row.function_namespace,
&row.function_name,
&row.function_args,
) {
let Some(schema) = shared.namespaces.name(namespace) else {
continue;
};
if is_system_schema(schema) {
continue;
}
let dep = match shared.extensions.owner(class::PG_PROC, oid) {
Some(extension) => DbObjectId::Extension {
name: extension.to_string(),
},
None => DbObjectId::Function {
schema: schema.to_string(),
name: name.clone(),
arguments: args.clone(),
},
};
policy.depends_on.push(dep);
}
}
for (_, policy) in &mut converted.objects {
dedup_preserving_order(&mut policy.depends_on);
}
converted.objects.sort_by(|(_, a), (_, b)| {
(&a.schema, &a.table_name, &a.name).cmp(&(&b.schema, &b.table_name, &b.name))
});
Ok(converted)
}
fn command(polcmd: &str) -> PolicyCommand {
match polcmd {
"r" => PolicyCommand::Select,
"a" => PolicyCommand::Insert,
"w" => PolicyCommand::Update,
"d" => PolicyCommand::Delete,
_ => PolicyCommand::All,
}
}
fn relation_dependency(
relkind: &str,
schema: &str,
name: &str,
policy: &Policy,
) -> Option<DbObjectId> {
match relkind {
"v" | "m" => Some(DbObjectId::View {
schema: schema.to_string(),
name: name.to_string(),
}),
"r" | "p" => {
if schema == policy.schema && name == policy.table_name {
return None;
}
Some(DbObjectId::Table {
schema: schema.to_string(),
name: name.to_string(),
})
}
_ => None,
}
}
async fn fetch_policies(conn: &mut PgConnection) -> Result<Vec<RawPolicy>> {
let rows = sqlx::query!(
r#"
SELECT
p.oid AS "oid!",
p.polname AS "name!",
c.oid AS "table_oid!",
c.relnamespace AS "table_namespace!",
c.relname AS "table_name!",
p.polcmd::text AS "command!",
p.polpermissive AS "permissive!",
COALESCE(
ARRAY(
SELECT rolname FROM pg_roles
WHERE oid = ANY(p.polroles)
ORDER BY rolname
),
'{}'::text[]
) AS "roles!: Vec<String>",
pg_catalog.pg_get_expr(p.polqual, p.polrelid) AS "using_expr?",
pg_catalog.pg_get_expr(p.polwithcheck, p.polrelid) AS "with_check_expr?"
FROM pg_policy p
JOIN pg_class c ON p.polrelid = c.oid
ORDER BY p.oid
"#
)
.fetch_all(&mut *conn)
.await?;
Ok(rows
.into_iter()
.map(|row| RawPolicy {
oid: row.oid,
name: row.name,
table_oid: row.table_oid,
table_namespace: row.table_namespace,
table_name: row.table_name,
command: row.command,
permissive: row.permissive,
roles: row.roles,
using_expr: row.using_expr,
with_check_expr: row.with_check_expr,
})
.collect())
}
async fn fetch_column_dependencies(
conn: &mut PgConnection,
) -> Result<Vec<RawPolicyColumnDependency>> {
let rows = sqlx::query!(
r#"
SELECT
p.oid AS "policy_oid!",
c.relnamespace AS "relation_namespace!",
c.relname AS "relation_name!",
c.relkind::text AS "relkind!",
a.attname AS "column_name!"
FROM pg_policy p
JOIN pg_depend d ON d.objid = p.oid AND d.classid = 'pg_policy'::regclass
JOIN pg_class c ON d.refobjid = c.oid AND d.refclassid = 'pg_class'::regclass
JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = d.refobjsubid
WHERE d.refobjsubid > 0
AND d.deptype = 'n'
ORDER BY p.oid, c.relname, a.attname
"#
)
.fetch_all(&mut *conn)
.await?;
Ok(rows
.into_iter()
.map(|row| RawPolicyColumnDependency {
policy_oid: row.policy_oid,
relation_namespace: row.relation_namespace,
relation_name: row.relation_name,
relkind: row.relkind,
column_name: row.column_name,
})
.collect())
}
async fn fetch_object_dependencies(
conn: &mut PgConnection,
) -> Result<Vec<RawPolicyObjectDependency>> {
let rows = sqlx::query!(
r#"
SELECT
p.oid AS "policy_oid!",
cls.relnamespace AS "relation_namespace?",
cls.relname AS "relation_name?",
cls.relkind::text AS "relkind?",
proc.oid AS "function_oid?",
proc.pronamespace AS "function_namespace?",
proc.proname AS "function_name?",
pg_catalog.pg_get_function_identity_arguments(proc.oid) AS "function_args?"
FROM pg_policy p
JOIN pg_depend d ON d.objid = p.oid AND d.classid = 'pg_policy'::regclass
LEFT JOIN pg_class cls ON d.refclassid = 'pg_class'::regclass AND d.refobjid = cls.oid
LEFT JOIN pg_proc proc ON d.refclassid = 'pg_proc'::regclass AND d.refobjid = proc.oid
WHERE d.refobjsubid = 0
AND d.deptype = 'n'
AND (cls.oid IS NOT NULL OR proc.oid IS NOT NULL)
ORDER BY p.oid
"#
)
.fetch_all(&mut *conn)
.await?;
Ok(rows
.into_iter()
.map(|row| RawPolicyObjectDependency {
policy_oid: row.policy_oid,
relation_namespace: row.relation_namespace,
relation_name: row.relation_name,
relkind: row.relkind,
function_oid: row.function_oid,
function_namespace: row.function_namespace,
function_name: row.function_name,
function_args: row.function_args,
})
.collect())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_every_polcmd_char_names_a_command() {
assert_eq!(command("*"), PolicyCommand::All);
assert_eq!(command("r"), PolicyCommand::Select);
assert_eq!(command("a"), PolicyCommand::Insert);
assert_eq!(command("w"), PolicyCommand::Update);
assert_eq!(command("d"), PolicyCommand::Delete);
}
}