AMBARI-9209. Add the ability to append a random value to values in LDAP attributes when generating principals in Active Directory (rlevas)
Project: http://git-wip-us.apache.org/repos/asf/ambari/repo Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/9f291484 Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/9f291484 Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/9f291484 Branch: refs/heads/2.0-preview Commit: 9f291484ae3764975e3f3f2c616288454bca41c5 Parents: ae82067 Author: Robert Levas <rle...@hortonworks.com> Authored: Wed Jan 21 11:20:30 2015 -0500 Committer: Yusaku Sako <yus...@hortonworks.com> Committed: Wed Jan 21 12:21:26 2015 -0800 ---------------------------------------------------------------------- .../kerberos/ADKerberosOperationHandler.java | 161 +++++----- .../kerberos/DeconstructedPrincipal.java | 201 +++++++++++++ .../kerberos/KerberosOperationHandler.java | 10 +- .../1.10.3-10/configuration/kerberos-env.xml | 16 +- .../ADKerberosOperationHandlerTest.java | 301 +++++++++++-------- .../kerberos/DeconstructedPrincipalTest.java | 113 +++++++ 6 files changed, 582 insertions(+), 220 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/ambari/blob/9f291484/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/ADKerberosOperationHandler.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/ADKerberosOperationHandler.java b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/ADKerberosOperationHandler.java index 20f7e60..b5de64f 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/ADKerberosOperationHandler.java +++ b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/ADKerberosOperationHandler.java @@ -21,7 +21,7 @@ package org.apache.ambari.server.serveraction.kerberos; import com.google.common.reflect.TypeToken; import com.google.gson.Gson; -import org.apache.ambari.server.utils.StageUtils; +import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.velocity.VelocityContext; @@ -42,8 +42,6 @@ import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.Properties; -import java.util.regex.Matcher; -import java.util.regex.Pattern; /** * Implementation of <code>KerberosOperationHandler</code> to created principal in Active Directory @@ -52,15 +50,6 @@ public class ADKerberosOperationHandler extends KerberosOperationHandler { private static Log LOG = LogFactory.getLog(ADKerberosOperationHandler.class); - /** - * Regular expression to parse the different principal formats: - * primary/instance@REALM - * primary@REALM - * primary/instance - * primary - */ - private static Pattern PATTERN_PRINCIPAL = Pattern.compile("^(([^ /@]+)(?:/([^ /@]+))?)(?:@(.+)?)?$"); - private static final String LDAP_CONTEXT_FACTORY_CLASS = "com.sun.jndi.ldap.LdapCtxFactory"; public final static String KERBEROS_ENV_LDAP_URL = "ldap_url"; @@ -213,27 +202,14 @@ public class ADKerberosOperationHandler extends KerberosOperationHandler { if (principal == null) { throw new KerberosOperationException("principal is null"); } - NamingEnumeration<SearchResult> searchResultEnum = null; + + DeconstructedPrincipal deconstructPrincipal = deconstructPrincipal(principal); + try { - searchResultEnum = ldapContext.search( - principalContainerDn, - "(userPrincipalName=" + principal + ")", - searchControls); - if (searchResultEnum.hasMore()) { - return true; - } + return (findPrincipalDN(deconstructPrincipal.getNormalizedPrincipal()) != null); } catch (NamingException ne) { throw new KerberosOperationException("can not check if principal exists: " + principal, ne); - } finally { - try { - if (searchResultEnum != null) { - searchResultEnum.close(); - } - } catch (NamingException ne) { - // ignore, we can not do anything about it - } } - return false; } /** @@ -262,31 +238,24 @@ public class ADKerberosOperationHandler extends KerberosOperationHandler { } // TODO: (rlevas) pass components and realm in separately (AMBARI-9122) - String realm = null; - String principal_primary = null; - String principal_instance = null; - - Matcher matcher = PATTERN_PRINCIPAL.matcher(principal); - if (matcher.matches()) { - principal = matcher.group(1); - principal_primary = matcher.group(2); - principal_instance = matcher.group(3); - realm = matcher.group(4); - } + DeconstructedPrincipal deconstructedPrincipal = deconstructPrincipal(principal); - if ((realm == null) || realm.isEmpty()) { - realm = getDefaultRealm(); + String realm = deconstructedPrincipal.getRealm(); + if (realm == null) { + realm = ""; } Map<String, Object> context = new HashMap<String, Object>(); - context.put("principal", principal); - context.put("principal_primary", principal_primary); - context.put("principal_instance", principal_instance); + 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 == null) ? null : realm.toLowerCase()); + 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())); Map<String, Object> data = processCreateTemplate(context); @@ -300,13 +269,11 @@ public class ADKerberosOperationHandler extends KerberosOperationHandler { if ("unicodePwd".equals(key)) { if (value instanceof String) { - Attribute passwordAttr = new BasicAttribute("unicodePwd"); // password try { - passwordAttr.add(((String) value).getBytes("UTF-16LE")); + 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); } - attributes.put(passwordAttr); } } else { Attribute attribute = new BasicAttribute(key); @@ -327,7 +294,7 @@ public class ADKerberosOperationHandler extends KerberosOperationHandler { } if (cn == null) { - cn = String.format("%s@%s", principal, realm); + cn = deconstructedPrincipal.getNormalizedPrincipal(); } try { Name name = new CompositeName().add(String.format("cn=%s,%s", cn, principalContainerDn)); @@ -359,26 +326,27 @@ public class ADKerberosOperationHandler extends KerberosOperationHandler { if (password == null) { throw new KerberosOperationException("principal password is null"); } + + DeconstructedPrincipal deconstructPrincipal = deconstructPrincipal(principal); + try { - if (!principalExists(principal)) { - throw new KerberosOperationException("principal not found : " + principal); + String dn = findPrincipalDN(deconstructPrincipal.getNormalizedPrincipal()); + + if (dn != null) { + ldapContext.modifyAttributes(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 (KerberosOperationException e) { - e.printStackTrace(); - } - try { - ModificationItem[] mods = new ModificationItem[1]; - String quotedPasswordVal = "\"" + password + "\""; - mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, - new BasicAttribute("UnicodePwd", quotedPasswordVal.getBytes("UTF-16LE"))); - ldapContext.modifyAttributes( - new CompositeName().add("cn=" + principal + "," + principalContainerDn), - mods); - } catch (NamingException ne) { - throw new KerberosOperationException("Can not set password for principal : " + principal, ne); - } catch (UnsupportedEncodingException ue) { - throw new KerberosOperationException("Unsupported encoding UTF-16LE", ue); + } 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; } @@ -399,18 +367,17 @@ public class ADKerberosOperationHandler extends KerberosOperationHandler { if (principal == null) { throw new KerberosOperationException("principal is null"); } + + DeconstructedPrincipal deconstructPrincipal = deconstructPrincipal(principal); + try { - if (!principalExists(principal)) { - return false; + String dn = findPrincipalDN(deconstructPrincipal.getNormalizedPrincipal()); + + if (dn != null) { + ldapContext.destroySubcontext(dn); } - } catch (KerberosOperationException e) { - e.printStackTrace(); - } - try { - Name name = new CompositeName().add("cn=" + principal + "," + principalContainerDn); - ldapContext.destroySubcontext(name); - } catch (NamingException ne) { - throw new KerberosOperationException("Can not remove principal: " + principal); + } catch (NamingException e) { + throw new KerberosOperationException(String.format("Can not remove principal %s: %s", principal, e.getMessage()), e); } return true; @@ -531,14 +498,14 @@ public class ADKerberosOperationHandler extends KerberosOperationHandler { if ((createTemplate == null) || createTemplate.isEmpty()) { template = "{" + "\"objectClass\": [\"top\", \"person\", \"organizationalPerson\", \"user\"]," + - "\"cn\": \"$principal\"," + + "\"cn\": \"$principal_name\"," + "#if( $is_service )" + - " \"servicePrincipalName\": \"$principal\"," + + " \"servicePrincipalName\": \"$principal_name\"," + "#end" + - "\"userPrincipalName\": \"$principal@$realm.toLowerCase()\"," + - "\"unicodePwd\": \"\\\"$password\\\"\"," + + "\"userPrincipalName\": \"$normalized_principal.toLowerCase()\"," + + "\"unicodePwd\": \"$password\"," + "\"accountExpires\": \"0\"," + - "\"userAccountControl\": \"512\"" + + "\"userAccountControl\": \"66048\"" + "}"; } else { template = createTemplate; @@ -566,4 +533,34 @@ public class ADKerberosOperationHandler extends KerberosOperationHandler { return data; } + private String findPrincipalDN(String normalizedPrincipal) throws NamingException, KerberosOperationException { + String dn = null; + + if (normalizedPrincipal != null) { + NamingEnumeration<SearchResult> results = null; + + try { + results = ldapContext.search( + principalContainerDn, + 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; + } } \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ambari/blob/9f291484/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/DeconstructedPrincipal.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/DeconstructedPrincipal.java b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/DeconstructedPrincipal.java new file mode 100644 index 0000000..f5d8156 --- /dev/null +++ b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/DeconstructedPrincipal.java @@ -0,0 +1,201 @@ +/* + * 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 javax.annotation.Nullable; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * DeconstructedPrincipal manages the different parts of a principal and can be used to get a + * normalized principal value + * <p/> + * A "normalized" principal has the following forms: + * <ul> + * <li>primary/instance@realm</li> + * <li>primary@realm</li> + * </ul> + * <p/> + * This class will create a DeconstructedPrincipal from a String containing a principal using + * {@link DeconstructedPrincipal#valueOf(String, String)} + */ +class DeconstructedPrincipal { + /** + * Regular expression to parse the different principal formats: + * <ul> + * <li>primary/instance@REALM</li> + * <li>primary@REALM</li> + * <li>primary/instance</li> + * <li>primary</li> + * </ul> + */ + private static Pattern PATTERN_PRINCIPAL = Pattern.compile("^([^ /@]+)(?:/([^ /@]+))?(?:@(.+)?)?$"); + + /** + * A String containing the "primary" component of a principal + */ + private final String primary; + + /** + * A String containing the "instance" component of a principal + */ + private final String instance; + + /** + * A String containing the "realm" component of a principal + */ + private final String realm; + + /** + * A String containing the principal name portion of the principal. + * The principal name is the combination of the primary and instance components. + * This value is generated using the primary, instance, and realm components. + */ + private final String principalName; + + /** + * A String containing the complete normalized principal + * The normalized principal is the combination of the primary, instance, and realm components. + * This value is generated using the primary, instance, and realm components. + */ + private final String normalizedPrincipal; + + /** + * Given a principal and a default realm, creates a new DeconstructedPrincipal + * <p/> + * If the supplied principal does not have a realm component, the default realm (supplied) will be + * used. + * + * @param principal a String containing the principal to deconstruct + * @param defaultRealm a String containing the default realm + * @return a new DeconstructedPrincipal + */ + public static DeconstructedPrincipal valueOf(String principal, @Nullable String defaultRealm) { + if (principal == null) { + throw new IllegalArgumentException("The principal may not be null"); + } + + Matcher matcher = PATTERN_PRINCIPAL.matcher(principal); + + if (matcher.matches()) { + String primary = matcher.group(1); + String instance = matcher.group(2); + String realm = matcher.group(3); + + if ((realm == null) || realm.isEmpty()) { + realm = defaultRealm; + } + + return new DeconstructedPrincipal(primary, instance, realm); + } else { + throw new IllegalArgumentException(String.format("Invalid principal value: %s", principal)); + } + } + + + /** + * Constructs a new DeconstructedPrincipal + * + * @param primary a String containing the "primary" component of the principal + * @param instance a String containing the "instance" component of the principal + * @param realm a String containing the "realm" component of the principal + */ + protected DeconstructedPrincipal(String primary, String instance, String realm) { + this.primary = primary; + this.instance = instance; + this.realm = realm; + + StringBuilder builder = new StringBuilder(); + + if (this.primary != null) { + builder.append(primary); + } + + if (this.instance != null) { + builder.append('/'); + builder.append(this.instance); + } + + this.principalName = builder.toString(); + + if (this.realm != null) { + builder.append('@'); + builder.append(this.realm); + } + + this.normalizedPrincipal = builder.toString(); + } + + /** + * Gets the primary component of this DeconstructedPrincipal + * + * @return a String containing the "primary" component of this DeconstructedPrincipal + */ + public String getPrimary() { + return primary; + } + + /** + * Gets the instance component of this DeconstructedPrincipal + * + * @return a String containing the "instance" component of this DeconstructedPrincipal + */ + public String getInstance() { + return instance; + } + + /** + * Gets the realm component of this DeconstructedPrincipal + * + * @return a String containing the "realm" component of this DeconstructedPrincipal + */ + public String getRealm() { + return realm; + } + + /** + * Gets the constructed principal name for this DeconstructedPrincipal + * <p/> + * The principal name is the combination of the primary and instance components: + * <ul> + * <li>primary/instance</li> + * <li>primary</li> + * </ul> + * + * @return a String containing the "realm" component of this DeconstructedPrincipal + */ + public String getPrincipalName() { + return principalName; + } + + /** + * Gets the constructed normalized principal for this DeconstructedPrincipal + * <p/> + * The normalized principal is the combination of the primary, instance, and realm components: + * <ul> + * <li>primary/instance@realm</li> + * <li>primary@realm</li> + * </ul> + * + * @return a String containing the "realm" component of this DeconstructedPrincipal + */ + public String getNormalizedPrincipal() { + return normalizedPrincipal; + } +} http://git-wip-us.apache.org/repos/asf/ambari/blob/9f291484/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/KerberosOperationHandler.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/KerberosOperationHandler.java b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/KerberosOperationHandler.java index 7a9233b..a23aa81 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/KerberosOperationHandler.java +++ b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/kerberos/KerberosOperationHandler.java @@ -63,7 +63,6 @@ public abstract class KerberosOperationHandler { private final static char[] SECURE_PASSWORD_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890?.!$%^*()-_+=~".toCharArray(); - /** * The default set of ciphers to use for creating keytab entries */ @@ -432,4 +431,13 @@ public abstract class KerberosOperationHandler { } } } + + protected DeconstructedPrincipal deconstructPrincipal(String principal) throws KerberosOperationException { + try { + return DeconstructedPrincipal.valueOf(principal, getDefaultRealm()); + } catch (IllegalArgumentException e) { + throw new KerberosOperationException(e.getMessage(), e); + } + } + } http://git-wip-us.apache.org/repos/asf/ambari/blob/9f291484/ambari-server/src/main/resources/common-services/KERBEROS/1.10.3-10/configuration/kerberos-env.xml ---------------------------------------------------------------------- diff --git a/ambari-server/src/main/resources/common-services/KERBEROS/1.10.3-10/configuration/kerberos-env.xml b/ambari-server/src/main/resources/common-services/KERBEROS/1.10.3-10/configuration/kerberos-env.xml index 85ae018..d37e736 100644 --- a/ambari-server/src/main/resources/common-services/KERBEROS/1.10.3-10/configuration/kerberos-env.xml +++ b/ambari-server/src/main/resources/common-services/KERBEROS/1.10.3-10/configuration/kerberos-env.xml @@ -40,19 +40,23 @@ <property require-input="true"> <name>create_attributes_template</name> <description> - Customizable JSON document representing the LDAP attributes needed to create a new Kerberos entity in the KDC (Velocity template engine). + A Velocity template to use to generate a JSON-formatted document containing the set of + attribute names and values needed to create a new Kerberos identity in the relevant KDC. + Variables include: + principal_name, principal_primary, principal_instance, realm, realm_lowercase, + normalized_principal, principal digest, password, is_service, container_dn </description> <value> { "objectClass": ["top", "person", "organizationalPerson", "user"], - "cn": "$principal", + "cn": "$principal_name", #if( $is_service ) - "servicePrincipalName": "$principal", + "servicePrincipalName": "$principal_name", #end - "userPrincipalName": "$principal@$realm.toLowerCase()", - "unicodePwd": "\"$password\"", + "userPrincipalName": "$normalized_principal.toLowerCase()", + "unicodePwd": "$password", "accountExpires": "0", - "userAccountControl": "512" + "userAccountControl": "66048" } </value> </property> http://git-wip-us.apache.org/repos/asf/ambari/blob/9f291484/ambari-server/src/test/java/org/apache/ambari/server/serveraction/kerberos/ADKerberosOperationHandlerTest.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/test/java/org/apache/ambari/server/serveraction/kerberos/ADKerberosOperationHandlerTest.java b/ambari-server/src/test/java/org/apache/ambari/server/serveraction/kerberos/ADKerberosOperationHandlerTest.java index 6a89dbb..8d2a3c4 100644 --- a/ambari-server/src/test/java/org/apache/ambari/server/serveraction/kerberos/ADKerberosOperationHandlerTest.java +++ b/ambari-server/src/test/java/org/apache/ambari/server/serveraction/kerberos/ADKerberosOperationHandlerTest.java @@ -19,6 +19,8 @@ package org.apache.ambari.server.serveraction.kerberos; import junit.framework.Assert; +import org.easymock.Capture; +import org.easymock.CaptureType; import org.easymock.EasyMockSupport; import org.easymock.IAnswer; import org.junit.Ignore; @@ -26,21 +28,22 @@ import org.junit.Test; import javax.naming.AuthenticationException; import javax.naming.CommunicationException; +import javax.naming.Name; import javax.naming.NamingEnumeration; +import javax.naming.directory.Attributes; +import javax.naming.directory.DirContext; import javax.naming.directory.SearchControls; import javax.naming.directory.SearchResult; import javax.naming.ldap.Control; import javax.naming.ldap.LdapContext; -import java.util.ArrayList; -import java.util.Arrays; +import java.nio.charset.Charset; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Properties; -import static org.easymock.EasyMock.anyObject; -import static org.easymock.EasyMock.expect; -import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.*; public class ADKerberosOperationHandlerTest extends EasyMockSupport { private static final String DEFAULT_ADMIN_PRINCIPAL = "cluser_admin@HDP01.LOCAL"; @@ -235,32 +238,30 @@ public class ADKerberosOperationHandlerTest extends EasyMockSupport { } }; + Capture<Name> capturedName = new Capture<Name>(CaptureType.ALL); + Capture<Attributes> capturedAttributes = new Capture<Attributes>(CaptureType.ALL); + ADKerberosOperationHandler handler = createMockBuilder(ADKerberosOperationHandler.class) .addMockedMethod(ADKerberosOperationHandler.class.getDeclaredMethod("createInitialLdapContext", Properties.class, Control[].class)) .addMockedMethod(ADKerberosOperationHandler.class.getDeclaredMethod("createSearchControls")) .createNiceMock(); + NamingEnumeration<SearchResult> searchResult = createNiceMock(NamingEnumeration.class); + expect(searchResult.hasMore()).andReturn(false).once(); + + LdapContext ldapContext = createNiceMock(LdapContext.class); + expect(ldapContext.search(anyObject(String.class), anyObject(String.class), anyObject(SearchControls.class))) + .andReturn(searchResult) + .once(); + + expect(ldapContext.createSubcontext(capture(capturedName), capture(capturedAttributes))) + .andReturn(createNiceMock(DirContext.class)) + .anyTimes(); + expect(handler.createInitialLdapContext(anyObject(Properties.class), anyObject(Control[].class))) - .andAnswer(new IAnswer<LdapContext>() { - @Override - public LdapContext answer() throws Throwable { - LdapContext ldapContext = createNiceMock(LdapContext.class); - expect(ldapContext.search(anyObject(String.class), anyObject(String.class), anyObject(SearchControls.class))) - .andAnswer(new IAnswer<NamingEnumeration<SearchResult>>() { - @Override - public NamingEnumeration<SearchResult> answer() throws Throwable { - NamingEnumeration<SearchResult> result = createNiceMock(NamingEnumeration.class); - expect(result.hasMore()).andReturn(false).once(); - replay(result); - return result; - } - }) - .once(); - replay(ldapContext); - return ldapContext; - } - }) + .andReturn(ldapContext) .once(); + expect(handler.createSearchControls()).andAnswer(new IAnswer<SearchControls>() { @Override public SearchControls answer() throws Throwable { @@ -273,45 +274,67 @@ public class ADKerberosOperationHandlerTest extends EasyMockSupport { replayAll(); handler.open(kc, DEFAULT_REALM, kerberosEnvMap); + handler.createPrincipal("nn/c6501.ambari.apache.org", "secret", true); + handler.createPrincipal("hdfs@" + DEFAULT_REALM, "secret", false); + handler.close(); - Map<String, Object> context = new HashMap<String, Object>(); - context.put("principal", "nn/c6501.ambari.apache.org"); - context.put("principal_primary", "nn"); - context.put("principal_instance", "c6501.ambari.apache.org"); - context.put("realm", "EXAMPLE.COM"); - context.put("realm_lowercase", "example.com"); - context.put("password", "secret"); - context.put("is_service", true); - context.put("container_dn", "ou=cluster,DC=EXAMPLE,DC=COM"); - - Map<String, Object> data; - - data = handler.processCreateTemplate(context); - - Assert.assertNotNull(data); - Assert.assertEquals(7, data.size()); - Assert.assertEquals(new ArrayList<String>(Arrays.asList("top", "person", "organizationalPerson", "user")), data.get("objectClass")); - Assert.assertEquals("nn/c6501.ambari.apache.org", data.get("cn")); - Assert.assertEquals("nn/c6501.ambari.apache.org", data.get("servicePrincipalName")); - Assert.assertEquals("nn/c6501.ambari.apache....@example.com", data.get("userPrincipalName")); - Assert.assertEquals("\"secret\"", data.get("unicodePwd")); - Assert.assertEquals("0", data.get("accountExpires")); - Assert.assertEquals("512", data.get("userAccountControl")); - - - context.put("is_service", false); - data = handler.processCreateTemplate(context); - - Assert.assertNotNull(data); - Assert.assertEquals(6, data.size()); - Assert.assertEquals(new ArrayList<String>(Arrays.asList("top", "person", "organizationalPerson", "user")), data.get("objectClass")); - Assert.assertEquals("nn/c6501.ambari.apache.org", data.get("cn")); - Assert.assertEquals("nn/c6501.ambari.apache....@example.com", data.get("userPrincipalName")); - Assert.assertEquals("\"secret\"", data.get("unicodePwd")); - Assert.assertEquals("0", data.get("accountExpires")); - Assert.assertEquals("512", data.get("userAccountControl")); + List<Attributes> attributesList = capturedAttributes.getValues(); + Attributes attributes; - handler.close(); + attributes = attributesList.get(0); + String[] objectClasses = new String[]{"top", "person", "organizationalPerson", "user"}; + + Assert.assertNotNull(attributes); + Assert.assertEquals(7, attributes.size()); + + Assert.assertNotNull(attributes.get("objectClass")); + Assert.assertEquals(objectClasses.length, attributes.get("objectClass").size()); + for (int i = 0; i < objectClasses.length; i++) { + Assert.assertEquals(objectClasses[i], attributes.get("objectClass").get(i)); + } + + Assert.assertNotNull(attributes.get("cn")); + Assert.assertEquals("nn/c6501.ambari.apache.org", attributes.get("cn").get()); + + Assert.assertNotNull(attributes.get("servicePrincipalName")); + Assert.assertEquals("nn/c6501.ambari.apache.org", attributes.get("servicePrincipalName").get()); + + Assert.assertNotNull(attributes.get("userPrincipalName")); + Assert.assertEquals("nn/c6501.ambari.apache.org@hdp01.local", attributes.get("userPrincipalName").get()); + + Assert.assertNotNull(attributes.get("unicodePwd")); + Assert.assertEquals("\"secret\"", new String((byte[]) attributes.get("unicodePwd").get(), Charset.forName("UTF-16LE"))); + + Assert.assertNotNull(attributes.get("accountExpires")); + Assert.assertEquals("0", attributes.get("accountExpires").get()); + + Assert.assertNotNull(attributes.get("userAccountControl")); + Assert.assertEquals("66048", attributes.get("userAccountControl").get()); + + attributes = attributesList.get(1); + Assert.assertNotNull(attributes); + Assert.assertEquals(6, attributes.size()); + + Assert.assertNotNull(attributes.get("objectClass")); + Assert.assertEquals(objectClasses.length, attributes.get("objectClass").size()); + for (int i = 0; i < objectClasses.length; i++) { + Assert.assertEquals(objectClasses[i], attributes.get("objectClass").get(i)); + } + + Assert.assertNotNull(attributes.get("cn")); + Assert.assertEquals("hdfs", attributes.get("cn").get()); + + Assert.assertNotNull(attributes.get("userPrincipalName")); + Assert.assertEquals("hdfs@hdp01.local", attributes.get("userPrincipalName").get()); + + Assert.assertNotNull(attributes.get("unicodePwd")); + Assert.assertEquals("\"secret\"", new String((byte[]) attributes.get("unicodePwd").get(), Charset.forName("UTF-16LE"))); + + Assert.assertNotNull(attributes.get("accountExpires")); + Assert.assertEquals("0", attributes.get("accountExpires").get()); + + Assert.assertNotNull(attributes.get("userAccountControl")); + Assert.assertEquals("66048", attributes.get("userAccountControl").get()); } @Test @@ -321,54 +344,52 @@ public class ADKerberosOperationHandlerTest extends EasyMockSupport { { put(ADKerberosOperationHandler.KERBEROS_ENV_LDAP_URL, DEFAULT_LDAP_URL); put(ADKerberosOperationHandler.KERBEROS_ENV_PRINCIPAL_CONTAINER_DN, DEFAULT_PRINCIPAL_CONTAINER_DN); - put(ADKerberosOperationHandler.KERBEROS_ENV_CREATE_ATTRIBUTES_TEMPLATE, "{" + + put(ADKerberosOperationHandler.KERBEROS_ENV_CREATE_ATTRIBUTES_TEMPLATE, "" + + "#set( $user = \"${principal_primary}-${principal_digest}\" )" + + "{" + " \"objectClass\": [" + " \"top\"," + " \"person\"," + " \"organizationalPerson\"," + " \"user\"" + " ]," + - " \"cn\": \"$principal@$realm\"," + - " \"dn\": \"$principal@$realm,$container_dn\"," + - " \"distinguishedName\": \"$principal@$realm,$container_dn\"," + - " \"sAMAccountName\": \"$principal\"," + + " \"cn\": \"$user\"," + + " \"sAMAccountName\": \"$user.substring(0,20)\"," + " #if( $is_service )" + - " \"servicePrincipalName\": \"$principal\"," + + " \"servicePrincipalName\": \"$principal_name\"," + " #end" + - " \"userPrincipalName\": \"$principal@$realm.toLowerCase()\"," + - " \"unicodePwd\": \"`$password`\"," + + " \"userPrincipalName\": \"$normalized_principal.toLowerCase()\"," + + " \"unicodePwd\": \"$password\"," + " \"accountExpires\": \"0\"," + " \"userAccountControl\": \"66048\"" + "}"); } }; + Capture<Name> capturedName = new Capture<Name>(); + Capture<Attributes> capturedAttributes = new Capture<Attributes>(); + ADKerberosOperationHandler handler = createMockBuilder(ADKerberosOperationHandler.class) .addMockedMethod(ADKerberosOperationHandler.class.getDeclaredMethod("createInitialLdapContext", Properties.class, Control[].class)) .addMockedMethod(ADKerberosOperationHandler.class.getDeclaredMethod("createSearchControls")) .createNiceMock(); + NamingEnumeration<SearchResult> searchResult = createNiceMock(NamingEnumeration.class); + expect(searchResult.hasMore()).andReturn(false).once(); + + LdapContext ldapContext = createNiceMock(LdapContext.class); + expect(ldapContext.search(anyObject(String.class), anyObject(String.class), anyObject(SearchControls.class))) + .andReturn(searchResult) + .once(); + + expect(ldapContext.createSubcontext(capture(capturedName), capture(capturedAttributes))) + .andReturn(createNiceMock(DirContext.class)) + .once(); + expect(handler.createInitialLdapContext(anyObject(Properties.class), anyObject(Control[].class))) - .andAnswer(new IAnswer<LdapContext>() { - @Override - public LdapContext answer() throws Throwable { - LdapContext ldapContext = createNiceMock(LdapContext.class); - expect(ldapContext.search(anyObject(String.class), anyObject(String.class), anyObject(SearchControls.class))) - .andAnswer(new IAnswer<NamingEnumeration<SearchResult>>() { - @Override - public NamingEnumeration<SearchResult> answer() throws Throwable { - NamingEnumeration<SearchResult> result = createNiceMock(NamingEnumeration.class); - expect(result.hasMore()).andReturn(false).once(); - replay(result); - return result; - } - }) - .once(); - replay(ldapContext); - return ldapContext; - } - }) + .andReturn(ldapContext) .once(); + expect(handler.createSearchControls()).andAnswer(new IAnswer<SearchControls>() { @Override public SearchControls answer() throws Throwable { @@ -381,34 +402,43 @@ public class ADKerberosOperationHandlerTest extends EasyMockSupport { replayAll(); handler.open(kc, DEFAULT_REALM, kerberosEnvMap); + handler.createPrincipal("nn/c6501.ambari.apache.org", "secret", true); + handler.close(); + Attributes attributes = capturedAttributes.getValue(); + String[] objectClasses = new String[]{"top", "person", "organizationalPerson", "user"}; - Map<String, Object> context = new HashMap<String, Object>(); - context.put("principal", "nn/c6501.ambari.apache.org"); - context.put("principal_primary", "nn"); - context.put("principal_instance", "c6501.ambari.apache.org"); - context.put("realm", "EXAMPLE.COM"); - context.put("realm_lowercase", "example.com"); - context.put("password", "secret"); - context.put("is_service", true); - context.put("container_dn", "ou=cluster,DC=EXAMPLE,DC=COM"); - - Map<String, Object> data = handler.processCreateTemplate(context); - - Assert.assertNotNull(data); - Assert.assertEquals(10, data.size()); - Assert.assertEquals(new ArrayList<String>(Arrays.asList("top", "person", "organizationalPerson", "user")), data.get("objectClass")); - Assert.assertEquals("nn/c6501.ambari.apache....@example.com", data.get("cn")); - Assert.assertEquals("nn/c6501.ambari.apache.org", data.get("servicePrincipalName")); - Assert.assertEquals("nn/c6501.ambari.apache....@example.com", data.get("userPrincipalName")); - Assert.assertEquals("nn/c6501.ambari.apache.org", data.get("sAMAccountName")); - Assert.assertEquals("nn/c6501.ambari.apache....@example.com,ou=cluster,DC=EXAMPLE,DC=COM", data.get("distinguishedName")); - Assert.assertEquals("nn/c6501.ambari.apache....@example.com,ou=cluster,DC=EXAMPLE,DC=COM", data.get("dn")); - Assert.assertEquals("`secret`", data.get("unicodePwd")); - Assert.assertEquals("0", data.get("accountExpires")); - Assert.assertEquals("66048", data.get("userAccountControl")); + Assert.assertNotNull(attributes); + Assert.assertEquals(8, attributes.size()); + + Assert.assertNotNull(attributes.get("objectClass")); + Assert.assertEquals(objectClasses.length, attributes.get("objectClass").size()); + for (int i = 0; i < objectClasses.length; i++) { + Assert.assertEquals(objectClasses[i], attributes.get("objectClass").get(i)); + } + + Assert.assertNotNull(attributes.get("cn")); + Assert.assertEquals("nn-995e1580db28198e7fda1417ab5d894c877937d2", attributes.get("cn").get()); + + Assert.assertNotNull(attributes.get("servicePrincipalName")); + Assert.assertEquals("nn/c6501.ambari.apache.org", attributes.get("servicePrincipalName").get()); + + Assert.assertNotNull(attributes.get("userPrincipalName")); + Assert.assertEquals("nn/c6501.ambari.apache.org@hdp01.local", attributes.get("userPrincipalName").get()); + + Assert.assertNotNull(attributes.get("sAMAccountName")); + Assert.assertTrue(attributes.get("sAMAccountName").get().toString().length() <= 20); + Assert.assertEquals("nn-995e1580db28198e7", attributes.get("sAMAccountName").get()); + + Assert.assertNotNull(attributes.get("unicodePwd")); + Assert.assertEquals("\"secret\"", new String((byte[]) attributes.get("unicodePwd").get(), Charset.forName("UTF-16LE"))); + + Assert.assertNotNull(attributes.get("accountExpires")); + Assert.assertEquals("0", attributes.get("accountExpires").get()); + + Assert.assertNotNull(attributes.get("userAccountControl")); + Assert.assertEquals("66048", attributes.get("userAccountControl").get()); - handler.close(); } /** @@ -458,31 +488,40 @@ public class ADKerberosOperationHandlerTest extends EasyMockSupport { // does the principal already exist? System.out.println("Principal exists: " + handler.principalExists("nn/c1508.ambari.apache.org")); - //create principal -// handler.createPrincipal("nn/c1508.ambari.apache.org@" + DEFAULT_REALM, handler.createSecurePassword(), true); - handler.close(); - kerberosEnvMap.put(ADKerberosOperationHandler.KERBEROS_ENV_CREATE_ATTRIBUTES_TEMPLATE, "{" + - "\"objectClass\": [\"top\", \"person\", \"organizationalPerson\", \"user\"]," + - "\"distinguishedName\": \"CN=$principal@$realm,$container_dn\"," + - "#if( $is_service )" + - "\"servicePrincipalName\": \"$principal\"," + - "#end" + - "\"userPrincipalName\": \"$principal@$realm.toLowerCase()\"," + - "\"unicodePwd\": \"\\\"$password\\\"\"," + - "\"accountExpires\": \"0\"," + - "\"userAccountControl\": \"66048\"" + - "}"); + kerberosEnvMap.put(ADKerberosOperationHandler.KERBEROS_ENV_CREATE_ATTRIBUTES_TEMPLATE, + "#set( $user = \"${principal_primary}-${principal_digest}\" )" + + "{" + + " \"objectClass\": [" + + " \"top\"," + + " \"person\"," + + " \"organizationalPerson\"," + + " \"user\"" + + " ]," + + " \"cn\": \"$user\"," + + " \"sAMAccountName\": \"$user.substring(0,20)\"," + + " #if( $is_service )" + + " \"servicePrincipalName\": \"$principal_name\"," + + " #end" + + " \"userPrincipalName\": \"$normalized_principal.toLowerCase()\"," + + " \"unicodePwd\": \"$password\"," + + " \"accountExpires\": \"0\"," + + " \"userAccountControl\": \"66048\"" + + "}" + ); handler.open(credentials, realm, kerberosEnvMap); + + // remove the principal + handler.removePrincipal("abcdefg"); + handler.removePrincipal("abcdefg/c1509.ambari.apache.org@" + DEFAULT_REALM); + handler.createPrincipal("abcdefg/c1509.ambari.apache.org@" + DEFAULT_REALM, handler.createSecurePassword(), true); + handler.createPrincipal("abcdefg@" + DEFAULT_REALM, handler.createSecurePassword(), false); //update the password - handler.setPrincipalPassword("nn/c1508.ambari.apache.org", handler.createSecurePassword()); - - // remove the principal - // handler.removeServicePrincipal("nn/c1508.ambari.apache.org"); + handler.setPrincipalPassword("abcdefg/c1509.ambari.apache.org@" + DEFAULT_REALM, handler.createSecurePassword()); handler.close(); } http://git-wip-us.apache.org/repos/asf/ambari/blob/9f291484/ambari-server/src/test/java/org/apache/ambari/server/serveraction/kerberos/DeconstructedPrincipalTest.java ---------------------------------------------------------------------- diff --git a/ambari-server/src/test/java/org/apache/ambari/server/serveraction/kerberos/DeconstructedPrincipalTest.java b/ambari-server/src/test/java/org/apache/ambari/server/serveraction/kerberos/DeconstructedPrincipalTest.java new file mode 100644 index 0000000..28dd08a --- /dev/null +++ b/ambari-server/src/test/java/org/apache/ambari/server/serveraction/kerberos/DeconstructedPrincipalTest.java @@ -0,0 +1,113 @@ +/* + * 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 org.junit.Test; + +import static org.junit.Assert.*; + +public class DeconstructedPrincipalTest { + + @Test(expected = IllegalArgumentException.class) + public void testNullPrincipal() throws Exception { + DeconstructedPrincipal.valueOf(null, null); + } + + @Test(expected = IllegalArgumentException.class) + public void testEmptyPrincipal() throws Exception { + DeconstructedPrincipal.valueOf("", null); + } + + @Test(expected = IllegalArgumentException.class) + public void testInvalidPrincipal() throws Exception { + DeconstructedPrincipal.valueOf("/invalid", null); + } + + @Test + public void testPrimary() throws Exception { + DeconstructedPrincipal deconstructedPrincipal = DeconstructedPrincipal.valueOf("primary", "REALM"); + + assertNotNull(deconstructedPrincipal); + assertEquals("primary", deconstructedPrincipal.getPrimary()); + assertNull(deconstructedPrincipal.getInstance()); + assertEquals("REALM", deconstructedPrincipal.getRealm()); + assertEquals("primary", deconstructedPrincipal.getPrincipalName()); + assertEquals("primary@REALM", deconstructedPrincipal.getNormalizedPrincipal()); + } + + @Test + public void testPrimaryRealm() throws Exception { + DeconstructedPrincipal deconstructedPrincipal = DeconstructedPrincipal.valueOf("primary@MYREALM", "REALM"); + + assertNotNull(deconstructedPrincipal); + assertEquals("primary", deconstructedPrincipal.getPrimary()); + assertNull(deconstructedPrincipal.getInstance()); + assertEquals("MYREALM", deconstructedPrincipal.getRealm()); + assertEquals("primary", deconstructedPrincipal.getPrincipalName()); + assertEquals("primary@MYREALM", deconstructedPrincipal.getNormalizedPrincipal()); + } + + @Test(expected = IllegalArgumentException.class) + public void testInstance() throws Exception { + DeconstructedPrincipal.valueOf("/instance", "REALM"); + } + + @Test(expected = IllegalArgumentException.class) + public void testInstanceRealm() throws Exception { + DeconstructedPrincipal.valueOf("/instance@MYREALM", "REALM"); + } + + @Test + public void testPrimaryInstance() throws Exception { + DeconstructedPrincipal deconstructedPrincipal = DeconstructedPrincipal.valueOf("primary/instance", "REALM"); + + assertNotNull(deconstructedPrincipal); + assertEquals("primary", deconstructedPrincipal.getPrimary()); + assertEquals("instance", deconstructedPrincipal.getInstance()); + assertEquals("instance", deconstructedPrincipal.getInstance()); + assertEquals("REALM", deconstructedPrincipal.getRealm()); + assertEquals("primary/instance", deconstructedPrincipal.getPrincipalName()); + assertEquals("primary/instance@REALM", deconstructedPrincipal.getNormalizedPrincipal()); + } + + @Test + public void testPrimaryInstanceRealm() throws Exception { + DeconstructedPrincipal deconstructedPrincipal = DeconstructedPrincipal.valueOf("primary/instance@MYREALM", "REALM"); + + assertNotNull(deconstructedPrincipal); + assertEquals("primary", deconstructedPrincipal.getPrimary()); + assertEquals("instance", deconstructedPrincipal.getInstance()); + assertEquals("MYREALM", deconstructedPrincipal.getRealm()); + assertEquals("primary/instance", deconstructedPrincipal.getPrincipalName()); + assertEquals("primary/instance@MYREALM", deconstructedPrincipal.getNormalizedPrincipal()); + } + + @Test + public void testOddCharacters() throws Exception { + DeconstructedPrincipal deconstructedPrincipal = DeconstructedPrincipal.valueOf("p_ri.ma-ry/i.n_s-tance@M_Y-REALM.COM", "REALM"); + + assertNotNull(deconstructedPrincipal); + assertEquals("p_ri.ma-ry", deconstructedPrincipal.getPrimary()); + assertEquals("i.n_s-tance", deconstructedPrincipal.getInstance()); + assertEquals("M_Y-REALM.COM", deconstructedPrincipal.getRealm()); + assertEquals("p_ri.ma-ry/i.n_s-tance", deconstructedPrincipal.getPrincipalName()); + assertEquals("p_ri.ma-ry/i.n_s-tance@M_Y-REALM.COM", deconstructedPrincipal.getNormalizedPrincipal()); + } + +} \ No newline at end of file