Browse Source

AMBARI-18587. Post user creation hook. (Laszlo Puskas via stoader)

Laszlo Puskas 9 years ago
parent
commit
a5fdae8022
37 changed files with 1875 additions and 85 deletions
  1. 8 0
      ambari-server/src/main/assemblies/server.xml
  2. 17 0
      ambari-server/src/main/java/org/apache/ambari/server/configuration/Configuration.java
  3. 29 13
      ambari-server/src/main/java/org/apache/ambari/server/controller/ControllerModule.java
  4. 11 1
      ambari-server/src/main/java/org/apache/ambari/server/events/AmbariEvent.java
  5. 33 0
      ambari-server/src/main/java/org/apache/ambari/server/hooks/AmbariEventFactory.java
  6. 26 0
      ambari-server/src/main/java/org/apache/ambari/server/hooks/HookContext.java
  7. 44 0
      ambari-server/src/main/java/org/apache/ambari/server/hooks/HookContextFactory.java
  8. 36 0
      ambari-server/src/main/java/org/apache/ambari/server/hooks/HookService.java
  9. 55 0
      ambari-server/src/main/java/org/apache/ambari/server/hooks/users/PostUserCreationHookContext.java
  10. 45 0
      ambari-server/src/main/java/org/apache/ambari/server/hooks/users/UserCreatedEvent.java
  11. 49 0
      ambari-server/src/main/java/org/apache/ambari/server/hooks/users/UserHookParams.java
  12. 279 0
      ambari-server/src/main/java/org/apache/ambari/server/hooks/users/UserHookService.java
  13. 85 34
      ambari-server/src/main/java/org/apache/ambari/server/security/authorization/Users.java
  14. 1 1
      ambari-server/src/main/java/org/apache/ambari/server/serveraction/AbstractServerAction.java
  15. 2 2
      ambari-server/src/main/java/org/apache/ambari/server/serveraction/ServerAction.java
  16. 46 0
      ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/CollectionPersisterService.java
  17. 24 0
      ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/CollectionPersisterServiceFactory.java
  18. 103 0
      ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/CsvFilePersisterService.java
  19. 163 0
      ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/PostUserCreationHookServerAction.java
  20. 26 0
      ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/ShellCommandCallableFactory.java
  21. 48 0
      ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/ShellCommandUtilityCallable.java
  22. 57 0
      ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/ShellCommandUtilityWrapper.java
  23. 17 8
      ambari-server/src/main/java/org/apache/ambari/server/topology/AsyncCallableService.java
  24. 1 1
      ambari-server/src/main/java/org/apache/ambari/server/utils/ShellCommandUtil.java
  25. 133 0
      ambari-server/src/main/resources/scripts/post-user-creation-hook.sh
  26. 4 0
      ambari-server/src/test/java/org/apache/ambari/server/controller/internal/ActiveWidgetLayoutResourceProviderTest.java
  27. 5 0
      ambari-server/src/test/java/org/apache/ambari/server/controller/internal/StackUpgradeConfigurationMergeTest.java
  28. 4 0
      ambari-server/src/test/java/org/apache/ambari/server/controller/internal/UserAuthorizationResourceProviderTest.java
  29. 4 0
      ambari-server/src/test/java/org/apache/ambari/server/controller/internal/UserResourceProviderTest.java
  30. 224 0
      ambari-server/src/test/java/org/apache/ambari/server/hooks/users/UserHookServiceTest.java
  31. 4 0
      ambari-server/src/test/java/org/apache/ambari/server/security/authorization/AmbariAuthorizationFilterTest.java
  32. 25 3
      ambari-server/src/test/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthenticationProviderForDNWithSpaceTest.java
  33. 10 0
      ambari-server/src/test/java/org/apache/ambari/server/security/authorization/UsersTest.java
  34. 182 0
      ambari-server/src/test/java/org/apache/ambari/server/serveraction/users/PostUserCreationHookServerActionTest.java
  35. 4 1
      ambari-server/src/test/java/org/apache/ambari/server/state/cluster/ClusterEffectiveVersionTest.java
  36. 38 21
      ambari-server/src/test/java/org/apache/ambari/server/topology/AsyncCallableServiceTest.java
  37. 33 0
      ambari-server/src/test/java/org/apache/ambari/server/upgrade/UpgradeCatalog240Test.java

+ 8 - 0
ambari-server/src/main/assemblies/server.xml

@@ -126,6 +126,9 @@
     <fileSet>
       <directory>src/main/resources/scripts</directory>
       <outputDirectory>/var/lib/ambari-server/resources/scripts</outputDirectory>
+      <excludes>
+        <exclude>post-user-creation-hook.sh</exclude>
+      </excludes>
     </fileSet>
     <fileSet>
       <directory>${ambari-admin-dir}/target</directory>
@@ -336,6 +339,11 @@
       <source>${basedir}/target/version</source>
       <outputDirectory>/var/lib/ambari-server/resources</outputDirectory>
     </file>
+    <file>
+      <fileMode>755</fileMode>
+      <source>src/main/resources/scripts/post-user-creation-hook.sh</source>
+      <outputDirectory>/var/lib/ambari-server/resources/scripts</outputDirectory>
+    </file>
   </files>    
   <dependencySets>
     <dependencySet>

+ 17 - 0
ambari-server/src/main/java/org/apache/ambari/server/configuration/Configuration.java

@@ -2408,6 +2408,14 @@ public class Configuration {
   public static final ConfigurationProperty<Boolean> ACTIVE_INSTANCE = new ConfigurationProperty<>(
           "active.instance", Boolean.TRUE);
 
+  @Markdown(description = "Indicates whether the post user creation is enabled or not. By default is false.")
+  public static final ConfigurationProperty<Boolean> POST_USER_CREATION_HOOK_ENABLED = new ConfigurationProperty<>(
+      "ambari.post.user.creation.hook.enabled", Boolean.FALSE);
+
+  @Markdown(description = "The location of the post user creation hook on the ambari server hosting machine.")
+  public static final ConfigurationProperty<String> POST_USER_CREATION_HOOK = new ConfigurationProperty<>(
+      "ambari.post.user.creation.hook", "/var/lib/ambari-server/resources/scripts/post-user-creation-hook.sh");
+
   /**
    * PropertyConfigurator checks log4j.properties file change every LOG4JMONITOR_DELAY milliseconds.
    */
@@ -5027,6 +5035,15 @@ public class Configuration {
     return Boolean.parseBoolean(getProperty(ACTIVE_INSTANCE));
   }
 
+  /**
+   * Indicates whether feature for user hook execution is enabled or not.
+   *
+   * @return true / false (defaults to false)
+   */
+  public boolean isUserHookEnabled() {
+    return Boolean.parseBoolean(getProperty(POST_USER_CREATION_HOOK_ENABLED));
+  }
+
   /**
    * @return the number of threads to use for parallel topology task creation if enabled
    */

+ 29 - 13
ambari-server/src/main/java/org/apache/ambari/server/controller/ControllerModule.java

@@ -62,6 +62,14 @@ import org.apache.ambari.server.controller.metrics.timeline.cache.TimelineMetric
 import org.apache.ambari.server.controller.metrics.timeline.cache.TimelineMetricCacheProvider;
 import org.apache.ambari.server.controller.spi.ResourceProvider;
 import org.apache.ambari.server.controller.utilities.KerberosChecker;
+import org.apache.ambari.server.events.AmbariEvent;
+import org.apache.ambari.server.hooks.AmbariEventFactory;
+import org.apache.ambari.server.hooks.HookContext;
+import org.apache.ambari.server.hooks.HookContextFactory;
+import org.apache.ambari.server.hooks.HookService;
+import org.apache.ambari.server.hooks.users.PostUserCreationHookContext;
+import org.apache.ambari.server.hooks.users.UserCreatedEvent;
+import org.apache.ambari.server.hooks.users.UserHookService;
 import org.apache.ambari.server.metadata.CachedRoleCommandOrderProvider;
 import org.apache.ambari.server.metadata.RoleCommandOrderProvider;
 import org.apache.ambari.server.notifications.DispatchFactory;
@@ -80,6 +88,10 @@ import org.apache.ambari.server.security.authorization.AuthorizationHelper;
 import org.apache.ambari.server.security.encryption.CredentialStoreService;
 import org.apache.ambari.server.security.encryption.CredentialStoreServiceImpl;
 import org.apache.ambari.server.serveraction.kerberos.KerberosOperationHandlerFactory;
+import org.apache.ambari.server.serveraction.users.CollectionPersisterService;
+import org.apache.ambari.server.serveraction.users.CollectionPersisterServiceFactory;
+import org.apache.ambari.server.serveraction.users.CsvFilePersisterService;
+import org.apache.ambari.server.serveraction.users.ShellCommandCallableFactory;
 import org.apache.ambari.server.stack.StackManagerFactory;
 import org.apache.ambari.server.stageplanner.RoleGraphFactory;
 import org.apache.ambari.server.state.Cluster;
@@ -343,14 +355,14 @@ public class ControllerModule extends AbstractModule {
 
     // Host role commands status summary max cache enable/disable
     bindConstant().annotatedWith(Names.named(HostRoleCommandDAO.HRC_STATUS_SUMMARY_CACHE_ENABLED)).
-      to(configuration.getHostRoleCommandStatusSummaryCacheEnabled());
+        to(configuration.getHostRoleCommandStatusSummaryCacheEnabled());
 
     // Host role commands status summary max cache size
     bindConstant().annotatedWith(Names.named(HostRoleCommandDAO.HRC_STATUS_SUMMARY_CACHE_SIZE)).
-      to(configuration.getHostRoleCommandStatusSummaryCacheSize());
+        to(configuration.getHostRoleCommandStatusSummaryCacheSize());
     // Host role command status summary cache expiry duration in minutes
     bindConstant().annotatedWith(Names.named(HostRoleCommandDAO.HRC_STATUS_SUMMARY_CACHE_EXPIRY_DURATION_MINUTES)).
-      to(configuration.getHostRoleCommandStatusSummaryCacheExpiryDuration());
+        to(configuration.getHostRoleCommandStatusSummaryCacheExpiryDuration());
 
     bind(AmbariManagementController.class).to(
         AmbariManagementControllerImpl.class);
@@ -374,6 +386,7 @@ public class ControllerModule extends AbstractModule {
     bindByAnnotation(null);
     bindNotificationDispatchers(null);
     registerUpgradeChecks(null);
+    bind(HookService.class).to(UserHookService.class);
   }
 
   // ----- helper methods ----------------------------------------------------
@@ -423,7 +436,7 @@ public class ControllerModule extends AbstractModule {
     install(new FactoryModuleBuilder().implement(
         Host.class, HostImpl.class).build(HostFactory.class));
     install(new FactoryModuleBuilder().implement(
-      Service.class, ServiceImpl.class).build(ServiceFactory.class));
+        Service.class, ServiceImpl.class).build(ServiceFactory.class));
 
     install(new FactoryModuleBuilder()
         .implement(ResourceProvider.class, Names.named("host"), HostResourceProvider.class)
@@ -439,8 +452,8 @@ public class ControllerModule extends AbstractModule {
         .build(ResourceProviderFactory.class));
 
     install(new FactoryModuleBuilder().implement(
-      ServiceComponent.class, ServiceComponentImpl.class).build(
-      ServiceComponentFactory.class));
+        ServiceComponent.class, ServiceComponentImpl.class).build(
+        ServiceComponentFactory.class));
     install(new FactoryModuleBuilder().implement(
         ServiceComponentHost.class, ServiceComponentHostImpl.class).build(
         ServiceComponentHostFactory.class));
@@ -449,7 +462,7 @@ public class ControllerModule extends AbstractModule {
     install(new FactoryModuleBuilder().implement(
         ConfigGroup.class, ConfigGroupImpl.class).build(ConfigGroupFactory.class));
     install(new FactoryModuleBuilder().implement(RequestExecution.class,
-      RequestExecutionImpl.class).build(RequestExecutionFactory.class));
+        RequestExecutionImpl.class).build(RequestExecutionFactory.class));
 
     bind(StageFactory.class).to(StageFactoryImpl.class);
     bind(RoleCommandOrderProvider.class).to(CachedRoleCommandOrderProvider.class);
@@ -464,6 +477,11 @@ public class ControllerModule extends AbstractModule {
     bind(HostRoleCommandFactory.class).to(HostRoleCommandFactoryImpl.class);
     bind(SecurityHelper.class).toInstance(SecurityHelperImpl.getInstance());
     bind(BlueprintFactory.class);
+
+    install(new FactoryModuleBuilder().implement(AmbariEvent.class, Names.named("userCreated"), UserCreatedEvent.class).build(AmbariEventFactory.class));
+    install(new FactoryModuleBuilder().implement(HookContext.class, PostUserCreationHookContext.class).build(HookContextFactory.class));
+    install(new FactoryModuleBuilder().implement(CollectionPersisterService.class, CsvFilePersisterService.class).build(CollectionPersisterServiceFactory.class));
+
   }
 
   /**
@@ -482,11 +500,9 @@ public class ControllerModule extends AbstractModule {
    *
    * @param beanDefinitions the set of bean definitions. If it is empty or
    *                        {@code null} scan will occur.
-   *
    * @return the set of bean definitions that was found during scan if
-   *         {@code beanDefinitions} was null or empty. Else original
-   *         {@code beanDefinitions} will be returned.
-   *
+   * {@code beanDefinitions} was null or empty. Else original
+   * {@code beanDefinitions} will be returned.
    */
   // Method is protected and returns a set of bean definitions for testing convenience.
   @SuppressWarnings("unchecked")
