blob: 48e04f40fdc0f06af41128be8c9f86c7ae30f2c7 [file] [log] [blame]
/*
* 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.serveraction.kerberos;
import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
import org.apache.ambari.server.security.credential.PrincipalKeyCredential;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.apache.velocity.exception.MethodInvocationException;
import org.apache.velocity.exception.ParseErrorException;
import org.apache.velocity.exception.ResourceNotFoundException;
import javax.naming.AuthenticationException;
import javax.naming.CommunicationException;
import javax.naming.Context;
import javax.naming.InvalidNameException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.BasicAttributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.ModificationItem;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.Control;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
/**
* Implementation of <code>KerberosOperationHandler</code> to created principal in Active Directory
*/
public class ADKerberosOperationHandler extends KerberosOperationHandler {
private static Log LOG = LogFactory.getLog(ADKerberosOperationHandler.class);
private static final String LDAP_CONTEXT_FACTORY_CLASS = "com.sun.jndi.ldap.LdapCtxFactory";
/**
* A String containing the URL for the LDAP interface for the relevant Active Directory
*/
private String ldapUrl = null;
/**
* A String containing the DN of the container for managing Active Directory accounts
*/
private String principalContainerDn = null;
/**
* The LdapName of the container for managing Active Directory accounts
*/
private LdapName principalContainerLdapName = null;
/**
* A String containing the Velocity template to use to generate the JSON structure declaring the
* attributes to use to create new Active Directory accounts.
* <p/>
* If this value is null, a default template will be used.
*/
private String createTemplate = null;
/**
* The relevant LDAP context, created upon opening this KerberosOperationHandler
*/
private LdapContext ldapContext = null;
/**
* The relevant SearchControls, created upon opening this KerberosOperationHandler
*/
private SearchControls searchControls = null;
/**
* The Gson instance to use to convert the template-generated JSON structure to a Map of attribute
* names to values.
*/
private Gson gson = new Gson();
/**
* Prepares and creates resources to be used by this KerberosOperationHandler
* <p/>
* It is expected that this KerberosOperationHandler will not be used before this call.
* <p/>
* It is expected that the kerberosConfiguration Map has the following properties:
* <ul>
* <li>ldap_url - ldapUrl of ldap back end where principals would be created</li>
* <li>container_dn - DN of the container in ldap back end where principals would be created</li>
* </il>
*
* @param administratorCredential a PrincipalKeyCredential containing the administrative credential
* for the relevant KDC
* @param realm a String declaring the default Kerberos realm (or domain)
* @param kerberosConfiguration a Map of key/value pairs containing data from the kerberos-env configuration set
* @throws KerberosKDCConnectionException if a connection to the KDC cannot be made
* @throws KerberosAdminAuthenticationException if the administrator credentials fail to authenticate
* @throws KerberosRealmException if the realm does not map to a KDC
* @throws KerberosOperationException if an unexpected error occurred
*/
@Override
public void open(PrincipalKeyCredential administratorCredential, String realm,
Map<String, String> kerberosConfiguration) throws KerberosOperationException {
if (isOpen()) {
close();
}
if (administratorCredential == null) {
throw new KerberosAdminAuthenticationException("administrator credential not provided");
}
if (realm == null) {
throw new KerberosRealmException("realm not provided");
}
if (kerberosConfiguration == null) {
throw new KerberosRealmException("kerberos-env configuration may not be null");
}
this.ldapUrl = kerberosConfiguration.get(KERBEROS_ENV_LDAP_URL);
if (this.ldapUrl == null) {
throw new KerberosKDCConnectionException("ldapUrl not provided");
}
this.principalContainerDn = kerberosConfiguration.get(KERBEROS_ENV_PRINCIPAL_CONTAINER_DN);
if (this.principalContainerDn == null) {
throw new KerberosLDAPContainerException("principalContainerDn not provided");
}
try {
this.principalContainerLdapName = new LdapName(principalContainerDn);
} catch (InvalidNameException e) {
throw new KerberosLDAPContainerException("principalContainerDn is not a valid LDAP name", e);
}
setAdministratorCredential(administratorCredential);
setDefaultRealm(realm);
setKeyEncryptionTypes(translateEncryptionTypes(kerberosConfiguration.get(KERBEROS_ENV_ENCRYPTION_TYPES), "\\s+"));
this.ldapContext = createLdapContext();
this.searchControls = createSearchControls();
this.createTemplate = kerberosConfiguration.get(KERBEROS_ENV_AD_CREATE_ATTRIBUTES_TEMPLATE);
this.gson = new Gson();
setOpen(true);
}
/**
* Closes and cleans up any resources used by this KerberosOperationHandler
* <p/>
* It is expected that this KerberosOperationHandler will not be used after this call.
*/
@Override
public void close() throws KerberosOperationException {
this.searchControls = null;
this.gson = null;
if (this.ldapContext != null) {
try {
this.ldapContext.close();
} catch (NamingException e) {
throw new KerberosOperationException("Unexpected error", e);
} finally {
this.ldapContext = null;
}
}
setOpen(false);
}
/**
* Test to see if the specified principal exists in a previously configured KDC
* <p/>
* The implementation is specific to a particular type of KDC.
*
* @param principal a String containing the principal to test
* @return true if the principal exists; false otherwise
* @throws KerberosOperationException
*/
@Override
public boolean principalExists(String principal) throws KerberosOperationException {
if (!isOpen()) {
throw new KerberosOperationException("This operation handler has not been opened");
}
if (principal == null) {
throw new KerberosOperationException("principal is null");
}
DeconstructedPrincipal deconstructPrincipal = createDeconstructPrincipal(principal);
try {
return (findPrincipalDN(deconstructPrincipal.getNormalizedPrincipal()) != null);
} catch (NamingException ne) {
throw new KerberosOperationException("can not check if principal exists: " + principal, ne);
}
}
/**
* Creates a new principal in a previously configured KDC
* <p/>
* The implementation is specific to a particular type of KDC.
*
* @param principal a String containing the principal to add
* @param password a String containing the password to use when creating the principal
* @param service a boolean value indicating whether the principal is to be created as a service principal or not
* @return an Integer declaring the generated key number
* @throws KerberosPrincipalAlreadyExistsException if the principal already exists
* @throws KerberosOperationException
*/
@Override
public Integer createPrincipal(String principal, String password, boolean service)
throws KerberosOperationException {
if (!isOpen()) {
throw new KerberosOperationException("This operation handler has not been opened");
}
if (principal == null) {
throw new KerberosOperationException("principal is null");
}
if (password == null) {
throw new KerberosOperationException("principal password is null");
}
if (principalExists(principal)) {
throw new KerberosPrincipalAlreadyExistsException(principal);
}
DeconstructedPrincipal deconstructedPrincipal = createDeconstructPrincipal(principal);
String realm = deconstructedPrincipal.getRealm();
if (realm == null) {
realm = "";
}
Map<String, Object> context = new HashMap<String, Object>();
context.put("normalized_principal", deconstructedPrincipal.getNormalizedPrincipal());
context.put("principal_name", deconstructedPrincipal.getPrincipalName());
context.put("principal_primary", deconstructedPrincipal.getPrimary());
context.put("principal_instance", deconstructedPrincipal.getInstance());
context.put("realm", realm);
context.put("realm_lowercase", realm.toLowerCase());
context.put("password", password);
context.put("is_service", service);
context.put("container_dn", this.principalContainerDn);
context.put("principal_digest", DigestUtils.sha1Hex(deconstructedPrincipal.getNormalizedPrincipal()));
context.put("principal_digest_256", DigestUtils.sha256Hex(deconstructedPrincipal.getNormalizedPrincipal()));
context.put("principal_digest_512", DigestUtils.sha512Hex(deconstructedPrincipal.getNormalizedPrincipal()));
Map<String, Object> data = processCreateTemplate(context);
Attributes attributes = new BasicAttributes();
String cn = null;
if (data != null) {
for (Map.Entry<String, Object> entry : data.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if ("unicodePwd".equals(key)) {
if (value instanceof String) {
try {
attributes.put(new BasicAttribute("unicodePwd", String.format("\"%s\"", password).getBytes("UTF-16LE")));
} catch (UnsupportedEncodingException ue) {
throw new KerberosOperationException("Can not encode password with UTF-16LE", ue);
}
}
} else {
Attribute attribute = new BasicAttribute(key);
if (value instanceof Collection) {
for (Object object : (Collection) value) {
attribute.add(object);
}
} else {
if ("cn".equals(key) && (value != null)) {
cn = value.toString();
} else if ("sAMAccountName".equals(key) && (value != null)) {
// Replace the following _illegal_ characters: [ ] : ; | = + * ? < > / , (space) \
value = value.toString().replaceAll("\\[|\\]|\\:|\\;|\\||\\=|\\+|\\*|\\?|\\<|\\>|\\/|\\\\|\\,|\\s", "_");
}
attribute.add(value);
}
attributes.put(attribute);
}
}
}
if (cn == null) {
cn = deconstructedPrincipal.getNormalizedPrincipal();
}
try {
Rdn rdn = new Rdn("cn", cn);
LdapName name = new LdapName(principalContainerLdapName.getRdns());
name.add(name.size(), rdn);
ldapContext.createSubcontext(name, attributes);
} catch (NamingException ne) {
throw new KerberosOperationException("Can not create principal : " + principal, ne);
}
return 0;
}
/**
* Updates the password for an existing principal in a previously configured KDC
* <p/>
* The implementation is specific to a particular type of KDC.
*
* @param principal a String containing the principal to update
* @param password a String containing the password to set
* @return an Integer declaring the new key number
* @throws KerberosPrincipalDoesNotExistException if the principal does not exist
* @throws KerberosOperationException
*/
@Override
public Integer setPrincipalPassword(String principal, String password) throws KerberosOperationException {
if (!isOpen()) {
throw new KerberosOperationException("This operation handler has not been opened");
}
if (principal == null) {
throw new KerberosOperationException("principal is null");
}
if (password == null) {
throw new KerberosOperationException("principal password is null");
}
if(!principalExists(principal)) {
throw new KerberosPrincipalDoesNotExistException(principal);
}
DeconstructedPrincipal deconstructPrincipal = createDeconstructPrincipal(principal);
try {
String dn = findPrincipalDN(deconstructPrincipal.getNormalizedPrincipal());
if (dn != null) {
ldapContext.modifyAttributes(
new LdapName(dn),
new ModificationItem[]{
new ModificationItem(DirContext.REPLACE_ATTRIBUTE, new BasicAttribute("unicodePwd", String.format("\"%s\"", password).getBytes("UTF-16LE")))
}
);
} else {
throw new KerberosOperationException(String.format("Can not set password for principal %s: Not Found", principal));
}
} catch (NamingException e) {
throw new KerberosOperationException(String.format("Can not set password for principal %s: %s", principal, e.getMessage()), e);
} catch (UnsupportedEncodingException e) {
throw new KerberosOperationException("Unsupported encoding UTF-16LE", e);
}
return 0;
}
/**
* Removes an existing principal in a previously configured KDC
* <p/>
* The implementation is specific to a particular type of KDC.
*
* @param principal a String containing the principal to remove
* @return true if the principal was successfully removed; otherwise false
* @throws KerberosOperationException
*/
@Override
public boolean removePrincipal(String principal) throws KerberosOperationException {
if (!isOpen()) {
throw new KerberosOperationException("This operation handler has not been opened");
}
if (principal == null) {
throw new KerberosOperationException("principal is null");
}
DeconstructedPrincipal deconstructPrincipal = createDeconstructPrincipal(principal);
try {
String dn = findPrincipalDN(deconstructPrincipal.getNormalizedPrincipal());
if (dn != null) {
ldapContext.destroySubcontext(new LdapName(dn));
}
} catch (NamingException e) {
throw new KerberosOperationException(String.format("Can not remove principal %s: %s", principal, e.getMessage()), e);
}
return true;
}
@Override
public boolean testAdministratorCredentials() throws KerberosOperationException {
if (!isOpen()) {
throw new KerberosOperationException("This operation handler has not been opened");
}
// If this KerberosOperationHandler was successfully opened, successful authentication has
// already occurred.
return true;
}
/**
* Helper method to create the LDAP context needed to interact with the Active Directory.
*
* @return the relevant LdapContext
* @throws KerberosKDCConnectionException if a connection to the KDC cannot be made
* @throws KerberosAdminAuthenticationException if the administrator credentials fail to authenticate
* @throws KerberosRealmException if the realm does not map to a KDC
* @throws KerberosOperationException if an unexpected error occurred
*/
protected LdapContext createLdapContext() throws KerberosOperationException {
PrincipalKeyCredential administratorCredential = getAdministratorCredential();
Properties properties = new Properties();
properties.put(Context.INITIAL_CONTEXT_FACTORY, LDAP_CONTEXT_FACTORY_CLASS);
properties.put(Context.PROVIDER_URL, ldapUrl);
properties.put(Context.SECURITY_PRINCIPAL, administratorCredential.getPrincipal());
properties.put(Context.SECURITY_CREDENTIALS, String.valueOf(administratorCredential.getKey()));
properties.put(Context.SECURITY_AUTHENTICATION, "simple");
properties.put(Context.REFERRAL, "follow");
properties.put("java.naming.ldap.factory.socket", TrustingSSLSocketFactory.class.getName());
try {
return createInitialLdapContext(properties, null);
} catch (CommunicationException e) {
String message = String.format("Failed to communicate with the Active Directory at %s: %s", ldapUrl, e.getMessage());
LOG.warn(message, e);
throw new KerberosKDCConnectionException(message, e);
} catch (AuthenticationException e) {
String message = String.format("Failed to authenticate with the Active Directory at %s: %s", ldapUrl, e.getMessage());
LOG.warn(message, e);
throw new KerberosAdminAuthenticationException(message, e);
} catch (NamingException e) {
String error = e.getMessage();
if (StringUtils.isEmpty(error)) {
String message = String.format("Failed to communicate with the Active Directory at %s: %s", ldapUrl, e.getMessage());
LOG.warn(message, e);
if (error.startsWith("Cannot parse url:")) {
throw new KerberosKDCConnectionException(message, e);
} else {
throw new KerberosOperationException(message, e);
}
} else {
throw new KerberosOperationException("Unexpected error condition", e);
}
}
}
/**
* Helper method to create the LDAP context needed to interact with the Active Directory.
* <p/>
* This is mainly used to help with building mocks for test cases.
*
* @param properties environment used to create the initial DirContext.
* Null indicates an empty environment.
* @param controls connection request controls for the initial context.
* If null, no connection request controls are used.
* @return the relevant LdapContext
* @throws NamingException if a naming exception is encountered
*/
protected LdapContext createInitialLdapContext(Properties properties, Control[] controls)
throws NamingException {
return new InitialLdapContext(properties, controls);
}
/**
* Helper method to create the SearchControls instance
*
* @return the relevant SearchControls
*/
protected SearchControls createSearchControls() {
SearchControls searchControls = new SearchControls();
searchControls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
searchControls.setReturningAttributes(new String[]{"cn"});
return searchControls;
}
/**
* Processes a Velocity template to generate a map of attributes and values to use to create
* Active Directory accounts.
* <p/>
* If a template was not set, a default template will be used.
*
* @param context a map of properties to pass to the Velocity engine
* @return a Map of attribute names and values to use for creating an Active Directory account
* @throws KerberosOperationException if an error occurs processing the template.
*/
protected Map<String, Object> processCreateTemplate(Map<String, Object> context)
throws KerberosOperationException {
if (gson == null) {
throw new KerberosOperationException("The JSON parser must not be null");
}
Map<String, Object> data = null;
String template;
StringWriter stringWriter = new StringWriter();
if (StringUtils.isEmpty(createTemplate)) {
template = "{" +
"\"objectClass\": [\"top\", \"person\", \"organizationalPerson\", \"user\"]," +
"\"cn\": \"$principal_name\"," +
"#if( $is_service )" +
" \"servicePrincipalName\": \"$principal_name\"," +
"#end" +
"\"userPrincipalName\": \"$normalized_principal\"," +
"\"unicodePwd\": \"$password\"," +
"\"accountExpires\": \"0\"," +
"\"userAccountControl\": \"66048\"" +
"}";
} else {
template = createTemplate;
}
try {
if (Velocity.evaluate(new VelocityContext(context), stringWriter, "Active Directory principal create template", template)) {
String json = stringWriter.toString();
Type type = new TypeToken<Map<String, Object>>() {
}.getType();
data = gson.fromJson(json, type);
}
} catch (ParseErrorException e) {
LOG.warn("Failed to parse Active Directory create principal template", e);
throw new KerberosOperationException("Failed to parse Active Directory create principal template", e);
} catch (MethodInvocationException e) {
LOG.warn("Failed to process Active Directory create principal template", e);
throw new KerberosOperationException("Failed to process Active Directory create principal template", e);
} catch (ResourceNotFoundException e) {
LOG.warn("Failed to process Active Directory create principal template", e);
throw new KerberosOperationException("Failed to process Active Directory create principal template", e);
}
return data;
}
private String findPrincipalDN(String normalizedPrincipal) throws NamingException, KerberosOperationException {
String dn = null;
if (normalizedPrincipal != null) {
NamingEnumeration<SearchResult> results = null;
try {
results = ldapContext.search(
principalContainerLdapName,
String.format("(userPrincipalName=%s)", normalizedPrincipal),
searchControls
);
if ((results != null) && results.hasMore()) {
SearchResult result = results.next();
dn = result.getNameInNamespace();
}
} finally {
try {
if (results != null) {
results.close();
}
} catch (NamingException ne) {
// ignore, we can not do anything about it
}
}
}
return dn;
}
}