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