/* * 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.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import org.apache.ambari.server.AmbariException; import org.apache.ambari.server.StaticallyInject; import org.apache.ambari.server.controller.AmbariManagementController; import org.apache.ambari.server.controller.spi.NoSuchParentResourceException; import org.apache.ambari.server.controller.spi.NoSuchResourceException; import org.apache.ambari.server.controller.spi.Predicate; import org.apache.ambari.server.controller.spi.Request; import org.apache.ambari.server.controller.spi.RequestStatus; import org.apache.ambari.server.controller.spi.Resource; import org.apache.ambari.server.controller.spi.Resource.Type; import org.apache.ambari.server.controller.spi.ResourceAlreadyExistsException; import org.apache.ambari.server.controller.spi.SystemException; import org.apache.ambari.server.controller.spi.UnsupportedPropertyException; import org.apache.ambari.server.controller.utilities.PropertyHelper; import org.apache.ambari.server.security.authorization.RoleAuthorization; import org.apache.ambari.server.security.credential.Credential; import org.apache.ambari.server.security.credential.PrincipalKeyCredential; import org.apache.ambari.server.security.encryption.CredentialStoreService; import org.apache.ambari.server.security.encryption.CredentialStoreType; import org.apache.commons.lang.StringUtils; import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.AssistedInject; /** * A write-only resource provider for securely stored credentials */ @StaticallyInject public class CredentialResourceProvider extends AbstractControllerResourceProvider { // ----- Property ID constants --------------------------------------------- public static final String CREDENTIAL_CLUSTER_NAME_PROPERTY_ID = PropertyHelper.getPropertyId("Credential", "cluster_name"); public static final String CREDENTIAL_ALIAS_PROPERTY_ID = PropertyHelper.getPropertyId("Credential", "alias"); public static final String CREDENTIAL_PRINCIPAL_PROPERTY_ID = PropertyHelper.getPropertyId("Credential", "principal"); public static final String CREDENTIAL_KEY_PROPERTY_ID = PropertyHelper.getPropertyId("Credential", "key"); public static final String CREDENTIAL_TYPE_PROPERTY_ID = PropertyHelper.getPropertyId("Credential", "type"); private static final Set<String> PK_PROPERTY_IDS; private static final Set<String> PROPERTY_IDS; private static final Map<Type, String> KEY_PROPERTY_IDS; static { Set<String> set; set = new HashSet<>(); set.add(CREDENTIAL_CLUSTER_NAME_PROPERTY_ID); set.add(CREDENTIAL_ALIAS_PROPERTY_ID); PK_PROPERTY_IDS = Collections.unmodifiableSet(set); set = new HashSet<>(); set.add(CREDENTIAL_CLUSTER_NAME_PROPERTY_ID); set.add(CREDENTIAL_ALIAS_PROPERTY_ID); set.add(CREDENTIAL_PRINCIPAL_PROPERTY_ID); set.add(CREDENTIAL_KEY_PROPERTY_ID); set.add(CREDENTIAL_TYPE_PROPERTY_ID); PROPERTY_IDS = Collections.unmodifiableSet(set); HashMap<Type, String> map = new HashMap<>(); map.put(Type.Cluster, CREDENTIAL_CLUSTER_NAME_PROPERTY_ID); map.put(Type.Credential, CREDENTIAL_ALIAS_PROPERTY_ID); KEY_PROPERTY_IDS = Collections.unmodifiableMap(map); } /** * The secure storage facility to use to store credentials. */ @Inject private CredentialStoreService credentialStoreService; /** * Create a new resource provider. */ @AssistedInject public CredentialResourceProvider(@Assisted AmbariManagementController managementController) { super(PROPERTY_IDS, KEY_PROPERTY_IDS, managementController); EnumSet<RoleAuthorization> authorizations = EnumSet.of( RoleAuthorization.CLUSTER_MANAGE_CREDENTIALS, RoleAuthorization.CLUSTER_TOGGLE_KERBEROS); setRequiredCreateAuthorizations(authorizations); setRequiredGetAuthorizations(authorizations); setRequiredUpdateAuthorizations(authorizations); setRequiredDeleteAuthorizations(authorizations); } @Override protected RequestStatus createResourcesAuthorized(final Request request) throws SystemException, UnsupportedPropertyException, ResourceAlreadyExistsException, NoSuchParentResourceException { for (final Map<String, Object> properties : request.getProperties()) { createResources(new CreateResourcesCommand(properties)); } notifyCreate(Type.Credential, request); return getRequestStatus(null); } @Override protected Set<Resource> getResourcesAuthorized(Request request, Predicate predicate) throws SystemException, UnsupportedPropertyException, NoSuchResourceException, NoSuchParentResourceException { Set<String> requestedIds = getRequestPropertyIds(request, predicate); Set<Resource> resources = new HashSet<>(); boolean sendNotFoundErrorIfEmpty = false; for (Map<String, Object> propertyMap : getPropertyMaps(predicate)) { String clusterName = (String) propertyMap.get(CREDENTIAL_CLUSTER_NAME_PROPERTY_ID); if (null == clusterName || clusterName.isEmpty()) { throw new IllegalArgumentException("Invalid argument, cluster name is required"); } String alias = (String) propertyMap.get(CREDENTIAL_ALIAS_PROPERTY_ID); if (!StringUtils.isEmpty(alias)) { try { if (credentialStoreService.containsCredential(clusterName, alias)) { resources.add(toResource(clusterName, alias, credentialStoreService.getCredentialStoreType(clusterName, alias), requestedIds)); } else { // Only throw a NoSuchResourceException if a specific item is being requested and it // wasn't found. If multiple resources are queried, one or may not exist and this // sendNotFoundErrorIfEmpty will be set to true. However if at least one resource is // found, the resources Set will not be empty and NoSuchResourceException will not be // thrown sendNotFoundErrorIfEmpty = true; } } catch (AmbariException e) { throw new SystemException(e.getLocalizedMessage(), e); } } else { try { Map<String, CredentialStoreType> results = credentialStoreService.listCredentials(clusterName); if (results != null) { for (Map.Entry<String, CredentialStoreType> entry : results.entrySet()) { resources.add(toResource(clusterName, entry.getKey(), entry.getValue(), requestedIds)); } } } catch (AmbariException e) { throw new SystemException(e.getLocalizedMessage(), e); } } } if (sendNotFoundErrorIfEmpty && resources.isEmpty()) { throw new NoSuchResourceException("The requested resource doesn't exist: Credential not found, " + predicate); } return resources; } @Override protected RequestStatus updateResourcesAuthorized(Request request, Predicate predicate) throws SystemException, UnsupportedPropertyException, NoSuchResourceException, NoSuchParentResourceException { for (Map<String, Object> requestPropMap : request.getProperties()) { for (Map<String, Object> propertyMap : getPropertyMaps(requestPropMap, predicate)) { if (modifyResources(new ModifyResourcesCommand(propertyMap)) == null) { throw new NoSuchResourceException("The requested resource doesn't exist: Credential not found, " + getAlias(propertyMap)); } } } notifyUpdate(Type.Credential, request, predicate); return getRequestStatus(null); } @Override protected RequestStatus deleteResourcesAuthorized(Request request, Predicate predicate) throws SystemException, UnsupportedPropertyException, NoSuchResourceException, NoSuchParentResourceException { final Set<Map<String, Object>> propertyMaps = getPropertyMaps(predicate); for (final Map<String, Object> properties : propertyMaps) { modifyResources(new DeleteResourcesCommand(properties)); } notifyDelete(Type.Credential, predicate); return getRequestStatus(null); } @Override protected Set<String> getPKPropertyIds() { return PK_PROPERTY_IDS; } /** * Give a map of credential-related properties attempts to create a Credential (PrincipalKeyCredential) * after validating that the required properties are present. * <p/> * The credential's principal is required, however a warning will be logged if a value for the key * is not supplied. * * @param properties a map of properties * @return a new Credential * @throws IllegalArgumentException if the map of properties does not contain enough information to create * a new PrincipalKeyCredential instance */ private Credential createCredential(Map<String, Object> properties) throws IllegalArgumentException { String principal; String key; if (properties.get(CREDENTIAL_PRINCIPAL_PROPERTY_ID) == null) { throw new IllegalArgumentException("Property " + CREDENTIAL_PRINCIPAL_PROPERTY_ID + " must be provided"); } else { principal = String.valueOf(properties.get(CREDENTIAL_PRINCIPAL_PROPERTY_ID)); } if (properties.get(CREDENTIAL_KEY_PROPERTY_ID) == null) { LOG.warn("The credential is being added without a key"); key = null; } else { key = String.valueOf(properties.get(CREDENTIAL_KEY_PROPERTY_ID)); } return new PrincipalKeyCredential(principal, key); } /** * Retrieves the <code>Credential/persist</code> property from the property map and determined if the value * represents Boolean true or false. * <p/> * If the <code>Credential/type</code> property is not is not available or is <code>null</code>, * an AmbariException will be thrown. * * @param properties a map of properties * @return true or false * @throws IllegalArgumentException if the <code>Credential/type</code> property is not set to * either <code>persisted</code> or <code>temporary</code> */ private CredentialStoreType getCredentialStoreType(Map<String, Object> properties) throws IllegalArgumentException { Object propertyValue = properties.get(CREDENTIAL_TYPE_PROPERTY_ID); if (propertyValue == null) { throw new IllegalArgumentException("Property " + CREDENTIAL_TYPE_PROPERTY_ID + " must be provided"); } else if (propertyValue instanceof String) { try { return CredentialStoreType.valueOf(((String) propertyValue).toUpperCase()); } catch (IllegalArgumentException e) { throw new IllegalArgumentException("Property " + CREDENTIAL_TYPE_PROPERTY_ID + " must be either 'persisted' or 'temporary'", e); } } else { throw new IllegalArgumentException("Property " + CREDENTIAL_TYPE_PROPERTY_ID + " must be a String"); } } /** * Retrieves the <code>Credential/cluster_name</code> property from the property map * * @param properties a map of properties * @return the cluster name * @throws IllegalArgumentException if <code>Credential/cluster_name</code> is not in the map or * <code>null</code> */ private String getClusterName(Map<String, Object> properties) throws IllegalArgumentException { if (properties.get(CREDENTIAL_CLUSTER_NAME_PROPERTY_ID) == null) { throw new IllegalArgumentException("Property " + CREDENTIAL_CLUSTER_NAME_PROPERTY_ID + " must be provided"); } else { return String.valueOf(properties.get(CREDENTIAL_CLUSTER_NAME_PROPERTY_ID)); } } /** * Retrieves the <code>Credential/alias</code> property from the property map * * @param properties a map of properties * @return the alias name * @throws IllegalArgumentException if <code>Credential/alias</code> is not in the map or * <code>null</code> */ private String getAlias(Map<String, Object> properties) throws IllegalArgumentException { if (properties.get(CREDENTIAL_ALIAS_PROPERTY_ID) == null) { throw new IllegalArgumentException("Property " + CREDENTIAL_ALIAS_PROPERTY_ID + " must be provided"); } else { return String.valueOf(properties.get(CREDENTIAL_ALIAS_PROPERTY_ID)); } } /** * Validates that the CredentialStoreService is available and has been initialized to support the * requested persisted or temporary storage facility. * * @param credentialStoreType a CredentialStoreType indicating which credential store facility to use * @throws IllegalArgumentException if the requested facility has not been initialized */ private void validateForCreateOrModify(CredentialStoreType credentialStoreType) throws IllegalArgumentException { if (!credentialStoreService.isInitialized(credentialStoreType)) { if (CredentialStoreType.PERSISTED == credentialStoreType) { // Throw IllegalArgumentException to cause a 400 error to be returned... throw new IllegalArgumentException("Credentials cannot be stored in Ambari's persistent secure " + "credential store since secure persistent storage has not yet be configured. " + "Use ambari-server setup-security to enable this feature."); } else if (CredentialStoreType.TEMPORARY == credentialStoreType) { // Throw IllegalArgumentException to cause a 400 error to be returned... throw new IllegalArgumentException("Credentials cannot be stored in Ambari's temporary secure " + "credential store since secure temporary storage has not yet be configured."); } } } /** * Creates a new resource from the given cluster name, alias, and persist values. * * @param clusterName a cluster name * @param alias an alias * @param credentialStoreType the relevant credential store type * @param requestedIds the properties to include in the resulting resource instance * @return a resource */ private Resource toResource(String clusterName, String alias, CredentialStoreType credentialStoreType, Set<String> requestedIds) { Resource resource = new ResourceImpl(Type.Credential); setResourceProperty(resource, CREDENTIAL_CLUSTER_NAME_PROPERTY_ID, clusterName, requestedIds); setResourceProperty(resource, CREDENTIAL_ALIAS_PROPERTY_ID, alias, requestedIds); setResourceProperty(resource, CREDENTIAL_TYPE_PROPERTY_ID, credentialStoreType.name().toLowerCase(), requestedIds); return resource; } /** * CreateResourcesCommand implements a Command that performs the steps to create a new credential * resources and store the data into the persisted or temporary credential stores, as indicated. */ private class CreateResourcesCommand implements Command<String> { private final Map<String, Object> properties; public CreateResourcesCommand(Map<String, Object> properties) { this.properties = properties; } @Override public String invoke() throws AmbariException { CredentialStoreType credentialStoreType = getCredentialStoreType(properties); validateForCreateOrModify(credentialStoreType); String clusterName = getClusterName(properties); String alias = getAlias(properties); if (credentialStoreService.containsCredential(clusterName, alias)) { throw new AmbariException("A credential with the alias of " + alias + " already exists"); } credentialStoreService.setCredential(clusterName, alias, createCredential(properties), credentialStoreType); return alias; } } /** * ModifyResourcesCommand implements a Command that performs the steps to update existing credential * resources. */ private class ModifyResourcesCommand implements Command<String> { private final Map<String, Object> properties; public ModifyResourcesCommand(Map<String, Object> properties) { this.properties = properties; } @Override public String invoke() throws AmbariException { String clusterName = getClusterName(properties); String alias = getAlias(properties); CredentialStoreType credentialStoreType = properties.containsKey(CREDENTIAL_TYPE_PROPERTY_ID) ? getCredentialStoreType(properties) : credentialStoreService.getCredentialStoreType(clusterName, alias); validateForCreateOrModify(credentialStoreType); Credential credential = credentialStoreService.getCredential(clusterName, alias); if (credential instanceof PrincipalKeyCredential) { PrincipalKeyCredential principalKeyCredential = (PrincipalKeyCredential) credential; Map<String, Object> credentialProperties = new HashMap<>(); // Make sure the credential to update is removed from the persisted or temporary store... the // updated data may change the persistence type. credentialStoreService.removeCredential(clusterName, alias); if (properties.containsKey(CREDENTIAL_PRINCIPAL_PROPERTY_ID)) { credentialProperties.put(CREDENTIAL_PRINCIPAL_PROPERTY_ID, properties.get(CREDENTIAL_PRINCIPAL_PROPERTY_ID)); } else { credentialProperties.put(CREDENTIAL_PRINCIPAL_PROPERTY_ID, principalKeyCredential.getPrincipal()); } if (properties.containsKey(CREDENTIAL_KEY_PROPERTY_ID)) { credentialProperties.put(CREDENTIAL_KEY_PROPERTY_ID, properties.get(CREDENTIAL_KEY_PROPERTY_ID)); } else { char[] credentialKey = principalKeyCredential.getKey(); if (credentialKey != null) { credentialProperties.put(CREDENTIAL_KEY_PROPERTY_ID, String.valueOf(credentialKey)); } } credentialStoreService.setCredential(clusterName, alias, createCredential(credentialProperties), credentialStoreType); return alias; } else { return null; } } } /** * DeleteResourcesCommand implements a Command that performs the steps to delete existing credential * resources. Credential resources will be deleted from both storage facilities, if necessary. */ private class DeleteResourcesCommand implements Command<String> { private final Map<String, Object> properties; public DeleteResourcesCommand(Map<String, Object> properties) { this.properties = properties; } @Override public String invoke() throws AmbariException { String clusterName = getClusterName(properties); String alias = getAlias(properties); credentialStoreService.removeCredential(clusterName, alias); return alias; } } }