/*
* 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.
*/
package org.apache.ambari.server.controller.internal;
import java.security.NoSuchAlgorithmException;
import java.security.Provider;
import java.security.Security;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.crypto.Cipher;
import org.apache.ambari.server.controller.RootServiceResponseFactory;
import org.apache.ambari.server.controller.spi.Predicate;
import org.apache.ambari.server.controller.spi.PropertyProvider;
import org.apache.ambari.server.controller.spi.Request;
import org.apache.ambari.server.controller.spi.Resource;
import org.apache.ambari.server.controller.spi.SystemException;
import org.apache.ambari.server.controller.utilities.PropertyHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* RootServiceComponentPropertyProvider is a PropertyProvider implementation providing additional
* properties for RootServiceComponent resources, like AMBARI_SERVER and AMBARI_AGENT.
* <p/>
* This implementation conditionally calculates and returns cipher and JCE details upon request.
* The Cipher data is cached after the first request for it and held in memory until Ambari is
* restarted. The user is required to explicitly query for one or both of the following properties
* (or fields) to get access to the data:
* <ul>
* <li>RootServiceComponents/jce_policy</li>
* <li>RootServiceComponents/ciphers</li>
* </ul>
* <p/>
* For example:
* <ul>
* <li><code>GET /api/v1/services/AMBARI/components/AMBARI_SERVER?fields=RootServiceComponents/ciphers</code></li>
* <li><code>GET /api/v1/services/AMBARI/components/AMBARI_SERVER?fields=RootServiceComponents/jce_policy</code></li>
* <li><code>GET /api/v1/services/AMBARI/components/AMBARI_SERVER?fields=RootServiceComponents/jce_policy,RootServiceComponents/ciphers</code></li>
* </ul>
* <p/>
* When querying for ciphers the returned data is a listing if installed cipher algorithms and their
* maximum key lengths. If all maximum key lengths are <code>2147483647</code>, then the unlimited
* key JCE policy is installed, else each item will have some smaller value (in bytes).
* <p/>
* For example:
* <pre>
* "ciphers" : {
* "sunjce.aes" : 2147483647,
* "sunjce.aeswrap" : 2147483647,
* "sunjce.aeswrap_128" : 2147483647,
* "sunjce.aeswrap_192" : 2147483647,
* "sunjce.aeswrap_256" : 2147483647,
* "sunjce.arcfour" : 2147483647,
* "sunjce.blowfish" : 2147483647,
* ...
* }
* </pre>
* <p/>
* When querying for the JDC policy, the returned data is a structure with details about the JCE
* policy - namely whether the unlimited key length policy is installed or not.
* <p/>
* For example:
* <pre>
* "jce_policy" : {
* "unlimited_key" : true
* }
* </pre>
*/
public class RootServiceComponentPropertyProvider extends BaseProvider implements PropertyProvider {
public static final String JCE_POLICY_PROPERTY_ID = PropertyHelper
.getPropertyId("RootServiceComponents", "jce_policy");
public static final String CIPHER_PROPERTIES_PROPERTY_ID = PropertyHelper
.getPropertyId("RootServiceComponents", "ciphers");
private static final Set<String> SUPPORTED_PROPERTY_IDS;
private final static Logger LOG = LoggerFactory.getLogger(RootServiceComponentPropertyProvider.class);
static {
Set<String> propertyIds = new HashSet<>();
propertyIds.add(JCE_POLICY_PROPERTY_ID);
propertyIds.add(CIPHER_PROPERTIES_PROPERTY_ID);
SUPPORTED_PROPERTY_IDS = Collections.unmodifiableSet(propertyIds);
}
/**
* Map of cipher algorithm names to maximum key lengths.
* <p/>
* This is cached in memory after the first time it is needed.
*/
private static final Map<String, Integer> CACHED_CIPHER_MAX_KEY_LENGTHS = new HashMap<>();
/**
* Constructor
*/
public RootServiceComponentPropertyProvider() {
super(SUPPORTED_PROPERTY_IDS);
}
@Override
public Set<Resource> populateResources(Set<Resource> resources, Request request, Predicate predicate) throws SystemException {
Set<String> requestedIds = request.getPropertyIds();
for (Resource resource : resources) {
// If this resource represents the AMBARI_SERVER component, handle it's specific properties...
if (RootServiceResponseFactory.Components.AMBARI_SERVER.name().equals(resource.getPropertyValue(RootServiceComponentResourceProvider.COMPONENT_NAME_PROPERTY_ID))) {
// Attempt to fill in the cipher details only if explicitly asked for.
if (requestedIds.contains(JCE_POLICY_PROPERTY_ID) || requestedIds.contains(CIPHER_PROPERTIES_PROPERTY_ID)) {
setCipherDetails(resource, requestedIds);
}
}
}
return resources;
}
/**
* Retrieve details about the active JCE policy and installed ciphers, then set the resource
* properties for the relevant resource.
* <p/>
* The following properties are set:
* <dl>
* <dt>RootServiceComponents/jce_policy/unlimited_key</dt>
* <dd>true if the unlimited key JCE policy is detected; false if it is not detected; null if unknown</dd>
* <dt>RootServiceComponents/ciphers</dt>
* <dd>A list of installed ciphers and their maximum key length values</dd>
* </dl>
*
* @param resource the resource to update
* @param requestedIds the request ids to populate
*/
private void setCipherDetails(Resource resource, Set<String> requestedIds) {
// Lazily fill the cache on first request....
synchronized (CACHED_CIPHER_MAX_KEY_LENGTHS) {
if (CACHED_CIPHER_MAX_KEY_LENGTHS.isEmpty()) {
// Get the list of cipher algorithms and determine maximum key lengths. Report as a resource property.
for (Provider provider : Security.getProviders()) {
String providerName = provider.getName();
for (Provider.Service service : provider.getServices()) {
String algorithmName = service.getAlgorithm();
if ("Cipher".equalsIgnoreCase(service.getType())) {
try {
CACHED_CIPHER_MAX_KEY_LENGTHS.put(String.format("%s.%s", providerName, algorithmName).toLowerCase(),
Cipher.getMaxAllowedKeyLength(algorithmName));
} catch (NoSuchAlgorithmException e) {
// This is unlikely since we are getting the algorithm names from the service providers.
// In any case, if a bad algorithm is listed it can be skipped since this is only for
// informational purposes.
LOG.warn(String.format("Failed to get the max key length of cipher %s, skipping.", algorithmName), e);
}
}
}
}
}
}
// Report each cipher as a resource property, if requested
if (requestedIds.contains(CIPHER_PROPERTIES_PROPERTY_ID)) {
for (Map.Entry<String, Integer> entry : CACHED_CIPHER_MAX_KEY_LENGTHS.entrySet()) {
setResourceProperty(resource,
PropertyHelper.getPropertyId(CIPHER_PROPERTIES_PROPERTY_ID, entry.getKey()),
entry.getValue(),
requestedIds);
}
}
// Determine if the unlimited key JCE policy is installed. This is determined by selecting a
// valid cipher algorithm and seeing if its max allowed key length value is set to the maximum
// size of an Integer.
if (requestedIds.contains(JCE_POLICY_PROPERTY_ID)) {
Boolean unlimitedKeyJCEPolicyInstalled = null;
Map.Entry<String, Integer> entry = CACHED_CIPHER_MAX_KEY_LENGTHS.entrySet().iterator().next();
if (entry != null) {
unlimitedKeyJCEPolicyInstalled = (Integer.MAX_VALUE == entry.getValue());
}
setResourceProperty(resource,
PropertyHelper.getPropertyId(JCE_POLICY_PROPERTY_ID, "unlimited_key"),
unlimitedKeyJCEPolicyInstalled,
requestedIds);
}
}
}