1use crate::names::NameOverrides;
57use postrust_core::schema_cache::{SchemaCache, Table};
58
59pub fn cache_for_role(
65 cache: &SchemaCache,
66 names: &NameOverrides,
67 role: &str,
68 backend: bool,
69) -> SchemaCache {
70 let mut view = cache.clone();
71
72 view.tables.retain(|_, table| {
73 let Some(granted) = names.permissions(&table.schema, &table.name, role) else {
74 return false;
77 };
78
79 table.insertable = table.insertable
88 && granted
89 .insert
90 .as_ref()
91 .is_some_and(|insert| backend || !insert.backend_only);
92 table.updatable = table.updatable && granted.update.is_some();
93 table.deletable = table.deletable && granted.delete.is_some();
94 let writes = table.insertable || table.updatable || table.deletable;
95
96 if granted.select.is_none() && !writes {
104 return false;
105 }
106
107 let (insertable, updatable) = (table.insertable, table.updatable);
121 table.columns.retain(|name, _| {
122 let read = granted
123 .select
124 .as_ref()
125 .is_some_and(|select| select.columns.allows(name));
126 let written = insertable
127 && granted
128 .insert
129 .as_ref()
130 .is_some_and(|insert| insert.columns.allows(name))
131 || updatable
132 && granted
133 .update
134 .as_ref()
135 .is_some_and(|update| update.columns.allows(name));
136 read || written
137 });
138
139 table.computed_columns.retain(|name, _| {
150 let exposed = names
151 .computed_field(&table.schema, &table.name, name)
152 .unwrap_or(name);
153 granted
154 .select
155 .as_ref()
156 .is_some_and(|select| select.computed_fields.iter().any(|f| f == exposed))
157 });
158
159 let counts = granted
165 .select
166 .as_ref()
167 .is_some_and(|select| select.allow_aggregations);
168
169 !table.columns.is_empty() || !table.computed_columns.is_empty() || writes || counts
173 });
174
175 let readable: std::collections::HashSet<_> = view
181 .tables
182 .iter()
183 .filter(|(_, table)| {
184 names
185 .permissions(&table.schema, &table.name, role)
186 .is_some_and(|granted| reads_anything(granted, table))
187 })
188 .map(|(key, _)| key.clone())
189 .collect();
190 let enumerated: std::collections::HashSet<_> = names
197 .enum_tables()
198 .into_iter()
199 .map(|(schema, table)| postrust_core::api_request::QualifiedIdentifier::new(schema, table))
200 .collect();
201 view.relationships.retain(|(source, _), links| {
202 if !view.tables.contains_key(source) {
203 return false;
204 }
205 links.retain(|link| {
206 readable.contains(link.foreign_table()) || enumerated.contains(link.foreign_table())
207 });
208 !links.is_empty()
209 });
210
211 view
212}
213
214pub fn reads_anything(granted: &crate::names::RolePermissions, table: &Table) -> bool {
222 let Some(select) = &granted.select else {
223 return false;
224 };
225 !table.computed_columns.is_empty()
226 || table.columns.keys().any(|name| select.columns.allows(name))
227}
228
229pub fn has_backend_only(names: &NameOverrides, role: &str) -> bool {
234 names.tables_with_permissions(role).any(|granted| {
235 granted
236 .insert
237 .as_ref()
238 .is_some_and(|insert| insert.backend_only)
239 })
240}
241
242#[derive(Clone, Copy)]
249pub struct Caller<'a> {
250 pub role: Option<&'a str>,
252 pub session: &'a std::collections::HashMap<String, String>,
255}
256
257impl Caller<'_> {
258 pub fn unrestricted(&self) -> bool {
260 match self.role {
261 None => true,
262 Some(role) => role == postrust_auth::hasura::ADMIN_ROLE,
263 }
264 }
265}
266
267#[derive(Debug, Clone, PartialEq, Eq)]
272pub enum Fault {
273 NoPermission {
278 role: String,
280 table: String,
282 },
283 MissingSessionVariable(String),
285}
286
287impl Fault {
288 pub fn code(&self) -> &'static str {
290 match self {
291 Self::NoPermission { .. } => "permission-error",
292 Self::MissingSessionVariable(_) => "not-found",
293 }
294 }
295}
296
297impl std::fmt::Display for Fault {
298 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
299 match self {
300 Self::NoPermission { role, table } => write!(
301 f,
302 "role \"{}\" has no permission to read \"{}\"",
303 role, table
304 ),
305 Self::MissingSessionVariable(message) => f.write_str(message),
306 }
307 }
308}
309
310pub const CHECK_FAILED: &str = "check constraint of an insert/update permission has failed";
315
316#[derive(Clone, Copy, Debug, PartialEq, Eq)]
318pub enum Verb {
319 Select,
321 Insert,
323 Update,
325 Delete,
327}
328
329impl Verb {
330 fn of<'a>(&self, granted: &'a crate::names::RolePermissions) -> Option<Grant<'a>> {
331 match self {
332 Self::Select => granted.select.as_ref().map(Grant::Select),
333 Self::Insert => granted.insert.as_ref().map(Grant::Insert),
334 Self::Update => granted.update.as_ref().map(Grant::Update),
335 Self::Delete => granted.delete.as_ref().map(Grant::Delete),
336 }
337 }
338}
339
340enum Grant<'a> {
342 Select(&'a crate::names::SelectPermission),
343 Insert(&'a crate::names::InsertPermission),
344 Update(&'a crate::names::UpdatePermission),
345 Delete(&'a crate::names::DeletePermission),
346}
347
348impl Grant<'_> {
349 fn filter(&self) -> Option<&serde_json::Value> {
354 match self {
355 Self::Select(p) => Some(&p.filter),
356 Self::Update(p) => Some(&p.filter),
357 Self::Delete(p) => Some(&p.filter),
358 Self::Insert(_) => None,
359 }
360 }
361
362 fn check(&self) -> Option<&serde_json::Value> {
368 match self {
369 Self::Insert(p) => Some(&p.check),
370 Self::Update(p) => match p.check.is_null() {
371 false => Some(&p.check),
372 true => Some(&p.filter),
373 },
374 Self::Select(_) | Self::Delete(_) => None,
375 }
376 }
377
378 fn set(&self) -> Option<&std::collections::HashMap<String, serde_json::Value>> {
380 match self {
381 Self::Insert(p) => Some(&p.set),
382 Self::Update(p) => Some(&p.set),
383 Self::Select(_) | Self::Delete(_) => None,
384 }
385 }
386}
387
388fn grant_for<'a>(
389 caller: &Caller<'_>,
390 names: &'a NameOverrides,
391 schema: &str,
392 table: &str,
393 verb: Verb,
394) -> Result<Option<Grant<'a>>, Fault> {
395 if caller.unrestricted() {
396 return Ok(None);
397 }
398 let role = caller.role.unwrap_or_default();
399 match names
400 .permissions(schema, table, role)
401 .and_then(|granted| verb.of(granted))
402 {
403 Some(grant) => Ok(Some(grant)),
404 None => Err(Fault::NoPermission {
408 role: role.to_string(),
409 table: table.to_string(),
410 }),
411 }
412}
413
414pub fn row_filter(
419 caller: &Caller<'_>,
420 names: &NameOverrides,
421 schema: &str,
422 table: &str,
423 verb: Verb,
424) -> Result<Option<serde_json::Value>, Fault> {
425 let Some(grant) = grant_for(caller, names, schema, table, verb)? else {
426 return Ok(None);
427 };
428 let Some(filter) = grant.filter() else {
429 return Ok(None);
430 };
431 permission_where(filter, caller.session, names, schema, table)
432 .map_err(Fault::MissingSessionVariable)
433}
434
435pub fn write_check(
437 caller: &Caller<'_>,
438 names: &NameOverrides,
439 schema: &str,
440 table: &str,
441 verb: Verb,
442) -> Result<Option<serde_json::Value>, Fault> {
443 let Some(grant) = grant_for(caller, names, schema, table, verb)? else {
444 return Ok(None);
445 };
446 let Some(check) = grant.check() else {
447 return Ok(None);
448 };
449 permission_where(check, caller.session, names, schema, table)
450 .map_err(Fault::MissingSessionVariable)
451}
452
453pub fn presets(
459 caller: &Caller<'_>,
460 names: &NameOverrides,
461 schema: &str,
462 table: &str,
463 verb: Verb,
464) -> Result<Vec<(String, serde_json::Value)>, Fault> {
465 let Some(grant) = grant_for(caller, names, schema, table, verb)? else {
466 return Ok(Vec::new());
467 };
468 let Some(set) = grant.set() else {
469 return Ok(Vec::new());
470 };
471
472 let mut filled = Vec::with_capacity(set.len());
473 for (column, value) in set {
474 let resolved = match value {
477 serde_json::Value::String(text) => match session_variable(text) {
478 Some(name) => {
479 resolve(name, caller.session, false).map_err(Fault::MissingSessionVariable)?
480 }
481 None => value.clone(),
482 },
483 other => other.clone(),
484 };
485 filled.push((column.clone(), resolved));
486 }
487 filled.sort_by(|a, b| a.0.cmp(&b.0));
490 Ok(filled)
491}
492
493pub fn read_predicate(
495 caller: &Caller<'_>,
496 names: &NameOverrides,
497 schema: &str,
498 table: &str,
499) -> Result<Option<serde_json::Value>, Fault> {
500 row_filter(caller, names, schema, table, Verb::Select)
501}
502
503pub fn read_limit(
508 caller: &Caller<'_>,
509 names: &NameOverrides,
510 schema: &str,
511 table: &str,
512) -> Option<i64> {
513 if caller.unrestricted() {
514 return None;
515 }
516 names
517 .permissions(schema, table, caller.role?)
518 .and_then(|granted| granted.select.as_ref())
519 .and_then(|select| select.limit)
520 .map(|limit| limit as i64)
521}
522
523pub fn permission_where(
552 filter: &serde_json::Value,
553 session: &std::collections::HashMap<String, String>,
554 names: &NameOverrides,
555 schema: &str,
556 table: &str,
557) -> Result<Option<serde_json::Value>, String> {
558 let rewritten = rewrite(filter, session, names, schema, table, None)?;
559 Ok(match rewritten {
560 serde_json::Value::Object(map) if map.is_empty() => None,
561 serde_json::Value::Null => None,
562 value => Some(value),
563 })
564}
565
566fn connective(key: &str) -> Option<&'static str> {
568 match key {
569 "$and" | "_and" => Some("_and"),
570 "$or" | "_or" => Some("_or"),
571 "$not" | "_not" => Some("_not"),
572 _ => None,
573 }
574}
575
576fn takes_a_list(operator: &str) -> bool {
579 matches!(operator, "_in" | "_nin" | "_has_keys_any" | "_has_keys_all")
580}
581
582fn rewrite(
583 value: &serde_json::Value,
584 session: &std::collections::HashMap<String, String>,
585 names: &NameOverrides,
586 schema: &str,
587 table: &str,
588 operator: Option<&str>,
591) -> Result<serde_json::Value, String> {
592 match value {
593 serde_json::Value::Object(map) if operator.is_some() => {
604 let mut out = serde_json::Map::with_capacity(map.len());
605 for (key, child) in map {
606 out.insert(
607 key.clone(),
608 rewrite(child, session, names, schema, table, operator)?,
609 );
610 }
611 Ok(serde_json::Value::Object(out))
612 }
613
614 serde_json::Value::Object(map) => {
615 let mut out = serde_json::Map::with_capacity(map.len());
616 for (key, child) in map {
617 if let Some(spelling) = connective(key) {
618 out.insert(
619 spelling.to_string(),
620 rewrite(child, session, names, schema, table, None)?,
621 );
622 continue;
623 }
624
625 let key = match key.strip_prefix('$') {
630 Some(rest) if !rest.is_empty() => std::borrow::Cow::Owned(format!("_{}", rest)),
631 _ => std::borrow::Cow::Borrowed(key.as_str()),
632 };
633
634 if key == "_exists" {
638 out.insert(
639 "_exists".to_string(),
640 rewrite_exists(child, session, names, schema)?,
641 );
642 continue;
643 }
644
645 if key.starts_with('_') {
646 out.insert(
650 key.to_string(),
651 rewrite(child, session, names, schema, table, Some(&key))?,
652 );
653 continue;
654 }
655
656 let exposed = names
665 .column(schema, table, &key)
666 .unwrap_or(&key)
667 .to_string();
668
669 let rewritten = rewrite(child, session, names, schema, table, None)?;
670 let wrapped = match &rewritten {
673 serde_json::Value::Object(_) => rewritten,
674 scalar => serde_json::json!({ "_eq": scalar }),
675 };
676 out.insert(exposed, wrapped);
677 }
678 Ok(serde_json::Value::Object(out))
679 }
680
681 serde_json::Value::Array(items) => items
682 .iter()
683 .map(|item| rewrite(item, session, names, schema, table, operator))
684 .collect::<Result<Vec<_>, _>>()
685 .map(serde_json::Value::Array),
686
687 serde_json::Value::String(text) => match session_variable(text) {
688 Some(name) => resolve(name, session, operator.is_some_and(takes_a_list)),
689 None => Ok(value.clone()),
690 },
691
692 other => Ok(other.clone()),
693 }
694}
695
696pub fn exists_target(spec: &serde_json::Value, default_schema: &str) -> Option<(String, String)> {
703 match spec.get("_table")? {
704 serde_json::Value::String(name) => Some((default_schema.to_string(), name.clone())),
705 serde_json::Value::Object(qualified) => {
706 let name = qualified.get("name").and_then(|v| v.as_str())?;
707 let schema = qualified
708 .get("schema")
709 .and_then(|v| v.as_str())
710 .unwrap_or(default_schema);
711 Some((schema.to_string(), name.to_string()))
712 }
713 _ => None,
714 }
715}
716
717fn rewrite_exists(
723 spec: &serde_json::Value,
724 session: &std::collections::HashMap<String, String>,
725 names: &NameOverrides,
726 default_schema: &str,
727) -> Result<serde_json::Value, String> {
728 let Some(map) = spec.as_object() else {
729 return Err("\"_exists\" takes a table and a predicate over it".to_string());
730 };
731 let Some((schema, table)) = exists_target(spec, default_schema) else {
732 return Err("\"_exists\" needs a \"_table\" to look in".to_string());
733 };
734
735 let mut out = serde_json::Map::with_capacity(2);
736 if let Some(named) = map.get("_table") {
737 out.insert("_table".to_string(), named.clone());
738 }
739 let predicate = map
740 .get("_where")
741 .cloned()
742 .unwrap_or_else(|| serde_json::json!({}));
743 out.insert(
744 "_where".to_string(),
745 rewrite(&predicate, session, names, &schema, &table, None)?,
746 );
747 Ok(serde_json::Value::Object(out))
748}
749
750fn session_variable(text: &str) -> Option<String> {
756 let lowered = text.to_ascii_lowercase();
757 let bare = lowered.strip_prefix("x-hasura-")?;
758 match bare.is_empty() {
759 true => None,
760 false => Some(bare.replace('-', "_")),
761 }
762}
763
764fn resolve(
766 name: String,
767 session: &std::collections::HashMap<String, String>,
768 as_a_list: bool,
769) -> Result<serde_json::Value, String> {
770 let Some(value) = session.get(&name) else {
771 return Err(format!(
774 "missing session variable: \"x-hasura-{}\"",
775 name.replace('_', "-")
776 ));
777 };
778
779 if !as_a_list {
780 return Ok(serde_json::Value::String(value.clone()));
781 }
782
783 let inner = value
789 .strip_prefix('{')
790 .and_then(|rest| rest.strip_suffix('}'));
791 Ok(match inner {
792 None => serde_json::json!([value]),
793 Some("") => serde_json::json!([]),
794 Some(items) => serde_json::Value::Array(
795 items
796 .split(',')
797 .map(|item| serde_json::Value::String(item.trim().trim_matches('"').to_string()))
798 .collect(),
799 ),
800 })
801}
802
803pub fn allows_aggregations(names: &NameOverrides, schema: &str, table: &str, role: &str) -> bool {
809 names
810 .permissions(schema, table, role)
811 .and_then(|granted| granted.select.as_ref())
812 .is_some_and(|select| select.allow_aggregations)
813}
814
815#[cfg(test)]
816mod tests {
817 use super::*;
818 use postrust_core::schema_cache::{Column, Table};
819 use postrust_core::QualifiedIdentifier;
820 use std::collections::{HashMap, HashSet};
821
822 fn column(name: &str, position: i32) -> Column {
823 Column {
824 name: name.to_string(),
825 description: None,
826 nullable: false,
827 data_type: "text".to_string(),
828 nominal_type: "text".to_string(),
829 max_len: None,
830 default: None,
831 enum_values: vec![],
832 is_pk: name == "id",
833 position,
834 domain_type: None,
835 always_generated: false,
836 }
837 }
838
839 fn table(name: &str, columns: &[&str]) -> Table {
840 Table {
841 schema: "public".to_string(),
842 name: name.to_string(),
843 description: None,
844 is_view: false,
845 insertable: true,
846 updatable: true,
847 deletable: true,
848 pk_cols: vec!["id".to_string()],
849 unique_constraints: Vec::new(),
850 columns: columns
851 .iter()
852 .enumerate()
853 .map(|(at, c)| (c.to_string(), column(c, at as i32 + 1)))
854 .collect(),
855 computed_columns: Default::default(),
856 is_partitioned: false,
857 }
858 }
859
860 fn cache(tables: Vec<Table>) -> SchemaCache {
861 let mut map = HashMap::new();
862 for table in tables {
863 map.insert(table.qualified_identifier(), table);
864 }
865 SchemaCache {
866 tables: map,
867 relationships: HashMap::new(),
868 routines: HashMap::new(),
869 timezones: HashSet::new(),
870 media_handlers: HashMap::new(),
871 pg_version: 150000,
872 representations: Default::default(),
873 }
874 }
875
876 const DOCUMENT: &str = r#"{
877 "tables": {
878 "public.article": {
879 "permissions": {
880 "user": {
881 "select": {"columns": ["id", "title"], "filter": {},
882 "allow_aggregations": true},
883 "insert": {"columns": "*", "check": {}}
884 },
885 "reader": {
886 "select": {"columns": "*", "filter": {}}
887 },
888 "narrow": {
889 "select": {"columns": ["id", "title"], "filter": {}},
890 "insert": {"columns": ["title"], "check": {}}
891 }
892 }
893 },
894 "public.secret": {
895 "permissions": {
896 "reader": {"select": {"columns": "*", "filter": {}}}
897 }
898 }
899 }
900 }"#;
901
902 fn names() -> NameOverrides {
903 NameOverrides::parse(DOCUMENT).unwrap()
904 }
905
906 #[test]
907 fn a_table_the_role_was_told_nothing_about_is_not_there() {
908 let view = cache_for_role(
909 &cache(vec![table("article", &["id"]), table("secret", &["id"])]),
910 &names(),
911 "user",
912 false,
913 );
914 assert!(view
915 .tables
916 .contains_key(&QualifiedIdentifier::new("public", "article")));
917 assert!(!view
920 .tables
921 .contains_key(&QualifiedIdentifier::new("public", "secret")));
922 }
923
924 #[test]
925 fn a_column_outside_the_permission_is_absent_rather_than_null() {
926 let view = cache_for_role(
929 &cache(vec![table("article", &["id", "title", "content"])]),
930 &names(),
931 "narrow",
932 false,
933 );
934 let article = &view.tables[&QualifiedIdentifier::new("public", "article")];
935 assert!(article.has_column("title"));
936 assert!(!article.has_column("content"));
937 }
938
939 #[test]
940 fn a_column_the_role_may_write_and_not_read_is_kept_for_the_write() {
941 let view = cache_for_role(
945 &cache(vec![table("article", &["id", "title", "content"])]),
946 &names(),
947 "user",
948 false,
949 );
950 let article = &view.tables[&QualifiedIdentifier::new("public", "article")];
951 assert!(article.has_column("content"));
952 }
953
954 #[test]
955 fn a_wildcard_keeps_every_column() {
956 let view = cache_for_role(
957 &cache(vec![table("article", &["id", "title", "content"])]),
958 &names(),
959 "reader",
960 false,
961 );
962 let article = &view.tables[&QualifiedIdentifier::new("public", "article")];
963 assert_eq!(article.columns.len(), 3);
964 }
965
966 #[test]
967 fn a_write_the_role_was_not_granted_is_not_a_root_field() {
968 let view = cache_for_role(
969 &cache(vec![table("article", &["id"])]),
970 &names(),
971 "user",
972 false,
973 );
974 let article = &view.tables[&QualifiedIdentifier::new("public", "article")];
975 assert!(article.insertable);
977 assert!(!article.updatable);
978 assert!(!article.deletable);
979 }
980
981 #[test]
982 fn a_permission_cannot_grant_a_write_the_database_refuses() {
983 let mut read_only = table("article", &["id"]);
984 read_only.insertable = false;
985 let view = cache_for_role(&cache(vec![read_only]), &names(), "user", false);
986 let article = &view.tables[&QualifiedIdentifier::new("public", "article")];
987 assert!(!article.insertable);
990 }
991
992 #[test]
995 fn a_computed_field_is_granted_under_the_name_it_is_exposed_as() {
996 let mut table = table("student_marks", &["id", "name"]);
997 table.computed_columns.insert(
998 "student_total_marks".to_string(),
999 postrust_core::schema_cache::ComputedColumn {
1000 function: postrust_core::api_request::QualifiedIdentifier::new(
1001 "public",
1002 "student_total_marks",
1003 ),
1004 data_type: "integer".to_string(),
1005 description: None,
1006 row_argument: None,
1007 session_argument: None,
1008 takes_arguments: false,
1009 },
1010 );
1011 let cache = cache(vec![table]);
1012 let names = NameOverrides::parse(
1013 r#"{"tables": {"public.student_marks": {
1014 "computed_fields": {"student_total_marks": "total_marks"},
1015 "permissions": {"tutor": {"select": {
1016 "columns": ["name"], "computed_fields": ["total_marks"], "filter": {}}}}}}}"#,
1017 )
1018 .unwrap();
1019 let view = cache_for_role(&cache, &names, "tutor", false);
1020 let table = view
1021 .get_table(&postrust_core::api_request::QualifiedIdentifier::new(
1022 "public",
1023 "student_marks",
1024 ))
1025 .expect("the table survives");
1026 assert!(
1027 table.computed_columns.contains_key("student_total_marks"),
1028 "granted under the name the client sees"
1029 );
1030 }
1031
1032 #[test]
1033 fn a_computed_field_is_granted_one_at_a_time() {
1034 let mut article = table("article", &["id"]);
1035 article.computed_columns.insert(
1036 "word_count".to_string(),
1037 postrust_core::schema_cache::ComputedColumn {
1038 function: QualifiedIdentifier::new("public", "word_count"),
1039 data_type: "integer".to_string(),
1040 description: None,
1041 row_argument: None,
1042 session_argument: None,
1043 takes_arguments: false,
1044 },
1045 );
1046 let view = cache_for_role(&cache(vec![article]), &names(), "user", false);
1047 let article = &view.tables[&QualifiedIdentifier::new("public", "article")];
1048 assert!(article.computed_columns.is_empty());
1050 }
1051
1052 #[test]
1053 fn a_table_that_can_only_be_written_keeps_the_write_and_gets_no_type() {
1054 let names = NameOverrides::parse(
1058 r#"{"tables": {"public.article": {"permissions":
1059 {"writer": {"insert": {"columns": "*", "check": {}}}}}}}"#,
1060 )
1061 .unwrap();
1062 let view = cache_for_role(
1063 &cache(vec![table("article", &["id"])]),
1064 &names,
1065 "writer",
1066 false,
1067 );
1068 let article = &view.tables[&QualifiedIdentifier::new("public", "article")];
1069 assert!(article.insertable);
1070 assert!(!article.updatable);
1071 assert!(!article.deletable);
1072 assert!(article.has_column("id"));
1073 let granted = names.permissions("public", "article", "writer").unwrap();
1074 assert!(!reads_anything(granted, article));
1075 }
1076
1077 #[test]
1081 fn a_key_to_an_enum_table_survives_a_role_that_cannot_read_it() {
1082 let users = table("users", &["id", "favorite_color"]);
1083 let colors = table("colors", &["value"]);
1084 let mut base = cache(vec![users, colors]);
1085 base.relationships.insert(
1086 (
1087 postrust_core::api_request::QualifiedIdentifier::new("public", "users"),
1088 "public".to_string(),
1089 ),
1090 vec![postrust_core::schema_cache::Relationship::ForeignKey {
1091 table: postrust_core::api_request::QualifiedIdentifier::new("public", "users"),
1092 foreign_table: postrust_core::api_request::QualifiedIdentifier::new(
1093 "public", "colors",
1094 ),
1095 is_self: false,
1096 cardinality: postrust_core::schema_cache::Cardinality::M2O {
1097 constraint: "users_favorite_color_fkey".to_string(),
1098 columns: vec![("favorite_color".to_string(), "value".to_string())],
1099 },
1100 table_is_view: false,
1101 foreign_table_is_view: false,
1102 constraint_name: "users_favorite_color_fkey".to_string(),
1103 }],
1104 );
1105
1106 let names = NameOverrides::parse(
1107 r#"{"tables": {
1108 "public.colors": {"enum": true},
1109 "public.users": {"permissions": {"anonymous":
1110 {"select": {"columns": "*", "filter": {}}}}}}}"#,
1111 )
1112 .unwrap();
1113 let view = cache_for_role(&base, &names, "anonymous", false);
1114
1115 assert!(
1116 !view
1117 .tables
1118 .contains_key(&postrust_core::api_request::QualifiedIdentifier::new(
1119 "public", "colors"
1120 )),
1121 "the table itself is not readable"
1122 );
1123 assert_eq!(
1124 view.relationships.len(),
1125 1,
1126 "and the key to it is kept anyway: {:?}",
1127 view.relationships
1128 );
1129 }
1130
1131 #[test]
1132 fn a_relationship_pointing_at_a_table_the_role_may_only_write_is_dropped() {
1133 let names = NameOverrides::parse(
1137 r#"{"tables": {
1138 "public.article": {"permissions":
1139 {"writer": {"select": {"columns": "*", "filter": {}}}}},
1140 "public.author": {"permissions":
1141 {"writer": {"insert": {"columns": "*", "check": {}}}}}}}"#,
1142 )
1143 .unwrap();
1144 let mut base = cache(vec![table("article", &["id"]), table("author", &["id"])]);
1145 base.relationships.insert(
1146 (
1147 QualifiedIdentifier::new("public", "article"),
1148 "public".to_string(),
1149 ),
1150 vec![postrust_core::schema_cache::Relationship::ForeignKey {
1151 table: QualifiedIdentifier::new("public", "article"),
1152 foreign_table: QualifiedIdentifier::new("public", "author"),
1153 is_self: false,
1154 cardinality: postrust_core::schema_cache::Cardinality::M2O {
1155 constraint: "article_author_id_fkey".to_string(),
1156 columns: vec![("author_id".to_string(), "id".to_string())],
1157 },
1158 table_is_view: false,
1159 foreign_table_is_view: false,
1160 constraint_name: "article_author_id_fkey".to_string(),
1161 }],
1162 );
1163 let view = cache_for_role(&base, &names, "writer", false);
1164 assert!(view
1165 .tables
1166 .contains_key(&QualifiedIdentifier::new("public", "author")));
1167 assert!(view.relationships.is_empty());
1168 }
1169
1170 #[test]
1173 fn an_operand_that_is_an_object_is_carried_through_whole() {
1174 let filter = serde_json::json!({
1175 "geom_col": {"$st_d_within": {
1176 "distance": 1,
1177 "from": {"type": "Polygon", "coordinates": [[[2, 0], [2, 1]]]}
1178 }}
1179 });
1180 let rewritten = permission_where(
1181 &filter,
1182 &HashMap::new(),
1183 &NameOverrides::default(),
1184 "public",
1185 "geom_table",
1186 )
1187 .expect("it rewrites")
1188 .expect("it restricts something");
1189 assert_eq!(
1190 rewritten,
1191 serde_json::json!({
1192 "geom_col": {"_st_d_within": {
1193 "distance": 1,
1194 "from": {"type": "Polygon", "coordinates": [[[2, 0], [2, 1]]]}
1195 }}
1196 }),
1197 "only the operator's own spelling changes"
1198 );
1199 }
1200
1201 #[test]
1204 fn a_session_variable_inside_an_operand_is_still_resolved() {
1205 let mut session = HashMap::new();
1206 session.insert("geom_val".to_string(), "{\"type\":\"Point\"}".to_string());
1207 let filter = serde_json::json!({
1208 "geom_col": {"$st_d_within": {"distance": 1, "from": "X-Hasura-Geom-Val"}}
1209 });
1210 let rewritten = permission_where(
1211 &filter,
1212 &session,
1213 &NameOverrides::default(),
1214 "public",
1215 "geom_table",
1216 )
1217 .expect("it rewrites")
1218 .expect("it restricts something");
1219 assert_eq!(
1220 rewritten["geom_col"]["_st_d_within"]["from"],
1221 serde_json::json!("{\"type\":\"Point\"}")
1222 );
1223 }
1224
1225 #[test]
1226 fn exists_keeps_its_table_and_rewrites_its_predicate() {
1227 let session = session(&[("user_id", "2")]);
1231 let rewritten = compiled(
1232 serde_json::json!({
1233 "_exists": {"_table": "user",
1234 "_where": {"id": "X-Hasura-User-Id", "is_admin": true}}
1235 }),
1236 &session,
1237 );
1238 assert_eq!(
1239 rewritten,
1240 serde_json::json!({
1241 "_exists": {"_table": "user",
1242 "_where": {"id": {"_eq": "2"},
1243 "is_admin": {"_eq": true}}}
1244 })
1245 );
1246 }
1247
1248 #[test]
1249 fn exists_names_its_table_either_way() {
1250 assert_eq!(
1251 exists_target(&serde_json::json!({"_table": "user"}), "public"),
1252 Some(("public".to_string(), "user".to_string()))
1253 );
1254 assert_eq!(
1255 exists_target(
1256 &serde_json::json!({"_table": {"schema": "auth", "name": "user"}}),
1257 "public"
1258 ),
1259 Some(("auth".to_string(), "user".to_string()))
1260 );
1261 assert_eq!(exists_target(&serde_json::json!({}), "public"), None);
1262 }
1263
1264 fn session(pairs: &[(&str, &str)]) -> HashMap<String, String> {
1265 pairs
1266 .iter()
1267 .map(|(k, v)| (k.to_string(), v.to_string()))
1268 .collect()
1269 }
1270
1271 fn compiled(filter: serde_json::Value, session: &HashMap<String, String>) -> serde_json::Value {
1272 permission_where(
1273 &filter,
1274 session,
1275 &NameOverrides::default(),
1276 "public",
1277 "article",
1278 )
1279 .unwrap()
1280 .unwrap()
1281 }
1282
1283 #[test]
1284 fn equality_written_without_saying_so_becomes_an_operator() {
1285 assert_eq!(
1286 compiled(serde_json::json!({"is_published": true}), &session(&[])),
1287 serde_json::json!({"is_published": {"_eq": true}})
1288 );
1289 }
1290
1291 #[test]
1292 fn the_older_connectives_are_the_newer_ones() {
1293 let filter = serde_json::json!({
1294 "$or": [{"author_id": "X-HASURA-USER-ID"}, {"is_published": true}]
1295 });
1296 assert_eq!(
1297 compiled(filter, &session(&[("user_id", "1")])),
1298 serde_json::json!({"_or": [
1299 {"author_id": {"_eq": "1"}},
1300 {"is_published": {"_eq": true}}
1301 ]})
1302 );
1303 }
1304
1305 #[test]
1306 fn the_older_operator_spellings_are_the_newer_ones_too() {
1307 assert_eq!(
1310 compiled(
1311 serde_json::json!({"id": {"$in": "X-Hasura-Allowed-Ids"}}),
1312 &session(&[("allowed_ids", "{1,2}")])
1313 ),
1314 serde_json::json!({"id": {"_in": ["1", "2"]}})
1315 );
1316 assert_eq!(
1317 compiled(serde_json::json!({"age": {"$gt": 18}}), &session(&[])),
1318 serde_json::json!({"age": {"_gt": 18}})
1319 );
1320 }
1321
1322 #[test]
1323 fn a_session_variable_is_read_however_it_is_spelled() {
1324 let by_shouting = compiled(
1326 serde_json::json!({"id": {"_eq": "X-HASURA-USER-ID"}}),
1327 &session(&[("user_id", "7")]),
1328 );
1329 let by_title = compiled(
1330 serde_json::json!({"id": {"_eq": "X-Hasura-User-Id"}}),
1331 &session(&[("user_id", "7")]),
1332 );
1333 assert_eq!(by_shouting, by_title);
1334 assert_eq!(by_shouting, serde_json::json!({"id": {"_eq": "7"}}));
1335 }
1336
1337 #[test]
1338 fn a_variable_the_caller_does_not_carry_is_refused() {
1339 let error = permission_where(
1340 &serde_json::json!({"id": "X-Hasura-User-Id"}),
1341 &session(&[]),
1342 &NameOverrides::default(),
1343 "public",
1344 "article",
1345 )
1346 .unwrap_err();
1347 assert!(error.contains("x-hasura-user-id"), "unhelpful: {}", error);
1350 }
1351
1352 #[test]
1353 fn a_list_operator_reads_its_variable_as_a_list() {
1354 assert_eq!(
1355 compiled(
1356 serde_json::json!({"name": {"_in": "X-Hasura-Free-Artists"}}),
1357 &session(&[("free_artists", "{Ann,Bob}")])
1358 ),
1359 serde_json::json!({"name": {"_in": ["Ann", "Bob"]}})
1360 );
1361 assert_eq!(
1363 compiled(
1364 serde_json::json!({"name": {"_in": "X-Hasura-Free-Artists"}}),
1365 &session(&[("free_artists", "{}")])
1366 ),
1367 serde_json::json!({"name": {"_in": []}})
1368 );
1369 assert_eq!(
1372 compiled(
1373 serde_json::json!({"name": {"_eq": "X-Hasura-Free-Artists"}}),
1374 &session(&[("free_artists", "{Ann,Bob}")])
1375 ),
1376 serde_json::json!({"name": {"_eq": "{Ann,Bob}"}})
1377 );
1378 }
1379
1380 #[test]
1381 fn a_predicate_over_a_relationship_survives_unchanged() {
1382 assert_eq!(
1386 compiled(
1387 serde_json::json!({"articles": {"is_published": {"_eq": true}}}),
1388 &session(&[])
1389 ),
1390 serde_json::json!({"articles": {"is_published": {"_eq": true}}})
1391 );
1392 }
1393
1394 #[test]
1395 fn an_empty_filter_restricts_nothing() {
1396 let none = permission_where(
1397 &serde_json::json!({}),
1398 &session(&[]),
1399 &NameOverrides::default(),
1400 "public",
1401 "article",
1402 )
1403 .unwrap();
1404 assert!(none.is_none());
1407 }
1408
1409 #[test]
1410 fn a_renamed_column_is_named_the_way_the_schema_exposes_it() {
1411 let names =
1412 NameOverrides::parse(r#"{"public.article": {"columns": {"author_id": "writtenBy"}}}"#)
1413 .unwrap();
1414 let compiled = permission_where(
1415 &serde_json::json!({"author_id": "X-Hasura-User-Id"}),
1416 &session(&[("user_id", "1")]),
1417 &names,
1418 "public",
1419 "article",
1420 )
1421 .unwrap()
1422 .unwrap();
1423 assert_eq!(compiled, serde_json::json!({"writtenBy": {"_eq": "1"}}));
1425 }
1426
1427 const WRITES: &str = r#"{
1428 "tables": {"public.article": {"permissions": {"user": {
1429 "select": {"columns": "*", "filter": {}},
1430 "insert": {"columns": "*", "check": {"is_published": false},
1431 "set": {"author_id": "X-Hasura-User-Id", "source": "web"},
1432 "backend_only": true},
1433 "update": {"columns": "*", "filter": {"author_id": "X-Hasura-User-Id"}},
1434 "delete": {"filter": {"author_id": "X-Hasura-User-Id"}}
1435 }}}}
1436 }"#;
1437
1438 fn writer() -> (NameOverrides, HashMap<String, String>) {
1439 (
1440 NameOverrides::parse(WRITES).unwrap(),
1441 session(&[("user_id", "3")]),
1442 )
1443 }
1444
1445 #[test]
1446 fn each_verb_is_asked_about_its_own_grant() {
1447 let (names, sess) = writer();
1448 let caller = Caller {
1449 role: Some("user"),
1450 session: &sess,
1451 };
1452 assert_eq!(
1454 row_filter(&caller, &names, "public", "article", Verb::Insert).unwrap(),
1455 None
1456 );
1457 for verb in [Verb::Update, Verb::Delete] {
1458 assert_eq!(
1459 row_filter(&caller, &names, "public", "article", verb).unwrap(),
1460 Some(serde_json::json!({"author_id": {"_eq": "3"}}))
1461 );
1462 }
1463 }
1464
1465 #[test]
1466 fn an_update_with_no_check_of_its_own_is_held_to_its_filter() {
1467 let (names, sess) = writer();
1470 let caller = Caller {
1471 role: Some("user"),
1472 session: &sess,
1473 };
1474 assert_eq!(
1475 write_check(&caller, &names, "public", "article", Verb::Update).unwrap(),
1476 Some(serde_json::json!({"author_id": {"_eq": "3"}}))
1477 );
1478 assert_eq!(
1479 write_check(&caller, &names, "public", "article", Verb::Insert).unwrap(),
1480 Some(serde_json::json!({"is_published": {"_eq": false}}))
1481 );
1482 }
1483
1484 #[test]
1485 fn a_preset_resolves_a_session_variable_and_keeps_a_literal() {
1486 let (names, sess) = writer();
1487 let caller = Caller {
1488 role: Some("user"),
1489 session: &sess,
1490 };
1491 assert_eq!(
1492 presets(&caller, &names, "public", "article", Verb::Insert).unwrap(),
1493 vec![
1494 ("author_id".to_string(), serde_json::json!("3")),
1495 ("source".to_string(), serde_json::json!("web")),
1496 ]
1497 );
1498 }
1499
1500 #[test]
1501 fn a_backend_only_insert_is_absent_until_a_backend_asks() {
1502 let (names, _) = writer();
1503 assert!(has_backend_only(&names, "user"));
1504 let of = |backend| {
1505 cache_for_role(
1506 &cache(vec![table("article", &["id"])]),
1507 &names,
1508 "user",
1509 backend,
1510 )
1511 .tables[&QualifiedIdentifier::new("public", "article")]
1512 .insertable
1513 };
1514 assert!(!of(false));
1515 assert!(of(true));
1516 }
1517
1518 #[test]
1519 fn aggregates_are_asked_for_separately_from_rows() {
1520 let names = names();
1521 assert!(allows_aggregations(&names, "public", "article", "user"));
1522 assert!(!allows_aggregations(&names, "public", "article", "reader"));
1524 assert!(!allows_aggregations(
1525 &names, "public", "article", "stranger"
1526 ));
1527 }
1528}