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

Project: http://git-wip-us.apache.org/repos/asf/ambari/repo
Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/a5fdae80
Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/a5fdae80
Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/a5fdae80

Branch: refs/heads/branch-2.5
Commit: a5fdae802210ae1f8d4fed2234f1651cbe61c2b5
Parents: 7ba1495
Author: Laszlo Puskas <lpus...@hortonworks.com>
Authored: Mon Nov 14 18:43:53 2016 +0100
Committer: Toader, Sebastian <stoa...@hortonworks.com>
Committed: Mon Nov 14 18:44:26 2016 +0100

----------------------------------------------------------------------
 ambari-server/src/main/assemblies/server.xml    |   8 +
 .../server/configuration/Configuration.java     |  17 ++
 .../server/controller/ControllerModule.java     |  42 ++-
 .../ambari/server/events/AmbariEvent.java       |  12 +-
 .../ambari/server/hooks/AmbariEventFactory.java |  33 +++
 .../apache/ambari/server/hooks/HookContext.java |  26 ++
 .../ambari/server/hooks/HookContextFactory.java |  44 +++
 .../apache/ambari/server/hooks/HookService.java |  36 +++
 .../users/PostUserCreationHookContext.java      |  55 ++++
 .../server/hooks/users/UserCreatedEvent.java    |  45 +++
 .../server/hooks/users/UserHookParams.java      |  49 ++++
 .../server/hooks/users/UserHookService.java     | 279 +++++++++++++++++++
 .../server/security/authorization/Users.java    | 119 +++++---
 .../serveraction/AbstractServerAction.java      |   2 +-
 .../server/serveraction/ServerAction.java       |   4 +-
 .../users/CollectionPersisterService.java       |  46 +++
 .../CollectionPersisterServiceFactory.java      |  24 ++
 .../users/CsvFilePersisterService.java          | 103 +++++++
 .../users/PostUserCreationHookServerAction.java | 163 +++++++++++
 .../users/ShellCommandCallableFactory.java      |  26 ++
 .../users/ShellCommandUtilityCallable.java      |  48 ++++
 .../users/ShellCommandUtilityWrapper.java       |  57 ++++
 .../server/topology/AsyncCallableService.java   |  25 +-
 .../ambari/server/utils/ShellCommandUtil.java   |   2 +-
 .../scripts/post-user-creation-hook.sh          | 133 +++++++++
 .../ActiveWidgetLayoutResourceProviderTest.java |   4 +
 .../StackUpgradeConfigurationMergeTest.java     |   5 +
 .../UserAuthorizationResourceProviderTest.java  |   4 +
 .../internal/UserResourceProviderTest.java      |   4 +
 .../server/hooks/users/UserHookServiceTest.java | 224 +++++++++++++++
 .../AmbariAuthorizationFilterTest.java          |   4 +
 ...uthenticationProviderForDNWithSpaceTest.java |  28 +-
 .../security/authorization/UsersTest.java       |  10 +
 .../PostUserCreationHookServerActionTest.java   | 182 ++++++++++++
 .../cluster/ClusterEffectiveVersionTest.java    |   5 +-
 .../topology/AsyncCallableServiceTest.java      |  59 ++--
 .../server/upgrade/UpgradeCatalog240Test.java   |  33 +++
 37 files changed, 1875 insertions(+), 85 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/assemblies/server.xml
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/assemblies/server.xml 
b/ambari-server/src/main/assemblies/server.xml
index b6e0723..9a193a9 100644
--- a/ambari-server/src/main/assemblies/server.xml
+++ b/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>

http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/configuration/Configuration.java
----------------------------------------------------------------------
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/configuration/Configuration.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/configuration/Configuration.java
index 45e2ea3..8d0dcce 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/configuration/Configuration.java
+++ 
b/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.
    */
@@ -5028,6 +5036,15 @@ public class Configuration {
   }
 
   /**
+   * 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
    */
   public int getParallelTopologyTaskCreationThreadPoolSize() {

http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/controller/ControllerModule.java
----------------------------------------------------------------------
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/controller/ControllerModule.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/controller/ControllerModule.java
index f593fc4..2c4bf5f 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/controller/ControllerModule.java
+++ 
b/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);
 

