/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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. */ /** * @author Alexey V. Varlamov */ package org.apache.harmony.security.fortress; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.net.URL; import java.security.AccessController; import java.security.CodeSource; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.Permission; import java.security.Principal; import java.security.UnresolvedPermission; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Properties; import java.util.Set; import java.util.StringTokenizer; import org.apache.harmony.security.DefaultPolicyScanner; import org.apache.harmony.security.DefaultPolicyScanner.GrantEntry; import org.apache.harmony.security.DefaultPolicyScanner.KeystoreEntry; import org.apache.harmony.security.DefaultPolicyScanner.PermissionEntry; import org.apache.harmony.security.DefaultPolicyScanner.PrincipalEntry; import org.apache.harmony.security.PolicyEntry; import org.apache.harmony.security.UnresolvedPrincipal; import org.apache.harmony.security.fortress.PolicyUtils.ExpansionFailedException; import org.apache.harmony.security.internal.nls.Messages; /** * This is a basic loader of policy files. It delegates lexical analysis to a * pluggable scanner and converts received tokens to a set of * {@link org.apache.harmony.security.PolicyEntry PolicyEntries}. For details of * policy format, see the {@link org.apache.harmony.security.DefaultPolicy * default policy description}. <br> * For ordinary uses, this class has just one public method <code>parse()</code> * , which performs the main task. Extensions of this parser may redefine * specific operations separately, by overriding corresponding protected * methods. <br> * This implementation is effectively thread-safe, as it has no field references * to data being processed (that is, passes all the data as method parameters). * * @see org.apache.harmony.security.DefaultPolicy * @see org.apache.harmony.security.DefaultPolicyScanner * @see org.apache.harmony.security.PolicyEntry */ public class DefaultPolicyParser { /** * Specific handler for expanding <i>self</i> and <i>alias</i> protocols. */ class PermissionExpander implements PolicyUtils.GeneralExpansionHandler { // Store KeyStore private KeyStore ks; // Store GrantEntry private DefaultPolicyScanner.GrantEntry ge; /** * Combined setter of all required fields. */ public PermissionExpander configure(DefaultPolicyScanner.GrantEntry ge, KeyStore ks) { this.ge = ge; this.ks = ks; return this; } // Formats a string describing the passed Principal. private String pc2str(Principal pc) { final String klass = pc.getClass().getName(); final String name = pc.getName(); final StringBuilder sb = new StringBuilder(klass.length() + name.length() + 5); return sb.append(klass).append(" \"").append(name).append("\"") //$NON-NLS-1$ //$NON-NLS-2$ .toString(); } /** * Resolves the following protocols: * <dl> * <dt>self * <dd>Denotes substitution to a principal information of the parental * GrantEntry. Returns a space-separated list of resolved Principals * (including wildcarded), formatting each as <b>class * "name"</b>. If parental GrantEntry has no Principals, * throws ExpansionFailedException. * <dt>alias:<i>name</i> * <dd>Denotes substitution of a KeyStore alias. Namely, if a KeyStore * has an X.509 certificate associated with the specified name, then * returns <b>org.apache.harmony.javax.security.auth.x500.X500Principal * "<i>DN</i>"</b> string, where <i>DN</i> is a certificate's * subject distinguished name. * </dl> * * @throws ExpansionFailedException * - if protocol is other than <i>self</i> or <i>alias</i>, * or if data resolution failed */ @Override public String resolve(String protocol, String data) throws PolicyUtils.ExpansionFailedException { if ("self".equals(protocol)) { //$NON-NLS-1$ // need expanding to list of principals in grant clause if (ge.principals != null && ge.principals.size() != 0) { final StringBuilder sb = new StringBuilder(); for (final PrincipalEntry pr : ge.principals) { if (pr.klass == null) { // aliased X500Principal try { sb.append(pc2str(getPrincipalByAlias(ks, pr.name))); } catch (final Exception e) { throw new PolicyUtils.ExpansionFailedException( Messages.getString( "security.143", pr.name), e); //$NON-NLS-1$ } } else { sb.append(pr.klass).append(" \"").append(pr.name) //$NON-NLS-1$ .append("\" "); //$NON-NLS-1$ } } return sb.toString(); } else { throw new PolicyUtils.ExpansionFailedException( Messages.getString("security.144")); //$NON-NLS-1$ } } if ("alias".equals(protocol)) { //$NON-NLS-1$ try { return pc2str(getPrincipalByAlias(ks, data)); } catch (final Exception e) { throw new PolicyUtils.ExpansionFailedException( Messages.getString("security.143", data), e); //$NON-NLS-1$ } } throw new PolicyUtils.ExpansionFailedException(Messages.getString( "security.145", protocol)); //$NON-NLS-1$ } } // Pluggable scanner for a specific file format private final DefaultPolicyScanner scanner; /** * Default constructor, * {@link org.apache.harmony.security.DefaultPolicyScanner * DefaultPolicyScanner} is used. */ public DefaultPolicyParser() { scanner = new DefaultPolicyScanner(); } /** * Extension constructor for plugging-in custom scanner. */ public DefaultPolicyParser(DefaultPolicyScanner s) { scanner = s; } /** * Returns a subject's X500Principal of an X509Certificate, which is * associated with the specified keystore alias. * * @param ks * KeyStore for resolving Certificate, may be <code>null</code> * @param alias * alias to a certificate * @return X500Principal with a subject distinguished name * @throws KeyStoreException * if KeyStore is <code>null</code> or if it failed to provide a * certificate * @throws CertificateException * if found certificate is not an X509Certificate */ protected Principal getPrincipalByAlias(KeyStore ks, String alias) throws KeyStoreException, CertificateException { if (ks == null) { throw new KeyStoreException(Messages.getString( "security.147", alias)); //$NON-NLS-1$ } // XXX cache found certs ?? final Certificate x509 = ks.getCertificate(alias); if (x509 instanceof X509Certificate) { return ((X509Certificate) x509).getSubjectX500Principal(); } else { throw new CertificateException(Messages.getString("security.148", //$NON-NLS-1$ alias, x509)); } } /** * Returns the first successfully loaded KeyStore, from the specified list * of possible locations. This method iterates over the list of * KeystoreEntries; for each entry expands <code>url</code> and * <code>type</code>, tries to construct instances of specified URL and * KeyStore and to load the keystore. If it is loaded, returns the keystore, * otherwise proceeds to the next KeystoreEntry. <br> * <b>Note:</b> an url may be relative to the policy file location or * absolute. * * @param keystores * list of available KeystoreEntries * @param base * the policy file location * @param system * system properties, used for property expansion * @param resolve * flag enabling/disabling property expansion * @return the first successfully loaded KeyStore or <code>null</code> */ protected KeyStore initKeyStore(List<KeystoreEntry> keystores, URL base, Properties system, boolean resolve) { for (int i = 0; i < keystores.size(); i++) { try { final DefaultPolicyScanner.KeystoreEntry ke = keystores.get(i); if (resolve) { ke.url = PolicyUtils.expandURL(ke.url, system); if (ke.type != null) { ke.type = PolicyUtils.expand(ke.type, system); } } if (ke.type == null || ke.type.length() == 0) { ke.type = KeyStore.getDefaultType(); } final KeyStore ks = KeyStore.getInstance(ke.type); final URL location = new URL(base, ke.url); final InputStream is = AccessController .doPrivileged(new PolicyUtils.URLLoader(location)); try { ks.load(is, null); } finally { is.close(); } return ks; } catch (final Exception e) { // TODO: log warning } } return null; } /** * This is the main business method. It manages loading process as follows: * the associated scanner is used to parse the stream to a set of * {@link org.apache.harmony.security.DefaultPolicyScanner.GrantEntry * composite tokens}, then this set is iterated and each token is translated * to a PolicyEntry. Semantically invalid tokens are ignored, the same as * void PolicyEntries. <br> * A policy file may refer to some KeyStore(s), and in this case the first * valid reference is initialized and used in processing tokens. * * @param location * an URL of a policy file to be loaded * @param system * system properties, used for property expansion * @return a collection of PolicyEntry objects, may be empty * @throws Exception * IO error while reading location or file syntax error */ public Collection<PolicyEntry> parse(URL location, Properties system) throws Exception { final boolean resolve = PolicyUtils.canExpandProperties(); final Reader r = new BufferedReader(new InputStreamReader( AccessController.doPrivileged(new PolicyUtils.URLLoader( location)))); final Collection<GrantEntry> grantEntries = new HashSet<GrantEntry>(); final List<KeystoreEntry> keystores = new ArrayList<KeystoreEntry>(); try { scanner.scanStream(r, grantEntries, keystores); } finally { r.close(); } // XXX KeyStore could be loaded lazily... final KeyStore ks = initKeyStore(keystores, location, system, resolve); final Collection<PolicyEntry> result = new HashSet<PolicyEntry>(); for (final GrantEntry ge : grantEntries) { try { final PolicyEntry pe = resolveGrant(ge, ks, system, resolve); if (!pe.isVoid()) { result.add(pe); } } catch (final Exception e) { // TODO: log warning } } return result; } /** * Translates GrantEntry token to PolicyEntry object. It goes step by step, * trying to resolve each component of the GrantEntry: * <ul> * <li>If <code>codebase</code> is specified, expand it and construct an * URL. * <li>If <code>signers</code> is specified, expand it and obtain * corresponding Certificates. * <li>If <code>principals</code> collection is specified, iterate over it. * For each PrincipalEntry, expand name and if no class specified, resolve * actual X500Principal from a KeyStore certificate; otherwise keep it as * UnresolvedPrincipal. * <li>Iterate over <code>permissions</code> collection. For each * PermissionEntry, try to resolve (see method * {@link #resolvePermission(DefaultPolicyScanner.PermissionEntry, DefaultPolicyScanner.GrantEntry, KeyStore, Properties, boolean) * resolvePermission()}) a corresponding permission. If resolution failed, * ignore the PermissionEntry. * </ul> * In fact, property expansion in the steps above is conditional and is * ruled by the parameter <i>resolve</i>. <br> * Finally a new PolicyEntry is created, which associates the trinity of * resolved URL, Certificates and Principals to a set of granted * Permissions. * * @param ge * GrantEntry token to be resolved * @param ks * KeyStore for resolving Certificates, may be <code>null</code> * @param system * system properties, used for property expansion * @param resolve * flag enabling/disabling property expansion * @return resolved PolicyEntry * @throws Exception * if unable to resolve codebase, signers or principals of the * GrantEntry * @see DefaultPolicyScanner.PrincipalEntry * @see DefaultPolicyScanner.PermissionEntry * @see org.apache.harmony.security.PolicyUtils */ protected PolicyEntry resolveGrant(DefaultPolicyScanner.GrantEntry ge, KeyStore ks, Properties system, boolean resolve) throws Exception { URL codebase = null; Certificate[] signers = null; final Set<Principal> principals = new HashSet<Principal>(); final Set<Permission> permissions = new HashSet<Permission>(); if (ge.codebase != null) { codebase = new URL(resolve ? PolicyUtils.expandURL(ge.codebase, system) : ge.codebase); } if (ge.signers != null) { if (resolve) { ge.signers = PolicyUtils.expand(ge.signers, system); } signers = resolveSigners(ks, ge.signers); } if (ge.principals != null) { for (final PrincipalEntry pe : ge.principals) { if (resolve) { pe.name = PolicyUtils.expand(pe.name, system); } if (pe.klass == null) { principals.add(getPrincipalByAlias(ks, pe.name)); } else { principals.add(new UnresolvedPrincipal(pe.klass, pe.name)); } } } if (ge.permissions != null) { for (final PermissionEntry pe : ge.permissions) { try { permissions.add(resolvePermission(pe, ge, ks, system, resolve)); } catch (final Exception e) { // TODO: log warning } } } return new PolicyEntry(new CodeSource(codebase, signers), principals, permissions); } /** * Translates PermissionEntry token to Permission object. First, it performs * general expansion for non-null <code>name</code> and properties expansion * for non-null <code>name</code>, <code>action</code> and * <code>signers</code>. Then, it obtains signing Certificates(if any), * tries to find a class specified by <code>klass</code> name and * instantiate a corresponding permission object. If class is not found or * it is signed improperly, returns UnresolvedPermission. * * @param pe * PermissionEntry token to be resolved * @param ge * parental GrantEntry of the PermissionEntry * @param ks * KeyStore for resolving Certificates, may be <code>null</code> * @param system * system properties, used for property expansion * @param resolve * flag enabling/disabling property expansion * @return resolved Permission object, either of concrete class or * UnresolvedPermission * @throws Exception * if failed to expand properties, or to get a Certificate, or * to create an instance of a successfully found class */ protected Permission resolvePermission( DefaultPolicyScanner.PermissionEntry pe, DefaultPolicyScanner.GrantEntry ge, KeyStore ks, Properties system, boolean resolve) throws Exception { if (pe.name != null) { pe.name = PolicyUtils.expandGeneral(pe.name, new PermissionExpander().configure(ge, ks)); } if (resolve) { if (pe.name != null) { pe.name = PolicyUtils.expand(pe.name, system); } if (pe.actions != null) { pe.actions = PolicyUtils.expand(pe.actions, system); } if (pe.signers != null) { pe.signers = PolicyUtils.expand(pe.signers, system); } } final Certificate[] signers = (pe.signers == null) ? null : resolveSigners(ks, pe.signers); try { final Class<?> klass = Class.forName(pe.klass); if (PolicyUtils.matchSubset(signers, klass.getSigners())) { return PolicyUtils.instantiatePermission(klass, pe.name, pe.actions); } } catch (final ClassNotFoundException cnfe) { } // maybe properly signed class will be loaded later return new UnresolvedPermission(pe.klass, pe.name, pe.actions, signers); } /** * Takes a comma-separated list of aliases and obtains corresponding * certificates. * * @param ks * KeyStore for resolving Certificates, may be <code>null</code> * @param signers * comma-separated list of certificate aliases, must be not * <code>null</code> * @return an array of signing Certificates * @throws Exception * if KeyStore is <code>null</code> or if it failed to provide a * certificate */ protected Certificate[] resolveSigners(KeyStore ks, String signers) throws Exception { if (ks == null) { throw new KeyStoreException(Messages.getString("security.146", //$NON-NLS-1$ signers)); } final Collection<Certificate> certs = new HashSet<Certificate>(); final StringTokenizer snt = new StringTokenizer(signers, ","); //$NON-NLS-1$ while (snt.hasMoreTokens()) { // XXX cache found certs ?? certs.add(ks.getCertificate(snt.nextToken().trim())); } return certs.toArray(new Certificate[certs.size()]); } }