coil-auth 0.1.1

Authorisation models and auth package support for the Coil framework.
Documentation
use super::*;

use super::conversion::{
    graph_node_from_subject, graph_node_from_subject_with_relation, typed_node, typed_tuple,
};
use super::index::{Evaluation, ExplainIndex, GraphNode};
use super::matching::{subject_matches_node, subject_matches_subject};
use super::rules::{inherit_rules_for, userset_jump_rules_for};

pub(super) fn build_relation_trace(
    schema: &Schema,
    tuples: &[Tuple],
    subject: &DefaultSubject,
    relation: Relation,
    object: &Entity,
    options: ExplainOptions,
) -> Result<ExplainTrace, CoilAuthError> {
    let options = options.normalized();
    let root = GraphNode {
        object: object.to_object(),
        relation: Some(relation.to_string()),
    };
    let subject = subject.to_subject();
    let index = ExplainIndex::new(tuples);
    let mut visiting = HashSet::new();

    match explain_node(schema, &index, &subject, &root, 1, options, &mut visiting)? {
        Evaluation::Allowed(mut steps) => {
            steps.insert(
                0,
                ExplainStep::Start {
                    node: typed_node(&root)?,
                },
            );
            Ok(ExplainTrace::Allowed(AllowedExplanation { steps }))
        }
        Evaluation::Denied(denied) => Ok(ExplainTrace::Denied(denied)),
    }
}

fn explain_node(
    schema: &Schema,
    index: &ExplainIndex,
    subject: &Subject,
    node: &GraphNode,
    depth: usize,
    options: ExplainOptions,
    visiting: &mut HashSet<GraphNode>,
) -> Result<Evaluation, CoilAuthError> {
    if options.cycle_protection && !visiting.insert(node.clone()) {
        return Ok(Evaluation::Denied(DeniedExplanation {
            node: typed_node(node)?,
            reason: DeniedReason::CycleDetected,
            attempts: Vec::new(),
        }));
    }

    let result = explain_node_inner(schema, index, subject, node, depth, options, visiting);

    if options.cycle_protection {
        visiting.remove(node);
    }

    result
}

fn explain_node_inner(
    schema: &Schema,
    index: &ExplainIndex,
    subject: &Subject,
    node: &GraphNode,
    depth: usize,
    options: ExplainOptions,
    visiting: &mut HashSet<GraphNode>,
) -> Result<Evaluation, CoilAuthError> {
    if subject_matches_node(node, subject) {
        return Ok(Evaluation::Allowed(vec![ExplainStep::DirectSubjectMatch {
            node: typed_node(node)?,
        }]));
    }

    for tuple in index.tuples_for(node) {
        if subject_matches_subject(&tuple.subject, subject) {
            return Ok(Evaluation::Allowed(vec![ExplainStep::TupleSubjectMatch {
                from: typed_node(node)?,
                tuple: typed_tuple(tuple)?,
            }]));
        }
    }

    if depth >= options.max_depth {
        return Ok(Evaluation::Denied(DeniedExplanation {
            node: typed_node(node)?,
            reason: DeniedReason::RecursionLimitReached {
                max_depth: options.max_depth,
            },
            attempts: Vec::new(),
        }));
    }

    let mut attempts = Vec::new();

    for rule in inherit_rules_for(schema, node)? {
        let next = GraphNode {
            object: node.object.clone(),
            relation: Some(rule.to_string()),
        };
        let step = ExplainStep::Inherit {
            from: typed_node(node)?,
            to: typed_node(&next)?,
        };

        match explain_node(schema, index, subject, &next, depth + 1, options, visiting)? {
            Evaluation::Allowed(mut steps) => {
                steps.insert(0, step);
                return Ok(Evaluation::Allowed(steps));
            }
            Evaluation::Denied(result) => {
                attempts.push(DeniedAttempt::Inherit {
                    step,
                    result: Box::new(result),
                });
            }
        }
    }

    for tuple in index.tuples_for(node) {
        let next = graph_node_from_subject(&tuple.subject);
        let step = ExplainStep::TupleTraversal {
            from: typed_node(node)?,
            tuple: typed_tuple(tuple)?,
            to: typed_node(&next)?,
        };

        match explain_node(schema, index, subject, &next, depth + 1, options, visiting)? {
            Evaluation::Allowed(mut steps) => {
                steps.insert(0, step);
                return Ok(Evaluation::Allowed(steps));
            }
            Evaluation::Denied(result) => {
                attempts.push(DeniedAttempt::TupleTraversal {
                    step,
                    result: Box::new(result),
                });
            }
        }
    }

    for (tuple_relation, target_relation, is_tuple_to_userset) in
        userset_jump_rules_for(schema, node)?
    {
        let jump_node = GraphNode {
            object: node.object.clone(),
            relation: Some(tuple_relation.to_string()),
        };

        for tuple in index.tuples_for(&jump_node) {
            let next = graph_node_from_subject_with_relation(&tuple.subject, target_relation);
            let step = if is_tuple_to_userset {
                ExplainStep::TupleToUserset {
                    from: typed_node(node)?,
                    via_tuple: typed_tuple(tuple)?,
                    to: typed_node(&next)?,
                }
            } else {
                ExplainStep::Computed {
                    from: typed_node(node)?,
                    via_tuple: typed_tuple(tuple)?,
                    to: typed_node(&next)?,
                }
            };

            match explain_node(schema, index, subject, &next, depth + 1, options, visiting)? {
                Evaluation::Allowed(mut steps) => {
                    steps.insert(0, step);
                    return Ok(Evaluation::Allowed(steps));
                }
                Evaluation::Denied(result) => {
                    attempts.push(if is_tuple_to_userset {
                        DeniedAttempt::TupleToUserset {
                            step,
                            result: Box::new(result),
                        }
                    } else {
                        DeniedAttempt::Computed {
                            step,
                            result: Box::new(result),
                        }
                    });
                }
            }
        }
    }

    Ok(Evaluation::Denied(DeniedExplanation {
        node: typed_node(node)?,
        reason: DeniedReason::NoMatchingPath,
        attempts,
    }))
}