1/*
2 *  Licensed to the Apache Software Foundation (ASF) under one or more
3 *  contributor license agreements.  See the NOTICE file distributed with
4 *  this work for additional information regarding copyright ownership.
5 *  The ASF licenses this file to You under the Apache License, Version 2.0
6 *  (the "License"); you may not use this file except in compliance with
7 *  the License.  You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 *  Unless required by applicable law or agreed to in writing, software
12 *  distributed under the License is distributed on an "AS IS" BASIS,
13 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *  See the License for the specific language governing permissions and
15 *  limitations under the License.
16 */
17
18/**
19* @author Alexey V. Varlamov
20* @version $Revision$
21*/
22
23package org.apache.harmony.security.fortress;
24
25import java.io.File;
26import java.io.InputStream;
27import java.lang.reflect.Constructor;
28import java.net.URI;
29import java.net.URISyntaxException;
30import java.net.URL;
31import java.security.AccessController;
32import java.security.Permission;
33import java.security.PermissionCollection;
34import java.security.Permissions;
35import java.security.PrivilegedAction;
36import java.security.PrivilegedExceptionAction;
37import java.security.Security;
38import java.util.ArrayList;
39import java.util.Collection;
40import java.util.Iterator;
41import java.util.List;
42import java.util.Properties;
43
44import org.apache.harmony.security.Util;
45import org.apache.harmony.security.internal.nls.Messages;
46
47/**
48 * This class consist of a number of static methods, which provide a common functionality
49 * for various policy and configuration providers.
50 *
51 */
52public class PolicyUtils {
53
54    // No reason to instantiate
55    private PolicyUtils() {}
56
57    /**
58     * Auxiliary action for opening InputStream from specified location.
59     */
60    public static class URLLoader implements PrivilegedExceptionAction<InputStream> {
61
62        /**
63         * URL of target location.
64         */
65        public URL location;
66
67        /**
68         *  Constructor with target URL parameter.
69         */
70        public URLLoader(URL location) {
71            this.location = location;
72        }
73
74        /**
75         * Returns InputStream from the target URL.
76         */
77        public InputStream run() throws Exception {
78            return location.openStream();
79        }
80    }
81
82    /**
83     * Auxiliary action for accessing system properties in a bundle.
84     */
85    public static class SystemKit implements PrivilegedAction<Properties> {
86
87        /**
88         * Returns system properties.
89         */
90        public Properties run() {
91            return System.getProperties();
92        }
93    }
94
95    /**
96     * Auxiliary action for accessing specific system property.
97     */
98    public static class SystemPropertyAccessor implements PrivilegedAction<String> {
99
100        /**
101         * A key of a required system property.
102         */
103        public String key;
104
105        /**
106         * Constructor with a property key parameter.
107         */
108        public SystemPropertyAccessor(String key) {
109            this.key = key;
110        }
111
112        /**
113         * Handy one-line replacement of
114         * &quot;provide key and supply action&quot; code block,
115         * for reusing existing action instance.
116         */
117        public PrivilegedAction<String> key(String key) {
118            this.key = key;
119            return this;
120        }
121
122        /**
123         * Returns specified system property.
124         */
125        public String run() {
126            return System.getProperty(key);
127        }
128    }
129
130    /**
131     * Auxiliary action for accessing specific security property.
132     */
133    public static class SecurityPropertyAccessor implements PrivilegedAction<String> {
134
135        private String key;
136
137        /**
138         * Constructor with a property key parameter.
139         */
140        public SecurityPropertyAccessor(String key) {
141            super();
142            this.key = key;
143        }
144
145        public PrivilegedAction<String> key(String key) {
146            this.key = key;
147            return this;
148        }
149
150        /**
151         * Returns specified security property.
152         */
153        public String run() {
154            return Security.getProperty(key);
155        }
156    }
157
158    /**
159     * Auxiliary action for loading a provider by specific security property.
160     */
161    public static class ProviderLoader<T> implements PrivilegedAction<T> {
162
163        private String key;
164
165        /**
166         * Acceptable provider superclass.
167         */
168        private Class<T> expectedType;
169
170        /**
171         * Constructor taking property key and acceptable provider
172         * superclass parameters.
173         */
174        public ProviderLoader(String key, Class<T> expected) {
175            super();
176            this.key = key;
177            this.expectedType = expected;
178        }
179
180        /**
181         * Returns provider instance by specified security property.
182         * The <code>key</code> should map to a fully qualified classname.
183         *
184         * @throws SecurityException if no value specified for the key
185         * in security properties or if an Exception has occurred
186         * during classloading and instantiating.
187         */
188        public T run() {
189            String klassName = Security.getProperty(key);
190            if (klassName == null || klassName.length() == 0) {
191                throw new SecurityException(Messages.getString("security.14C", //$NON-NLS-1$
192                                            key));
193            }
194            // TODO accurate classloading
195            try {
196                Class<?> klass = Class.forName(klassName, true,
197                        Thread.currentThread().getContextClassLoader());
198                if (expectedType != null && klass.isAssignableFrom(expectedType)){
199                    throw new SecurityException(Messages.getString("security.14D", //$NON-NLS-1$
200                                              klassName, expectedType.getName()));
201                }
202                //FIXME expectedType.cast(klass.newInstance());
203                return (T)klass.newInstance();
204            }
205            catch (SecurityException se){
206                throw se;
207            }
208            catch (Exception e) {
209                // TODO log error ??
210                SecurityException se = new SecurityException(
211                        Messages.getString("security.14E", klassName)); //$NON-NLS-1$
212                se.initCause(e);
213                throw se;
214            }
215        }
216    }
217
218    /**
219     * Specific exception to signal that property expansion failed
220     * due to unknown key.
221     */
222    public static class ExpansionFailedException extends Exception {
223
224        /**
225         * @serial
226         */
227        private static final long serialVersionUID = 2869748055182612000L;
228
229        /**
230         * Constructor with user-friendly message parameter.
231         */
232        public ExpansionFailedException(String message) {
233            super(message);
234        }
235
236        /**
237         * Constructor with user-friendly message and causing error.
238         */
239        public ExpansionFailedException(String message, Throwable cause) {
240            super(message, cause);
241        }
242    }
243
244    /**
245     * Substitutes all entries like ${some.key}, found in specified string,
246     * for specified values.
247     * If some key is unknown, throws ExpansionFailedException.
248     * @param str the string to be expanded
249     * @param properties available key-value mappings
250     * @return expanded string
251     * @throws ExpansionFailedException
252     */
253    public static String expand(String str, Properties properties)
254            throws ExpansionFailedException {
255        final String START_MARK = "${"; //$NON-NLS-1$
256        final String END_MARK = "}"; //$NON-NLS-1$
257        final int START_OFFSET = START_MARK.length();
258        final int END_OFFSET = END_MARK.length();
259
260        StringBuilder result = new StringBuilder(str);
261        int start = result.indexOf(START_MARK);
262        while (start >= 0) {
263            int end = result.indexOf(END_MARK, start);
264            if (end >= 0) {
265                String key = result.substring(start + START_OFFSET, end);
266                String value = properties.getProperty(key);
267                if (value != null) {
268                    result.replace(start, end + END_OFFSET, value);
269                    start += value.length();
270                } else {
271                    throw new ExpansionFailedException(Messages.getString("security.14F", key)); //$NON-NLS-1$
272                }
273            }
274            start = result.indexOf(START_MARK, start);
275        }
276        return result.toString();
277    }
278
279    /**
280     * Handy shortcut for
281     * <code>expand(str, properties).replace(File.separatorChar, '/')</code>.
282     * @see #expand(String, Properties)
283     */
284    public static String expandURL(String str, Properties properties)
285            throws ExpansionFailedException {
286        return expand(str, properties).replace(File.separatorChar, '/');
287    }
288
289    /**
290     * Normalizes URLs to standard ones, eliminating pathname symbols.
291     *
292     * @param codebase -
293     *            the original URL.
294     * @return - the normalized URL.
295     */
296    public static URL normalizeURL(URL codebase) {
297        if (codebase != null && "file".equals(codebase.getProtocol())) { //$NON-NLS-1$
298            try {
299                if (codebase.getHost().length() == 0) {
300                    String path = codebase.getFile();
301
302                    if (path.length() == 0) {
303                        // codebase is "file:"
304                        path = "*";
305                    }
306                    return filePathToURI(new File(path)
307                            .getAbsolutePath()).normalize().toURL();
308                } else {
309                    // codebase is "file://<smth>"
310                    return codebase.toURI().normalize().toURL();
311                }
312            } catch (Exception e) {
313                // Ignore
314            }
315        }
316        return codebase;
317    }
318
319    /**
320     * Converts a file path to URI without accessing file system
321     * (like {File#toURI()} does).
322     *
323     * @param path -
324     *            file path.
325     * @return - the resulting URI.
326     * @throw URISyntaxException
327     */
328    public static URI filePathToURI(String path) throws URISyntaxException {
329        path = path.replace(File.separatorChar, '/');
330
331        if (!path.startsWith("/")) { //$NON-NLS-1$
332            return new URI("file", null, //$NON-NLS-1$
333                    new StringBuilder(path.length() + 1).append('/')
334                            .append(path).toString(), null, null);
335        }
336        return new URI("file", null, path, null, null); //$NON-NLS-1$
337    }
338
339    /**
340     * Instances of this interface are intended for resolving
341     * generalized expansion expressions, of the form ${{protocol:data}}.
342     * Such functionality is applicable to security policy files, for example.
343     * @see #expandGeneral(String, GeneralExpansionHandler)
344     */
345    public static interface GeneralExpansionHandler {
346
347        /**
348         * Resolves general expansion expressions of the form ${{protocol:data}}.
349         * @param protocol denotes type of resolution
350         * @param data data to be resolved, optional (may be null)
351         * @return resolved value, must not be null
352         * @throws PolicyUtils.ExpansionFailedException if expansion is impossible
353         */
354        String resolve(String protocol, String data)
355                throws ExpansionFailedException;
356    }
357
358    /**
359     * Substitutes all entries like ${{protocol:data}}, found in specified string,
360     * for values resolved by passed handler.
361     * The data part may be empty, and in this case expression
362     * may have simplified form, as ${{protocol}}.
363     * If some entry cannot be resolved, throws ExpansionFailedException;
364     * @param str the string to be expanded
365     * @param handler the handler to resolve data denoted by protocol
366     * @return expanded string
367     * @throws ExpansionFailedException
368     */
369    public static String expandGeneral(String str,
370            GeneralExpansionHandler handler) throws ExpansionFailedException {
371        final String START_MARK = "${{"; //$NON-NLS-1$
372        final String END_MARK = "}}"; //$NON-NLS-1$
373        final int START_OFFSET = START_MARK.length();
374        final int END_OFFSET = END_MARK.length();
375
376        StringBuilder result = new StringBuilder(str);
377        int start = result.indexOf(START_MARK);
378        while (start >= 0) {
379            int end = result.indexOf(END_MARK, start);
380            if (end >= 0) {
381                String key = result.substring(start + START_OFFSET, end);
382                int separator = key.indexOf(':');
383                String protocol = (separator >= 0) ? key
384                        .substring(0, separator) : key;
385                String data = (separator >= 0) ? key.substring(separator + 1)
386                        : null;
387                String value = handler.resolve(protocol, data);
388                result.replace(start, end + END_OFFSET, value);
389                start += value.length();
390            }
391            start = result.indexOf(START_MARK, start);
392        }
393        return result.toString();
394    }
395
396    /**
397     * A key to security properties, deciding whether usage of
398     * dynamic policy location via system properties is allowed.
399     * @see #getPolicyURLs(Properties, String, String)
400     */
401    public static final String POLICY_ALLOW_DYNAMIC = "policy.allowSystemProperty"; //$NON-NLS-1$
402
403    /**
404     * A key to security properties, deciding whether expansion of
405     * system properties is allowed
406     * (in security properties values, policy files, etc).
407     * @see #expand(String, Properties)
408     */
409    public static final String POLICY_EXPAND = "policy.expandProperties"; //$NON-NLS-1$
410
411    /**
412     * Positive value of switching properties.
413     */
414    public static final String TRUE = "true"; //$NON-NLS-1$
415
416    /**
417     * Negative value of switching properties.
418     */
419    public static final String FALSE = "false"; //$NON-NLS-1$
420
421    /**
422     * Returns false if current security settings disable to perform
423     * properties expansion, true otherwise.
424     * @see #expand(String, Properties)
425     */
426    public static boolean canExpandProperties() {
427        return !Util.equalsIgnoreCase(FALSE,AccessController
428                .doPrivileged(new SecurityPropertyAccessor(POLICY_EXPAND)));
429    }
430
431    /**
432     * Obtains a list of locations for a policy or configuration provider.
433     * The search algorithm is as follows:
434     * <ol>
435     * <li> Look in security properties for keys of form <code>prefix + n</code>,
436     * where <i>n</i> is an integer and <i>prefix</i> is a passed parameter.
437     * Sequence starts with <code>n=1</code>, and keeps incrementing <i>n</i>
438     * until next key is not found. <br>
439     * For each obtained key, try to construct an URL instance. On success,
440     * add the URL to the list; otherwise ignore it.
441     * <li>
442     *         If security settings do not prohibit (through
443     *         {@link #POLICY_ALLOW_DYNAMIC the &quot;policy.allowSystemProperty&quot; property})
444     *         to use additional policy location, read the system property under the
445     *         passed key parameter. If property exists, it may designate a file or
446     *         an absolute URL. Thus, first check if there is a file with that name,
447     *         and if so, convert the pathname to URL. Otherwise, try to instantiate
448     *         an URL directly. If succeeded, append the URL to the list
449     * <li>
450     *         If the additional location from the step above was specified to the
451     *         system via &quot;==&quot; (i.e. starts with '='), discard all URLs above
452     *         and use this only URL.
453     * </ol>
454     * <b>Note:</b> all property values (both security and system) related to URLs are
455     * subject to {@link #expand(String, Properties) property expansion}, regardless
456     * of the &quot;policy.expandProperties&quot; security setting.
457     *
458     * @param system system properties
459     * @param systemUrlKey key to additional policy location
460     * @param securityUrlPrefix prefix to numbered locations in security properties
461     * @return array of URLs to provider's configuration files, may be empty.
462     */
463    public static URL[] getPolicyURLs(final Properties system,
464            final String systemUrlKey, final String securityUrlPrefix) {
465
466        final SecurityPropertyAccessor security = new SecurityPropertyAccessor(
467                null);
468        final List<URL> urls = new ArrayList<URL>();
469        boolean dynamicOnly = false;
470        URL dynamicURL = null;
471
472        //first check if policy is set via system properties
473        if (!Util.equalsIgnoreCase(FALSE, AccessController
474                .doPrivileged(security.key(POLICY_ALLOW_DYNAMIC)))) {
475            String location = system.getProperty(systemUrlKey);
476            if (location != null) {
477                if (location.startsWith("=")) { //$NON-NLS-1$
478                    //overrides all other urls
479                    dynamicOnly = true;
480                    location = location.substring(1);
481                }
482                try {
483                    location = expandURL(location, system);
484                    // location can be a file, but we need an url...
485                    final File f = new File(location);
486                    dynamicURL = AccessController
487                            .doPrivileged(new PrivilegedExceptionAction<URL>() {
488
489                                public URL run() throws Exception {
490                                    if (f.exists()) {
491                                        return f.toURI().toURL();
492                                    } else {
493                                        return null;
494                                    }
495                                }
496                            });
497                    if (dynamicURL == null) {
498                        dynamicURL = new URL(location);
499                    }
500                }
501                catch (Exception e) {
502                    // TODO: log error
503                    // System.err.println("Error detecting system policy location: "+e);
504                }
505            }
506        }
507        //next read urls from security.properties
508        if (!dynamicOnly) {
509            int i = 1;
510            while (true) {
511                String location = AccessController
512                        .doPrivileged(security.key(new StringBuilder(
513                                securityUrlPrefix).append(i++).toString()));
514                if (location == null) {
515                    break;
516                }
517                try {
518                    location = expandURL(location, system);
519                    URL anURL = new URL(location);
520                    if (anURL != null) {
521                        urls.add(anURL);
522                    }
523                }
524                catch (Exception e) {
525                    // TODO: log error
526                    // System.err.println("Error detecting security policy location: "+e);
527                }
528            }
529        }
530        if (dynamicURL != null) {
531            urls.add(dynamicURL);
532        }
533        return urls.toArray(new URL[urls.size()]);
534    }
535
536    /**
537     * Converts common-purpose collection of Permissions to PermissionCollection.
538     *
539     * @param perms a collection containing arbitrary permissions, may be null
540     * @return mutable heterogeneous PermissionCollection containing all Permissions
541     * from the specified collection
542     */
543    public static PermissionCollection toPermissionCollection(
544            Collection<Permission> perms) {
545        Permissions pc = new Permissions();
546        if (perms != null) {
547            for (Iterator<Permission> iter = perms.iterator(); iter.hasNext();) {
548                Permission element = iter.next();
549                pc.add(element);
550            }
551        }
552        return pc;
553    }
554
555    // Empty set of arguments to default constructor of a Permission.
556    private static final Class[] NO_ARGS = {};
557
558    // One-arg set of arguments to default constructor of a Permission.
559    private static final Class[] ONE_ARGS = { String.class };
560
561    // Two-args set of arguments to default constructor of a Permission.
562    private static final Class[] TWO_ARGS = { String.class, String.class };
563
564    /**
565     * Tries to find a suitable constructor and instantiate a new Permission
566     * with specified parameters.
567     *
568     * @param targetType class of expected Permission instance
569     * @param targetName name of expected Permission instance
570     * @param targetActions actions of expected Permission instance
571     * @return a new Permission instance
572     * @throws IllegalArgumentException if no suitable constructor found
573     * @throws Exception any exception thrown by Constructor.newInstance()
574     */
575    public static Permission instantiatePermission(Class<?> targetType,
576            String targetName, String targetActions) throws Exception {
577
578        // let's guess the best order for trying constructors
579        Class[][] argTypes = null;
580        Object[][] args = null;
581        if (targetActions != null) {
582            argTypes = new Class[][] { TWO_ARGS, ONE_ARGS, NO_ARGS };
583            args = new Object[][] { { targetName, targetActions },
584                    { targetName }, {} };
585        } else if (targetName != null) {
586            argTypes = new Class[][] { ONE_ARGS, TWO_ARGS, NO_ARGS };
587            args = new Object[][] { { targetName },
588                    { targetName, targetActions }, {} };
589        } else {
590            argTypes = new Class[][] { NO_ARGS, ONE_ARGS, TWO_ARGS };
591            args = new Object[][] { {}, { targetName },
592                    { targetName, targetActions } };
593        }
594
595        // finally try to instantiate actual permission
596        for (int i = 0; i < argTypes.length; i++) {
597            try {
598                Constructor<?> ctor = targetType.getConstructor(argTypes[i]);
599                return (Permission)ctor.newInstance(args[i]);
600            }
601            catch (NoSuchMethodException ignore) {}
602        }
603        throw new IllegalArgumentException(
604                Messages.getString("security.150", targetType));//$NON-NLS-1$
605    }
606
607    /**
608     * Checks whether the objects from <code>what</code> array are all
609     * presented in <code>where</code> array.
610     *
611     * @param what first array, may be <code>null</code>
612     * @param where  second array, may be <code>null</code>
613     * @return <code>true</code> if the first array is <code>null</code>
614     * or if each and every object (ignoring null values)
615     * from the first array has a twin in the second array; <code>false</code> otherwise
616     */
617    public static boolean matchSubset(Object[] what, Object[] where) {
618        if (what == null) {
619            return true;
620        }
621
622        for (int i = 0; i < what.length; i++) {
623            if (what[i] != null) {
624                if (where == null) {
625                    return false;
626                }
627                boolean found = false;
628                for (int j = 0; j < where.length; j++) {
629                    if (what[i].equals(where[j])) {
630                        found = true;
631                        break;
632                    }
633                }
634                if (!found) {
635                    return false;
636                }
637            }
638        }
639        return true;
640    }
641}
642