http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/events/AmbariEvent.java
----------------------------------------------------------------------
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/events/AmbariEvent.java 
b/ambari-server/src/main/java/org/apache/ambari/server/events/AmbariEvent.java
index 1d3ec39..7ec5972 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/events/AmbariEvent.java
+++ 
b/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;
   }
 
   /**

http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/hooks/AmbariEventFactory.java
----------------------------------------------------------------------
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/hooks/AmbariEventFactory.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/hooks/AmbariEventFactory.java
new file mode 100644
index 0000000..57cbd37
--- /dev/null
+++ 
b/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);
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/hooks/HookContext.java
----------------------------------------------------------------------
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/hooks/HookContext.java 
b/ambari-server/src/main/java/org/apache/ambari/server/hooks/HookContext.java
new file mode 100644
index 0000000..03bcc52
--- /dev/null
+++ 
b/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 {
+
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/hooks/HookContextFactory.java
----------------------------------------------------------------------
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/hooks/HookContextFactory.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/hooks/HookContextFactory.java
new file mode 100644
index 0000000..0a56d85
--- /dev/null
+++ 
b/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);
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/hooks/HookService.java
----------------------------------------------------------------------
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/hooks/HookService.java 
b/ambari-server/src/main/java/org/apache/ambari/server/hooks/HookService.java
new file mode 100644
index 0000000..f51ee1f
--- /dev/null
+++ 
b/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);
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/hooks/users/PostUserCreationHookContext.java
----------------------------------------------------------------------
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/hooks/users/PostUserCreationHookContext.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/hooks/users/PostUserCreationHookContext.java
new file mode 100644
index 0000000..c9e2f74
--- /dev/null
+++ 
b/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 +
+        '}';
+  }
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/hooks/users/UserCreatedEvent.java
----------------------------------------------------------------------
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/hooks/users/UserCreatedEvent.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/hooks/users/UserCreatedEvent.java
new file mode 100644
index 0000000..8e914dc
--- /dev/null
+++ 
b/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;
+  }
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/hooks/users/UserHookParams.java
----------------------------------------------------------------------
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/hooks/users/UserHookParams.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/hooks/users/UserHookParams.java
new file mode 100644
index 0000000..6970dcc
--- /dev/null
+++ 
b/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;
+  }
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/hooks/users/UserHookService.java
----------------------------------------------------------------------
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/hooks/users/UserHookService.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/hooks/users/UserHookService.java
new file mode 100644
index 0000000..c4ff1e4
--- /dev/null
+++ 
b/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;
+    }
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/Users.java
----------------------------------------------------------------------
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/Users.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/Users.java
index 5c45306..fb61449 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/security/authorization/Users.java
+++ 
b/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);

http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/serveraction/AbstractServerAction.java
----------------------------------------------------------------------
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/AbstractServerAction.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/AbstractServerAction.java
index ca4a92c..c1f2aaf 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/AbstractServerAction.java
+++ 
b/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

http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/serveraction/ServerAction.java
----------------------------------------------------------------------
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/ServerAction.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/ServerAction.java
index 7c69f52..4137ed1 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/ServerAction.java
+++ 
b/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

http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/CollectionPersisterService.java
----------------------------------------------------------------------
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/CollectionPersisterService.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/CollectionPersisterService.java
new file mode 100644
index 0000000..85f9671
--- /dev/null
+++ 
b/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);
+
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/CollectionPersisterServiceFactory.java
----------------------------------------------------------------------
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/CollectionPersisterServiceFactory.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/CollectionPersisterServiceFactory.java
new file mode 100644
index 0000000..5906e30
--- /dev/null
+++ 
b/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);
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/CsvFilePersisterService.java
----------------------------------------------------------------------
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/CsvFilePersisterService.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/CsvFilePersisterService.java
new file mode 100644
index 0000000..d8ffe98
--- /dev/null
+++ 
b/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);
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/PostUserCreationHookServerAction.java
----------------------------------------------------------------------
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/PostUserCreationHookServerAction.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/PostUserCreationHookServerAction.java
new file mode 100644
index 0000000..e713128
--- /dev/null
+++ 
b/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;
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/ShellCommandCallableFactory.java
----------------------------------------------------------------------
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/ShellCommandCallableFactory.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/ShellCommandCallableFactory.java
new file mode 100644
index 0000000..e34a88c
--- /dev/null
+++ 
b/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);
+
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/ShellCommandUtilityCallable.java
----------------------------------------------------------------------
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/ShellCommandUtilityCallable.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/ShellCommandUtilityCallable.java
new file mode 100644
index 0000000..a59bc75
--- /dev/null
+++ 
b/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);
+  }
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/ShellCommandUtilityWrapper.java
----------------------------------------------------------------------
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/ShellCommandUtilityWrapper.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/serveraction/users/ShellCommandUtilityWrapper.java
new file mode 100644
index 0000000..0539f76
--- /dev/null
+++ 
b/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);
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/topology/AsyncCallableService.java
----------------------------------------------------------------------
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/topology/AsyncCallableService.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/topology/AsyncCallableService.java
index fc7f190..95ab6b0 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/topology/AsyncCallableService.java
+++ 
b/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;
+  }
+
 }

http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/java/org/apache/ambari/server/utils/ShellCommandUtil.java
----------------------------------------------------------------------
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/utils/ShellCommandUtil.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/utils/ShellCommandUtil.java
index 8aa9c08..99f47c5 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/utils/ShellCommandUtil.java
+++ 
b/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;

http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/main/resources/scripts/post-user-creation-hook.sh
----------------------------------------------------------------------
diff --git 
a/ambari-server/src/main/resources/scripts/post-user-creation-hook.sh 
b/ambari-server/src/main/resources/scripts/post-user-creation-hook.sh
new file mode 100755
index 0000000..34169c1
--- /dev/null
+++ b/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 "$@"

http://git-wip-us.apache.org/repos/asf/ambari/blob/a5fdae80/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/ActiveWidgetLayoutResourceProviderTest.java
----------------------------------------------------------------------
diff --git 
a/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/ActiveWidgetLayoutResourceProviderTest.java
 
b/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/ActiveWidgetLayoutResourceProviderTest.java
index c592a20..28a8bc8 100644
--- 
a/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/ActiveWidgetLayoutResourceProviderTest.java
+++ 
b/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));
       }
     });
   }

Reply via email to