package com.getperka.flatpack.policy; /* * #%L * FlatPack Security Policy * %% * Copyright (C) 2012 - 2013 Perka Inc. * %% * 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 * * http://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. * #L% */ import java.util.ArrayList; import java.util.List; import org.parboiled.Action; import org.parboiled.BaseParser; import org.parboiled.Context; import org.parboiled.Parboiled; import org.parboiled.Rule; import org.parboiled.annotations.Cached; import org.parboiled.annotations.DontExtend; import org.parboiled.annotations.DontLabel; import org.parboiled.annotations.Label; import org.parboiled.support.StringVar; import org.parboiled.support.Var; import com.getperka.flatpack.ext.Property; import com.getperka.flatpack.ext.PropertyPath; import com.getperka.flatpack.policy.pst.ActionDefinition; import com.getperka.flatpack.policy.pst.AllowBlock; import com.getperka.flatpack.policy.pst.AllowRule; import com.getperka.flatpack.policy.pst.GroupBlock; import com.getperka.flatpack.policy.pst.GroupDefinition; import com.getperka.flatpack.policy.pst.HasInheritFrom; import com.getperka.flatpack.policy.pst.HasName; import com.getperka.flatpack.policy.pst.Ident; import com.getperka.flatpack.policy.pst.PackagePolicy; import com.getperka.flatpack.policy.pst.PolicyBlock; import com.getperka.flatpack.policy.pst.PolicyFile; import com.getperka.flatpack.policy.pst.PolicyNode; import com.getperka.flatpack.policy.pst.PropertyList; import com.getperka.flatpack.policy.pst.PropertyPolicy; import com.getperka.flatpack.policy.pst.TypePolicy; import com.getperka.flatpack.security.SecurityAction; import com.getperka.flatpack.security.SecurityGroup; /** * The grammar definition for the policy file. * <p> * The method names in this type use parboiled's <a * href="https://github.com/sirthias/parboiled/wiki/Style-Guide">naming scheme</a>, where methods * returning {@link Rule} objects are named with a capitalized first letter. Since most rules will * push an object onto the value stack, the rules are generally named in accordance with the object * type that they will push. * <p> * Also note that due to parboiled's extensive class rewriting, the code-coverage numbers on this * class are completely wrong. */ class PolicyParser extends BaseParser<Object> { private static final String WILDCARD = "*"; private static final PolicyParser parser = Parboiled.createParser(PolicyParser.class); /** * Return a new instance of a PolicyParser. */ public static PolicyParser get() { return parser.newInstance(); } /** * The top-level parse rule. */ public Rule PolicyFile() { Var<PolicyFile> x = new Var<PolicyFile>(new PolicyFile()); return Sequence( PolicyBlock(x), EOI); } /** * Tweak the value-stack push method to record the line number on which the current rule started. */ @Override public boolean push(Object value) { if (value instanceof PolicyNode) { PolicyNode x = (PolicyNode) value; int startIndex = getContext().getStartIndex(); x.setLineNumber(getContext().getInputBuffer().getPosition(startIndex).line); } return super.push(value); } /** * Makes string literals passed to Rule objects also consume any trailing whitespace. */ @Override @DontExtend protected Rule fromStringLiteral(String string) { return Sequence(String(string), WS()); } /** * An individual ACL rule: * * <pre> * groupName none * * groupName to verbName.actionName * * inheritedProperty.groupName to ... * </pre> */ Rule AclRule() { return Sequence( FirstOf( Sequence( Ident(Property.class), ".", Ident(SecurityGroup.class), new Action<Object>() { @Override public boolean run(Context<Object> ctx) { Ident<SecurityGroup> group = popIdent(SecurityGroup.class); Ident<Property> property = popIdent(Property.class); push(new Ident<SecurityGroup>(SecurityGroup.class, property, group)); return true; } }), WildcardOrIdent(SecurityGroup.class)), FirstOf( // Special syntax for a zero-length list Sequence("none", ACTION(push(new ArrayList<Ident<SecurityAction>>()))), // Or require at least one Sequence( "to", OneOrListOf(VerbActionOrWildcard(), Ident.class, ",") ) ), new Action<Object>() { @Override public boolean run(Context<Object> ctx) { AllowRule x = new AllowRule(); x.setSecurityActions(popIdentList(SecurityAction.class)); x.setGroupName(popIdent(SecurityGroup.class)); push(x); return true; } }); } /** * Defines an action verb. * * <pre> * verb name = action, anotherAction, ...; * </pre> */ Rule ActionDef() { final Var<ActionDefinition> var = new Var<ActionDefinition>(new ActionDefinition()); return Sequence( "action", NodeName(ActionDefinition.class, var), "=", OneOrListOf(Ident(SecurityAction.class), Ident.class, ","), ";", new Action<Object>() { @Override public boolean run(Context<Object> ctx) { ActionDefinition x = var.get(); x.setActions(popIdentList(SecurityAction.class)); push(x); return true; } }); } /** * An inheritable block which holds zero or more {@link AllowRule}. Also supports a single-line * version. * * <pre> * allow [ only ] [ inherit somePropertyName ] { * aclRule; * ...; * aclRule; * } * </pre> * * <pre> * allow [ inherit somePropertyName ] [ aclRule ]; * </pre> */ Rule Allow() { final Var<AllowBlock> var = new Var<AllowBlock>(new AllowBlock()); final Var<Boolean> only = new Var<Boolean>(false); return Sequence( "allow", Optional("only", ACTION(only.set(true))), MaybeInherit(Property.class, var), ZeroOneOrBlock(AclRule(), AllowRule.class), new Action<Object>() { @Override @SuppressWarnings("unchecked") public boolean run(Context<Object> ctx) { AllowBlock x = var.get(); x.setAclRules((List<AllowRule>) pop()); x.setOnly(only.get()); push(x); return true; } }); } /** * Implement single-line and multi-line comments. */ @DontLabel Rule Comment() { return FirstOf( Sequence( String("//"), ZeroOrMore(NoneOf(new char[] { '\n', '\r' })), WS()), Sequence( String("/*"), ZeroOrMore( FirstOf( NoneOf(WILDCARD), Sequence(String(WILDCARD), NoneOf("/")))), String("*/"), WS())); } @Cached <R, E> Rule CompoundIdent(final Class<R> referentType, final Class<E> elementType) { @SuppressWarnings("rawtypes") final Var<List<Ident>> parts = new Var<List<Ident>>(new ArrayList<Ident>()); return Sequence( Ident(elementType), popToList(Ident.class, parts), ZeroOrMore(".", Ident(elementType), popToList(Ident.class, parts)), new Action<Object>() { @Override public boolean run(Context<Object> ctx) { @SuppressWarnings({ "unchecked", "rawtypes" }) List<Ident<?>> list = (List) parts.get(); if (list.size() == 1) { push(new Ident<R>(referentType, list.get(0).getSimpleName())); } else { push(new Ident<R>(referentType, list)); } return true; } }); } /** * An inheritable group which holds zero or more {@link GroupDefinition}. * * <pre> * group [ inherit somePropertyName ] { * groupDefinition; * ... * groupDefinition; * } * </pre> * * <pre> * group [ inherit somePropertyName ] [ groupDefinition ]; * </pre> */ Rule Group() { final Var<GroupBlock> var = new Var<GroupBlock>(new GroupBlock()); return Sequence( "group", MaybeInherit(Property.class, var), ZeroOneOrBlock(GroupDefinition(), GroupDefinition.class), new Action<Object>() { @Override @SuppressWarnings("unchecked") public boolean run(Context<Object> ctx) { GroupBlock x = var.get(); x.setDefinitions((List<GroupDefinition>) pop()); push(x); return true; } }); } /** * Associates one or more {@link PropertyPath property paths} with a group name. * * <pre> * groupName = some.property.path [ , another.path ] * groupName empty * </pre> */ Rule GroupDefinition() { final Var<GroupDefinition> var = new Var<GroupDefinition>(new GroupDefinition()); return Sequence( NodeName(SecurityGroup.class, var), FirstOf( Sequence( "empty", ACTION(push(new ArrayList<Object>())) ), Sequence( "=", OneOrListOf(CompoundIdent(PropertyPath.class, Property.class), Ident.class, ","))), new Action<Object>() { @Override public boolean run(Context<Object> ctx) { GroupDefinition x = var.get(); x.setPaths(popIdentList(PropertyPath.class)); push(x); return true; } }); } /** * A lazy, by-name reference to another object. The syntax for these identifiers uses the same * rules as Java identifiers. */ @Cached <R> Rule Ident(Class<R> referentType) { StringVar x = new StringVar(); return Sequence( ANY, ACTION(Character.isJavaIdentifierStart(matchedChar()) && x.append(matchedChar())), ZeroOrMore( ANY, ACTION(Character.isJavaIdentifierPart(matchedChar()) && x.append(matchedChar())) ), ACTION(push(new Ident<R>(referentType, x.get()))), WS()); } /** * Support rule to allow an optional {@code inherit ident} clause. If the inherit clause is * present, an {@link Ident} will be created and passed to * {@link HasInheritFrom#setInheritFrom(Ident) target}. */ @Cached <P extends HasInheritFrom<R>, R> Rule MaybeInherit(final Class<R> clazz, final Var<P> target) { return Optional( "inherit", Ident(clazz), new Action<Object>() { @Override public boolean run(Context<Object> ctx) { target.get().setInheritFrom(popIdent(clazz)); return true; } }); } /** * Support rule to parse a single {@link Ident} and set {@code x}'s {@link HasName#setName(Ident) * name}. */ @Cached <P extends HasName<R>, R> Rule NodeName(final Class<R> clazz, final Var<P> x) { return Sequence( FirstOf( Ident(clazz), ACTION(push(new Ident<R>(clazz, "$" + getContext().getPosition().line))) ), new Action<Object>() { @Override public boolean run(Context<Object> ctx) { Ident<R> ident = popIdent(clazz); P node = x.get(); node.setName(ident); if (ident.getReferentType().isInstance(node)) { ident.setReferent(ident.getReferentType().cast(node)); } return true; } }); } /** * A hack when generics get in the way. */ @Cached @SuppressWarnings({ "unchecked", "rawtypes" }) Rule NodeNameRaw(Class clazz, final Var x) { return NodeName(clazz, x); } /** * Matches at least one instance of {@code r}, which must push exactly one value onto the stack. * The matched values will be added to a list, which will be placed on the stack. */ @Cached <T extends PolicyNode> Rule OneOrListOf(Rule r, Class<T> clazz, String separator) { Var<List<T>> var = new Var<List<T>>(new ArrayList<T>()); return Sequence( r, ACTION(popToList(clazz, var)), ZeroOrMore( separator, r, ACTION(popToList(clazz, var))), ACTION(clazz == null || push(var.get()))); } Rule PackagePolicy() { Var<PackagePolicy> x = new Var<PackagePolicy>(new PackagePolicy()); return Sequence( "package", NodeName(PackagePolicy.class, x), "{", PolicyBlock(x), "}"); } /** * Main contents blocks. */ @Cached Rule PolicyBlock(Var<? extends PolicyBlock> x) { return Sequence( WS(), ZeroOrMore(FirstOf( Sequence(Allow(), ACTION(x.get().getAllows().add((AllowBlock) pop()))), Sequence(PackagePolicy(), ACTION(x.get().getPackagePolicies() .add((PackagePolicy) pop()))), Sequence(TypePolicy(), ACTION(x.get().getTypePolicies().add((TypePolicy) pop()))), Sequence(ActionDef(), ACTION(x.get().getVerbs().add((ActionDefinition) pop()))) )), ACTION(push(x.get()))); } <T> Ident<T> popIdent(Class<T> clazz) { return ((Ident<?>) pop()).cast(clazz); } <T> List<Ident<T>> popIdentList(Class<T> clazz) { @SuppressWarnings("unchecked") List<Ident<T>> toReturn = (List<Ident<T>>) pop(); for (Ident<T> ident : toReturn) { ident.cast(clazz); } return toReturn; } /** * Utility method to pop a value from the value stack, cast it to {@code clazz} and add it to * {@code list}. if {@code clazz} is {@code null}, this method is a no-op. */ <T> boolean popToList(Class<T> clazz, Var<List<T>> list) { if (clazz != null) { list.get().add(clazz.cast(pop())); } return true; } /** * One or more property-name references. * * <pre> * property ident [ , ident [ ... ] ]; * </pre> */ Rule PropertyList() { return Sequence( "property", OneOrListOf(Ident(Property.class), Ident.class, ","), ";", new Action<Object>() { @Override public boolean run(Context<Object> ctx) { PropertyList x = new PropertyList(); x.setPropertyNames(popIdentList(Property.class)); push(x); return true; } }); } /** * An named block that contains {@link AllowBlock} and {@link PropertyList} nodes. * * <pre> * policy name { * property a, b, c; * allow { * ... * } * } * </pre> */ Rule PropertyPolicy() { Var<PropertyPolicy> x = new Var<PropertyPolicy>(new PropertyPolicy()); return Sequence( "policy", NodeName(PropertyPolicy.class, x), "{", ZeroOrMore(FirstOf( Sequence( Allow(), ACTION(x.get().getAllows().add((AllowBlock) pop()))), Sequence( PropertyList(), ACTION(x.get().getPropertyLists().add((PropertyList) pop()))) )), "}", ACTION(push(x.get()))); } /** * Defines policies for an entity type. * * <pre> * type typeName { * allow { ... } * group { ... } * policy { ... } * verb ...; * } * </pre> */ Rule TypePolicy() { final Var<TypePolicy> x = new Var<TypePolicy>(new TypePolicy()); return Sequence( "type", NodeNameRaw(Class.class, x), "{", ZeroOrMore(FirstOf( Sequence(Allow(), ACTION(x.get().getAllows().add((AllowBlock) pop()))), Sequence(Group(), ACTION(x.get().getGroups().add((GroupBlock) pop()))), Sequence(PropertyPolicy(), ACTION(x.get().getPolicies().add((PropertyPolicy) pop()))), Sequence(ActionDef(), ACTION(x.get().getVerbs().add((ActionDefinition) pop()))))), "}", ACTION(push(x.get()))); } /** * A reference to an action. This may be a single {@link Ident} or a wildcard, possibly qualified * by a verb name. * * <pre> * * * *.* * Foo.bar * Foo.* * bar * </pre> */ Rule VerbActionOrWildcard() { return FirstOf( Sequence( WILDCARD, Optional(".", WILDCARD), ACTION(push(new Ident<SecurityAction>(SecurityAction.class, "*")))), Sequence( Ident(ActionDefinition.class), ".", WildcardOrIdent(SecurityAction.class), ACTION(swap() && push(new Ident<SecurityAction>(SecurityAction.class, popIdent(ActionDefinition.class), popIdent(SecurityAction.class))))), Ident(SecurityAction.class) ); } /** * A single identifier or a wildcard. */ <R> Rule WildcardOrIdent(Class<R> referentType) { return FirstOf( Sequence(WILDCARD, ACTION(push(new Ident<R>(referentType, WILDCARD)))), Ident(referentType)); } /** * Matches whitespace or {@code #} comments. */ @Label("whitespace") Rule WS() { return Sequence( ZeroOrMore(AnyOf(new char[] { ' ', '\n', '\r', '\t' })), Optional(Comment())); } /** * A utility rule to allow another rule to be specified zero, one, or any number of times. * * <pre> * { * rule; * rule; * ... * } * * rule; * * ; * </pre> */ @Cached <T> Rule ZeroOneOrBlock(Rule r, Class<T> clazz) { Var<List<T>> var = new Var<List<T>>(new ArrayList<T>()); return Sequence( FirstOf( Sequence( "{", ZeroOrMore(r, ";", ACTION(popToList(clazz, var))), "}"), Sequence( r, ";", ACTION(popToList(clazz, var))), ";"), ACTION(clazz == null || push(var.get()))); } }