Skip to main content

postrust_graphql/
role.rs

1//! What one role can see.
2//!
3//! Hasura builds a separate GraphQL schema for every role, and the difference
4//! between them is not cosmetic. A role with no `select` permission on a table
5//! does not get a table it is refused access to -- it gets a schema with no
6//! such field to read it by, and naming one is a validation failure rather
7//! than a denial. The same goes one level down: a column outside the
8//! permission is absent from the type, not merely null.
9//!
10//! That shape is what the corpus tests. `artist_select_query_Track_fail.yaml`
11//! expects `field "Track" not found in type: 'query_root'`, which no amount of
12//! filtering at execution time produces.
13//!
14//! # Why this is a filtered cache rather than a flag threaded through the
15//! builders
16//!
17//! A select permission is a view of a table, and a set of them is a view of
18//! the database. Reducing the schema cache to what a role may see and then
19//! building the schema from *that* means the builders need to know nothing
20//! about permissions: a table that was dropped generates no root field, no
21//! relationship pointing at it and no boolean expression naming it, because
22//! the code that would have generated them cannot see it either.
23//!
24//! The alternative -- asking "may this role?" at each of the several dozen
25//! places a name is emitted -- has to be right every time, and is wrong the
26//! moment someone adds a place. This has to be right once.
27//!
28//! # The one thing a cache cannot hold: two column sets
29//!
30//! Hasura grants reading and writing separately, so a role may be allowed to
31//! set a column it is not allowed to see -- ten permissions in the corpus do
32//! -- and may be allowed to write a table it cannot read at all, which eight
33//! more do. A schema cache has one column list per table and no place to say
34//! which half of it is which.
35//!
36//! The union goes in the cache, and the split is made once, where the object
37//! type's fields are built: [`crate::schema::object::TableObjectType::fields`]
38//! is what may be read and `writable_fields` is what may be written. That
39//! keeps the property above -- everything a read is made of (the type, its
40//! boolean expression, its ordering, its column enum, its aggregates) is
41//! derived from `fields` and narrows with it, and the write inputs are built
42//! from the other list and narrowed again by the permission that names them.
43//!
44//! A table with no readable field is the limit of that: it has no GraphQL type
45//! at all, because a type with no fields is not a legal one. Its bulk writes
46//! remain and answer with `affected_rows` alone -- there is no `returning` to
47//! hang rows on -- and everything that would return a row of it (the query
48//! roots, `insert_one`, the `_by_pk` writes, a relationship pointing at it, a
49//! function returning it) is left out with the type.
50//!
51//! What is left over goes on [`SchemaConfig`]: `allow_aggregations`, which is
52//! a fact about a permission rather than about a table.
53//!
54//! [`SchemaConfig`]: crate::schema::SchemaConfig
55
56use crate::names::NameOverrides;
57use postrust_core::schema_cache::{SchemaCache, Table};
58
59/// Reduce a schema cache to what one role may see.
60///
61/// Returns the cache unchanged when the document grants that role nothing to
62/// narrow -- which is the case for `admin`, and for every role when the
63/// document carries no permissions at all.
64pub 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            // Named nothing about this table. With a permission document in
75            // play that is a statement, not a silence: the role cannot see it.
76            return false;
77        };
78
79        // Which of the four grants apply, decided before the columns because
80        // the columns depend on the answer.
81        //
82        // `backend_only` narrows the insert once more: such a field is
83        // reachable only by a caller that proved it holds the admin secret and
84        // asked for it by name, so for everyone else it is not there at all --
85        // which is the point of the flag. A client must not be able to reach
86        // it by naming a role.
87        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        // Reading is not what makes a table exist here, and that is the whole
97        // of this change. Hasura lets a role write a table it cannot read, and
98        // eight permissions in its corpus do exactly that: `insert_account`
99        // for `user`, `insert_leads` for `sales`, `insert_author` for
100        // `student`. Such a mutation answers `affected_rows` and has no
101        // `returning` to hang rows on, because there is no type to read the
102        // written row out of.
103        if granted.select.is_none() && !writes {
104            return false;
105        }
106
107        // The columns the role may read, together with the ones it may write
108        // without reading. Everything else is not merely unreadable but
109        // absent, which is what makes a permission a statement about the
110        // schema rather than about a request.
111        //
112        // This is the one place both column sets are held at once, and the
113        // reason they can be: a schema cache has a single column list, so the
114        // union goes in it and the narrower read set is applied once, where
115        // the object type's fields are built. Everything a read is made of --
116        // the type, its boolean expression, its ordering, its column enum, its
117        // aggregates -- is derived from those fields and narrows with them.
118        // The write inputs are built from the union instead and narrowed again
119        // by the write permission that names them.
120        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        // A computed field runs a function over the whole row, so it can
140        // answer questions the column list was written to prevent. Hasura
141        // grants them one at a time and grants none by default -- and grants
142        // none at all to a role that cannot read the table, since there is
143        // nowhere for the answer to appear.
144        //
145        // Named as the client sees it. The map is keyed by the *function*,
146        // and a permission grants the *field* -- `student_total_marks` and
147        // `total_marks` are the same thing under the two names, and comparing
148        // them directly withheld every computed field a schema had renamed.
149        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        // A select permission naming no columns at all, with
160        // `allow_aggregations`, is how Hasura grants "how many" without
161        // granting "which": the table is there to be counted and has nothing
162        // to read. It gets an aggregate root and no row type, the same shape a
163        // table this role may only write has.
164        let counts = granted
165            .select
166            .as_ref()
167            .is_some_and(|select| select.allow_aggregations);
168
169        // Nothing to read, nothing to write and nothing to count is nothing at
170        // all: the role is told the table is not there, which is the same
171        // answer in the direction that withholds.
172        !table.columns.is_empty() || !table.computed_columns.is_empty() || writes || counts
173    });
174
175    // A relationship whose other end the role cannot *read* is not a field it
176    // gets a null for -- there is nothing to name it after, because a table
177    // this role may only write has no type. Both ends are checked: the map is
178    // keyed by the source, and the target may have gone even where the source
179    // stayed.
180    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    // A table whose rows are a generated enum is the exception. A column with
191    // a key to one is *typed* as that enum, and the type is not the table's
192    // data: `colors_enum` is published to a role that may not read `colors` at
193    // all, and Hasura publishes it too. The key is what says which column is
194    // typed by which enum, so it has to survive here -- and no field comes of
195    // it, since a relationship to an enum table is never exposed.
196    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
214/// Whether this role can read anything of this table at all.
215///
216/// The question a relationship target has to answer, and the question that
217/// decides whether the table gets a GraphQL type: a role with a write grant
218/// and no select permission keeps the table in the cache, and a type with no
219/// fields is not a legal type. Asked of an already-reduced table, so the
220/// computed fields it still carries are the granted ones.
221pub 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
229/// Whether any of this role's permissions is reachable only by a backend.
230///
231/// Which decides whether the role needs a second schema at all: without one,
232/// what a backend caller may name is what everyone may name.
233pub 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/// Who is asking, for the places that have to narrow rows to them.
243///
244/// The two request-scoped facts a permission needs, carried together because
245/// they are always needed together and because the SQL builders they reach are
246/// several calls below the context that holds them. `role: None` is the
247/// unrestricted read: no permission document, or an administrator.
248#[derive(Clone, Copy)]
249pub struct Caller<'a> {
250    /// The role to narrow rows to. `None` is the unrestricted read.
251    pub role: Option<&'a str>,
252    /// Session variables the permissions may refer to, such as
253    /// `x-hasura-user-id`.
254    pub session: &'a std::collections::HashMap<String, String>,
255}
256
257impl Caller<'_> {
258    /// Whether any permission applies to this caller at all.
259    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/// Why a read could not be narrowed to the caller.
268///
269/// Two different answers with two different codes in Hasura, so they are kept
270/// apart here rather than collapsed into one message.
271#[derive(Debug, Clone, PartialEq, Eq)]
272pub enum Fault {
273    /// The role has no select permission on the table. Unreachable through a
274    /// role's own schema, which has no such field -- so reaching it means
275    /// something was built that should not have been, and the safe answer is
276    /// to refuse rather than to read every row.
277    NoPermission {
278        /// The role that was asking.
279        role: String,
280        /// The table it has no select permission on.
281        table: String,
282    },
283    /// A permission names a session variable the caller does not carry.
284    MissingSessionVariable(String),
285}
286
287impl Fault {
288    /// Hasura's `extensions.code` for this.
289    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
310/// Hasura's wording for a written row that a permission refuses.
311///
312/// One message for both verbs, which is Hasura's choice and not a shortcut
313/// taken here: `check constraint of an insert/update permission has failed`.
314pub const CHECK_FAILED: &str = "check constraint of an insert/update permission has failed";
315
316/// Which of the four grants is being asked about.
317#[derive(Clone, Copy, Debug, PartialEq, Eq)]
318pub enum Verb {
319    /// Reading rows.
320    Select,
321    /// Adding rows.
322    Insert,
323    /// Changing existing rows.
324    Update,
325    /// Removing rows.
326    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
340/// One grant, whichever it is.
341enum 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    /// Which rows this grant applies to, read before the operation.
350    ///
351    /// An insert has none: there are no rows to choose from yet, which is
352    /// exactly why it has a `check` instead.
353    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    /// What a written row has to satisfy afterwards.
363    ///
364    /// An update with no `check` of its own is held to its `filter`, which is
365    /// Hasura's rule: a row you may change is a row you may change into
366    /// something you may still change.
367    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    /// Columns the server fills in, whatever the request said.
379    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        // Reaching here means a schema was built naming an operation this role
405        // has no permission for, which should not happen -- and the safe
406        // answer to "which rows" is none of them, not all of them.
407        None => Err(Fault::NoPermission {
408            role: role.to_string(),
409            table: table.to_string(),
410        }),
411    }
412}
413
414/// Which rows a role's permission lets one operation reach.
415///
416/// `Ok(None)` where nothing restricts it: an unrestricted caller, a filter of
417/// `{}`, or an insert, which has no rows to choose from.
418pub 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
435/// What a written row has to satisfy for the write to stand.
436pub 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
453/// The columns the server fills in itself, with their values.
454///
455/// This is how `author_id` comes from the caller's identity rather than from
456/// the caller: a preset overrides whatever the request said, so a client that
457/// names the column anyway does not get to choose it.
458pub 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        // A preset's value is a session variable as often as it is a literal,
475        // and it is resolved the same way a filter's operand is.
476        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    // Sorted, so the SQL a preset produces does not depend on how a hash map
488    // felt about its keys.
489    filled.sort_by(|a, b| a.0.cmp(&b.0));
490    Ok(filled)
491}
492
493/// The predicate a role's select permission adds to a read of one table.
494pub 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
503/// How many rows a role's permission lets one read return.
504///
505/// A ceiling rather than a default, and `None` where the permission names one
506/// no more than the configuration does.
507pub 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
523/// Turn a permission's row filter into the boolean expression a `where`
524/// argument would have been.
525///
526/// The two are the same language. Hasura's `filter` is written in the same
527/// shape a client writes `where` in, so there is no second predicate compiler
528/// here and no second set of operators to keep in step -- the filter is
529/// rewritten into a `<table>_bool_exp` and handed to the one that already
530/// exists. Everything it can express, a permission can express, including a
531/// predicate over a relationship.
532///
533/// Three things metadata spells differently, and they are all this does:
534///
535///   * `{"author_id": 1}` is `_eq` written without saying so. Metadata omits
536///     the operator where it is equality; a `where` argument never does.
537///   * `$and`, `$or` and `$not` are the older spellings of `_and`, `_or` and
538///     `_not`, and the corpus uses both -- sometimes in one file.
539///   * `"X-Hasura-User-Id"` is not a string to compare against but the name of
540///     a session variable to compare against the value of. This is the half
541///     that makes a permission about the caller rather than about the table.
542///
543/// `Ok(None)` means the filter restricts nothing, which is what `{}` says and
544/// is a real answer: a role may be granted every row. It is distinct from
545/// having no permission at all, which is settled before this is reached.
546///
547/// An `Err` names a session variable the caller does not carry. That is a
548/// refusal rather than an empty result, because a filter that silently
549/// compares against nothing is a filter that matches nothing for a reason the
550/// client cannot see -- and Hasura says so too, in these words.
551pub 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
566/// The older spellings, and what they are now.
567fn 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
576/// Whether an operator takes a list, which decides how a session variable
577/// spells itself.
578fn 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    // The operator this value is the operand of, where it is one. Only used to
589    // decide whether a session variable is one value or a list of them.
590    operator: Option<&str>,
591) -> Result<serde_json::Value, String> {
592    match value {
593        // Inside an operator, an object is the operator's *operand* and not a
594        // nested boolean expression: `_st_d_within` takes `{distance, from}`
595        // and `_contains` takes a whole document. Reading their keys as
596        // columns renamed them, wrapped their values in `_eq`, and handed
597        // PostGIS something that was no longer GeoJSON -- `invalid GeoJson
598        // representation`, about a shape this server had taken apart.
599        //
600        // Session variables inside one are still resolved. That is the only
601        // thing a permission's operand needs rewriting for, and `{"from":
602        // "X-Hasura-Geom-Val"}` is how the corpus writes it.
603        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                // `$in`, `$gt`, `$is_null`: the whole operator family has an
626                // older spelling, not just the three connectives above, and
627                // the corpus writes both. Everything after the sigil is the
628                // operator this server already knows.
629                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                // A predicate over another table, so what is inside it is
635                // spelled in *that* table's names rather than in this one's.
636                // Everything else here carries the outer table down.
637                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                    // An operator. Its operand may be a session variable, and
647                    // whether that is one value or several depends on which
648                    // operator it is.
649                    out.insert(
650                        key.to_string(),
651                        rewrite(child, session, names, schema, table, Some(&key))?,
652                    );
653                    continue;
654                }
655
656                // A column, or a relationship. A renamed column is exposed
657                // under the other name and the compiler below reads exposed
658                // names, so the rename is applied here. A relationship is not
659                // in the column map and so is left alone -- along with the
660                // columns inside it, which belong to a table this does not
661                // know the name of. A rename on the far side of a relationship
662                // inside a permission is the one spelling this does not
663                // follow.
664                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                // Equality written without saying so: metadata omits the
671                // operator, a boolean expression never does.
672                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
696/// The table `_exists` looks in, as `(schema, table)`.
697///
698/// Hasura's metadata writes the name two ways -- `_table: user` and `_table:
699/// {schema: public, name: user}` -- and its own corpus uses the first. An
700/// unqualified name is read in the schema of the table the permission is on,
701/// which is where a permission written without a schema means to look.
702pub 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
717/// `_exists`, rewritten so that its predicate reads against the table it names.
718///
719/// The `_table` is a name and not a predicate, so it is copied through
720/// untouched: rewriting it would read a table called `x-hasura-anything` as a
721/// session variable, and would wrap it in an `_eq` besides.
722fn 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
750/// The session variable a string names, if it names one.
751///
752/// Case does not matter: the corpus writes `X-HASURA-USER-ID` and
753/// `X-Hasura-User-Id` for the same variable, sometimes in adjacent lines of
754/// one file.
755fn 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
764/// The value a session variable stands for.
765fn 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        // Hasura's wording, and Hasura's decision to refuse rather than to
772        // compare against nothing.
773        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    // A list arrives as a PostgreSQL array literal, because a header carries
784    // one string and that is the spelling both ends already agree on:
785    // `X-Hasura-Allowed-Ids: {1,2,3}`. Unbraced is read as a single-item list
786    // rather than refused, since a one-element list is the case a caller is
787    // most likely to write bare.
788    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
803/// Whether a role may ask for aggregates over a table.
804///
805/// Separate from reading its rows because counting rows you cannot see is a
806/// way of seeing them, and Hasura keeps the two apart for that reason. A role
807/// the document says nothing about is not reached here: it has no table.
808pub 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        // `secret` names `reader` and not `user`, so for `user` it does not
918        // exist -- which is a different answer from existing and refusing.
919        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        // `narrow` reads id and title and writes title. `content` is named by
927        // neither, so it is not in the cache under any heading.
928        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        // `user` reads id and title and inserts every column. `content` is in
942        // the cache because the insert names it -- the read half is taken out
943        // where the object type's fields are built, not here.
944        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        // Granted insert; told nothing about update or delete.
976        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        // The document grants `insert` and the database does not. Metadata
988        // above the database cannot widen what is beneath it.
989        assert!(!article.insertable);
990    }
991
992    /// A permission grants the field, and the map is keyed by the function.
993    /// Comparing the two directly withheld every renamed computed field.
994    #[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        // The permission names no computed fields, and none is the default.
1049        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        // `writer` may insert and not read. The table stays, with the columns
1055        // the insert may name and nothing to read them back with: nothing this
1056        // role asks can produce a row of it.
1057        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    /// A generated enum is a type, not the table's data. A role granted
1078    /// nothing on the table still sees the enum, and still has a column typed
1079    /// by it -- so the key that says which column is kept.
1080    #[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        // There is nothing to name such a field after: the far side has no
1134        // type, and naming one that was never registered is a schema that will
1135        // not build.
1136        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    /// An operator's operand is a value, not a boolean expression. Reading
1171    /// its keys as columns took a shape apart before PostGIS ever saw it.
1172    #[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    /// And a session variable inside such an operand is still resolved, which
1202    /// is the one thing it does need rewriting for.
1203    #[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        // The table is a name, not a value: it must survive untouched, while
1228        // the predicate beside it gets the session variable and the `_eq` that
1229        // metadata leaves implicit.
1230        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        // Not only the three connectives: the whole family has a `$` spelling
1308        // and the corpus writes both.
1309        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        // The corpus writes both, sometimes in one file.
1325        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        // Refused rather than compared against nothing: a filter that matches
1348        // no rows for a reason the client cannot see is worse than a refusal.
1349        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        // An empty array literal is no items, not one empty item.
1362        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        // And the same variable under an operator that takes one value stays
1370        // one value.
1371        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        // Nothing special to do: a relationship in a permission is the same
1383        // shape a relationship in `where` is, and the compiler below already
1384        // turns one into a correlated EXISTS.
1385        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        // A real answer, and not the same as having no permission at all --
1405        // which is settled before this is reached.
1406        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        // The permission names the column; the compiler below reads the field.
1424        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        // An insert has no rows to choose from, which is why it has a check.
1453        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        // Hasura's rule: a row you may change is a row you may change into
1468        // something you may still change.
1469        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        // `reader` may read every column and was not granted aggregates.
1523        assert!(!allows_aggregations(&names, "public", "article", "reader"));
1524        assert!(!allows_aggregations(
1525            &names, "public", "article", "stranger"
1526        ));
1527    }
1528}