cedar-policy-validator 4.2.2

Validator for the Cedar Policy language.
Documentation
//
// Copyright Cedar Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

use std::str::FromStr;
use std::sync::Arc;
use crate::cedar_schema::err::{RawErrorRecovery, RawUserError, UserError};
use cedar_policy_core::parser::{Node, Loc, unescape::to_unescaped_string, cst::Ref};
use cedar_policy_core::ast::Id;
use smol_str::SmolStr;
use smol_str::ToSmolStr;
use crate::cedar_schema::ast::{
    Path,
    EntityDecl,
    Declaration,
    Namespace,
    Schema as ASchema,
    Type as SType,
    AttrDecl,
    ActionDecl,
    PR,
    AppDecl,
    TypeDecl,
    PrimitiveType,
    QualName,
    PRAppDecl};
use nonempty::{NonEmpty, nonempty};
use itertools::Either;

use lalrpop_util::{ParseError, ErrorRecovery};


/// `errors` collects generated errors.
///
/// `src` is the (full) original source being parsed, which the source locations l,r index into.
grammar<'err, 's>(errors: &'err mut Vec<RawErrorRecovery<'input>>, src: &'s Arc<str>);

extern {
    type Error = RawUserError;
}

match {
    // Whitespace and comments
    r"\s*" => { }, // The default whitespace skipping is disabled an `ignore pattern` is specified
    r"//[^\n\r]*[\n\r]*" => { }, // Skip `// comments`

    // keywords
    "namespace" => NAMESPACE,
    "entity" => ENTITY,
    "in" => IN,
    "type" => TYPE,
    "Set" => SET,
    "appliesTo" => APPLIESTO,
    "principal" => PRINCIPAL,
    "action" => ACTION,
    "resource" => RESOURCE,
    "context" => CONTEXT,
    "attributes" => ATTRIBUTES,
    "tags" => TAGS,
    "Long" => LONG,
    "String" => STRING,
    "Bool" => BOOL,

    // data input
    r"[_a-zA-Z][_a-zA-Z0-9]*" => IDENTIFIER,
    // The `NUMBER` token is a positive integer.
    // Negative number literals are negation operations.
    r"[0-9]+" => NUMBER,
    r#""(\\.|[^"\\])*""# => STRINGLIT,

    // other tokens
    ",", ";", ":", "::", "{", "}", "[", "]",
    "<", ">", "=", "?",

}


// Schema := {Namespace}
pub Schema: ASchema = {
    <ns: Namespace*> => ns,
}

// Namespace := 'namespace' Path '{' {Decl} '}'
Namespace: Node<Namespace> = {
    <l:@L> NAMESPACE <p: Path> "{" <decls: Decl*> "}" <r:@R>
        => Node::with_source_loc(Namespace { name: Some(Node::with_source_loc(p, Loc::new(l..r, Arc::clone(src)))), decls}, Loc::new(l..r, Arc::clone(src))),
     <l:@L> <decl: Decl> <r:@R> => Node::with_source_loc(Namespace {name: None, decls: vec![decl]}, Loc::new(l..r, Arc::clone(src))),
}

// Decl := Entity | Action | TypeDecl
Decl: Node<Declaration> = {
    <e:Entity> => e,
    <a:Action> => a,
    <t:TypeDecl> => t,
}

// Entity := 'entity' Idents ['in' EntOrTypes] [['='] RecType] ';'
Entity: Node<Declaration> = {
    <l:@L> ENTITY <ets: Idents> <ps:(IN <EntTypes>)?> <ds:("="? "{" <AttrDecls?> "}")?> <ts:(TAGS <Type>)?> ";" <r:@R>
        => Node::with_source_loc(Declaration::Entity(EntityDecl { names: ets, member_of_types: ps.unwrap_or_default(), attrs: ds.map(|ds| ds.unwrap_or_default()).unwrap_or_default(), tags: ts }), Loc::new(l..r, Arc::clone(src))),
}

// Action := 'action' Names ['in' QualNameOrNames]
Action: Node<Declaration> = {
    <l:@L> ACTION <ns:Names> <ps:(IN <QualNameOrQualNames>)?> <ads:(APPLIESTO "{" <AppDecls> "}")?> <attrs:(ATTRIBUTES "{" "}")?>";" <r:@R>
        => Node::with_source_loc(Declaration::Action(ActionDecl { names: ns, parents: ps, app_decls: ads}), Loc::new(l..r, Arc::clone(src))),
}

TypeDecl: Node<Declaration> = {
    <l:@L> TYPE <i:Ident> "=" <t:Type> ";" <r:@R>
        => Node::with_source_loc(Declaration::Type(TypeDecl { name : i, def : t}), Loc::new(l..r, Arc::clone(src))),
}

// AppDecls := ('principal' | 'resource') ':' EntOrTyps [',' | ',' AppDecls]
//          | 'context' ':' (Path | RecType) [',' | ',' AppDecls]
AppDecls: Node<NonEmpty<Node<AppDecl>>> = {
    <l:@L> <pr: PrincipalOrResource> ":" <ets:EntTypes> ","? <r:@R>
        =>?
                NonEmpty::collect(ets.into_iter()).ok_or(ParseError::User {
                    error: Node::with_source_loc(UserError::EmptyList, Loc::new(l..r, Arc::clone(src)))})
                    .map(|ets|
                        Node::with_source_loc(
                            nonempty![Node::with_source_loc(AppDecl::PR(PRAppDecl { kind:pr, entity_tys: ets}), Loc::new(l..r, Arc::clone(src)))],
                            Loc::new(l..r, Arc::clone(src)))),
    <l:@L> <pr: PrincipalOrResource> ":" <ets:EntTypes> "," <r:@R> <mut ds: AppDecls>
        =>?
                NonEmpty::collect(ets.into_iter()).ok_or(ParseError::User {
                    error: Node::with_source_loc(UserError::EmptyList, Loc::new(l..r, Arc::clone(src)))})
                    .map(|ets|
                        {
                            let (mut ds, _) = ds.into_inner();
                            ds.insert(0, Node::with_source_loc(AppDecl::PR(PRAppDecl { kind:pr, entity_tys: ets}), Loc::new(l..r, Arc::clone(src))));
                            Node::with_source_loc(ds, Loc::new(l..r, Arc::clone(src)))
                        }),
    <l:@L> CONTEXT ":" <p:Path> ","? <r:@R>
        =>  Node::with_source_loc(
                nonempty![Node::with_source_loc(AppDecl::Context(Either::Left(p)), Loc::new(l..r, Arc::clone(src)))],
                Loc::new(l..r, Arc::clone(src))),
    <l:@L> CONTEXT ":" <p:Path> "," <r:@R> <mut ds: AppDecls>
        => {
            let (mut ds, _) = ds.into_inner();
            ds.insert(0, Node::with_source_loc(AppDecl::Context(Either::Left(p)), Loc::new(l..r, Arc::clone(src))));
            Node::with_source_loc(
                ds,
                Loc::new(l..r, Arc::clone(src)))
        },
    <l:@L> CONTEXT ":" "{" <attrs:AttrDecls?> "}" ","? <r:@R>
        =>
            Node::with_source_loc(
                nonempty![Node::with_source_loc(AppDecl::Context(Either::Right(attrs.unwrap_or_default())), Loc::new(l..r, Arc::clone(src)))],
                Loc::new(l..r, Arc::clone(src))),
    <l:@L> CONTEXT ":" "{" <attrs:AttrDecls?> "}" "," <r:@R> <mut ds: AppDecls>
        => {
            let (mut ds, _) = ds.into_inner();
            ds.insert(0, Node::with_source_loc(AppDecl::Context(Either::Right(attrs.unwrap_or_default())), Loc::new(l..r, Arc::clone(src))));
            Node::with_source_loc(
                ds,
                Loc::new(l..r, Arc::clone(src)))
        },
}

// SetType := 'Set' '<' Type '>'
// RecType := '{' [AttrDecls] '}'
// Type := PRIMTYPE | Path | SetType | RecType
pub Type: Node<SType> = {
    <p:Path>
        => { let loc = p.loc().clone(); Node::with_source_loc(SType::Ident(p), loc) },
    <l:@L> SET "<" <t:Type> ">" <r:@R>
        => Node::with_source_loc(SType::Set(Box::new(t)), Loc::new(l..r, Arc::clone(src))),
    <l:@L> "{" <ds:AttrDecls?> "}" <r:@R>
        => Node::with_source_loc(SType::Record(ds.unwrap_or_default()), Loc::new(l..r, Arc::clone(src))),
}

// AttrDecls := Name ['?'] ':' Type [',' | ',' AttrDecls]
AttrDecls: Vec<Node<AttrDecl>> = {
    <l:@L> <name: Name> <required:"?"?> ":" <ty:Type> ","? <r:@R>
        => vec![Node::with_source_loc(AttrDecl { name, required: required.is_none(), ty}, Loc::new(l..r, Arc::clone(src)))],
    <l:@L> <name: Name> <required:"?"?> ":" <ty:Type> "," <r:@R> <mut ds: AttrDecls>
        => {ds.insert(0, Node::with_source_loc(AttrDecl { name, required: required.is_none(), ty}, Loc::new(l..r, Arc::clone(src)))); ds},
}


Comma<E>: Vec<E> = {
    <e:E?> => e.into_iter().collect(),
    <mut es:(<E> ",")+> <e:E> => {
        es.push(e);
        es
    },
}

// IDENT := ['_''a'-'z''A'-'Z']['_''a'-'z''A'-'Z''0'-'9']*
Ident: Node<Id> = {
    <l:@L> NAMESPACE <r:@R>
        => Node::with_source_loc("namespace".parse().unwrap(), Loc::new(l..r, Arc::clone(src))),
    <l:@L> ENTITY <r:@R>
        => Node::with_source_loc("entity".parse().unwrap(), Loc::new(l..r, Arc::clone(src))),
    <l:@L> SET <r:@R>
        => Node::with_source_loc("Set".parse().unwrap(), Loc::new(l..r, Arc::clone(src))),
    <l:@L> APPLIESTO <r:@R>
        => Node::with_source_loc("appliesTo".parse().unwrap(), Loc::new(l..r, Arc::clone(src))),
    <l:@L> PRINCIPAL <r:@R>
        => Node::with_source_loc("principal".parse().unwrap(), Loc::new(l..r, Arc::clone(src))),
    <l:@L> ACTION <r:@R>
        => Node::with_source_loc("action".parse().unwrap(), Loc::new(l..r, Arc::clone(src))),
    <l:@L> RESOURCE <r:@R>
        => Node::with_source_loc("resource".parse().unwrap(), Loc::new(l..r, Arc::clone(src))),
    <l:@L> CONTEXT <r:@R>
        => Node::with_source_loc("context".parse().unwrap(), Loc::new(l..r, Arc::clone(src))),
    <l:@L> ATTRIBUTES <r:@R>
        => Node::with_source_loc("attributes".parse().unwrap(), Loc::new(l..r, Arc::clone(src))),
    <l:@L> TAGS <r:@R>
        => Node::with_source_loc("tags".parse().unwrap(), Loc::new(l..r, Arc::clone(src))),
    <l:@L> BOOL <r:@R>
        => Node::with_source_loc("Bool".parse().unwrap(), Loc::new(l..r, Arc::clone(src))),
    <l:@L> LONG <r:@R>
        => Node::with_source_loc("Long".parse().unwrap(), Loc::new(l..r, Arc::clone(src))),
    <l:@L> STRING <r:@R>
        => Node::with_source_loc("String".parse().unwrap(), Loc::new(l..r, Arc::clone(src))),
    <l:@L> TYPE <r:@R>
        => Node::with_source_loc("type".parse().unwrap(), Loc::new(l..r, Arc::clone(src))),
    <l:@L> IN <r:@R>
        =>? Err(ParseError::User {
            error: Node::with_source_loc(UserError::ReservedIdentifierUsed("in".into()), Loc::new(l..r, Arc::clone(src)))
        }),
    <l:@L> <i:IDENTIFIER> <r:@R>
        =>? Id::from_str(i)
        .map(|id : Id| Node::with_source_loc(id, Loc::new(l..r, Arc::clone(src))))
        .map_err(|err : cedar_policy_core::parser::err::ParseErrors|
            ParseError::User {
                error: Node::with_source_loc(UserError::ReservedIdentifierUsed(i.to_smolstr()), Loc::new(l..r, Arc::clone(src)))
            }
        )
}

STR: Node<SmolStr> = {
    <l:@L> <s:STRINGLIT> <r:@R>
        =>? to_unescaped_string(&s[1..(s.len() - 1)]).map_or_else(|e| Err(ParseError::User {
            error: Node::with_source_loc(UserError::StringEscape(e), Loc::new(l..r, Arc::clone(src))),
        }), |v| Ok(Node::with_source_loc(v, Loc::new(l..r, Arc::clone(src))))),
}

// Name := IDENT | STR
Name: Node<SmolStr> = {
   <id: Ident> => id.map(|id| id.to_smolstr()),
   <s: STR> => s,
}

// QualName      := Name | Path '::' STR
QualName : Node<QualName> = {
    <l:@L> <p : PathInline> "::" <s:STR> <r:@R> => Node::with_source_loc(QualName::qualified(p, s.node), Loc::new(l..r, Arc::clone(src))),
    <l:@L> <i : Name> <r:@R> => Node::with_source_loc(QualName::unqualified(i.node), Loc::new(l..r, Arc::clone(src))),
}

// Path := IDENT {'::' IDENT}
#[inline]
PathInline: Path = {
    <l:@L> <i:Ident> <r:@R>
        => Path::single(i.node, Loc::new(l..r, Arc::clone(src))),
    <l:@L> <is:(<Ident> "::")+> <i:Ident> <r:@R>
        => Path::new(i.node, is.into_iter().map(|n| n.node), Loc::new(l..r, Arc::clone(src))),
}

// Idents := IDENT {',' IDENT}
Idents: Vec<Node<Id>> = {
    <i:Ident> => vec![i],
    <mut is:(<Ident> ",")+> <i:Ident> => {
        is.push(i);
        is
    },
}

// Names := Name {',' Name}
Names: NonEmpty<Node<SmolStr>> = {
    <n:Name> => NonEmpty::new(n),
    <mut ns:(<Name> ",")+> <n:Name> => {
        let mut all = NonEmpty::new(n);
        all.append(&mut ns);
        all
    },
}


// Qualnames := Qualname {',' Qualname }

QualNames : NonEmpty<Node<QualName>> = {
    <n:QualName> => NonEmpty::singleton(n),
    <ns:(<QualName> ",")+> <n:QualName> => {
        NonEmpty {
            head : n,
            tail : ns
        }
    },
}


PrincipalOrResource: Node<PR> = {
    <l:@L> PRINCIPAL <r:@R> => Node::with_source_loc(PR::Principal, Loc::new(l..r, Arc::clone(src))),
    <l:@L> RESOURCE <r:@R> => Node::with_source_loc(PR::Resource, Loc::new(l..r, Arc::clone(src))),
}


// EntType   := Path
// EntTypes  := Path {',' Path}
// EntOrTyps := EntType | '[' [EntTypes] ']'
EntTypes: Vec<Path> = {
    <et: Path>
        => vec![et],
    "[" <ets: Comma<Path>> "]"
        => ets,
}


NameOrNames: Vec<Node<SmolStr>> = {
    <n : Name> => vec![n],
    "[" <ns:Comma<Name>> "]" => ns,
}

QualNameOrQualNames: NonEmpty<Node<QualName>> = {
    <qn : QualName> => NonEmpty::singleton(qn),
    "[" <ns: QualNames> "]" => ns,
}

Path: Path = PathInline;