Skip to content

Commit 331f8d1

Browse files
authoredApr 30, 2025
chore(schema_cache): query policies (#379)
1 parent 63e5bed commit 331f8d1

File tree

5 files changed

+305
-2
lines changed

5 files changed

+305
-2
lines changed
 

‎.sqlx/query-47bbad9dc2cec0231ef726790a9b0a5d9c628c6a2704f5523fb9ee45414350c7.json

+62
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎crates/pgt_schema_cache/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
mod columns;
66
mod functions;
7+
mod policies;
78
mod schema_cache;
89
mod schemas;
910
mod tables;
+225
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
use crate::schema_cache::SchemaCacheItem;
2+
3+
#[derive(Debug, Clone, PartialEq, Eq)]
4+
pub enum PolicyCommand {
5+
Select,
6+
Insert,
7+
Update,
8+
Delete,
9+
All,
10+
}
11+
12+
impl From<&str> for PolicyCommand {
13+
fn from(value: &str) -> Self {
14+
match value {
15+
"SELECT" => PolicyCommand::Select,
16+
"INSERT" => PolicyCommand::Insert,
17+
"UPDATE" => PolicyCommand::Update,
18+
"DELETE" => PolicyCommand::Delete,
19+
"ALL" => PolicyCommand::All,
20+
_ => panic!("Invalid Policy Command {}", value),
21+
}
22+
}
23+
}
24+
impl From<String> for PolicyCommand {
25+
fn from(value: String) -> Self {
26+
PolicyCommand::from(value.as_str())
27+
}
28+
}
29+
30+
#[derive(Debug, Clone, PartialEq, Eq)]
31+
struct PolicyQueried {
32+
name: String,
33+
table_name: String,
34+
schema_name: String,
35+
is_permissive: String,
36+
command: String,
37+
role_names: Option<Vec<String>>,
38+
security_qualification: Option<String>,
39+
with_check: Option<String>,
40+
}
41+
42+
impl From<PolicyQueried> for Policy {
43+
fn from(value: PolicyQueried) -> Self {
44+
Self {
45+
name: value.name,
46+
table_name: value.table_name,
47+
schema_name: value.schema_name,
48+
is_permissive: value.is_permissive == "PERMISSIVE",
49+
command: PolicyCommand::from(value.command),
50+
role_names: value.role_names.unwrap_or_default(),
51+
security_qualification: value.security_qualification,
52+
with_check: value.with_check,
53+
}
54+
}
55+
}
56+
57+
#[derive(Debug, Clone, PartialEq, Eq)]
58+
pub struct Policy {
59+
name: String,
60+
table_name: String,
61+
schema_name: String,
62+
is_permissive: bool,
63+
command: PolicyCommand,
64+
role_names: Vec<String>,
65+
security_qualification: Option<String>,
66+
with_check: Option<String>,
67+
}
68+
69+
impl SchemaCacheItem for Policy {
70+
type Item = Policy;
71+
72+
async fn load(pool: &sqlx::PgPool) -> Result<Vec<Self::Item>, sqlx::Error> {
73+
let policies = sqlx::query_file_as!(PolicyQueried, "src/queries/policies.sql")
74+
.fetch_all(pool)
75+
.await?;
76+
77+
Ok(policies.into_iter().map(Policy::from).collect())
78+
}
79+
}
80+
81+
#[cfg(test)]
82+
mod tests {
83+
use pgt_test_utils::test_database::get_new_test_db;
84+
use sqlx::Executor;
85+
86+
use crate::{SchemaCache, policies::PolicyCommand};
87+
88+
#[tokio::test]
89+
async fn loads_policies() {
90+
let test_db = get_new_test_db().await;
91+
92+
let setup = r#"
93+
do $$
94+
begin
95+
if not exists (
96+
select from pg_catalog.pg_roles
97+
where rolname = 'admin'
98+
) then
99+
create role admin;
100+
end if;
101+
end $$;
102+
103+
104+
create table public.users (
105+
id serial primary key,
106+
name varchar(255) not null
107+
);
108+
109+
-- multiple policies to test various commands
110+
create policy public_policy
111+
on public.users
112+
for select
113+
to public
114+
using (true);
115+
116+
create policy public_policy_del
117+
on public.users
118+
for delete
119+
to public
120+
using (true);
121+
122+
create policy public_policy_ins
123+
on public.users
124+
for insert
125+
to public
126+
with check (true);
127+
128+
create policy admin_policy
129+
on public.users
130+
for all
131+
to admin
132+
with check (true);
133+
134+
do $$
135+
begin
136+
if not exists (
137+
select from pg_catalog.pg_roles
138+
where rolname = 'owner'
139+
) then
140+
create role owner;
141+
end if;
142+
end $$;
143+
144+
create schema real_estate;
145+
146+
create table real_estate.properties (
147+
id serial primary key,
148+
owner_id int not null
149+
);
150+
151+
create policy owner_policy
152+
on real_estate.properties
153+
for update
154+
to owner
155+
using (owner_id = current_user::int);
156+
"#;
157+
158+
test_db
159+
.execute(setup)
160+
.await
161+
.expect("Failed to setup test database");
162+
163+
let cache = SchemaCache::load(&test_db)
164+
.await
165+
.expect("Failed to load Schema Cache");
166+
167+
let public_policies = cache
168+
.policies
169+
.iter()
170+
.filter(|p| p.schema_name == "public")
171+
.count();
172+
173+
assert_eq!(public_policies, 4);
174+
175+
let real_estate_policies = cache
176+
.policies
177+
.iter()
178+
.filter(|p| p.schema_name == "real_estate")
179+
.count();
180+
181+
assert_eq!(real_estate_policies, 1);
182+
183+
let public_policy = cache
184+
.policies
185+
.iter()
186+
.find(|p| p.name == "public_policy")
187+
.unwrap();
188+
assert_eq!(public_policy.table_name, "users");
189+
assert_eq!(public_policy.schema_name, "public");
190+
assert!(public_policy.is_permissive);
191+
assert_eq!(public_policy.command, PolicyCommand::Select);
192+
assert_eq!(public_policy.role_names, vec!["public"]);
193+
assert_eq!(public_policy.security_qualification, Some("true".into()));
194+
assert_eq!(public_policy.with_check, None);
195+
196+
let admin_policy = cache
197+
.policies
198+
.iter()
199+
.find(|p| p.name == "admin_policy")
200+
.unwrap();
201+
assert_eq!(admin_policy.table_name, "users");
202+
assert_eq!(admin_policy.schema_name, "public");
203+
assert!(admin_policy.is_permissive);
204+
assert_eq!(admin_policy.command, PolicyCommand::All);
205+
assert_eq!(admin_policy.role_names, vec!["admin"]);
206+
assert_eq!(admin_policy.security_qualification, None);
207+
assert_eq!(admin_policy.with_check, Some("true".into()));
208+
209+
let owner_policy = cache
210+
.policies
211+
.iter()
212+
.find(|p| p.name == "owner_policy")
213+
.unwrap();
214+
assert_eq!(owner_policy.table_name, "properties");
215+
assert_eq!(owner_policy.schema_name, "real_estate");
216+
assert!(owner_policy.is_permissive);
217+
assert_eq!(owner_policy.command, PolicyCommand::Update);
218+
assert_eq!(owner_policy.role_names, vec!["owner"]);
219+
assert_eq!(
220+
owner_policy.security_qualification,
221+
Some("(owner_id = (CURRENT_USER)::integer)".into())
222+
);
223+
assert_eq!(owner_policy.with_check, None);
224+
}
225+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
select
2+
schemaname as "schema_name!",
3+
tablename as "table_name!",
4+
policyname as "name!",
5+
permissive as "is_permissive!",
6+
roles as "role_names!",
7+
cmd as "command!",
8+
qual as "security_qualification",
9+
with_check
10+
from
11+
pg_catalog.pg_policies;

‎crates/pgt_schema_cache/src/schema_cache.rs

+6-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use sqlx::postgres::PgPool;
22

33
use crate::columns::Column;
44
use crate::functions::Function;
5+
use crate::policies::Policy;
56
use crate::schemas::Schema;
67
use crate::tables::Table;
78
use crate::types::PostgresType;
@@ -15,17 +16,19 @@ pub struct SchemaCache {
1516
pub types: Vec<PostgresType>,
1617
pub versions: Vec<Version>,
1718
pub columns: Vec<Column>,
19+
pub policies: Vec<Policy>,
1820
}
1921

2022
impl SchemaCache {
2123
pub async fn load(pool: &PgPool) -> Result<SchemaCache, sqlx::Error> {
22-
let (schemas, tables, functions, types, versions, columns) = futures_util::try_join!(
24+
let (schemas, tables, functions, types, versions, columns, policies) = futures_util::try_join!(
2325
Schema::load(pool),
2426
Table::load(pool),
2527
Function::load(pool),
2628
PostgresType::load(pool),
2729
Version::load(pool),
28-
Column::load(pool)
30+
Column::load(pool),
31+
Policy::load(pool),
2932
)?;
3033

3134
Ok(SchemaCache {
@@ -35,6 +38,7 @@ impl SchemaCache {
3538
types,
3639
versions,
3740
columns,
41+
policies,
3842
})
3943
}
4044

0 commit comments

Comments
 (0)
Please sign in to comment.