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>,
251 pub session: &'a std::collections::HashMap<String, String>,
252}
253
254impl Caller<'_> {
255 pub fn unrestricted(&self) -> bool {
257 match self.role {
258 None => true,
259 Some(role) => role == postrust_auth::hasura::ADMIN_ROLE,
260 }
261 }
262}
263
264#[derive(Debug, Clone, PartialEq, Eq)]
269pub enum Fault {
270 NoPermission { role: String, table: String },
275 MissingSessionVariable(String),
277}
278
279impl Fault {
280 pub fn code(&self) -> &'static str {
282 match self {
283 Self::NoPermission { .. } => "permission-error",
284 Self::MissingSessionVariable(_) => "not-found",
285 }
286 }
287}
288
289impl std::fmt::Display for Fault {
290 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
291 match self {
292 Self::NoPermission { role, table } => write!(
293 f,
294 "role \"{}\" has no permission to read \"{}\"",
295 role, table
296 ),
297 Self::MissingSessionVariable(message) => f.write_str(message),
298 }
299 }
300}
301
302pub const CHECK_FAILED: &str = "check constraint of an insert/update permission has failed";
307
308#[derive(Clone, Copy, Debug, PartialEq, Eq)]
310pub enum Verb {
311 Select,
312 Insert,
313 Update,
314 Delete,
315}
316
317impl Verb {
318 fn of<'a>(&self, granted: &'a crate::names::RolePermissions) -> Option<Grant<'a>> {
319 match self {
320 Self::Select => granted.select.as_ref().map(Grant::Select),
321 Self::Insert => granted.insert.as_ref().map(Grant::Insert),
322 Self::Update => granted.update.as_ref().map(Grant::Update),
323 Self::Delete => granted.delete.as_ref().map(Grant::Delete),
324 }
325 }
326}
327
328enum Grant<'a> {
330 Select(&'a crate::names::SelectPermission),
331 Insert(&'a crate::names::InsertPermission),
332 Update(&'a crate::names::UpdatePermission),
333 Delete(&'a crate::names::DeletePermission),
334}
335
336impl Grant<'_> {
337 fn filter(&self) -> Option<&serde_json::Value> {
342 match self {
343 Self::Select(p) => Some(&p.filter),
344 Self::Update(p) => Some(&p.filter),
345 Self::Delete(p) => Some(&p.filter),
346 Self::Insert(_) => None,
347 }
348 }
349
350 fn check(&self) -> Option<&serde_json::Value> {
356 match self {
357 Self::Insert(p) => Some(&p.check),
358 Self::Update(p) => match p.check.is_null() {
359 false => Some(&p.check),
360 true => Some(&p.filter),
361 },
362 Self::Select(_) | Self::Delete(_) => None,
363 }
364 }
365
366 fn set(&self) -> Option<&std::collections::HashMap<String, serde_json::Value>> {
368 match self {
369 Self::Insert(p) => Some(&p.set),
370 Self::Update(p) => Some(&p.set),
371 Self::Select(_) | Self::Delete(_) => None,
372 }
373 }
374}
375
376fn grant_for<'a>(
377 caller: &Caller<'_>,
378 names: &'a NameOverrides,
379 schema: &str,
380 table: &str,
381 verb: Verb,
382) -> Result<Option<Grant<'a>>, Fault> {
383 if caller.unrestricted() {
384 return Ok(None);
385 }
386 let role = caller.role.unwrap_or_default();
387 match names
388 .permissions(schema, table, role)
389 .and_then(|granted| verb.of(granted))
390 {
391 Some(grant) => Ok(Some(grant)),
392 None => Err(Fault::NoPermission {
396 role: role.to_string(),
397 table: table.to_string(),
398 }),
399 }
400}
401
402pub fn row_filter(
407 caller: &Caller<'_>,
408 names: &NameOverrides,
409 schema: &str,
410 table: &str,
411 verb: Verb,
412) -> Result<Option<serde_json::Value>, Fault> {
413 let Some(grant) = grant_for(caller, names, schema, table, verb)? else {
414 return Ok(None);
415 };
416 let Some(filter) = grant.filter() else {
417 return Ok(None);
418 };
419 permission_where(filter, caller.session, names, schema, table)
420 .map_err(Fault::MissingSessionVariable)
421}
422
423pub fn write_check(
425 caller: &Caller<'_>,
426 names: &NameOverrides,
427 schema: &str,
428 table: &str,
429 verb: Verb,
430) -> Result<Option<serde_json::Value>, Fault> {
431 let Some(grant) = grant_for(caller, names, schema, table, verb)? else {
432 return Ok(None);
433 };
434 let Some(check) = grant.check() else {
435 return Ok(None);
436 };
437 permission_where(check, caller.session, names, schema, table)
438 .map_err(Fault::MissingSessionVariable)
439}
440
441pub fn presets(
447 caller: &Caller<'_>,
448 names: &NameOverrides,
449 schema: &str,
450 table: &str,
451 verb: Verb,
452) -> Result<Vec<(String, serde_json::Value)>, Fault> {
453 let Some(grant) = grant_for(caller, names, schema, table, verb)? else {
454 return Ok(Vec::new());
455 };
456 let Some(set) = grant.set() else {
457 return Ok(Vec::new());
458 };
459
460 let mut filled = Vec::with_capacity(set.len());
461 for (column, value) in set {
462 let resolved = match value {
465 serde_json::Value::String(text) => match session_variable(text) {
466 Some(name) => {
467 resolve(name, caller.session, false).map_err(Fault::MissingSessionVariable)?
468 }
469 None => value.clone(),
470 },
471 other => other.clone(),
472 };
473 filled.push((column.clone(), resolved));
474 }
475 filled.sort_by(|a, b| a.0.cmp(&b.0));
478 Ok(filled)
479}
480
481pub fn read_predicate(
483 caller: &Caller<'_>,
484 names: &NameOverrides,
485 schema: &str,
486 table: &str,
487) -> Result<Option<serde_json::Value>, Fault> {
488 row_filter(caller, names, schema, table, Verb::Select)
489}
490
491pub fn read_limit(
496 caller: &Caller<'_>,
497 names: &NameOverrides,
498 schema: &str,
499 table: &str,
500) -> Option<i64> {
501 if caller.unrestricted() {
502 return None;
503 }
504 names
505 .permissions(schema, table, caller.role?)
506 .and_then(|granted| granted.select.as_ref())
507 .and_then(|select| select.limit)
508 .map(|limit| limit as i64)
509}
510
511pub fn permission_where(
540 filter: &serde_json::Value,
541 session: &std::collections::HashMap<String, String>,
542 names: &NameOverrides,
543 schema: &str,
544 table: &str,
545) -> Result<Option<serde_json::Value>, String> {
546 let rewritten = rewrite(filter, session, names, schema, table, None)?;
547 Ok(match rewritten {
548 serde_json::Value::Object(map) if map.is_empty() => None,
549 serde_json::Value::Null => None,
550 value => Some(value),
551 })
552}
553
554fn connective(key: &str) -> Option<&'static str> {
556 match key {
557 "$and" | "_and" => Some("_and"),
558 "$or" | "_or" => Some("_or"),
559 "$not" | "_not" => Some("_not"),
560 _ => None,
561 }
562}
563
564fn takes_a_list(operator: &str) -> bool {
567 matches!(operator, "_in" | "_nin" | "_has_keys_any" | "_has_keys_all")
568}
569
570fn rewrite(
571 value: &serde_json::Value,
572 session: &std::collections::HashMap<String, String>,
573 names: &NameOverrides,
574 schema: &str,
575 table: &str,
576 operator: Option<&str>,
579) -> Result<serde_json::Value, String> {
580 match value {
581 serde_json::Value::Object(map) if operator.is_some() => {
592 let mut out = serde_json::Map::with_capacity(map.len());
593 for (key, child) in map {
594 out.insert(
595 key.clone(),
596 rewrite(child, session, names, schema, table, operator)?,
597 );
598 }
599 Ok(serde_json::Value::Object(out))
600 }
601
602 serde_json::Value::Object(map) => {
603 let mut out = serde_json::Map::with_capacity(map.len());
604 for (key, child) in map {
605 if let Some(spelling) = connective(key) {
606 out.insert(
607 spelling.to_string(),
608 rewrite(child, session, names, schema, table, None)?,
609 );
610 continue;
611 }
612
613 let key = match key.strip_prefix('$') {
618 Some(rest) if !rest.is_empty() => std::borrow::Cow::Owned(format!("_{}", rest)),
619 _ => std::borrow::Cow::Borrowed(key.as_str()),
620 };
621
622 if key == "_exists" {
626 out.insert(
627 "_exists".to_string(),
628 rewrite_exists(child, session, names, schema)?,
629 );
630 continue;
631 }
632
633 if key.starts_with('_') {
634 out.insert(
638 key.to_string(),
639 rewrite(child, session, names, schema, table, Some(&key))?,
640 );
641 continue;
642 }
643
644 let exposed = names
653 .column(schema, table, &key)
654 .unwrap_or(&key)
655 .to_string();
656
657 let rewritten = rewrite(child, session, names, schema, table, None)?;
658 let wrapped = match &rewritten {
661 serde_json::Value::Object(_) => rewritten,
662 scalar => serde_json::json!({ "_eq": scalar }),
663 };
664 out.insert(exposed, wrapped);
665 }
666 Ok(serde_json::Value::Object(out))
667 }
668
669 serde_json::Value::Array(items) => items
670 .iter()
671 .map(|item| rewrite(item, session, names, schema, table, operator))
672 .collect::<Result<Vec<_>, _>>()
673 .map(serde_json::Value::Array),
674
675 serde_json::Value::String(text) => match session_variable(text) {
676 Some(name) => resolve(name, session, operator.is_some_and(takes_a_list)),
677 None => Ok(value.clone()),
678 },
679
680 other => Ok(other.clone()),
681 }
682}
683
684pub fn exists_target(spec: &serde_json::Value, default_schema: &str) -> Option<(String, String)> {
691 match spec.get("_table")? {
692 serde_json::Value::String(name) => Some((default_schema.to_string(), name.clone())),
693 serde_json::Value::Object(qualified) => {
694 let name = qualified.get("name").and_then(|v| v.as_str())?;
695 let schema = qualified
696 .get("schema")
697 .and_then(|v| v.as_str())
698 .unwrap_or(default_schema);
699 Some((schema.to_string(), name.to_string()))
700 }
701 _ => None,
702 }
703}
704
705fn rewrite_exists(
711 spec: &serde_json::Value,
712 session: &std::collections::HashMap<String, String>,
713 names: &NameOverrides,
714 default_schema: &str,
715) -> Result<serde_json::Value, String> {
716 let Some(map) = spec.as_object() else {
717 return Err("\"_exists\" takes a table and a predicate over it".to_string());
718 };
719 let Some((schema, table)) = exists_target(spec, default_schema) else {
720 return Err("\"_exists\" needs a \"_table\" to look in".to_string());
721 };
722
723 let mut out = serde_json::Map::with_capacity(2);
724 if let Some(named) = map.get("_table") {
725 out.insert("_table".to_string(), named.clone());
726 }
727 let predicate = map
728 .get("_where")
729 .cloned()
730 .unwrap_or_else(|| serde_json::json!({}));
731 out.insert(
732 "_where".to_string(),
733 rewrite(&predicate, session, names, &schema, &table, None)?,
734 );
735 Ok(serde_json::Value::Object(out))
736}
737
738fn session_variable(text: &str) -> Option<String> {
744 let lowered = text.to_ascii_lowercase();
745 let bare = lowered.strip_prefix("x-hasura-")?;
746 match bare.is_empty() {
747 true => None,
748 false => Some(bare.replace('-', "_")),
749 }
750}
751
752fn resolve(
754 name: String,
755 session: &std::collections::HashMap<String, String>,
756 as_a_list: bool,
757) -> Result<serde_json::Value, String> {
758 let Some(value) = session.get(&name) else {
759 return Err(format!(
762 "missing session variable: \"x-hasura-{}\"",
763 name.replace('_', "-")
764 ));
765 };
766
767 if !as_a_list {
768 return Ok(serde_json::Value::String(value.clone()));
769 }
770
771 let inner = value
777 .strip_prefix('{')
778 .and_then(|rest| rest.strip_suffix('}'));
779 Ok(match inner {
780 None => serde_json::json!([value]),
781 Some("") => serde_json::json!([]),
782 Some(items) => serde_json::Value::Array(
783 items
784 .split(',')
785 .map(|item| serde_json::Value::String(item.trim().trim_matches('"').to_string()))
786 .collect(),
787 ),
788 })
789}
790
791pub fn allows_aggregations(names: &NameOverrides, schema: &str, table: &str, role: &str) -> bool {
797 names
798 .permissions(schema, table, role)
799 .and_then(|granted| granted.select.as_ref())
800 .is_some_and(|select| select.allow_aggregations)
801}
802
803#[cfg(test)]
804mod tests {
805 use super::*;
806 use postrust_core::schema_cache::{Column, Table};
807 use postrust_core::QualifiedIdentifier;
808 use std::collections::{HashMap, HashSet};
809
810 fn column(name: &str, position: i32) -> Column {
811 Column {
812 name: name.to_string(),
813 description: None,
814 nullable: false,
815 data_type: "text".to_string(),
816 nominal_type: "text".to_string(),
817 max_len: None,
818 default: None,
819 enum_values: vec![],
820 is_pk: name == "id",
821 position,
822 domain_type: None,
823 always_generated: false,
824 }
825 }
826
827 fn table(name: &str, columns: &[&str]) -> Table {
828 Table {
829 schema: "public".to_string(),
830 name: name.to_string(),
831 description: None,
832 is_view: false,
833 insertable: true,
834 updatable: true,
835 deletable: true,
836 pk_cols: vec!["id".to_string()],
837 unique_constraints: Vec::new(),
838 columns: columns
839 .iter()
840 .enumerate()
841 .map(|(at, c)| (c.to_string(), column(c, at as i32 + 1)))
842 .collect(),
843 computed_columns: Default::default(),
844 is_partitioned: false,
845 }
846 }
847
848 fn cache(tables: Vec<Table>) -> SchemaCache {
849 let mut map = HashMap::new();
850 for table in tables {
851 map.insert(table.qualified_identifier(), table);
852 }
853 SchemaCache {
854 tables: map,
855 relationships: HashMap::new(),
856 routines: HashMap::new(),
857 timezones: HashSet::new(),
858 media_handlers: HashMap::new(),
859 pg_version: 150000,
860 representations: Default::default(),
861 }
862 }
863
864 const DOCUMENT: &str = r#"{
865 "tables": {
866 "public.article": {
867 "permissions": {
868 "user": {
869 "select": {"columns": ["id", "title"], "filter": {},
870 "allow_aggregations": true},
871 "insert": {"columns": "*", "check": {}}
872 },
873 "reader": {
874 "select": {"columns": "*", "filter": {}}
875 },
876 "narrow": {
877 "select": {"columns": ["id", "title"], "filter": {}},
878 "insert": {"columns": ["title"], "check": {}}
879 }
880 }
881 },
882 "public.secret": {
883 "permissions": {
884 "reader": {"select": {"columns": "*", "filter": {}}}
885 }
886 }
887 }
888 }"#;
889
890 fn names() -> NameOverrides {
891 NameOverrides::parse(DOCUMENT).unwrap()
892 }
893
894 #[test]
895 fn a_table_the_role_was_told_nothing_about_is_not_there() {
896 let view = cache_for_role(
897 &cache(vec![table("article", &["id"]), table("secret", &["id"])]),
898 &names(),
899 "user",
900 false,
901 );
902 assert!(view
903 .tables
904 .contains_key(&QualifiedIdentifier::new("public", "article")));
905 assert!(!view
908 .tables
909 .contains_key(&QualifiedIdentifier::new("public", "secret")));
910 }
911
912 #[test]
913 fn a_column_outside_the_permission_is_absent_rather_than_null() {
914 let view = cache_for_role(
917 &cache(vec![table("article", &["id", "title", "content"])]),
918 &names(),
919 "narrow",
920 false,
921 );
922 let article = &view.tables[&QualifiedIdentifier::new("public", "article")];
923 assert!(article.has_column("title"));
924 assert!(!article.has_column("content"));
925 }
926
927 #[test]
928 fn a_column_the_role_may_write_and_not_read_is_kept_for_the_write() {
929 let view = cache_for_role(
933 &cache(vec![table("article", &["id", "title", "content"])]),
934 &names(),
935 "user",
936 false,
937 );
938 let article = &view.tables[&QualifiedIdentifier::new("public", "article")];
939 assert!(article.has_column("content"));
940 }
941
942 #[test]
943 fn a_wildcard_keeps_every_column() {
944 let view = cache_for_role(
945 &cache(vec![table("article", &["id", "title", "content"])]),
946 &names(),
947 "reader",
948 false,
949 );
950 let article = &view.tables[&QualifiedIdentifier::new("public", "article")];
951 assert_eq!(article.columns.len(), 3);
952 }
953
954 #[test]
955 fn a_write_the_role_was_not_granted_is_not_a_root_field() {
956 let view = cache_for_role(
957 &cache(vec![table("article", &["id"])]),
958 &names(),
959 "user",
960 false,
961 );
962 let article = &view.tables[&QualifiedIdentifier::new("public", "article")];
963 assert!(article.insertable);
965 assert!(!article.updatable);
966 assert!(!article.deletable);
967 }
968
969 #[test]
970 fn a_permission_cannot_grant_a_write_the_database_refuses() {
971 let mut read_only = table("article", &["id"]);
972 read_only.insertable = false;
973 let view = cache_for_role(&cache(vec![read_only]), &names(), "user", false);
974 let article = &view.tables[&QualifiedIdentifier::new("public", "article")];
975 assert!(!article.insertable);
978 }
979
980 #[test]
983 fn a_computed_field_is_granted_under_the_name_it_is_exposed_as() {
984 let mut table = table("student_marks", &["id", "name"]);
985 table.computed_columns.insert(
986 "student_total_marks".to_string(),
987 postrust_core::schema_cache::ComputedColumn {
988 function: postrust_core::api_request::QualifiedIdentifier::new(
989 "public",
990 "student_total_marks",
991 ),
992 data_type: "integer".to_string(),
993 description: None,
994 row_argument: None,
995 session_argument: None,
996 takes_arguments: false,
997 },
998 );
999 let cache = cache(vec![table]);
1000 let names = NameOverrides::parse(
1001 r#"{"tables": {"public.student_marks": {
1002 "computed_fields": {"student_total_marks": "total_marks"},
1003 "permissions": {"tutor": {"select": {
1004 "columns": ["name"], "computed_fields": ["total_marks"], "filter": {}}}}}}}"#,
1005 )
1006 .unwrap();
1007 let view = cache_for_role(&cache, &names, "tutor", false);
1008 let table = view
1009 .get_table(&postrust_core::api_request::QualifiedIdentifier::new(
1010 "public",
1011 "student_marks",
1012 ))
1013 .expect("the table survives");
1014 assert!(
1015 table.computed_columns.contains_key("student_total_marks"),
1016 "granted under the name the client sees"
1017 );
1018 }
1019
1020 #[test]
1021 fn a_computed_field_is_granted_one_at_a_time() {
1022 let mut article = table("article", &["id"]);
1023 article.computed_columns.insert(
1024 "word_count".to_string(),
1025 postrust_core::schema_cache::ComputedColumn {
1026 function: QualifiedIdentifier::new("public", "word_count"),
1027 data_type: "integer".to_string(),
1028 description: None,
1029 row_argument: None,
1030 session_argument: None,
1031 takes_arguments: false,
1032 },
1033 );
1034 let view = cache_for_role(&cache(vec![article]), &names(), "user", false);
1035 let article = &view.tables[&QualifiedIdentifier::new("public", "article")];
1036 assert!(article.computed_columns.is_empty());
1038 }
1039
1040 #[test]
1041 fn a_table_that_can_only_be_written_keeps_the_write_and_gets_no_type() {
1042 let names = NameOverrides::parse(
1046 r#"{"tables": {"public.article": {"permissions":
1047 {"writer": {"insert": {"columns": "*", "check": {}}}}}}}"#,
1048 )
1049 .unwrap();
1050 let view = cache_for_role(
1051 &cache(vec![table("article", &["id"])]),
1052 &names,
1053 "writer",
1054 false,
1055 );
1056 let article = &view.tables[&QualifiedIdentifier::new("public", "article")];
1057 assert!(article.insertable);
1058 assert!(!article.updatable);
1059 assert!(!article.deletable);
1060 assert!(article.has_column("id"));
1061 let granted = names.permissions("public", "article", "writer").unwrap();
1062 assert!(!reads_anything(granted, article));
1063 }
1064
1065 #[test]
1069 fn a_key_to_an_enum_table_survives_a_role_that_cannot_read_it() {
1070 let users = table("users", &["id", "favorite_color"]);
1071 let colors = table("colors", &["value"]);
1072 let mut base = cache(vec![users, colors]);
1073 base.relationships.insert(
1074 (
1075 postrust_core::api_request::QualifiedIdentifier::new("public", "users"),
1076 "public".to_string(),
1077 ),
1078 vec![postrust_core::schema_cache::Relationship::ForeignKey {
1079 table: postrust_core::api_request::QualifiedIdentifier::new("public", "users"),
1080 foreign_table: postrust_core::api_request::QualifiedIdentifier::new(
1081 "public", "colors",
1082 ),
1083 is_self: false,
1084 cardinality: postrust_core::schema_cache::Cardinality::M2O {
1085 constraint: "users_favorite_color_fkey".to_string(),
1086 columns: vec![("favorite_color".to_string(), "value".to_string())],
1087 },
1088 table_is_view: false,
1089 foreign_table_is_view: false,
1090 constraint_name: "users_favorite_color_fkey".to_string(),
1091 }],
1092 );
1093
1094 let names = NameOverrides::parse(
1095 r#"{"tables": {
1096 "public.colors": {"enum": true},
1097 "public.users": {"permissions": {"anonymous":
1098 {"select": {"columns": "*", "filter": {}}}}}}}"#,
1099 )
1100 .unwrap();
1101 let view = cache_for_role(&base, &names, "anonymous", false);
1102
1103 assert!(
1104 !view
1105 .tables
1106 .contains_key(&postrust_core::api_request::QualifiedIdentifier::new(
1107 "public", "colors"
1108 )),
1109 "the table itself is not readable"
1110 );
1111 assert_eq!(
1112 view.relationships.len(),
1113 1,
1114 "and the key to it is kept anyway: {:?}",
1115 view.relationships
1116 );
1117 }
1118
1119 #[test]
1120 fn a_relationship_pointing_at_a_table_the_role_may_only_write_is_dropped() {
1121 let names = NameOverrides::parse(
1125 r#"{"tables": {
1126 "public.article": {"permissions":
1127 {"writer": {"select": {"columns": "*", "filter": {}}}}},
1128 "public.author": {"permissions":
1129 {"writer": {"insert": {"columns": "*", "check": {}}}}}}}"#,
1130 )
1131 .unwrap();
1132 let mut base = cache(vec![table("article", &["id"]), table("author", &["id"])]);
1133 base.relationships.insert(
1134 (
1135 QualifiedIdentifier::new("public", "article"),
1136 "public".to_string(),
1137 ),
1138 vec![postrust_core::schema_cache::Relationship::ForeignKey {
1139 table: QualifiedIdentifier::new("public", "article"),
1140 foreign_table: QualifiedIdentifier::new("public", "author"),
1141 is_self: false,
1142 cardinality: postrust_core::schema_cache::Cardinality::M2O {
1143 constraint: "article_author_id_fkey".to_string(),
1144 columns: vec![("author_id".to_string(), "id".to_string())],
1145 },
1146 table_is_view: false,
1147 foreign_table_is_view: false,
1148 constraint_name: "article_author_id_fkey".to_string(),
1149 }],
1150 );
1151 let view = cache_for_role(&base, &names, "writer", false);
1152 assert!(view
1153 .tables
1154 .contains_key(&QualifiedIdentifier::new("public", "author")));
1155 assert!(view.relationships.is_empty());
1156 }
1157
1158 #[test]
1161 fn an_operand_that_is_an_object_is_carried_through_whole() {
1162 let filter = serde_json::json!({
1163 "geom_col": {"$st_d_within": {
1164 "distance": 1,
1165 "from": {"type": "Polygon", "coordinates": [[[2, 0], [2, 1]]]}
1166 }}
1167 });
1168 let rewritten = permission_where(
1169 &filter,
1170 &HashMap::new(),
1171 &NameOverrides::default(),
1172 "public",
1173 "geom_table",
1174 )
1175 .expect("it rewrites")
1176 .expect("it restricts something");
1177 assert_eq!(
1178 rewritten,
1179 serde_json::json!({
1180 "geom_col": {"_st_d_within": {
1181 "distance": 1,
1182 "from": {"type": "Polygon", "coordinates": [[[2, 0], [2, 1]]]}
1183 }}
1184 }),
1185 "only the operator's own spelling changes"
1186 );
1187 }
1188
1189 #[test]
1192 fn a_session_variable_inside_an_operand_is_still_resolved() {
1193 let mut session = HashMap::new();
1194 session.insert("geom_val".to_string(), "{\"type\":\"Point\"}".to_string());
1195 let filter = serde_json::json!({
1196 "geom_col": {"$st_d_within": {"distance": 1, "from": "X-Hasura-Geom-Val"}}
1197 });
1198 let rewritten = permission_where(
1199 &filter,
1200 &session,
1201 &NameOverrides::default(),
1202 "public",
1203 "geom_table",
1204 )
1205 .expect("it rewrites")
1206 .expect("it restricts something");
1207 assert_eq!(
1208 rewritten["geom_col"]["_st_d_within"]["from"],
1209 serde_json::json!("{\"type\":\"Point\"}")
1210 );
1211 }
1212
1213 #[test]
1214 fn exists_keeps_its_table_and_rewrites_its_predicate() {
1215 let session = session(&[("user_id", "2")]);
1219 let rewritten = compiled(
1220 serde_json::json!({
1221 "_exists": {"_table": "user",
1222 "_where": {"id": "X-Hasura-User-Id", "is_admin": true}}
1223 }),
1224 &session,
1225 );
1226 assert_eq!(
1227 rewritten,
1228 serde_json::json!({
1229 "_exists": {"_table": "user",
1230 "_where": {"id": {"_eq": "2"},
1231 "is_admin": {"_eq": true}}}
1232 })
1233 );
1234 }
1235
1236 #[test]
1237 fn exists_names_its_table_either_way() {
1238 assert_eq!(
1239 exists_target(&serde_json::json!({"_table": "user"}), "public"),
1240 Some(("public".to_string(), "user".to_string()))
1241 );
1242 assert_eq!(
1243 exists_target(
1244 &serde_json::json!({"_table": {"schema": "auth", "name": "user"}}),
1245 "public"
1246 ),
1247 Some(("auth".to_string(), "user".to_string()))
1248 );
1249 assert_eq!(exists_target(&serde_json::json!({}), "public"), None);
1250 }
1251
1252 fn session(pairs: &[(&str, &str)]) -> HashMap<String, String> {
1253 pairs
1254 .iter()
1255 .map(|(k, v)| (k.to_string(), v.to_string()))
1256 .collect()
1257 }
1258
1259 fn compiled(filter: serde_json::Value, session: &HashMap<String, String>) -> serde_json::Value {
1260 permission_where(
1261 &filter,
1262 session,
1263 &NameOverrides::default(),
1264 "public",
1265 "article",
1266 )
1267 .unwrap()
1268 .unwrap()
1269 }
1270
1271 #[test]
1272 fn equality_written_without_saying_so_becomes_an_operator() {
1273 assert_eq!(
1274 compiled(serde_json::json!({"is_published": true}), &session(&[])),
1275 serde_json::json!({"is_published": {"_eq": true}})
1276 );
1277 }
1278
1279 #[test]
1280 fn the_older_connectives_are_the_newer_ones() {
1281 let filter = serde_json::json!({
1282 "$or": [{"author_id": "X-HASURA-USER-ID"}, {"is_published": true}]
1283 });
1284 assert_eq!(
1285 compiled(filter, &session(&[("user_id", "1")])),
1286 serde_json::json!({"_or": [
1287 {"author_id": {"_eq": "1"}},
1288 {"is_published": {"_eq": true}}
1289 ]})
1290 );
1291 }
1292
1293 #[test]
1294 fn the_older_operator_spellings_are_the_newer_ones_too() {
1295 assert_eq!(
1298 compiled(
1299 serde_json::json!({"id": {"$in": "X-Hasura-Allowed-Ids"}}),
1300 &session(&[("allowed_ids", "{1,2}")])
1301 ),
1302 serde_json::json!({"id": {"_in": ["1", "2"]}})
1303 );
1304 assert_eq!(
1305 compiled(serde_json::json!({"age": {"$gt": 18}}), &session(&[])),
1306 serde_json::json!({"age": {"_gt": 18}})
1307 );
1308 }
1309
1310 #[test]
1311 fn a_session_variable_is_read_however_it_is_spelled() {
1312 let by_shouting = compiled(
1314 serde_json::json!({"id": {"_eq": "X-HASURA-USER-ID"}}),
1315 &session(&[("user_id", "7")]),
1316 );
1317 let by_title = compiled(
1318 serde_json::json!({"id": {"_eq": "X-Hasura-User-Id"}}),
1319 &session(&[("user_id", "7")]),
1320 );
1321 assert_eq!(by_shouting, by_title);
1322 assert_eq!(by_shouting, serde_json::json!({"id": {"_eq": "7"}}));
1323 }
1324
1325 #[test]
1326 fn a_variable_the_caller_does_not_carry_is_refused() {
1327 let error = permission_where(
1328 &serde_json::json!({"id": "X-Hasura-User-Id"}),
1329 &session(&[]),
1330 &NameOverrides::default(),
1331 "public",
1332 "article",
1333 )
1334 .unwrap_err();
1335 assert!(error.contains("x-hasura-user-id"), "unhelpful: {}", error);
1338 }
1339
1340 #[test]
1341 fn a_list_operator_reads_its_variable_as_a_list() {
1342 assert_eq!(
1343 compiled(
1344 serde_json::json!({"name": {"_in": "X-Hasura-Free-Artists"}}),
1345 &session(&[("free_artists", "{Ann,Bob}")])
1346 ),
1347 serde_json::json!({"name": {"_in": ["Ann", "Bob"]}})
1348 );
1349 assert_eq!(
1351 compiled(
1352 serde_json::json!({"name": {"_in": "X-Hasura-Free-Artists"}}),
1353 &session(&[("free_artists", "{}")])
1354 ),
1355 serde_json::json!({"name": {"_in": []}})
1356 );
1357 assert_eq!(
1360 compiled(
1361 serde_json::json!({"name": {"_eq": "X-Hasura-Free-Artists"}}),
1362 &session(&[("free_artists", "{Ann,Bob}")])
1363 ),
1364 serde_json::json!({"name": {"_eq": "{Ann,Bob}"}})
1365 );
1366 }
1367
1368 #[test]
1369 fn a_predicate_over_a_relationship_survives_unchanged() {
1370 assert_eq!(
1374 compiled(
1375 serde_json::json!({"articles": {"is_published": {"_eq": true}}}),
1376 &session(&[])
1377 ),
1378 serde_json::json!({"articles": {"is_published": {"_eq": true}}})
1379 );
1380 }
1381
1382 #[test]
1383 fn an_empty_filter_restricts_nothing() {
1384 let none = permission_where(
1385 &serde_json::json!({}),
1386 &session(&[]),
1387 &NameOverrides::default(),
1388 "public",
1389 "article",
1390 )
1391 .unwrap();
1392 assert!(none.is_none());
1395 }
1396
1397 #[test]
1398 fn a_renamed_column_is_named_the_way_the_schema_exposes_it() {
1399 let names =
1400 NameOverrides::parse(r#"{"public.article": {"columns": {"author_id": "writtenBy"}}}"#)
1401 .unwrap();
1402 let compiled = permission_where(
1403 &serde_json::json!({"author_id": "X-Hasura-User-Id"}),
1404 &session(&[("user_id", "1")]),
1405 &names,
1406 "public",
1407 "article",
1408 )
1409 .unwrap()
1410 .unwrap();
1411 assert_eq!(compiled, serde_json::json!({"writtenBy": {"_eq": "1"}}));
1413 }
1414
1415 const WRITES: &str = r#"{
1416 "tables": {"public.article": {"permissions": {"user": {
1417 "select": {"columns": "*", "filter": {}},
1418 "insert": {"columns": "*", "check": {"is_published": false},
1419 "set": {"author_id": "X-Hasura-User-Id", "source": "web"},
1420 "backend_only": true},
1421 "update": {"columns": "*", "filter": {"author_id": "X-Hasura-User-Id"}},
1422 "delete": {"filter": {"author_id": "X-Hasura-User-Id"}}
1423 }}}}
1424 }"#;
1425
1426 fn writer() -> (NameOverrides, HashMap<String, String>) {
1427 (
1428 NameOverrides::parse(WRITES).unwrap(),
1429 session(&[("user_id", "3")]),
1430 )
1431 }
1432
1433 #[test]
1434 fn each_verb_is_asked_about_its_own_grant() {
1435 let (names, sess) = writer();
1436 let caller = Caller {
1437 role: Some("user"),
1438 session: &sess,
1439 };
1440 assert_eq!(
1442 row_filter(&caller, &names, "public", "article", Verb::Insert).unwrap(),
1443 None
1444 );
1445 for verb in [Verb::Update, Verb::Delete] {
1446 assert_eq!(
1447 row_filter(&caller, &names, "public", "article", verb).unwrap(),
1448 Some(serde_json::json!({"author_id": {"_eq": "3"}}))
1449 );
1450 }
1451 }
1452
1453 #[test]
1454 fn an_update_with_no_check_of_its_own_is_held_to_its_filter() {
1455 let (names, sess) = writer();
1458 let caller = Caller {
1459 role: Some("user"),
1460 session: &sess,
1461 };
1462 assert_eq!(
1463 write_check(&caller, &names, "public", "article", Verb::Update).unwrap(),
1464 Some(serde_json::json!({"author_id": {"_eq": "3"}}))
1465 );
1466 assert_eq!(
1467 write_check(&caller, &names, "public", "article", Verb::Insert).unwrap(),
1468 Some(serde_json::json!({"is_published": {"_eq": false}}))
1469 );
1470 }
1471
1472 #[test]
1473 fn a_preset_resolves_a_session_variable_and_keeps_a_literal() {
1474 let (names, sess) = writer();
1475 let caller = Caller {
1476 role: Some("user"),
1477 session: &sess,
1478 };
1479 assert_eq!(
1480 presets(&caller, &names, "public", "article", Verb::Insert).unwrap(),
1481 vec![
1482 ("author_id".to_string(), serde_json::json!("3")),
1483 ("source".to_string(), serde_json::json!("web")),
1484 ]
1485 );
1486 }
1487
1488 #[test]
1489 fn a_backend_only_insert_is_absent_until_a_backend_asks() {
1490 let (names, _) = writer();
1491 assert!(has_backend_only(&names, "user"));
1492 let of = |backend| {
1493 cache_for_role(
1494 &cache(vec![table("article", &["id"])]),
1495 &names,
1496 "user",
1497 backend,
1498 )
1499 .tables[&QualifiedIdentifier::new("public", "article")]
1500 .insertable
1501 };
1502 assert!(!of(false));
1503 assert!(of(true));
1504 }
1505
1506 #[test]
1507 fn aggregates_are_asked_for_separately_from_rows() {
1508 let names = names();
1509 assert!(allows_aggregations(&names, "public", "article", "user"));
1510 assert!(!allows_aggregations(&names, "public", "article", "reader"));
1512 assert!(!allows_aggregations(
1513 &names, "public", "article", "stranger"
1514 ));
1515 }
1516}