@@ -575,11 +591,11 @@ public class ControllerModule extends AbstractModule {
 
     if (null == beanDefinitions || beanDefinitions.isEmpty()) {
       ClassPathScanningCandidateComponentProvider scanner =
-        new ClassPathScanningCandidateComponentProvider(false);
+          new ClassPathScanningCandidateComponentProvider(false);
 
       // match all implementations of the dispatcher interface
       AssignableTypeFilter filter = new AssignableTypeFilter(
-        NotificationDispatcher.class);
+          NotificationDispatcher.class);
 
       scanner.addIncludeFilter(filter);
 

+ 11 - 1
ambari-server/src/main/java/org/apache/ambari/server/events/AmbariEvent.java

@@ -125,7 +125,17 @@ public abstract class AmbariEvent {
     /**
      * Cluster configuration changed.
      */
-    CLUSTER_CONFIG_CHANGED;
+    CLUSTER_CONFIG_CHANGED,
+
+    /**
+     * Metrics Collector force refresh needed.
+     */
+    METRICS_COLLECTOR_HOST_DOWN,
+
+    /**
+     * Local user has been created.
+     */
+    USER_CREATED;
   }
 
   /**

+ 33 - 0
ambari-server/src/main/java/org/apache/ambari/server/hooks/AmbariEventFactory.java

@@ -0,0 +1,33 @@
+/**
+ * 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.hooks;
+
+import org.apache.ambari.server.events.AmbariEvent;
+
+import com.google.inject.name.Named;
+
+/**
+ * Factory interface definition for AmbariEvent implementations.
+ * Instances created using this interface are managed by the IoC (GUICE) framework.
+ */
+public interface AmbariEventFactory {
+
+  @Named("userCreated")
+  AmbariEvent newUserCreatedEvent(HookContext context);
+}

+ 26 - 0
ambari-server/src/main/java/org/apache/ambari/server/hooks/HookContext.java

@@ -0,0 +1,26 @@
+/**
+ * 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.hooks;
+
+/**
+ * Marks a context of a hook implementation.
+ */
+public interface HookContext {
+
+}

+ 44 - 0
ambari-server/src/main/java/org/apache/ambari/server/hooks/HookContextFactory.java

@@ -0,0 +1,44 @@
+/**
+ * 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.hooks;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Factory interface definition to control creation of HookContext implementation.
+ * The stateless factory interface makes possible to leverage the IoC framework in managing instances.
+ */
+public interface HookContextFactory {
+  /**
+   * Factory method for HookContext implementations.
+   *
+   * @param userName the username to be inferred to the instance being created
+   * @return a HookContext instance
+   */
+  HookContext createUserHookContext(String userName);
+
+  /**
+   * Factory method for BatchUserHookContext instances.
+   *
+   * @param userGroups a map with userNames as keys and group list as values.
+   * @return a new BatchUserHookContext instance
+   */
+  HookContext createBatchUserHookContext(Map<String, Set<String>> userGroups);
+}

+ 36 - 0
ambari-server/src/main/java/org/apache/ambari/server/hooks/HookService.java

@@ -0,0 +1,36 @@
+/**
+ *  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.hooks;
+
+/**
+ * Interface defining a contract for hook services. Hook services are responsible for executing additional logic  / hooks that can be tied
+ * to different events or steps in the application.
+ */
+public interface HookService {
+
+  /**
+   * Entrypoint for the hook logic.
+   *
+   * @param hookContext the context on which the hook logic is to be executed
+   *
+   * @return true if the hook gets triggered, false otherwise
+   */
+  boolean execute(HookContext hookContext);
+}

+ 55 - 0
ambari-server/src/main/java/org/apache/ambari/server/hooks/users/PostUserCreationHookContext.java

@@ -0,0 +1,55 @@
+/**
+ * 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.hooks.users;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.ambari.server.hooks.HookContext;
+
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+
+public class PostUserCreationHookContext implements HookContext {
+  private Map<String, Set<String>> userGroups = new HashMap<>();
+
+  @AssistedInject
+  public PostUserCreationHookContext(@Assisted Map<String, Set<String>> userGroups) {
+    this.userGroups = userGroups;
+  }
+
+  @AssistedInject
+  public PostUserCreationHookContext(@Assisted String userName) {
+    userGroups.put(userName, Collections.<String>emptySet());
+  }
+
+
+  public Map<String, Set<String>> getUserGroups() {
+    return userGroups;
+  }
+
+  @Override
+  public String toString() {
+    return "BatchUserHookContext{" +
+        "userGroups=" + userGroups +
+        '}';
+  }
+}

+ 45 - 0
ambari-server/src/main/java/org/apache/ambari/server/hooks/users/UserCreatedEvent.java

@@ -0,0 +1,45 @@
+/**
+ * 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.hooks.users;
+
+import javax.inject.Inject;
+
+import org.apache.ambari.server.events.AmbariEvent;
+import org.apache.ambari.server.hooks.HookContext;
+
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+
+/**
+ * Event signaling a user creation.
+ */
+public class UserCreatedEvent extends AmbariEvent {
+
+  private HookContext context;
+
+  @AssistedInject
+  public UserCreatedEvent(@Assisted HookContext context) {
+    super(AmbariEventType.USER_CREATED);
+    this.context = context;
+  }
+
+  public HookContext getContext() {
+    return context;
+  }
+}

+ 49 - 0
ambari-server/src/main/java/org/apache/ambari/server/hooks/users/UserHookParams.java

@@ -0,0 +1,49 @@
+/**
+ * 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.hooks.users;
+
+/**
+ * Command parameter identifier list for the post user creation hook.
+ */
+public enum UserHookParams {
+
+  SCRIPT("hook-script"),
+  // the payload the hook operates on
+  PAYLOAD("cmd-payload"),
+
+  CLUSTER_ID("cluster-id"),
+  CLUSTER_NAME("cluster-name"),
+  CMD_TIME_FRAME("cmd-timeframe"),
+  CMD_INPUT_FILE("cmd-input-file"),
+  // identify security related values
+  CLUSTER_SECURITY_TYPE("cluster-security-type"),
+  CMD_HDFS_PRINCIPAL("cmd-hdfs-principal"),
+  CMD_HDFS_KEYTAB("cmd-hdfs-keytab");
+
+
+  private String param;
+
+  UserHookParams(String param) {
+    this.param = param;
+  }
+
+  public String param() {
+    return param;
+  }
+}

+ 279 - 0
ambari-server/src/main/java/org/apache/ambari/server/hooks/users/UserHookService.java

@@ -0,0 +1,279 @@
+/**
+ * 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.hooks.users;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import org.apache.ambari.server.AmbariException;
+import org.apache.ambari.server.Role;
+import org.apache.ambari.server.RoleCommand;
+import org.apache.ambari.server.actionmanager.ActionManager;
+import org.apache.ambari.server.actionmanager.RequestFactory;
+import org.apache.ambari.server.actionmanager.Stage;
+import org.apache.ambari.server.actionmanager.StageFactory;
+import org.apache.ambari.server.configuration.Configuration;
+import org.apache.ambari.server.controller.internal.RequestStageContainer;
+import org.apache.ambari.server.events.publishers.AmbariEventPublisher;
+import org.apache.ambari.server.hooks.AmbariEventFactory;
+import org.apache.ambari.server.hooks.HookContext;
+import org.apache.ambari.server.hooks.HookService;
+import org.apache.ambari.server.serveraction.users.PostUserCreationHookServerAction;
+import org.apache.ambari.server.state.Cluster;
+import org.apache.ambari.server.state.Clusters;
+import org.apache.ambari.server.state.svccomphost.ServiceComponentHostServerActionEvent;
+import org.codehaus.jackson.map.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.eventbus.Subscribe;
+
+/**
+ * Service in charge for handling user initialization related logic.
+ * It's expected that this implementation encapsulates all the logic around the user initialization hook:
+ * 1. validates the context  (all the input is available)
+ * 2. checks if prerequisites are satisfied for the hook execution
+ * 3. triggers the hook execution flow
+ * 4. executes the flow (on a separate thread)
+ */
+@Singleton
+public class UserHookService implements HookService {
+
+  private static final Logger LOGGER = LoggerFactory.getLogger(UserHookService.class);
+
+  private static final String POST_USER_CREATION_REQUEST_CONTEXT = "Post user creation hook for [ %s ] users";
+  private static final String INPUT_FILE_PREFIX = "user_hook_input_%s.csv";
+
+  // constants for getting security related properties
+  private static final String HADOOP_ENV = "hadoop-env";
+  private static final String HDFS_USER_KEYTAB = "hdfs_user_keytab";
+  private static final String HDFS_PRINCIPAL_NAME = "hdfs_principal_name";
+
+  @Inject
+  private AmbariEventFactory eventFactory;
+
+  @Inject
+  private AmbariEventPublisher ambariEventPublisher;
+
+  @Inject
+  private ActionManager actionManager;
+
+  @Inject
+  private RequestFactory requestFactory;
+
+  @Inject
+  private StageFactory stageFactory;
+
+  @Inject
+  private Configuration configuration;
+
+  @Inject
+  private Clusters clusters;
+
+  @Inject
+  private ObjectMapper objectMapper;
+
+  // executed by the IoC framework after creating the object (guice)
+  @Inject
+  private void register() {
+    ambariEventPublisher.register(this);
+  }
+
+  @Override
+  public boolean execute(HookContext hookContext) {
+    LOGGER.info("Executing user hook for {}. ", hookContext);
+
+    PostUserCreationHookContext hookCtx = validateHookInput(hookContext);
+
+    if (!checkUserHookPrerequisites()) {
+      LOGGER.warn("Prerequisites for user hook are not satisfied. Hook not triggered");
+      return false;
+    }
+
+    if (hookCtx.getUserGroups().isEmpty()) {
+      LOGGER.info("No users found for executing user hook for");
+      return false;
+    }
+
+    UserCreatedEvent userCreatedEvent = (UserCreatedEvent) eventFactory.newUserCreatedEvent(hookCtx);
+
+    LOGGER.info("Triggering user hook for user: {}", hookContext);
+    ambariEventPublisher.publish(userCreatedEvent);
+
+    return true;
+  }
+
+  @Subscribe
+  public void onUserCreatedEvent(UserCreatedEvent event) throws AmbariException {
+    LOGGER.info("Preparing hook execution for event: {}", event);
+
+    try {
+      RequestStageContainer requestStageContainer = new RequestStageContainer(actionManager.getNextRequestId(), null, requestFactory, actionManager);
+      ClusterData clsData = getClusterData();
+
+      PostUserCreationHookContext ctx = (PostUserCreationHookContext) event.getContext();
+
+      String stageContextText = String.format(POST_USER_CREATION_REQUEST_CONTEXT, ctx.getUserGroups().size());
+
+      Stage stage = stageFactory.createNew(requestStageContainer.getId(), configuration.getServerTempDir() + File.pathSeparatorChar + requestStageContainer.getId(), clsData.getClusterName(),
+          clsData.getClusterId(), stageContextText, "{}", "{}", "{}");
+      stage.setStageId(requestStageContainer.getLastStageId());
+
+      ServiceComponentHostServerActionEvent serverActionEvent = new ServiceComponentHostServerActionEvent("ambari-server-host", System.currentTimeMillis());
+      Map<String, String> commandParams = prepareCommandParams(ctx, clsData);
+
+      stage.addServerActionCommand(PostUserCreationHookServerAction.class.getName(), "ambari", Role.AMBARI_SERVER_ACTION,
+          RoleCommand.EXECUTE, clsData.getClusterName(), serverActionEvent, commandParams, stageContextText, null, null, false, false);
+
+      requestStageContainer.addStages(Collections.singletonList(stage));
+      requestStageContainer.persist();
+
+    } catch (IOException e) {
+      LOGGER.error("Failed to assemble stage for server action. Event: {}", event);
+      throw new AmbariException("Failed to assemble stage for server action", e);
+    }
+
+  }
+
+  private Map<String, String> prepareCommandParams(PostUserCreationHookContext context, ClusterData clusterData) throws IOException {
+
+    Map<String, String> commandParams = new HashMap<>();
+
+    commandParams.put(UserHookParams.SCRIPT.param(), configuration.getProperty(Configuration.POST_USER_CREATION_HOOK));
+
+    commandParams.put(UserHookParams.CLUSTER_ID.param(), String.valueOf(clusterData.getClusterId()));
+    commandParams.put(UserHookParams.CLUSTER_NAME.param(), clusterData.getClusterName());
+    commandParams.put(UserHookParams.CLUSTER_SECURITY_TYPE.param(), clusterData.getSecurityType());
+
+    commandParams.put(UserHookParams.CMD_HDFS_KEYTAB.param(), clusterData.getKeytab());
+    commandParams.put(UserHookParams.CMD_HDFS_PRINCIPAL.param(), clusterData.getPrincipal());
+    commandParams.put(UserHookParams.CMD_INPUT_FILE.param(), generateInputFileName());
+
+    commandParams.put(UserHookParams.PAYLOAD.param(), objectMapper.writeValueAsString(context.getUserGroups()));
+
+    return commandParams;
+  }
+
+  private String generateInputFileName() {
+    String inputFileName = String.format(INPUT_FILE_PREFIX, Calendar.getInstance().getTimeInMillis());
+    LOGGER.debug("Command input file name: {}", inputFileName);
+
+    return configuration.getServerTempDir() + File.separator + inputFileName;
+  }
+
+  private boolean checkUserHookPrerequisites() {
+
+    if (!configuration.isUserHookEnabled()) {
+      LOGGER.warn("Post user creation hook disabled.");
+      return false;
+    }
+
+    if (clusters.getClusters().isEmpty()) {
+      LOGGER.warn("There's no cluster found. Post user creation hook won't be executed.");
+      return false;
+    }
+
+    return true;
+  }
+
+  private PostUserCreationHookContext validateHookInput(HookContext hookContext) {
+    // perform any other validation steps, such as existence of fields etc...
+    return (PostUserCreationHookContext) hookContext;
+  }
+
+  private ClusterData getClusterData() {
+    //default value for unsecure clusters
+    String keyTab = "NA";
+    String principal = "NA";
+
+    // cluster data is needed multiple times during the stage creation, cached it locally ...
+    Map.Entry<String, Cluster> clustersMapEntry = clusters.getClusters().entrySet().iterator().next();
+
+    Cluster cluster = clustersMapEntry.getValue();
+
+    switch (cluster.getSecurityType()) {
+      case KERBEROS:
+        // get the principal
+        Map<String, String> hadoopEnv = cluster.getDesiredConfigByType(HADOOP_ENV).getProperties();
+        keyTab = hadoopEnv.get(HDFS_USER_KEYTAB);
+        principal = hadoopEnv.get(HDFS_PRINCIPAL_NAME);
+        break;
+      case NONE:
+        // break; let the flow enter the default case
+      default:
+        LOGGER.debug("The cluster security is not set. Security type: {}", cluster.getSecurityType());
+        break;
+    }
+
+    return new ClusterData(cluster.getClusterName(), cluster.getClusterId(), cluster.getSecurityType().name(), principal, keyTab);
+  }
+
+  private void getSecurityData(Configuration configuraiton) {
+    //principal
+
+    //keytab
+  }
+
+  /**
+   * Local representation of cluster data.
+   */
+  private static final class ClusterData {
+    private String clusterName;
+    private Long clusterId;
+    private String securityType;
+    private String principal;
+    private String keytab;
+
+    public ClusterData(String clusterName, Long clusterId, String securityType, String principal, String keytab) {
+      this.clusterName = clusterName;
+      this.clusterId = clusterId;
+      this.securityType = securityType;
+      this.principal = principal;
+      this.keytab = keytab;
+    }
+
+    public String getClusterName() {
+      return clusterName;
+    }
+
+    public Long getClusterId() {
+      return clusterId;
+    }
+
+    public String getSecurityType() {
+      return securityType;
+    }
+
+    public String getPrincipal() {
+      return principal;
+    }
+
+    public String getKeytab() {
+      return keytab;
+    }
+  }
+
+}

+ 85 - 34
ambari-server/src/main/java/org/apache/ambari/server/security/authorization/Users.java

@@ -17,11 +17,23 @@
  */
 package org.apache.ambari.server.security.authorization;
 
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.inject.Inject;
 import javax.persistence.EntityManager;
 
 import org.apache.ambari.server.AmbariException;
 import org.apache.ambari.server.configuration.Configuration;
+import org.apache.ambari.server.hooks.HookContextFactory;
+import org.apache.ambari.server.hooks.HookService;
 import org.apache.ambari.server.orm.dao.GroupDAO;
 import org.apache.ambari.server.orm.dao.MemberDAO;
 import org.apache.ambari.server.orm.dao.PermissionDAO;
@@ -47,7 +59,6 @@ import org.springframework.security.core.context.SecurityContext;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.crypto.password.PasswordEncoder;
 
-import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import com.google.inject.persist.Transactional;
@@ -83,7 +94,13 @@ public class Users {
   @Inject
   protected Configuration configuration;
   @Inject
-  private  AmbariLdapAuthenticationProvider ldapAuthenticationProvider;
+  private AmbariLdapAuthenticationProvider ldapAuthenticationProvider;
+
+  @Inject
+  private Provider<HookService> hookServiceProvider;
+
+  @Inject
+  private HookContextFactory hookContextFactory;
 
   public List<User> getAllUsers() {
     List<UserEntity> userEntities = userDAO.findAll();
@@ -98,6 +115,7 @@ public class Users {
 
   /**
    * This method works incorrectly, userName is not unique if users have different types
+   *
    * @return One user. Priority is LOCAL -> LDAP -> JWT
    */
   @Deprecated
@@ -125,6 +143,7 @@ public class Users {
   /**
    * Retrieves User then userName is unique in users DB. Will return null if there no user with provided userName or
    * there are some users with provided userName but with different types.
+   *
    * @param userName
    * @return User if userName is unique in DB, null otherwise
    */
@@ -147,6 +166,7 @@ public class Users {
 
   /**
    * Modifies password of local user
+   *
    * @throws AmbariException
    */
   public synchronized void modifyPassword(String userName, String currentUserPassword, String newPassword) throws AmbariException {
@@ -166,14 +186,14 @@ public class Users {
       try {
         ldapAuthenticationProvider.authenticate(
             new UsernamePasswordAuthenticationToken(currentUserName, currentUserPassword));
-      isLdapUser = true;
+        isLdapUser = true;
       } catch (InvalidUsernamePasswordCombinationException ex) {
         throw new AmbariException(ex.getMessage());
       }
     }
 
     boolean isCurrentUserAdmin = false;
-    for (PrivilegeEntity privilegeEntity: currentUserEntity.getPrincipal().getPrivileges()) {
+    for (PrivilegeEntity privilegeEntity : currentUserEntity.getPrincipal().getPrivileges()) {
       if (privilegeEntity.getPermission().getPermissionName().equals(PermissionEntity.AMBARI_ADMINISTRATOR_PERMISSION_NAME)) {
         isCurrentUserAdmin = true;
         break;
@@ -270,8 +290,8 @@ public class Users {
    * @param userName user name
    * @param password password
    * @param userType user type
-   * @param active is user active
-   * @param admin is user admin
+   * @param active   is user active
+   * @param admin    is user admin
    * @throws AmbariException if user already exists
    */
   public synchronized void createUser(String userName, String password, UserType userType, Boolean active, Boolean
@@ -319,14 +339,17 @@ public class Users {
     if (admin != null && admin) {
       grantAdminPrivilege(userEntity.getUserId());
     }
+
+    // execute user initialization hook if required ()
+    hookServiceProvider.get().execute(hookContextFactory.createUserHookContext(userName));
   }
 
   public synchronized void removeUser(User user) throws AmbariException {
     UserEntity userEntity = userDAO.findByPK(user.getUserId());
     if (userEntity != null) {
-      if (!isUserCanBeRemoved(userEntity)){
+      if (!isUserCanBeRemoved(userEntity)) {
         throw new AmbariException("Could not remove user " + userEntity.getUserName() +
-              ". System should have at least one administrator.");
+            ". System should have at least one administrator.");
       }
       userDAO.remove(userEntity);
     } else {
@@ -357,12 +380,12 @@ public class Users {
       return null;
     } else {
       final Set<User> users = new HashSet<User>();
-      for (MemberEntity memberEntity: groupEntity.getMemberEntities()) {
+      for (MemberEntity memberEntity : groupEntity.getMemberEntities()) {
         if (memberEntity.getUser() != null) {
           users.add(new User(memberEntity.getUser()));
         } else {
           LOG.error("Wrong state, not found user for member '{}' (group: '{}')",
-            memberEntity.getMemberId(), memberEntity.getGroup().getGroupName());
+              memberEntity.getMemberId(), memberEntity.getGroup().getGroupName());
         }
       }
       return users;
@@ -402,7 +425,7 @@ public class Users {
     final List<GroupEntity> groupEntities = groupDAO.findAll();
     final List<Group> groups = new ArrayList<Group>(groupEntities.size());
 
-    for (GroupEntity groupEntity: groupEntities) {
+    for (GroupEntity groupEntity : groupEntities) {
       groups.add(new Group(groupEntity));
     }
 
@@ -421,7 +444,7 @@ public class Users {
     if (groupEntity == null) {
       throw new AmbariException("Group " + groupName + " doesn't exist");
     }
-    for (MemberEntity member: groupEntity.getMemberEntities()) {
+    for (MemberEntity member : groupEntity.getMemberEntities()) {
       members.add(member.getUser().getUserName());
     }
     return members;
@@ -463,7 +486,7 @@ public class Users {
    */
   public synchronized void revokeAdminPrivilege(Integer userId) {
     final UserEntity user = userDAO.findByPK(userId);
-    for (PrivilegeEntity privilege: user.getPrincipal().getPrivileges()) {
+    for (PrivilegeEntity privilege : user.getPrincipal().getPrivileges()) {
       if (privilege.getPermission().getPermissionName().equals(PermissionEntity.AMBARI_ADMINISTRATOR_PERMISSION_NAME)) {
         user.getPrincipal().getPrivileges().remove(privilege);
         principalDAO.merge(user.getPrincipal()); //explicit merge for Derby support
@@ -518,7 +541,7 @@ public class Users {
 
     if (isUserInGroup(userEntity, groupEntity)) {
       MemberEntity memberEntity = null;
-      for (MemberEntity entity: userEntity.getMemberEntities()) {
+      for (MemberEntity entity : userEntity.getMemberEntities()) {
         if (entity.getGroup().equals(groupEntity)) {
           memberEntity = entity;
           break;
@@ -541,7 +564,7 @@ public class Users {
    * @param userEntity user to be checked
    * @return true if user can be removed
    */
-  public synchronized boolean isUserCanBeRemoved(UserEntity userEntity){
+  public synchronized boolean isUserCanBeRemoved(UserEntity userEntity) {
     List<PrincipalEntity> adminPrincipals = principalDAO.findByPermissionId(PermissionEntity.AMBARI_ADMINISTRATOR_PERMISSION);
     Set<UserEntity> userEntitysSet = new HashSet<UserEntity>(userDAO.findUsersByPrincipal(adminPrincipals));
     return (userEntitysSet.contains(userEntity) && userEntitysSet.size() < 2) ? false : true;
@@ -550,12 +573,12 @@ public class Users {
   /**
    * Performs a check if given user belongs to given group.
    *
-   * @param userEntity user entity
+   * @param userEntity  user entity
    * @param groupEntity group entity
    * @return true if user presents in group
    */
   private boolean isUserInGroup(UserEntity userEntity, GroupEntity groupEntity) {
-    for (MemberEntity memberEntity: userEntity.getMemberEntities()) {
+    for (MemberEntity memberEntity : userEntity.getMemberEntities()) {
       if (memberEntity.getGroup().equals(groupEntity)) {
         return true;
       }
@@ -574,11 +597,11 @@ public class Users {
 
     // prefetch all user and group data to avoid heavy queries in membership creation
 
-    for (UserEntity userEntity: userDAO.findAll()) {
+    for (UserEntity userEntity : userDAO.findAll()) {
       allUsers.put(userEntity.getUserName(), userEntity);
     }
 
-    for (GroupEntity groupEntity: groupDAO.findAll()) {
+    for (GroupEntity groupEntity : groupDAO.findAll()) {
       allGroups.put(groupEntity.getGroupName(), groupEntity);
     }
 
@@ -589,7 +612,7 @@ public class Users {
 
     // remove users
     final Set<UserEntity> usersToRemove = new HashSet<UserEntity>();
-    for (String userName: batchInfo.getUsersToBeRemoved()) {
+    for (String userName : batchInfo.getUsersToBeRemoved()) {
       UserEntity userEntity = userDAO.findUserByName(userName);
       if (userEntity == null) {
         continue;
@@ -601,7 +624,7 @@ public class Users {
 
     // remove groups
     final Set<GroupEntity> groupsToRemove = new HashSet<GroupEntity>();
-    for (String groupName: batchInfo.getGroupsToBeRemoved()) {
+    for (String groupName : batchInfo.getGroupsToBeRemoved()) {
       final GroupEntity groupEntity = groupDAO.findGroupByName(groupName);
       allGroups.remove(groupEntity.getGroupName());
       groupsToRemove.add(groupEntity);
@@ -610,7 +633,7 @@ public class Users {
 
     // update users
     final Set<UserEntity> usersToBecomeLdap = new HashSet<UserEntity>();
-    for (String userName: batchInfo.getUsersToBecomeLdap()) {
+    for (String userName : batchInfo.getUsersToBecomeLdap()) {
       UserEntity userEntity = userDAO.findLocalUserByName(userName);
       if (userEntity == null) {
         userEntity = userDAO.findLdapUserByName(userName);
@@ -626,7 +649,7 @@ public class Users {
 
     // update groups
     final Set<GroupEntity> groupsToBecomeLdap = new HashSet<GroupEntity>();
-    for (String groupName: batchInfo.getGroupsToBecomeLdap()) {
+    for (String groupName : batchInfo.getGroupsToBecomeLdap()) {
       final GroupEntity groupEntity = groupDAO.findGroupByName(groupName);
       groupEntity.setLdapGroup(true);
       allGroups.put(groupEntity.getGroupName(), groupEntity);
@@ -639,7 +662,7 @@ public class Users {
 
     // prepare create users
     final Set<UserEntity> usersToCreate = new HashSet<UserEntity>();
-    for (String userName: batchInfo.getUsersToBeCreated()) {
+    for (String userName : batchInfo.getUsersToBeCreated()) {
       final PrincipalEntity principalEntity = new PrincipalEntity();
       principalEntity.setPrincipalType(userPrincipalType);
       principalsToCreate.add(principalEntity);
@@ -656,7 +679,7 @@ public class Users {
 
     // prepare create groups
     final Set<GroupEntity> groupsToCreate = new HashSet<GroupEntity>();
-    for (String groupName: batchInfo.getGroupsToBeCreated()) {
+    for (String groupName : batchInfo.getGroupsToBeCreated()) {
       final PrincipalEntity principalEntity = new PrincipalEntity();
       principalEntity.setPrincipalType(groupPrincipalType);
       principalsToCreate.add(principalEntity);
@@ -678,7 +701,7 @@ public class Users {
     // create membership
     final Set<MemberEntity> membersToCreate = new HashSet<MemberEntity>();
     final Set<GroupEntity> groupsToUpdate = new HashSet<GroupEntity>();
-    for (LdapUserGroupMemberDto member: batchInfo.getMembershipToAdd()) {
+    for (LdapUserGroupMemberDto member : batchInfo.getMembershipToAdd()) {
       final MemberEntity memberEntity = new MemberEntity();
       final GroupEntity groupEntity = allGroups.get(member.getGroupName());
       memberEntity.setGroup(groupEntity);
@@ -692,7 +715,7 @@ public class Users {
 
     // remove membership
     final Set<MemberEntity> membersToRemove = new HashSet<MemberEntity>();
-    for (LdapUserGroupMemberDto member: batchInfo.getMembershipToRemove()) {
+    for (LdapUserGroupMemberDto member : batchInfo.getMembershipToRemove()) {
       MemberEntity memberEntity = memberDAO.findByUserAndGroup(member.getUserName(), member.getGroupName());
       if (memberEntity != null) {
         membersToRemove.add(memberEntity);
@@ -702,6 +725,36 @@ public class Users {
 
     // clear cached entities
     entityManagerProvider.get().getEntityManagerFactory().getCache().evictAll();
+
+    if (!usersToCreate.isEmpty()) {
+      // entry point in the hook logic
+      hookServiceProvider.get().execute(hookContextFactory.createBatchUserHookContext(getUsersToGroupMap(usersToCreate)));
+    }
+
+  }
+
+  /**
+   * Assembles a map where the keys are usernames and values are Lists with groups associated with users.
+   *
+   * @param usersToCreate a list with user entities
+   * @return the a populated map instance
+   */
+  private Map<String, Set<String>> getUsersToGroupMap(Set<UserEntity> usersToCreate) {
+    Map<String, Set<String>> usersToGroups = new HashMap<>();
+
+    for (UserEntity userEntity : usersToCreate) {
+
+      // make sure user entities are refreshed so that membership is updated
+      userEntity = userDAO.findByPK(userEntity.getUserId());
+
+      usersToGroups.put(userEntity.getUserName(), new HashSet<String>());
+
+      for (MemberEntity memberEntity : userEntity.getMemberEntities()) {
+        usersToGroups.get(userEntity.getUserName()).add(memberEntity.getGroup().getGroupName());
+      }
+    }
+
+    return usersToGroups;
   }
 
   /**
@@ -740,10 +793,9 @@ public class Users {
     List<PrivilegeEntity> implicitPrivilegeEntities = getImplicitPrivileges(explicitPrivilegeEntities);
     List<PrivilegeEntity> privilegeEntities;
 
-    if(implicitPrivilegeEntities.isEmpty()) {
+    if (implicitPrivilegeEntities.isEmpty()) {
       privilegeEntities = explicitPrivilegeEntities;
-    }
-    else {
+    } else {
       privilegeEntities = new LinkedList<PrivilegeEntity>();
       privilegeEntities.addAll(explicitPrivilegeEntities);
       privilegeEntities.addAll(implicitPrivilegeEntities);
@@ -782,10 +834,9 @@ public class Users {
     List<PrivilegeEntity> implicitPrivilegeEntities = getImplicitPrivileges(explicitPrivilegeEntities);
     List<PrivilegeEntity> privilegeEntities;
 
-    if(implicitPrivilegeEntities.isEmpty()) {
+    if (implicitPrivilegeEntities.isEmpty()) {
       privilegeEntities = explicitPrivilegeEntities;
-    }
-    else {
+    } else {
       privilegeEntities = new LinkedList<PrivilegeEntity>();
       privilegeEntities.addAll(explicitPrivilegeEntities);
       privilegeEntities.addAll(implicitPrivilegeEntities);

+ 1 - 1
ambari-server/src/main/java/org/apache/ambari/server/serveraction/AbstractServerAction.java

@@ -35,7 +35,7 @@ import com.google.inject.Inject;
 import com.google.inject.Injector;
 
 /**
- * AbstractServerActionImpl is an abstract implementation of a ServerAction.
+ * AbstractServerAction is an abstract implementation of a ServerAction.
  * <p/>
  * This abstract implementation provides common facilities for all ServerActions, such as
  * maintaining the ExecutionCommand and HostRoleCommand properties. It also provides a convenient

+ 2 - 2
ambari-server/src/main/java/org/apache/ambari/server/serveraction/ServerAction.java

@@ -30,8 +30,8 @@ import java.util.concurrent.ConcurrentMap;
  */
 public interface ServerAction {
 
-  public static final String ACTION_NAME      = "ACTION_NAME";
-  public static final String ACTION_USER_NAME = "ACTION_USER_NAME";
+  String ACTION_NAME = "ACTION_NAME";
+  String ACTION_USER_NAME = "ACTION_USER_NAME";
 
   /**
    * The default timeout (in seconds) to use for potentially long running tasks such as creating

+ 46 - 0
ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/CollectionPersisterService.java

@@ -0,0 +1,46 @@
+/**
+ * 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.users;
+
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * Contract defining operations that persist collections of data.
+ */
+public interface CollectionPersisterService<K, V> {
+
+  /**
+   * Persists the provided collection of data.
+   *
+   * @param collectionData the data to be persisted
+   * @return true if all the records persisted successfully, false otherwise.
+   */
+  boolean persist(Collection<V> collectionData);
+
+
+  /**
+   * Persists the provided map of data.
+   *
+   * @param mapData the data to be persisted.
+   * @return true if all the records persisted successfully, false otherwise.
+   */
+  boolean persistMap(Map<K, V> mapData);
+
+}

+ 24 - 0
ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/CollectionPersisterServiceFactory.java

@@ -0,0 +1,24 @@
+/**
+ * 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.users;
+
+public interface CollectionPersisterServiceFactory {
+
+  CsvFilePersisterService createCsvFilePersisterService(String csvFile);
+}

+ 103 - 0
ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/CsvFilePersisterService.java

@@ -0,0 +1,103 @@
+/**
+ * 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.users;
+
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import org.apache.commons.csv.CSVFormat;
+import org.apache.commons.csv.CSVPrinter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+
+@Singleton
+public class CsvFilePersisterService implements CollectionPersisterService<String, List<String>> {
+
+  private static final Logger LOGGER = LoggerFactory.getLogger(CsvFilePersisterService.class);
+  private String NEW_LINE_SEPARATOR = "\n";
+
+  private String csvFile;
+  private CSVPrinter csvPrinter;
+  private FileWriter fileWriter;
+
+  @AssistedInject
+  public CsvFilePersisterService(@Assisted String csvFile) {
+    this.csvFile = csvFile;
+  }
+
+  @Inject
+  public void init() throws IOException {
+    // make 3rd party dependencies be managed by the container (probably constructor binding or factory is needed)
+    fileWriter = new FileWriter(csvFile);
+    csvPrinter = new CSVPrinter(fileWriter, CSVFormat.DEFAULT.withRecordSeparator(NEW_LINE_SEPARATOR));
+  }
+
+
+  @Override
+  public boolean persist(Collection<List<String>> collectionData) {
+
+    try {
+      LOGGER.info("Persisting collection to csv file");
+
+      csvPrinter.printRecords(collectionData);
+
+      LOGGER.info("Collection successfully persisted to csv file.");
+
+      return true;
+    } catch (IOException e) {
+      LOGGER.error("Failed to persist the collection to csv file", e);
+      return false;
+    } finally {
+      try {
+        fileWriter.flush();
+        fileWriter.close();
+        csvPrinter.close();
+      } catch (IOException e) {
+        LOGGER.error("Error while flushing/closing fileWriter/csvPrinter", e);
+      }
+    }
+  }
+
+  @Override
+  public boolean persistMap(Map<String, List<String>> mapData) {
+
+    LOGGER.info("Persisting map data to csv file");
+    Collection<List<String>> collectionData = new ArrayList<>();
+
+    for (String key : mapData.keySet()) {
+      List<String> record = new ArrayList<>();
+      record.add(key);
+      record.addAll(mapData.get(key));
+      collectionData.add(record);
+    }
+
+    return persist(collectionData);
+  }
+
+}

+ 163 - 0
ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/PostUserCreationHookServerAction.java

@@ -0,0 +1,163 @@
+/**
+ * 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.users;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import org.apache.ambari.server.AmbariException;
+import org.apache.ambari.server.actionmanager.HostRoleStatus;
+import org.apache.ambari.server.agent.CommandReport;
+import org.apache.ambari.server.hooks.users.UserHookParams;
+import org.apache.ambari.server.serveraction.AbstractServerAction;
+import org.apache.ambari.server.topology.AsyncCallableService;
+import org.apache.ambari.server.utils.ShellCommandUtil;
+import org.codehaus.jackson.map.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Splitter;
+
+@Singleton
+public class PostUserCreationHookServerAction extends AbstractServerAction {
+  private static final Logger LOGGER = LoggerFactory.getLogger(PostUserCreationHookServerAction.class);
+  private static final int MAX_SYMBOLS_PER_LOG_MESSAGE = 7900;
+
+  @Inject
+  private ShellCommandUtilityWrapper shellCommandUtilityWrapper;
+
+  @Inject
+  private ObjectMapper objectMapper;
+
+  @Inject
+  private CollectionPersisterServiceFactory collectionPersisterServiceFactory;
+
+  @Inject
+  public PostUserCreationHookServerAction() {
+    super();
+  }
+
+  @Override
+  public CommandReport execute(ConcurrentMap<String, Object> requestSharedDataContext) throws AmbariException, InterruptedException {
+    LOGGER.debug("Executing custom script server action; Context: {}", requestSharedDataContext);
+    ShellCommandUtil.Result result = null;
+    CommandReport cmdReport = null;
+
+    try {
+
+      Map<String, String> commandParams = getCommandParameters();
+      validateCommandParams(commandParams);
+
+      //persist user data to csv
+      CollectionPersisterService csvPersisterService = collectionPersisterServiceFactory.createCsvFilePersisterService(commandParams.get(UserHookParams.CMD_INPUT_FILE.param()));
+      csvPersisterService.persistMap(getPayload(commandParams));
+
+      String[] cmd = assembleCommand(commandParams);
+
+      result = shellCommandUtilityWrapper.runCommand(cmd);
+
+      // long command results need to be split to chunks to feed external log processors (eg.: syslog)
+      logCommandResult(Arrays.asList(cmd).toString(), result);
+
+      cmdReport = createCommandReport(result.getExitCode(), result.isSuccessful() ?
+          HostRoleStatus.COMPLETED : HostRoleStatus.FAILED, "{}", result.getStdout(), result.getStderr());
+
+      LOGGER.debug("Command report: {}", cmdReport);
+
+
+    } catch (InterruptedException e) {
+      LOGGER.error("The server action thread has been interrupted", e);
+      throw e;
+    } catch (Exception e) {
+      LOGGER.error("Server action is about to quit due to an exception.", e);
+      throw new AmbariException("Server action execution failed to complete!", e);
+    }
+
+    return cmdReport;
+  }
+
+  private void logCommandResult(String command, ShellCommandUtil.Result result) {
+    LOGGER.info("Execution of command [ {} ] - {}", command, result.isSuccessful() ? "succeeded" : "failed");
+    String stringToLog = result.isSuccessful() ? result.getStdout() : result.getStderr();
+    if (stringToLog == null) stringToLog = "";
+    List<String> logLines = Splitter.fixedLength(MAX_SYMBOLS_PER_LOG_MESSAGE).splitToList(stringToLog);
+    LOGGER.info("BEGIN - {} for command {}", result.isSuccessful() ? "stdout" : "stderr", command);
+    for (String line : logLines) {
+      LOGGER.info("command output *** : {}", line);
+    }
+    LOGGER.info("END - {} for command {}", result.isSuccessful() ? "stdout" : "stderr", command);
+  }
+
+
+  private String[] assembleCommand(Map<String, String> params) {
+    String[] cmdArray = new String[]{
+        params.get(UserHookParams.SCRIPT.param()),
+        params.get(UserHookParams.CMD_INPUT_FILE.param()),
+        params.get(UserHookParams.CLUSTER_SECURITY_TYPE.param()),
+        params.get(UserHookParams.CMD_HDFS_PRINCIPAL.param()),
+        params.get(UserHookParams.CMD_HDFS_KEYTAB.param())
+    };
+    LOGGER.debug("Server action command to be executed: {}", cmdArray);
+    return cmdArray;
+  }
+
+  /**
+   * Validates command parameters, throws exception in case required parameters are missing
+   */
+  private void validateCommandParams(Map<String, String> commandParams) {
+
+    LOGGER.info("Validating command parameters ...");
+
+    if (!commandParams.containsKey(UserHookParams.PAYLOAD.param())) {
+      LOGGER.error("Missing command parameter: {}; Failing the server action.", UserHookParams.PAYLOAD.param());
+      throw new IllegalArgumentException("Missing command parameter: [" + UserHookParams.PAYLOAD.param() + "]");
+    }
+
+    if (!commandParams.containsKey(UserHookParams.SCRIPT.param())) {
+      LOGGER.error("Missing command parameter: {}; Failing the server action.", UserHookParams.SCRIPT.param());
+      throw new IllegalArgumentException("Missing command parameter: [" + UserHookParams.SCRIPT.param() + "]");
+    }
+
+    if (!commandParams.containsKey(UserHookParams.CMD_INPUT_FILE.param())) {
+      LOGGER.error("Missing command parameter: {}; Failing the server action.", UserHookParams.CMD_INPUT_FILE.param());
+      throw new IllegalArgumentException("Missing command parameter: [" + UserHookParams.CMD_INPUT_FILE.param() + "]");
+    }
+
+    if (!commandParams.containsKey(UserHookParams.CLUSTER_SECURITY_TYPE.param())) {
+      LOGGER.error("Missing command parameter: {}; Failing the server action.", UserHookParams.CLUSTER_SECURITY_TYPE.param());
+      throw new IllegalArgumentException("Missing command parameter: [" + UserHookParams.CLUSTER_SECURITY_TYPE.param() + "]");
+    }
+
+    LOGGER.info("Command parameter validation passed.");
+  }
+
+  private Map<String, List<String>> getPayload(Map<String, String> commandParams) throws IOException {
+    Map<String, List<String>> payload = objectMapper.readValue(commandParams.get(UserHookParams.PAYLOAD.param()), Map.class);
+    return payload;
+  }
+
+}

+ 26 - 0
ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/ShellCommandCallableFactory.java

@@ -0,0 +1,26 @@
+/**
+ *  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.users;
+
+public interface ShellCommandCallableFactory {
+
+  ShellCommandUtilityCallable createRunCommandCallable(String[] arguments);
+
+}

+ 48 - 0
ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/ShellCommandUtilityCallable.java

@@ -0,0 +1,48 @@
+/**
+ * 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.users;
+
+import java.util.concurrent.Callable;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import org.apache.ambari.server.utils.ShellCommandUtil;
+
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+
+/**
+ * Wraps a shell command execution into a callable.
+ */
+@Singleton
+public class ShellCommandUtilityCallable implements Callable<ShellCommandUtil.Result> {
+
+  private String[] args;
+
+  @AssistedInject
+  public ShellCommandUtilityCallable(@Assisted String[] args) {
+    this.args = args;
+  }
+
+  @Override
+  public ShellCommandUtil.Result call() throws Exception {
+    return ShellCommandUtil.runCommand(args);
+  }
+}

+ 57 - 0
ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/ShellCommandUtilityWrapper.java

@@ -0,0 +1,57 @@
+/**
+ * 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.users;
+
+import java.io.IOException;
+import java.util.Map;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import org.apache.ambari.server.utils.ShellCommandUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Wraps the {@link org.apache.ambari.server.utils.ShellCommandUtil} utility.
+ * Simply delegates to the utility. It's intended to be used instead of directly invoking the utility class.
+ *
+ * Classes using the wrapper will be decoupled from the static utility; thus can be used and tested easily.
+ */
+@Singleton
+public class ShellCommandUtilityWrapper {
+
+  private static final Logger LOGGER = LoggerFactory.getLogger(ShellCommandUtilityWrapper.class);
+
+  @Inject
+  public ShellCommandUtilityWrapper() {
+    super();
+  }
+
+  public ShellCommandUtil.Result runCommand(String[] args) throws IOException, InterruptedException {
+    LOGGER.info("Running command: {}", args);
+    return ShellCommandUtil.runCommand(args);
+  }
+
+  public ShellCommandUtil.Result runCommand(String[] args, Map<String, String> vars) throws IOException, InterruptedException {
+    LOGGER.info("Running command: {}, variables: {}", args, vars);
+    return ShellCommandUtil.runCommand(args, vars);
+  }
+
+}

+ 17 - 8
ambari-server/src/main/java/org/apache/ambari/server/topology/AsyncCallableService.java

@@ -18,15 +18,17 @@
 
 package org.apache.ambari.server.topology;
 
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
 import java.util.Calendar;
+import java.util.HashSet;
+import java.util.Set;
 import java.util.concurrent.Callable;
 import java.util.concurrent.Future;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 /**
  * Callable service implementation for executing tasks asynchronously.
  * The service repeatedly tries to execute the provided task till it successfully completes, or the provided timeout
@@ -34,7 +36,7 @@ import java.util.concurrent.TimeUnit;
  *
  * @param <T> the type returned by the task to be executed
  */
-public class AsyncCallableService<T> implements Callable<Boolean> {
+public class AsyncCallableService<T> implements Callable<T> {
 
   private static final Logger LOG = LoggerFactory.getLogger(AsyncCallableService.class);
 
@@ -51,8 +53,9 @@ public class AsyncCallableService<T> implements Callable<Boolean> {
   // the delay between two consecutive execution trials in milliseconds
   private final long delay;
 
+  private T serviceResult;
 
-  private Boolean serviceResult = Boolean.FALSE;
+  private final Set<Exception> errors = new HashSet<>();
 
   public AsyncCallableService(Callable<T> task, long timeout, long delay,
                               ScheduledExecutorService executorService) {
@@ -63,7 +66,7 @@ public class AsyncCallableService<T> implements Callable<Boolean> {
   }
 
   @Override
-  public Boolean call() {
+  public T call() {
 
     long startTimeInMillis = Calendar.getInstance().getTimeInMillis();
     LOG.info("Task execution started at: {}", startTimeInMillis);
@@ -104,11 +107,13 @@ public class AsyncCallableService<T> implements Callable<Boolean> {
 
       // task failures are expected to be reportesd as exceptions
       LOG.debug("Task successfully executed: {}", taskResult);
-      setServiceResult(Boolean.TRUE);
+      setServiceResult(taskResult);
+      errors.clear();
       completed = true;
     } catch (Exception e) {
       // Future.isDone always true here!
       LOG.info("Exception during task execution: ", e);
+      errors.add(e);
     }
     return completed;
   }
@@ -117,8 +122,12 @@ public class AsyncCallableService<T> implements Callable<Boolean> {
     return timeout < Calendar.getInstance().getTimeInMillis() - startTimeInMillis;
   }
 
-  private void setServiceResult(Boolean serviceResult) {
+  private void setServiceResult(T serviceResult) {
     this.serviceResult = serviceResult;
   }
 
+  public Set<Exception> getErrors() {
+    return errors;
+  }
+
 }

+ 1 - 1
ambari-server/src/main/java/org/apache/ambari/server/utils/ShellCommandUtil.java

@@ -502,7 +502,7 @@ public class ShellCommandUtil {
 
   public static class Result {
 
-    Result(int exitCode, String stdout, String stderr) {
+    public  Result(int exitCode, String stdout, String stderr) {
       this.exitCode = exitCode;
       this.stdout = stdout;
       this.stderr = stderr;

+ 133 - 0
ambari-server/src/main/resources/scripts/post-user-creation-hook.sh

@@ -0,0 +1,133 @@
+#!/usr/bin/env bash
+#
+# 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.
+#
+
+: "${DEBUG:=0}"
+
+validate_input_arguments(){
+
+# the first argument is the csv file | username, group1, group2
+CSV_FILE="$1"
+: "${CSV_FILE:?"Missing csv file input for the post-user creation hook"}"
+
+# the second argument is the cluster security type
+SECURITY_TYPE=$2
+: "${SECURITY_TYPE:?"Missing security type input for the post-user creation hook"}"
+
+}
+
+
+# wraps the command passed in as argument to be called via the ambari sudo
+ambari_sudo(){
+
+ARG_STR="$1"
+CMD_STR="/var/lib/ambari-server/ambari-sudo.sh su hdfs -l -s /bin/bash -c '$ARG_STR'"
+
+eval "$CMD_STR"
+}
+
+setup_security(){
+if [ "$SECURITY_TYPE" ==  "KERBEROS" ]
+then
+  HDFS_PRINCIPAL=$3
+: "${HDFS_PRINCIPAL:?"Missing hdfs principal for the post-user creation hook"}"
+
+  HDFS_KEYTAB=$4
+: "${HDFS_KEYTAB:?"Missing hdfs principal for the post-user creation hook"}"
+
+  echo "The cluster is secure, calling kinit ..."
+  kinit_cmd="/usr/bin/kinit -kt $HDFS_KEYTAB $HDFS_PRINCIPAL"
+
+  ambari_sudo "$kinit_cmd"
+else
+ echo "The cluster security type is $SECURITY_TYPE"
+fi
+}
+
+check_tools(){
+echo "Checking for required tools ..."
+
+# check for hadoop
+ambari_sudo "type hadoop > /dev/null 2>&1 || { echo >&2 \"hadoop client not installed\"; exit 1; }"
+
+# check for the hdfs
+ambari_sudo "hadoop fs -ls / > /dev/null 2>&1 || { echo >&2 \"hadoop dfs not available\"; exit 1; }"
+
+echo "Checking for required tools ... DONE."
+
+}
+
+prepare_input(){
+# perform any specific logic on the arguments
+echo "Processing post user creation hook payload ..."
+
+JSON_INPUT="$CSV_FILE.json"
+echo "Generating json file $JSON_INPUT ..."
+
+echo "[" | cat > "$JSON_INPUT"
+while read -r LINE
+do
+  USR_NAME=$(echo "$LINE" | awk -F, '{print $1}')
+
+  cat <<EOF >> "$JSON_INPUT"
+    {
+    "target":"/user/$USR_NAME",
+    "type":"directory",
+    "action":"create",
+    "owner":"$USR_NAME",
+    "group":"hadoop",
+    "manageIfExists": "true"
+  },
+EOF
+done <"$CSV_FILE"
+
+sed -i '$ d' "$JSON_INPUT"
+echo $'}\n]' | cat >> "$JSON_INPUT"
+echo "Generating file $JSON_INPUT ... DONE."
+echo "Processing post user creation hook payload ... DONE."
+
+}
+
+
+# encapsulates the logic of the post user creation script
+main(){
+
+echo $DEBUG;
+
+if [ "$DEBUG" != "0" ]; then echo "Switch debug ON";set -x; else echo "debug: OFF"; fi
+
+echo "Executing user hook with parameters: $*"
+
+validate_input_arguments "$@"
+
+setup_security "$@"
+
+check_tools
+
+prepare_input
+
+# the default implementation creates user home folders; the first argument must be the username
+ambari_sudo "yarn jar /var/lib/ambari-server/resources/stacks/HDP/2.0.6/hooks/before-START/files/fast-hdfs-resource.jar $JSON_INPUT"
+
+if [ "$DEBUG" -gt "0" ]; then echo "Switch debug OFF";set -x;unset DEBUG; else echo "debug: OFF"; fi
+unset DEBUG
+}
+
+
+main "$@"

+ 4 - 0
ambari-server/src/test/java/org/apache/ambari/server/controller/internal/ActiveWidgetLayoutResourceProviderTest.java

@@ -38,6 +38,8 @@ import org.apache.ambari.server.controller.spi.ResourceProvider;
 import org.apache.ambari.server.controller.spi.SystemException;
 import org.apache.ambari.server.controller.utilities.PredicateBuilder;
 import org.apache.ambari.server.controller.utilities.PropertyHelper;
+import org.apache.ambari.server.hooks.HookContextFactory;
+import org.apache.ambari.server.hooks.HookService;
 import org.apache.ambari.server.metadata.CachedRoleCommandOrderProvider;
 import org.apache.ambari.server.metadata.RoleCommandOrderProvider;
 import org.apache.ambari.server.orm.DBAccessor;
@@ -400,6 +402,8 @@ public class ActiveWidgetLayoutResourceProviderTest extends EasyMockSupport {
         bind(UserDAO.class).toInstance(createMock(UserDAO.class));
         bind(WidgetLayoutDAO.class).toInstance(createMock(WidgetLayoutDAO.class));
         bind(HostRoleCommandDAO.class).toInstance(createMock(HostRoleCommandDAO.class));
+        bind(HookContextFactory.class).toInstance(createMock(HookContextFactory.class));
+        bind(HookService.class).toInstance(createMock(HookService.class));
       }
     });
   }

+ 5 - 0
ambari-server/src/test/java/org/apache/ambari/server/controller/internal/StackUpgradeConfigurationMergeTest.java

@@ -31,6 +31,8 @@ import org.apache.ambari.server.controller.AbstractRootServiceResponseFactory;
 import org.apache.ambari.server.controller.AmbariManagementController;
 import org.apache.ambari.server.controller.KerberosHelper;
 import org.apache.ambari.server.controller.spi.ClusterController;
+import org.apache.ambari.server.hooks.HookContextFactory;
+import org.apache.ambari.server.hooks.HookService;
 import org.apache.ambari.server.orm.DBAccessor;
 import org.apache.ambari.server.orm.dao.HostRoleCommandDAO;
 import org.apache.ambari.server.orm.dao.RepositoryVersionDAO;
@@ -291,6 +293,9 @@ public class StackUpgradeConfigurationMergeTest extends EasyMockSupport {
       binder.bind(Users.class).toInstance(createNiceMock(Users.class));
       binder.bind(ConfigHelper.class).toInstance(createNiceMock(ConfigHelper.class));
       binder.bind(RepositoryVersionDAO.class).toInstance(createNiceMock(RepositoryVersionDAO.class));
+      binder.bind(HookContextFactory.class).toInstance(createMock(HookContextFactory.class));
+      binder.bind(HookService.class).toInstance(createMock(HookService.class));
+
 
       binder.requestStaticInjection(UpgradeResourceProvider.class);
     }

+ 4 - 0
ambari-server/src/test/java/org/apache/ambari/server/controller/internal/UserAuthorizationResourceProviderTest.java

@@ -43,6 +43,8 @@ import org.apache.ambari.server.controller.spi.ResourceProvider;
 import org.apache.ambari.server.controller.spi.SystemException;
 import org.apache.ambari.server.controller.utilities.PredicateBuilder;
 import org.apache.ambari.server.controller.utilities.PropertyHelper;
+import org.apache.ambari.server.hooks.HookContextFactory;
+import org.apache.ambari.server.hooks.HookService;
 import org.apache.ambari.server.metadata.CachedRoleCommandOrderProvider;
 import org.apache.ambari.server.metadata.RoleCommandOrderProvider;
 import org.apache.ambari.server.orm.DBAccessor;
@@ -413,6 +415,8 @@ public class UserAuthorizationResourceProviderTest extends EasyMockSupport {
         bind(ResourceTypeDAO.class).toInstance(createMock(ResourceTypeDAO.class));
         bind(PermissionDAO.class).toInstance(createMock(PermissionDAO.class));
         bind(HostRoleCommandDAO.class).toInstance(createMock(HostRoleCommandDAO.class));
+        bind(HookContextFactory.class).toInstance(createMock(HookContextFactory.class));
+        bind(HookService.class).toInstance(createMock(HookService.class));
       }
     });
   }

+ 4 - 0
ambari-server/src/test/java/org/apache/ambari/server/controller/internal/UserResourceProviderTest.java

@@ -38,6 +38,8 @@ import org.apache.ambari.server.controller.spi.Resource;
 import org.apache.ambari.server.controller.spi.ResourceProvider;
 import org.apache.ambari.server.controller.utilities.PredicateBuilder;
 import org.apache.ambari.server.controller.utilities.PropertyHelper;
+import org.apache.ambari.server.hooks.HookContextFactory;
+import org.apache.ambari.server.hooks.HookService;
 import org.apache.ambari.server.metadata.CachedRoleCommandOrderProvider;
 import org.apache.ambari.server.metadata.RoleCommandOrderProvider;
 import org.apache.ambari.server.orm.DBAccessor;
@@ -246,6 +248,8 @@ public class UserResourceProviderTest extends EasyMockSupport {
         bind(RoleCommandOrderProvider.class).to(CachedRoleCommandOrderProvider.class);
         bind(CredentialStoreService.class).to(CredentialStoreServiceImpl.class);
         bind(HostRoleCommandDAO.class).toInstance(createMock(HostRoleCommandDAO.class));
+        bind(HookService.class).toInstance(createMock(HookService.class));
+        bind(HookContextFactory.class).toInstance(createMock(HookContextFactory.class));
       }
     });
   }

+ 224 - 0
ambari-server/src/test/java/org/apache/ambari/server/hooks/users/UserHookServiceTest.java

@@ -0,0 +1,224 @@
+/**
+ * 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.hooks.users;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.ambari.server.Role;
+import org.apache.ambari.server.RoleCommand;
+import org.apache.ambari.server.actionmanager.ActionManager;
+import org.apache.ambari.server.actionmanager.RequestFactory;
+import org.apache.ambari.server.actionmanager.Stage;
+import org.apache.ambari.server.actionmanager.StageFactory;
+import org.apache.ambari.server.configuration.Configuration;
+import org.apache.ambari.server.events.publishers.AmbariEventPublisher;
+import org.apache.ambari.server.hooks.AmbariEventFactory;
+import org.apache.ambari.server.hooks.HookContext;
+import org.apache.ambari.server.state.Cluster;
+import org.apache.ambari.server.state.Clusters;
+import org.apache.ambari.server.state.SecurityType;
+import org.apache.ambari.server.state.svccomphost.ServiceComponentHostServerActionEvent;
+import org.codehaus.jackson.map.ObjectMapper;
+import org.easymock.Capture;
+import org.easymock.EasyMock;
+import org.easymock.EasyMockRule;
+import org.easymock.EasyMockSupport;
+import org.easymock.Mock;
+import org.easymock.TestSubject;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+
+public class UserHookServiceTest extends EasyMockSupport {
+
+  @Rule
+  public EasyMockRule mocks = new EasyMockRule(this);
+
+  @Mock
+  private AmbariEventFactory eventFactoryMock;
+
+  @Mock
+  private AmbariEventPublisher ambariEventPublisherMock;
+
+  @Mock
+  private ActionManager actionManagerMock;
+
+  @Mock
+  private RequestFactory requestFactoryMock;
+
+  @Mock
+  private StageFactory stageFactoryMock;
+
+  @Mock
+  private Configuration configurationMock;
+
+  @Mock
+  private Clusters clustersMock;
+
+  @Mock
+  private ObjectMapper objectMapperMock;
+
+  @Mock
+  private Map<String, Cluster> clustersMap;
+
+  @Mock
+  private Cluster clusterMock;
+
+  @Mock
+  private Stage stageMock;
+
+  @TestSubject
+  private UserHookService hookService = new UserHookService();
+
+
+  private HookContext hookContext;
+  private Map<String, Set<String>> usersToGroups;
+  private UserCreatedEvent userCreatedEvent;
+
+  @Before
+  public void before() throws Exception {
+    usersToGroups = new HashMap<>();
+    usersToGroups.put("testUser", new HashSet<String>(Arrays.asList("hdfs", "yarn")));
+    hookContext = new PostUserCreationHookContext(usersToGroups);
+
+    userCreatedEvent = new UserCreatedEvent(hookContext);
+
+    resetAll();
+
+  }
+
+  @Test
+  public void shouldServiceQuitWhenFeatureIsDisabled() {
+    // GIVEN
+    EasyMock.expect(configurationMock.isUserHookEnabled()).andReturn(Boolean.FALSE);
+    replayAll();
+
+    // WHEN
+    Boolean triggered = hookService.execute(hookContext);
+
+    //THEN
+    Assert.assertFalse("The hook must not be triggered if feature is disabled!", triggered);
+
+  }
+
+
+  @Test
+  public void shouldServiceQuitWhenClusterDoesNotExist() {
+    // GIVEN
+    EasyMock.expect(configurationMock.isUserHookEnabled()).andReturn(Boolean.TRUE);
+    EasyMock.expect(clustersMap.isEmpty()).andReturn(Boolean.TRUE);
+    EasyMock.expect(clustersMock.getClusters()).andReturn(clustersMap);
+
+
+    replayAll();
+
+    // WHEN
+    Boolean triggered = hookService.execute(hookContext);
+
+    //THEN
+    Assert.assertFalse("The hook must not be triggered if there's no cluster!", triggered);
+
+  }
+
+
+  @Test
+  public void shouldServiceQuitWhenCalledWithEmptyContext() {
+    // GIVEN
+    EasyMock.expect(configurationMock.isUserHookEnabled()).andReturn(Boolean.TRUE);
+    EasyMock.expect(clustersMap.isEmpty()).andReturn(Boolean.FALSE);
+    EasyMock.expect(clustersMock.getClusters()).andReturn(clustersMap);
+
+    replayAll();
+
+    // WHEN
+    Boolean triggered = hookService.execute(new PostUserCreationHookContext(Collections.<String, Set<String>>emptyMap()));
+
+    //THEN
+    Assert.assertFalse("The hook should not be triggered if there is no users in the context!", triggered);
+
+  }
+
+
+  @Test
+  public void shouldServiceTriggerHookWhenPrerequisitesAreSatisfied() {
+    // GIVEN
+    EasyMock.expect(configurationMock.isUserHookEnabled()).andReturn(Boolean.TRUE);
+    EasyMock.expect(clustersMap.isEmpty()).andReturn(Boolean.FALSE);
+    EasyMock.expect(clustersMock.getClusters()).andReturn(clustersMap);
+
+    Capture<HookContext> contextCapture = EasyMock.newCapture();
+    EasyMock.expect(eventFactoryMock.newUserCreatedEvent(EasyMock.capture(contextCapture))).andReturn(userCreatedEvent);
+
+    Capture<UserCreatedEvent> userCreatedEventCapture = EasyMock.newCapture();
+    ambariEventPublisherMock.publish(EasyMock.capture(userCreatedEventCapture));
+
+    replayAll();
+
+    // WHEN
+    Boolean triggered = hookService.execute(hookContext);
+
+    //THEN
+    Assert.assertTrue("The hook must be triggered if prerequisites satisfied!", triggered);
+    Assert.assertEquals("The hook context the event is generated from is not as expected ", hookContext, contextCapture.getValue());
+    Assert.assertEquals("The user created event is not the expected ", userCreatedEvent, userCreatedEventCapture.getValue());
+
+  }
+
+  @Test
+  public void shouldCommandParametersBeSet() throws Exception {
+    // GIVEN
+    Map<String, Cluster> clsMap = new HashMap<>();
+    clsMap.put("test-cluster", clusterMock);
+
+    EasyMock.expect(clusterMock.getClusterId()).andReturn(1l);
+    EasyMock.expect(clusterMock.getClusterName()).andReturn("test-cluster");
+    EasyMock.expect(clusterMock.getSecurityType()).andReturn(SecurityType.NONE).times(3);
+
+
+    EasyMock.expect(actionManagerMock.getNextRequestId()).andReturn(1l);
+    EasyMock.expect(clustersMock.getClusters()).andReturn(clsMap);
+    EasyMock.expect(configurationMock.getServerTempDir()).andReturn("/var/lib/ambari-server/tmp").times(2);
+    EasyMock.expect(configurationMock.getProperty(Configuration.POST_USER_CREATION_HOOK)).andReturn("/var/lib/ambari-server/resources/scripts/post-user-creation-hook.sh").anyTimes();
+    EasyMock.expect(objectMapperMock.writeValueAsString(((PostUserCreationHookContext) userCreatedEvent.getContext()).getUserGroups())).andReturn("{testUser=[hdfs, yarn]}");
+    stageMock.setStageId(-1);
+
+    // TBD refine expectations to validate the logic / eg capture arguments
+    stageMock.addServerActionCommand(EasyMock.anyString(), EasyMock.anyString(), EasyMock.anyObject(Role.class), EasyMock.anyObject(RoleCommand.class), EasyMock.anyString(), EasyMock.anyObject(ServiceComponentHostServerActionEvent.class),
+        EasyMock.anyObject(Map.class), EasyMock.anyString(), EasyMock.anyObject(Map.class), EasyMock.anyInt(), EasyMock.anyBoolean(), EasyMock.anyBoolean());
+    EasyMock.expect(requestFactoryMock.createNewFromStages(Arrays.asList(stageMock))).andReturn(null);
+    EasyMock.expect(stageFactoryMock.createNew(1, "/var/lib/ambari-server/tmp:1", "test-cluster", 1, "Post user creation hook for [ 1 ] users", "{}", "{}", "{}")).andReturn(stageMock);
+
+
+    replayAll();
+
+    //WHEN
+    hookService.onUserCreatedEvent(userCreatedEvent);
+
+    //THEN
+    // TBD assertions on the captured arguments!
+
+  }
+}

+ 4 - 0
ambari-server/src/test/java/org/apache/ambari/server/security/authorization/AmbariAuthorizationFilterTest.java

@@ -28,6 +28,8 @@ import junit.framework.Assert;
 
 import org.apache.ambari.server.audit.AuditLogger;
 import org.apache.ambari.server.configuration.Configuration;
+import org.apache.ambari.server.hooks.HookContextFactory;
+import org.apache.ambari.server.hooks.HookService;
 import org.apache.ambari.server.orm.DBAccessor;
 import org.apache.ambari.server.orm.dao.UserDAO;
 import org.apache.ambari.server.security.AmbariEntryPoint;
@@ -322,6 +324,8 @@ public class AmbariAuthorizationFilterTest {
         bind(PasswordEncoder.class).toInstance(EasyMock.createMock(PasswordEncoder.class));
         bind(OsFamily.class).toInstance(EasyMock.createMock(OsFamily.class));
         bind(AuditLogger.class).toInstance(EasyMock.createNiceMock(AuditLogger.class));
+        bind(HookService.class).toInstance(EasyMock.createMock(HookService.class));
+        bind(HookContextFactory.class).toInstance(EasyMock.createMock(HookContextFactory.class));
       }
     });
 

+ 25 - 3
ambari-server/src/test/java/org/apache/ambari/server/security/authorization/AmbariLdapAuthenticationProviderForDNWithSpaceTest.java

@@ -17,13 +17,18 @@
  */
 package org.apache.ambari.server.security.authorization;
 
+import java.util.Properties;
+
 import com.google.inject.Guice;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 import com.google.inject.persist.PersistService;
+import com.google.inject.persist.jpa.AmbariJpaPersistModule;
+import com.google.inject.persist.jpa.AmbariJpaPersistService;
 
 import org.apache.ambari.server.audit.AuditLoggerModule;
 import org.apache.ambari.server.configuration.Configuration;
+import org.apache.ambari.server.controller.ControllerModule;
 import org.apache.ambari.server.orm.GuiceJpaInitializer;
 import org.apache.ambari.server.orm.dao.UserDAO;
 import org.apache.ambari.server.security.ClientSecurityType;
@@ -71,14 +76,16 @@ public class AmbariLdapAuthenticationProviderForDNWithSpaceTest extends AmbariLd
   private UserDAO userDAO;
   @Inject
   private Users users;
+
   @Inject
   Configuration configuration;
 
   @Before
-  public void setUp() {
-    injector = Guice.createInjector(new AuditLoggerModule(), new AuthorizationTestModuleForLdapDNWithSpace());
-    injector.injectMembers(this);
+  public void setUp() throws Exception {
+    injector = Guice.createInjector(new ControllerModule(getTestProperties()), new AuditLoggerModule());
     injector.getInstance(GuiceJpaInitializer.class);
+    injector.injectMembers(this);
+
     configuration.setClientSecurityType(ClientSecurityType.LDAP);
     configuration.setProperty(Configuration.LDAP_PRIMARY_URL, "localhost:" + getLdapServer().getPort());
   }
@@ -112,4 +119,19 @@ public class AmbariLdapAuthenticationProviderForDNWithSpaceTest extends AmbariLd
     Authentication auth = authenticationProvider.authenticate(authentication);
     assertTrue(auth == null);
   }
+
+
+  protected Properties getTestProperties() {
+    Properties properties = new Properties();
+    properties.setProperty(Configuration.CLIENT_SECURITY.getKey(), "ldap");
+    properties.setProperty(Configuration.SERVER_PERSISTENCE_TYPE.getKey(), "in-memory");
+    properties.setProperty(Configuration.METADATA_DIR_PATH.getKey(), "src/test/resources/stacks");
+    properties.setProperty(Configuration.SERVER_VERSION_FILE.getKey(), "src/test/resources/version");
+    properties.setProperty(Configuration.OS_VERSION.getKey(), "centos5");
+    properties.setProperty(Configuration.SHARED_RESOURCES_DIR.getKey(), "src/test/resources/");
+    //make ambari detect active configuration
+    properties.setProperty(Configuration.LDAP_BASE_DN.getKey(), "dc=ambari,dc=the apache,dc=org");
+    properties.setProperty(Configuration.LDAP_GROUP_BASE.getKey(), "ou=the groups,dc=ambari,dc=the apache,dc=org");
+    return properties;
+  }
 }

+ 10 - 0
ambari-server/src/test/java/org/apache/ambari/server/security/authorization/UsersTest.java

@@ -21,7 +21,12 @@ package org.apache.ambari.server.security.authorization;
 import com.google.inject.AbstractModule;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
+
 import junit.framework.Assert;
+
+import org.apache.ambari.server.hooks.HookContextFactory;
+import org.apache.ambari.server.hooks.HookService;
+import org.apache.ambari.server.hooks.users.UserHookService;
 import org.apache.ambari.server.orm.DBAccessor;
 import org.apache.ambari.server.orm.dao.MemberDAO;
 import org.apache.ambari.server.orm.dao.PrivilegeDAO;
@@ -51,6 +56,8 @@ import static org.easymock.EasyMock.expect;
 import static org.easymock.EasyMock.newCapture;
 
 public class UsersTest extends EasyMockSupport {
+
+
   @Test
   public void testGetUserAuthorities() throws Exception {
     Injector injector = getInjector();
@@ -106,6 +113,7 @@ public class UsersTest extends EasyMockSupport {
     expect(privilegeDAO.findAllByPrincipal(capture(principalEntitiesCapture))).andReturn(privilegeEntities).times(1);
     expect(privilegeDAO.findAllByPrincipal(capture(rolePrincipalEntitiesCapture))).andReturn(rolePrivilegeEntities).times(1);
 
+
     replayAll();
 
     Users user = injector.getInstance(Users.class);
@@ -139,6 +147,8 @@ public class UsersTest extends EasyMockSupport {
         bind(MemberDAO.class).toInstance(createMock(MemberDAO.class));
         bind(PrivilegeDAO.class).toInstance(createMock(PrivilegeDAO.class));
         bind(PasswordEncoder.class).toInstance(createMock(PasswordEncoder.class));
+        bind(HookService.class).toInstance(createMock(HookService.class));
+        bind(HookContextFactory.class).toInstance(createMock(HookContextFactory.class));
       }
     });
   }

+ 182 - 0
ambari-server/src/test/java/org/apache/ambari/server/serveraction/users/PostUserCreationHookServerActionTest.java

@@ -0,0 +1,182 @@
+/**
+ * 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.users;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentMap;
+
+import org.apache.ambari.server.AmbariException;
+import org.apache.ambari.server.Role;
+import org.apache.ambari.server.RoleCommand;
+import org.apache.ambari.server.actionmanager.HostRoleCommand;
+import org.apache.ambari.server.agent.CommandReport;
+import org.apache.ambari.server.agent.ExecutionCommand;
+import org.apache.ambari.server.hooks.users.UserHookParams;
+import org.apache.ambari.server.state.SecurityType;
+import org.apache.ambari.server.utils.ShellCommandUtil;
+import org.codehaus.jackson.map.ObjectMapper;
+import org.easymock.Capture;
+import org.easymock.EasyMock;
+import org.easymock.EasyMockRule;
+import org.easymock.EasyMockSupport;
+import org.easymock.Mock;
+import org.easymock.TestSubject;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.Maps;
+
+/**
+ * Test suite for the PostUserCreationHookServer action class.
+ */
+public class PostUserCreationHookServerActionTest extends EasyMockSupport {
+
+  private static final Logger LOGGER = LoggerFactory.getLogger(PostUserCreationHookServerActionTest.class);
+
+  @Rule
+  public EasyMockRule mocks = new EasyMockRule(this);
+
+  @Mock
+  private ShellCommandUtilityWrapper shellCommandUtilityWrapper;
+
+  @Mock
+  private ExecutionCommand executionCommand;
+
+  @Mock
+  private HostRoleCommand hostRoleCommand;
+
+  @Mock
+  private ObjectMapper objectMapperMock;
+
+  @Mock
+  private CollectionPersisterServiceFactory collectionPersisterServiceFactoryMock;
+
+  @Mock
+  private CsvFilePersisterService collectionPersisterService;
+
+  @TestSubject
+  private PostUserCreationHookServerAction customScriptServerAction = new PostUserCreationHookServerAction();
+
+  private ConcurrentMap<String, Object> requestSharedDataContext = Maps.newConcurrentMap();
+
+  private Capture<String[]> commandCapture = null;
+
+  private Map<String, List<String>> payload = new HashMap<>();
+
+  private ObjectMapper om = new ObjectMapper();
+
+  @Before
+  public void before() throws IOException, InterruptedException {
+    payload.clear();
+    resetAll();
+
+    EasyMock.expect(hostRoleCommand.getRequestId()).andReturn(-1l).times(2);
+    EasyMock.expect(hostRoleCommand.getStageId()).andReturn(-1l).times(2);
+  }
+
+
+  @Test
+  public void shouldCommandStringBeAssembledCorrectlyForSingleUser() throws Exception {
+    // GIVEN
+
+    payload = mockPayload(1);
+    mockExecutionCommand(payload.size());
+    String payloadJson = om.writeValueAsString(payload);
+
+    // command params as passed to the serveraction implementation
+    Map<String, String> commandParams = new HashMap<>();
+    commandParams.put(UserHookParams.PAYLOAD.param(), payloadJson);
+    commandParams.put(UserHookParams.SCRIPT.param(), "/hookfolder/hook.name");
+    commandParams.put(UserHookParams.CMD_TIME_FRAME.param(), "1000");
+    commandParams.put(UserHookParams.CMD_INPUT_FILE.param(), "/test/user_data.csv");
+    commandParams.put(UserHookParams.CLUSTER_SECURITY_TYPE.param(), SecurityType.KERBEROS.name());
+
+    EasyMock.expect(executionCommand.getCommandParams()).andReturn(commandParams);
+    EasyMock.expect(objectMapperMock.readValue(payloadJson, Map.class)).andReturn(payload);
+
+    // captures the command arguments passed to the shell callable through the factory
+    commandCapture = EasyMock.newCapture();
+
+    // the callable mock returns a dummy result, no assertions made on the result
+    EasyMock.expect(shellCommandUtilityWrapper.runCommand(EasyMock.capture(commandCapture))).andReturn(new ShellCommandUtil.Result(0, null, null)).times(payload.size());
+
+    customScriptServerAction.setExecutionCommand(executionCommand);
+
+    EasyMock.expect(collectionPersisterServiceFactoryMock.createCsvFilePersisterService(EasyMock.anyString())).andReturn(collectionPersisterService);
+    EasyMock.expect(collectionPersisterService.persistMap(EasyMock.anyObject(Map.class))).andReturn(Boolean.TRUE);
+
+    replayAll();
+
+    // WHEN
+    CommandReport commandReport = customScriptServerAction.execute(requestSharedDataContext);
+
+    // THEN
+    String[] commandArray = commandCapture.getValue();
+    Assert.assertNotNull("The command to be executed must not be null!", commandArray);
+
+    Assert.assertEquals("The command argument array length is not as expected!", 5, commandArray.length);
+    Assert.assertEquals("The command script is not as expected", "/hookfolder/hook.name", commandArray[0]);
+  }
+
+
+  @Test(expected = AmbariException.class)
+  public void shouldServerActionFailWhenCommandParametersAreMissing() throws Exception {
+    //GIVEN
+    Map<String, String> commandParams = new HashMap<>();
+    // the execution command lacks the required command parameters (commandparams is an empty list)
+    EasyMock.expect(executionCommand.getCommandParams()).andReturn(commandParams).times(2);
+
+    customScriptServerAction.setExecutionCommand(executionCommand);
+    replayAll();
+
+    // WHEN
+    CommandReport commandReport = customScriptServerAction.execute(requestSharedDataContext);
+
+    //THEN
+    //exception is thrown
+  }
+
+  private void mockExecutionCommand(int callCnt) {
+    EasyMock.expect(executionCommand.getRoleCommand()).andReturn(RoleCommand.EXECUTE).times(callCnt);
+    EasyMock.expect(executionCommand.getClusterName()).andReturn("unit-test-cluster").times(callCnt);
+    EasyMock.expect(executionCommand.getConfigurationTags()).andReturn(Collections.<String, Map<String, String>>emptyMap()).times(callCnt);
+    EasyMock.expect(executionCommand.getRole()).andReturn(Role.AMBARI_SERVER_ACTION.toString()).times(callCnt);
+    EasyMock.expect(executionCommand.getServiceName()).andReturn("custom-hook-script").times(callCnt);
+    EasyMock.expect(executionCommand.getTaskId()).andReturn(-1l).times(callCnt);
+  }
+
+  private Map<String, List<String>> mockPayload(int size) {
+    Map<String, List<String>> ret = new HashMap<>();
+    for (int i = 0; i < size; i++) {
+      ret.put("user-" + i, Arrays.asList("hdfs" + i, "yarn" + i));
+    }
+    return ret;
+  }
+
+
+}

+ 4 - 1
ambari-server/src/test/java/org/apache/ambari/server/state/cluster/ClusterEffectiveVersionTest.java

@@ -32,6 +32,8 @@ import org.apache.ambari.server.controller.AmbariManagementController;
 import org.apache.ambari.server.controller.KerberosHelper;
 import org.apache.ambari.server.controller.spi.ClusterController;
 import org.apache.ambari.server.events.publishers.AmbariEventPublisher;
+import org.apache.ambari.server.hooks.HookContextFactory;
+import org.apache.ambari.server.hooks.HookService;
 import org.apache.ambari.server.orm.DBAccessor;
 import org.apache.ambari.server.orm.dao.ClusterDAO;
 import org.apache.ambari.server.orm.dao.HostRoleCommandDAO;
@@ -270,7 +272,8 @@ public class ClusterEffectiveVersionTest extends EasyMockSupport {
       binder.bind(KerberosHelper.class).toInstance(EasyMock.createNiceMock(KerberosHelper.class));
       binder.bind(Users.class).toInstance(EasyMock.createNiceMock(Users.class));
       binder.bind(AmbariEventPublisher.class).toInstance(createNiceMock(AmbariEventPublisher.class));
-
+      binder.bind(HookContextFactory.class).toInstance(createMock(HookContextFactory.class));
+      binder.bind(HookService.class).toInstance(createMock(HookService.class));
       binder.install(new FactoryModuleBuilder().implement(
           Cluster.class, ClusterImpl.class).build(ClusterFactory.class));
 

+ 38 - 21
ambari-server/src/test/java/org/apache/ambari/server/topology/AsyncCallableServiceTest.java

@@ -19,6 +19,7 @@
 package org.apache.ambari.server.topology;
 
 import org.easymock.EasyMockRule;
+import org.easymock.EasyMockSupport;
 import org.easymock.Mock;
 import org.easymock.MockType;
 import org.junit.Assert;
@@ -29,14 +30,17 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 
 import static org.easymock.EasyMock.expect;
-import static org.easymock.EasyMock.replay;
 import static org.easymock.EasyMock.verify;
 
-public class AsyncCallableServiceTest {
+public class AsyncCallableServiceTest extends EasyMockSupport {
   public static final Logger LOGGER = LoggerFactory.getLogger(AsyncCallableService.class);
 
   @Rule
@@ -45,7 +49,11 @@ public class AsyncCallableServiceTest {
   @Mock(type = MockType.STRICT)
   private Callable<Boolean> taskMock;
 
-  private ExecutorService singleThreadedExecutor = Executors.newSingleThreadExecutor();
+  @Mock
+  private ScheduledExecutorService executorServiceMock;
+
+  @Mock
+  private ScheduledFuture<Boolean> futureMock;
 
   private long timeout;
 
@@ -66,18 +74,30 @@ public class AsyncCallableServiceTest {
   @Test
   public void testCallableServiceShouldCancelTaskWhenTimeoutExceeded() throws Exception {
     // GIVEN
+
+    //the timeout period should be small!!!
+    timeout = 1l;
+
     // the task to be executed never completes successfully
-    expect(taskMock.call()).andThrow(new IllegalStateException("Prerequisites are not yet satisfied!")).anyTimes();
-    replay(taskMock);
-    asyncCallableService = new AsyncCallableService(taskMock, timeout, delay, Executors.newScheduledThreadPool(1));
+    expect(futureMock.get(timeout, TimeUnit.MILLISECONDS)).andThrow(new TimeoutException("Testing the timeout exceeded case"));
+    expect(futureMock.isDone()).andReturn(Boolean.FALSE);
+
+    // this is only called when a timeout occurs
+    expect(futureMock.cancel(true)).andReturn(Boolean.TRUE);
+
+    expect(executorServiceMock.submit(taskMock)).andReturn(futureMock);
+
+    replayAll();
+
+    asyncCallableService = new AsyncCallableService(taskMock, timeout, delay, executorServiceMock);
 
     // WHEN
     Boolean serviceResult = asyncCallableService.call();
 
     // THEN
     verify();
-    Assert.assertNotNull("Service result must not be null", serviceResult);
-    Assert.assertFalse("The expected boolean result is 'false'!", serviceResult);
+    Assert.assertNull("Service result must be null", serviceResult);
+    Assert.assertFalse("The service should have errors!", asyncCallableService.getErrors().isEmpty());
   }
 
   @Test
@@ -98,8 +118,8 @@ public class AsyncCallableServiceTest {
     Boolean serviceResult = asyncCallableService.call();
 
     // THEN
-    Assert.assertNotNull("Service result must not be null", serviceResult);
-    Assert.assertFalse("The expected boolean result is 'false'!", serviceResult);
+    Assert.assertNull("Service result must be null", serviceResult);
+    Assert.assertFalse("The service should have errors!", asyncCallableService.getErrors().isEmpty());
   }
 
   @Test
@@ -107,7 +127,8 @@ public class AsyncCallableServiceTest {
     // GIVEN
     // the task to be executed never completes successfully
     expect(taskMock.call()).andReturn(Boolean.TRUE).times(1);
-    replay(taskMock);
+
+    replayAll();
     asyncCallableService = new AsyncCallableService(taskMock, timeout, delay, Executors.newScheduledThreadPool(2));
 
     // WHEN
@@ -124,9 +145,8 @@ public class AsyncCallableServiceTest {
     // GIVEN
 
     // the task to be throws exception
-    expect(taskMock.call()).andThrow(new IllegalStateException("****************** TESTING ****************")).times
-        (2,3);
-    replay(taskMock);
+    expect(taskMock.call()).andThrow(new IllegalStateException("****************** TESTING ****************")).times(2, 3);
+    replayAll();
     asyncCallableService = new AsyncCallableService(taskMock, timeout, delay, Executors.newScheduledThreadPool(2));
 
     // WHEN
@@ -135,8 +155,7 @@ public class AsyncCallableServiceTest {
     // THEN
     verify();
     // THEN
-    Assert.assertNotNull("Service result must not be null", serviceResult);
-    Assert.assertFalse("The expected boolean result is 'false'!", serviceResult);
+    Assert.assertNull("Service result must be null", serviceResult);
 
   }
 
@@ -158,9 +177,7 @@ public class AsyncCallableServiceTest {
     Boolean serviceResult = asyncCallableService.call();
 
     // THEN
-    Assert.assertNotNull("Service result must not be null", serviceResult);
-    Assert.assertFalse("The expected boolean result is 'false'!", serviceResult);
-
-
+    verify();
+    Assert.assertNull("Service result must be null", serviceResult);
   }
 }

+ 33 - 0
ambari-server/src/test/java/org/apache/ambari/server/upgrade/UpgradeCatalog240Test.java

@@ -67,6 +67,8 @@ import org.apache.ambari.server.controller.AmbariManagementController;
 import org.apache.ambari.server.controller.AmbariManagementControllerImpl;
 import org.apache.ambari.server.controller.KerberosHelper;
 import org.apache.ambari.server.controller.MaintenanceStateHelper;
+import org.apache.ambari.server.hooks.HookContextFactory;
+import org.apache.ambari.server.hooks.HookService;
 import org.apache.ambari.server.orm.DBAccessor;
 import org.apache.ambari.server.orm.GuiceJpaInitializer;
 import org.apache.ambari.server.orm.InMemoryDefaultTestModule;
@@ -338,6 +340,8 @@ public class UpgradeCatalog240Test {
         binder.bind(OsFamily.class).toInstance(createNiceMock(OsFamily.class));
         binder.bind(EntityManager.class).toInstance(entityManager);
         binder.bind(PasswordEncoder.class).toInstance(createNiceMock(PasswordEncoder.class));
+        binder.bind(HookContextFactory.class).toInstance(createMock(HookContextFactory.class));
+        binder.bind(HookService.class).toInstance(createMock(HookService.class));
       }
       };
 
@@ -736,6 +740,8 @@ public class UpgradeCatalog240Test {
         binder.bind(DBAccessor.class).toInstance(createNiceMock(DBAccessor.class));
         binder.bind(OsFamily.class).toInstance(createNiceMock(OsFamily.class));
         binder.bind(PasswordEncoder.class).toInstance(createNiceMock(PasswordEncoder.class));
+        binder.bind(HookContextFactory.class).toInstance(createMock(HookContextFactory.class));
+        binder.bind(HookService.class).toInstance(createMock(HookService.class));
       }
     });
 
@@ -818,6 +824,8 @@ public class UpgradeCatalog240Test {
         binder.bind(DBAccessor.class).toInstance(createNiceMock(DBAccessor.class));
         binder.bind(OsFamily.class).toInstance(createNiceMock(OsFamily.class));
         binder.bind(PasswordEncoder.class).toInstance(createNiceMock(PasswordEncoder.class));
+        binder.bind(HookContextFactory.class).toInstance(createMock(HookContextFactory.class));
+        binder.bind(HookService.class).toInstance(createMock(HookService.class));
       }
     });
 
@@ -885,6 +893,9 @@ public class UpgradeCatalog240Test {
         binder.bind(DBAccessor.class).toInstance(createNiceMock(DBAccessor.class));
         binder.bind(OsFamily.class).toInstance(createNiceMock(OsFamily.class));
         binder.bind(PasswordEncoder.class).toInstance(createNiceMock(PasswordEncoder.class));
+        binder.bind(HookContextFactory.class).toInstance(createMock(HookContextFactory.class));
+        binder.bind(HookService.class).toInstance(createMock(HookService.class));
+
       }
     });
 
@@ -1526,6 +1537,8 @@ public class UpgradeCatalog240Test {
         bind(OsFamily.class).toInstance(osFamily);
         bind(EntityManager.class).toInstance(entityManager);
         bind(PasswordEncoder.class).toInstance(createNiceMock(PasswordEncoder.class));
+        bind(HookContextFactory.class).toInstance(createMock(HookContextFactory.class));
+        bind(HookService.class).toInstance(createMock(HookService.class));
       }
     });
 
@@ -1662,6 +1675,8 @@ public class UpgradeCatalog240Test {
         bind(OsFamily.class).toInstance(osFamily);
         bind(EntityManager.class).toInstance(entityManager);
         bind(PasswordEncoder.class).toInstance(createNiceMock(PasswordEncoder.class));
+        bind(HookContextFactory.class).toInstance(createMock(HookContextFactory.class));
+        bind(HookService.class).toInstance(createMock(HookService.class));
       }
     });
 
@@ -1720,6 +1735,8 @@ public class UpgradeCatalog240Test {
         bind(DBAccessor.class).toInstance(createNiceMock(DBAccessor.class));
         bind(OsFamily.class).toInstance(createNiceMock(OsFamily.class));
         bind(PasswordEncoder.class).toInstance(createNiceMock(PasswordEncoder.class));
+        bind(HookContextFactory.class).toInstance(createMock(HookContextFactory.class));
+        bind(HookService.class).toInstance(createMock(HookService.class));
       }
     });
 
@@ -1780,6 +1797,8 @@ public class UpgradeCatalog240Test {
         bind(DBAccessor.class).toInstance(createNiceMock(DBAccessor.class));
         bind(OsFamily.class).toInstance(createNiceMock(OsFamily.class));
         bind(PasswordEncoder.class).toInstance(createNiceMock(PasswordEncoder.class));
+        bind(HookContextFactory.class).toInstance(createMock(HookContextFactory.class));
+        bind(HookService.class).toInstance(createMock(HookService.class));
       }
     });
 
@@ -1982,6 +2001,8 @@ public class UpgradeCatalog240Test {
         bind(DBAccessor.class).toInstance(ems.createNiceMock(DBAccessor.class));
         bind(OsFamily.class).toInstance(ems.createNiceMock(OsFamily.class));
         bind(PasswordEncoder.class).toInstance(createNiceMock(PasswordEncoder.class));
+        bind(HookContextFactory.class).toInstance(createMock(HookContextFactory.class));
+        bind(HookService.class).toInstance(createMock(HookService.class));
       }
     });
 
@@ -2040,6 +2061,8 @@ public class UpgradeCatalog240Test {
         bind(StackManagerFactory.class).toInstance(createNiceMock(StackManagerFactory.class));
         bind(AmbariMetaInfo.class).toInstance(metaInfo);
         bind(PasswordEncoder.class).toInstance(createNiceMock(PasswordEncoder.class));
+        bind(HookContextFactory.class).toInstance(createMock(HookContextFactory.class));
+        bind(HookService.class).toInstance(createMock(HookService.class));
       }
     });
     expect(controller.getClusters()).andReturn(clusters).anyTimes();
@@ -2097,6 +2120,8 @@ public class UpgradeCatalog240Test {
         bind(RemoteAmbariClusterDAO.class).toInstance(clusterDAO);
         bind(ViewInstanceDAO.class).toInstance(instanceDAO);
         bind(PasswordEncoder.class).toInstance(createNiceMock(PasswordEncoder.class));
+        bind(HookContextFactory.class).toInstance(createMock(HookContextFactory.class));
+        bind(HookService.class).toInstance(createMock(HookService.class));
       }
     });
 
@@ -2248,6 +2273,8 @@ public class UpgradeCatalog240Test {
         bind(DBAccessor.class).toInstance(createNiceMock(DBAccessor.class));
         bind(OsFamily.class).toInstance(createNiceMock(OsFamily.class));
         bind(PasswordEncoder.class).toInstance(createNiceMock(PasswordEncoder.class));
+        bind(HookContextFactory.class).toInstance(createMock(HookContextFactory.class));
+        bind(HookService.class).toInstance(createMock(HookService.class));
       }
     });
 
@@ -2375,6 +2402,8 @@ public class UpgradeCatalog240Test {
         bind(EntityManager.class).toInstance(entityManager);
         bind(DBAccessor.class).toInstance(createNiceMock(DBAccessor.class));
         bind(OsFamily.class).toInstance(createNiceMock(OsFamily.class));
+        bind(HookContextFactory.class).toInstance(createMock(HookContextFactory.class));
+        bind(HookService.class).toInstance(createMock(HookService.class));
       }
     });
 
@@ -2499,6 +2528,8 @@ public class UpgradeCatalog240Test {
         bind(DBAccessor.class).toInstance(createMock(DBAccessor.class));
         bind(OsFamily.class).toInstance(createNiceMock(OsFamily.class));
         bind(EntityManager.class).toInstance(entityManager);
+        bind(HookContextFactory.class).toInstance(createMock(HookContextFactory.class));
+        bind(HookService.class).toInstance(createMock(HookService.class));
       }
     });
 
@@ -2535,6 +2566,8 @@ public class UpgradeCatalog240Test {
         bind(DBAccessor.class).toInstance(createMock(DBAccessor.class));
         bind(OsFamily.class).toInstance(createNiceMock(OsFamily.class));
         bind(EntityManager.class).toInstance(entityManager);
+        bind(HookContextFactory.class).toInstance(createMock(HookContextFactory.class));
+        bind(HookService.class).toInstance(createMock(HookService.class));
       }
     });