    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *      https://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017
    018package org.apache.commons.jexl3.introspection;
    019
    020import java.util.HashMap;
    021import java.util.HashSet;
    022import java.util.Map;
    023import java.util.Objects;
    024import java.util.Set;
    025import java.util.concurrent.ConcurrentHashMap;
    026
    027/**
    028 * A sandbox describes permissions on a class by explicitly allowing or forbidding
    029 * access to methods and properties through "allowlists" and "blocklists".
    030 *
    031 * <p>A <strong>allowlist</strong> explicitly allows methods/properties for a class;</p>
    032 *
    033 * <ul>
    034 *   <li>If a allowlist is empty and thus does not contain any names,
    035 *       all properties/methods are allowed for its class.</li>
    036 *   <li>If it is not empty, the only allowed properties/methods are the ones contained.</li>
    037 * </ul>
    038 *
    039 * <p>A <strong>blocklist</strong> explicitly forbids methods/properties for a class;</p>
    040 *
    041 * <ul>
    042 *   <li>If a blocklist is empty and thus does not contain any names,
    043 *       all properties/methods are forbidden for its class.</li>
    044 *   <li>If it is not empty, the only forbidden properties/methods are the ones contained.</li>
    045 * </ul>
    046 *
    047 * <p>Permissions are composed of three lists, read, write, execute, each being
    048 * "allow" or "block":</p>
    049 *
    050 * <ul>
    051 *   <li><strong>read</strong> controls readable properties </li>
    052 *   <li><strong>write</strong> controls writable properties</li>
    053 *   <li><strong>execute</strong> controls executable methods and constructor</li>
    054 * </ul>
    055 *
    056 * <p>When specified, permissions - allow or block lists - can be created inheritable
    057 * on interfaces or classes and thus applicable to their implementations or derived
    058 * classes; the sandbox must be created with the 'inheritable' flag for this behavior
    059 * to be triggered. Note that even in this configuration, it is still possible to
    060 * add non-inheritable permissions.
    061 * Adding inheritable lists to a non inheritable sandbox has no added effect;
    062 * permissions only apply to their specified class.</p>
    063 *
    064 * <p>Note that a JexlUberspect always uses a <em>copy</em> of the JexlSandbox
    065 * used to built it preventing permission changes after its instantiation.</p>
    066 *
    067 * @since 3.0
    068 */
    069public final class JexlSandbox {
    070
    071    /**
    072     * The marker string for explicitly disallowed null properties.
    073     */
    074    public static final String NULL = "?";
    075
    076    /**
    077     * The pass-thru name set.
    078     */
    079    static final Names ALLOW_NAMES = new Names() {
    080        @Override
    081        public boolean add(final String name) {
    082            return false;
    083        }
    084
    085        @Override
    086        public String toString() {
    087            return "allowAll";
    088        }
    089    };
    090
    091    /**
    092     * The block-all name set.
    093     */
    094    private static final Names BLOCK_NAMES = new Names() {
    095        @Override
    096        public boolean add(final String name) {
    097            return false;
    098        }
    099
    100        @Override
    101        public String get(final String name) {
    102            return name == null ? NULL : null;
    103        }
    104
    105        @Override
    106        public String toString() {
    107            return "blockAll";
    108        }
    109    };
    110
    111    /**
    112     * The block-all permissions.
    113     */
    114    private static final Permissions BLOCK_ALL = new Permissions(false, BLOCK_NAMES, BLOCK_NAMES, BLOCK_NAMES);
    115
    116    /**
    117     * The pass-thru permissions.
    118     */
    119    private static final Permissions ALLOW_ALL = new Permissions(false, ALLOW_NAMES, ALLOW_NAMES, ALLOW_NAMES);
    120
    121    /**
    122     * The map from class names to permissions.
    123     */
    124    private final Map<String, Permissions> sandbox;
    125
    126    /**
    127     * Whether permissions can be inherited (through implementation or extension).
    128     */
    129    private final boolean inherit;
    130
    131    /**
    132     * Default behavior, block or allow.
    133     */
    134    private final boolean allow;
    135
    136    /**
    137     * Creates a new default sandbox.
    138     * <p>In the absence of explicit permissions on a class, the
    139     * sandbox is an allow-box, allow-listing that class for all permissions (read, write and execute).
    140     */
    141    public JexlSandbox() {
    142        this(true, false, null);
    143    }
    144
    145    /**
    146     * Creates a new default sandbox.
    147     * <p>A allow-box considers no permissions as &quot;everything is allowed&quot; when
    148     * a block-box considers no permissions as &quot;nothing is allowed&quot;.
    149     *
    150     * @param ab whether this sandbox is allow (true) or block (false)
    151     *           if no permission is explicitly defined for a class.
    152     * @since 3.1
    153     */
    154    public JexlSandbox(final boolean ab) {
    155        this(ab, false, null);
    156    }
    157
    158    /**
    159     * Creates a sandbox.
    160     *
    161     * @param ab  whether this sandbox is allow (true) or block (false)
    162     * @param inh whether permissions on interfaces and classes are inherited (true) or not (false)
    163     * @since 3.2
    164     */
    165    public JexlSandbox(final boolean ab, final boolean inh) {
    166        this(ab, inh, null);
    167    }
    168
    169    /**
    170     * Creates a sandbox based on an existing permissions map.
    171     *
    172     * @param ab  whether this sandbox is allow (true) or block (false)
    173     * @param inh whether permissions are inherited, default false
    174     * @param map the permissions map
    175     * @since 3.2
    176     */
    177    private JexlSandbox(final boolean ab, final boolean inh, final Map<String, Permissions> map) {
    178        allow = ab;
    179        inherit = inh;
    180        sandbox = map != null ? map : new HashMap<>();
    181    }
    182
    183    /**
    184     * Gets a class by name, crude mechanism for backwards (&lt;3.2 ) compatibility.
    185     *
    186     * @param cname the class name
    187     * @return the class
    188     */
    189    static Class<?> forName(final String cname) {
    190        try {
    191            return Class.forName(cname);
    192        } catch (final Exception xany) {
    193            return null;
    194        }
    195    }
    196
    197    /**
    198     * Creates a new set of permissions based on allow lists for methods and properties for a given class.
    199     * <p>The sandbox inheritance property will apply to the permissions created by this method
    200     *
    201     * @param clazz the allowed class name
    202     * @return the permissions instance
    203     */
    204    public Permissions allow(final String clazz) {
    205        return permissions(clazz, true, true, true);
    206    }
    207
    208    /**
    209     * Creates a new set of permissions based on block lists for methods and properties for a given class.
    210     * <p>The sandbox inheritance property will apply to the permissions created by this method
    211     *
    212     * @param clazz the blocked class name
    213     * @return the permissions instance
    214     */
    215    public Permissions block(final String clazz) {
    216        return permissions(clazz, false, false, false);
    217    }
    218
    219    /**
    220     * Gets a copy of this sandbox
    221     *
    222     * @return a copy of this sandbox
    223     */
    224    public JexlSandbox copy() {
    225        // modified concurrently at runtime so...
    226        final Map<String, Permissions> map = new ConcurrentHashMap<>();
    227        for (final Map.Entry<String, Permissions> entry : sandbox.entrySet()) {
    228            map.put(entry.getKey(), entry.getValue().copy());
    229        }
    230        return new JexlSandbox(allow, inherit, map);
    231    }
    232
    233    /**
    234     * Gets the execute permission value for a given method of a class.
    235     *
    236     * @param clazz the class
    237     * @param name  the method name
    238     * @return null if not allowed, the name of the method to use otherwise
    239     */
    240    public String execute(final Class<?> clazz, final String name) {
    241        final String m = get(clazz).execute().get(name);
    242        return "".equals(name) && m != null ? clazz.getName() : m;
    243    }
    244
    245    /**
    246     * Gets the execute permission value for a given method of a class.
    247     *
    248     * @param clazz the class name
    249     * @param name  the method name
    250     * @return null if not allowed, the name of the method to use otherwise
    251     * @deprecated 3.3
    252     */
    253    @Deprecated
    254    public String execute(final String clazz, final String name) {
    255        final String m = get(clazz).execute().get(name);
    256        return "".equals(name) && m != null ? clazz : m;
    257    }
    258
    259    /**
    260     * Gets the set of permissions associated to a class.
    261     *
    262     * @param clazz the class name
    263     * @return the defined permissions or an all-allow permission instance if none were defined
    264     */
    265    public Permissions get(final String clazz) {
    266        return get(forName(clazz));
    267    }
    268
    269    /**
    270     * Gets the permissions associated to a class.
    271     *
    272     * @param clazz the class
    273     * @return the permissions
    274     */
    275    @SuppressWarnings("null")
    276    public Permissions get(final Class<?> clazz) {
    277        // argument clazz cannot be null since permissions would be not null and block:
    278        // we only store the result for classes we actively seek permissions for.
    279        return compute(clazz, true);
    280    }
    281
    282    private static Permissions inheritable(final Permissions p) {
    283        return p != null && p.isInheritable() ? p : null;
    284    }
    285
    286    /**
    287     * Computes and optionally stores the permissions associated to a class.
    288     *
    289     * @param clazz the class
    290     * @param store whether the computed permissions should be stored in the sandbox
    291     * @return the permissions
    292     */
    293    private Permissions compute(final Class<?> clazz, final boolean store) {
    294        // belt and suspender; recursion should not lead here
    295        if (clazz == null) {
    296            return BLOCK_ALL;
    297        }
    298        final String className = clazz.getName();
    299        Permissions permissions = sandbox.get(className);
    300        if (permissions == null) {
    301            if (inherit) {
    302                // find first inherited interface that defines permissions
    303                final Class<?>[] interfaces = clazz.getInterfaces();
    304                for (int i = 0; permissions == null && i < interfaces.length; ++i) {
    305                    permissions = inheritable(compute(interfaces[i], false));
    306                }
    307                // nothing defined yet, find first superclass that defines permissions
    308                if (permissions == null) {
    309                    // let's recurse on super classes
    310                    final Class<?> superClazz = clazz.getSuperclass();
    311                    if (null != superClazz) {
    312                        permissions = inheritable(compute(superClazz, false));
    313                    }
    314                }
    315            }
    316            // nothing was inheritable
    317            if (permissions == null) {
    318                permissions = allow ? ALLOW_ALL : BLOCK_ALL;
    319            }
    320            // store the info to avoid doing this costly look-up
    321            if (store) {
    322                sandbox.put(className, permissions);
    323            }
    324        }
    325        return permissions;
    326    }
    327
    328    /**
    329     * Creates the set of permissions for a given class.
    330     * <p>The sandbox inheritance property will apply to the permissions created by this method
    331     *
    332     * @param clazz       the class for which these permissions apply
    333     * @param readFlag    whether the readable property list is allow - true - or block - false -
    334     * @param writeFlag   whether the writable property list is allow - true - or block - false -
    335     * @param executeFlag whether the executable method list is allow - true - or block - false -
    336     * @return the set of permissions
    337     */
    338    public Permissions permissions(final String clazz,
    339                                   final boolean readFlag,
    340                                   final boolean writeFlag,
    341                                   final boolean executeFlag) {
    342        return permissions(clazz, inherit, readFlag, writeFlag, executeFlag);
    343    }
    344
    345    /**
    346     * Creates the set of permissions for a given class.
    347     *
    348     * @param clazz  the class for which these permissions apply
    349     * @param inhf   whether these permissions are inheritable
    350     * @param readf  whether the readable property list is allow - true - or block - false -
    351     * @param writef whether the writable property list is allow - true - or block - false -
    352     * @param execf  whether the executable method list is allow - true - or block - false -
    353     * @return the set of permissions
    354     */
    355    public Permissions permissions(final String clazz,
    356                                   final boolean inhf,
    357                                   final boolean readf,
    358                                   final boolean writef,
    359                                   final boolean execf) {
    360        final Permissions box = new Permissions(inhf, readf, writef, execf);
    361        sandbox.put(clazz, box);
    362        return box;
    363    }
    364
    365    /**
    366     * Gets the read permission value for a given property of a class.
    367     *
    368     * @param clazz the class
    369     * @param name  the property name
    370     * @return null (or NULL if name is null) if not allowed, the name of the property to use otherwise
    371     */
    372    public String read(final Class<?> clazz, final String name) {
    373        return get(clazz).read().get(name);
    374    }
    375
    376    /**
    377     * Gets the write permission value for a given property of a class.
    378     *
    379     * @param clazz the class
    380     * @param name  the property name
    381     * @return null (or NULL if name is null) if not allowed, the name of the property to use otherwise
    382     */
    383    public String write(final Class<?> clazz, final String name) {
    384        return get(clazz).write().get(name);
    385    }
    386
    387    /**
    388     * Gets the write permission value for a given property of a class.
    389     *
    390     * @param clazz the class name
    391     * @param name  the property name
    392     * @return null if not allowed, the name of the property to use otherwise
    393     * @deprecated 3.3
    394     */
    395    @Deprecated
    396    public String write(final String clazz, final String name) {
    397        return get(clazz).write().get(name);
    398    }
    399
    400    /**
    401     * An allow set of names.
    402     */
    403    static class AllowSet extends Names {
    404
    405        /**
    406         * The map of controlled names and aliases.
    407         */
    408        private Map<String, String> names;
    409
    410        @Override
    411        public boolean add(final String name) {
    412            if (names == null) {
    413                names = new HashMap<>();
    414            }
    415            return names.put(name, name) == null;
    416        }
    417
    418        @Override
    419        public boolean alias(final String name, final String alias) {
    420            if (names == null) {
    421                names = new HashMap<>();
    422            }
    423            return names.put(alias, name) == null;
    424        }
    425
    426        @Override
    427        protected Names copy() {
    428            final AllowSet copy = new AllowSet();
    429            copy.names = names == null ? null : new HashMap<>(names);
    430            return copy;
    431        }
    432
    433        @Override
    434        public String get(final String name) {
    435            if (names == null) {
    436                return name;
    437            }
    438            final String actual = names.get(name);
    439            // if null is not explicitly allowed, explicit null aka NULL
    440            if (name == null && actual == null && !names.containsKey(null)) {
    441                return JexlSandbox.NULL;
    442            }
    443            return actual;
    444        }
    445
    446        @Override
    447        public String toString() {
    448            return "allow{" + (names == null ? "all" : Objects.toString(names.entrySet())) + "}";
    449        }
    450    }
    451
    452    /**
    453     * A block set of names.
    454     */
    455    static class BlockSet extends Names {
    456
    457        /**
    458         * The set of controlled names.
    459         */
    460        private Set<String> names;
    461
    462        @Override
    463        public boolean add(final String name) {
    464            if (names == null) {
    465                names = new HashSet<>();
    466            }
    467            return names.add(name);
    468        }
    469
    470        @Override
    471        protected Names copy() {
    472            final BlockSet copy = new BlockSet();
    473            copy.names = names == null ? null : new HashSet<>(names);
    474            return copy;
    475        }
    476
    477        @Override
    478        public String get(final String name) {
    479            // if name is null and contained in set, explicit null aka NULL
    480            if (names != null && !names.contains(name)) {
    481                return name;
    482            }
    483            if (name != null) {
    484                return null;
    485            }
    486            return NULL;
    487        }
    488
    489        @Override
    490        public String toString() {
    491            return "block{" + (names == null ? "all" : Objects.toString(names)) + "}";
    492        }
    493    }
    494
    495    /**
    496     * A base set of names.
    497     */
    498    public abstract static class Names {
    499
    500        /** Default constructor */
    501        public Names() {} // Keep Javadoc happy
    502
    503        /**
    504         * Adds a name to this set.
    505         *
    506         * @param name the name to add
    507         * @return true if the name was really added, false if not
    508         */
    509        public abstract boolean add(String name);
    510
    511        /**
    512         * Adds an alias to a name to this set.
    513         * <p>This only has an effect on allow lists.</p>
    514         *
    515         * @param name  the name to alias
    516         * @param alias the alias
    517         * @return true if the alias was added, false if it was already present
    518         */
    519        public boolean alias(final String name, final String alias) {
    520            return false;
    521        }
    522
    523        /**
    524         * Gets a copy of these Names
    525         *
    526         * @return a copy of these Names
    527         */
    528        protected Names copy() {
    529            return this;
    530        }
    531
    532        /**
    533         * Gets whether a given name is allowed or not.
    534         *
    535         * @param name the method/property name to check
    536         * @return null (or NULL if name is null) if not allowed, the actual name to use otherwise
    537         */
    538        public String get(final String name) {
    539            return name;
    540        }
    541    }
    542
    543    /**
    544     * Contains the allow or block lists for properties and methods for a given class.
    545     */
    546    public static final class Permissions {
    547
    548        /**
    549         * Whether these permissions are inheritable, ie can be used by derived classes.
    550         */
    551        private final boolean inheritable;
    552
    553        /**
    554         * The controlled readable properties.
    555         */
    556        private final Names read;
    557
    558        /**
    559         * The controlled  writable properties.
    560         */
    561        private final Names write;
    562
    563        /**
    564         * The controlled methods.
    565         */
    566        private final Names execute;
    567
    568        /**
    569         * Creates a new permissions instance.
    570         *
    571         * @param inherit     whether these permissions are inheritable
    572         * @param readFlag    whether the read property list is allow or block
    573         * @param writeFlag   whether the write property list is allow or block
    574         * @param executeFlag whether the method list is allow of block
    575         */
    576        Permissions(final boolean inherit, final boolean readFlag, final boolean writeFlag, final boolean executeFlag) {
    577            this(inherit,
    578                readFlag ? new AllowSet() : new BlockSet(),
    579                writeFlag ? new AllowSet() : new BlockSet(),
    580                executeFlag ? new AllowSet() : new BlockSet());
    581        }
    582
    583        /**
    584         * Creates a new permissions instance.
    585         *
    586         * @param inherit  whether these permissions are inheritable
    587         * @param nread    the read set
    588         * @param nwrite   the write set
    589         * @param nexecute the method set
    590         */
    591        Permissions(final boolean inherit, final Names nread, final Names nwrite, final Names nexecute) {
    592            this.read = nread != null ? nread : ALLOW_NAMES;
    593            this.write = nwrite != null ? nwrite : ALLOW_NAMES;
    594            this.execute = nexecute != null ? nexecute : ALLOW_NAMES;
    595            this.inheritable = inherit;
    596        }
    597
    598        /**
    599         * @return a copy of these permissions
    600         */
    601        Permissions copy() {
    602            return new Permissions(inheritable, read.copy(), write.copy(), execute.copy());
    603        }
    604
    605        /**
    606         * Gets the set of method names in these permissions.
    607         *
    608         * @return the set of method names
    609         */
    610        public Names execute() {
    611            return execute;
    612        }
    613
    614        /**
    615         * Adds a list of executable methods names to these permissions.
    616         * <p>The constructor is denoted as the empty-string, all other methods by their names.</p>
    617         *
    618         * @param methodNames the method names
    619         * @return {@code this} instance of permissions
    620         */
    621        public Permissions execute(final String... methodNames) {
    622            for (final String methodName : methodNames) {
    623                execute.add(methodName);
    624            }
    625            return this;
    626        }
    627
    628        /**
    629         * Do these permissions apply to derived classes?
    630         *
    631         * @return whether these permissions apply to derived classes.
    632         */
    633        public boolean isInheritable() {
    634            return inheritable;
    635        }
    636
    637        /**
    638         * Gets the set of readable property names in these permissions.
    639         *
    640         * @return the set of property names
    641         */
    642        public Names read() {
    643            return read;
    644        }
    645
    646        /**
    647         * Adds a list of readable property names to these permissions.
    648         *
    649         * @param propertyNames the property names
    650         * @return {@code this} instance of permissions
    651         */
    652        public Permissions read(final String... propertyNames) {
    653            for (final String propertyName : propertyNames) {
    654                read.add(propertyName);
    655            }
    656            return this;
    657        }
    658
    659        /**
    660         * Gets the set of writable property names in these permissions.
    661         *
    662         * @return the set of property names
    663         */
    664        public Names write() {
    665            return write;
    666        }
    667
    668        /**
    669         * Adds a list of writable property names to these permissions.
    670         *
    671         * @param propertyNames the property names
    672         * @return {@code this} instance of permissions
    673         */
    674        public Permissions write(final String... propertyNames) {
    675            for (final String propertyName : propertyNames) {
    676                write.add(propertyName);
    677            }
    678            return this;
    679        }
    680    }
    681
    682    /**
    683     * @deprecated since 3.2, use {@link BlockSet}
    684     */
    685    @Deprecated
    686    public static final class BlackSet extends BlockSet {
    687
    688        /** Default constructor */
    689        public BlackSet() { } // Keep Javadoc happy
    690    }
    691
    692    /**
    693     * @deprecated since 3.2, use {@link AllowSet}
    694     */
    695    @Deprecated
    696    public static final class WhiteSet extends AllowSet {
    697
    698        /** Default constructor */
    699        public WhiteSet() { } // Keep Javadoc happy
    700    }
    701
    702    /**
    703     * Use block() instead.
    704     *
    705     * @param clazz the blocked class name
    706     * @return the permissions instance
    707     * @deprecated 3.3
    708     */
    709    @Deprecated
    710    public Permissions black(final String clazz) {
    711        return block(clazz);
    712    }
    713
    714    /**
    715     * Gets the read permission value for a given property of a class.
    716     *
    717     * @param clazz the class name
    718     * @param name  the property name
    719     * @return null if not allowed, the name of the property to use otherwise
    720     * @deprecated 3.3
    721     */
    722    @Deprecated
    723    public String read(final String clazz, final String name) {
    724        return get(clazz).read().get(name);
    725    }
    726
    727    /**
    728     * Use allow() instead.
    729     *
    730     * @param clazz the allowed class name
    731     * @return the permissions instance
    732     * @deprecated 3.3
    733     */
    734    @Deprecated
    735    public Permissions white(final String clazz) {
    736        return allow(clazz);
    737    }
    738}