[KARAF-6579] Add Commands jaas:realm-add and jaas:module-add to manag… (#1057)

diff --git a/jaas/command/src/main/java/org/apache/karaf/jaas/command/ModuleAddCommand.java b/jaas/command/src/main/java/org/apache/karaf/jaas/command/ModuleAddCommand.java
new file mode 100644
index 0000000..934b118
--- /dev/null
+++ b/jaas/command/src/main/java/org/apache/karaf/jaas/command/ModuleAddCommand.java
@@ -0,0 +1,131 @@
+/*
+ * 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.karaf.jaas.command;
+
+import org.apache.karaf.jaas.command.completers.LoginModuleNameCompleter;
+import org.apache.karaf.jaas.config.JaasRealm;
+import org.apache.karaf.jaas.config.impl.Config;
+import org.apache.karaf.jaas.config.impl.Module;
+import org.apache.karaf.jaas.modules.BackingEngine;
+import org.apache.karaf.shell.api.action.Argument;
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.Completion;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Properties;
+
+@Command(scope = "jaas", name = "module-add", description = "Add a Login Module")
+@Service
+public class ModuleAddCommand extends JaasCommandSupport {
+
+    @Argument(index = 0, name = "loginModule", description = "Class Name of Login Module", required = true, multiValued = false)
+    @Completion(LoginModuleNameCompleter.class)
+    private String loginModule;
+
+    @Argument(index = 1, name = "properties", description = "Pair of Properties (key value)", required = false, multiValued = true)
+    private List<String> propertiesList;
+
+    @Override
+    protected Object doExecute(BackingEngine engine) throws Exception {
+        return null;
+    }
+
+    @Override
+    public Object execute() throws Exception {
+        // Fetch Realm
+        JaasRealm realm = (JaasRealm) session.get(JAAS_REALM);
+
+        if (realm == null) {
+            System.err.println("No JAAS Realm has been selected");
+            throw new IllegalStateException("No JAAS Realm has been selected");
+        }
+        if (!(realm instanceof Config)) {
+            System.err.println("Selected JAAS Realm was not added via jaas:add-realm, only those are supported!");
+            throw new IllegalStateException("Selected JAAS Realm was not added via jaas:add-realm, only those are supported!");
+        }
+
+        if (!checkIfClassExists(loginModule)) {
+            System.err.println("Module class '" + loginModule + "' is unknown!");
+            throw new IllegalArgumentException("Module class '" + loginModule + "' is unknown!");
+        }
+        Module module = createModuleFromCmdParameters(loginModule, propertiesList);
+
+        // Add the Login Module to the current Realm
+        List<Module> modulesList = new ArrayList<>(Arrays.asList(((Config) realm).getModules()));
+        modulesList.add(module);
+        Module[] newModules = modulesList.toArray(new Module[]{});
+        ((Config) realm).setModules(newModules);
+        return null;
+    }
+
+    /**
+     * Parses the Command Line Parameters given to create a valid Module and Properties from it.
+     * @param loginModule Class Name of the login Module
+     * @param propertiesList List of Properties interpreted as "key1 value1 key2 value2"
+     * @return Module
+     */
+    static Module createModuleFromCmdParameters(String loginModule, List<String> propertiesList) {
+        // Parse Properties
+        if (propertiesList != null && propertiesList.size() > 0 && (propertiesList.size() % 2) == 1) {
+            // Properties are uneven... bad
+            System.err.println("Properties have to be given as \"key1 value1 key2 value2 ...\" but number of Arguments is uneven!");
+            return null;
+        }
+        Properties properties = new Properties();
+        if (propertiesList != null) {
+            for (int i = 0; i < propertiesList.size(); i += 2) {
+                properties.put(propertiesList.get(i), propertiesList.get(i + 1));
+            }
+        }
+        // Assemble Login Module
+        Module module = new Module();
+        module.setClassName(loginModule);
+        module.setFlags("required");
+        module.setOptions(properties);
+        return module;
+    }
+
+    public String getLoginModule() {
+        return loginModule;
+    }
+
+    public void setLoginModule(String loginModule) {
+        this.loginModule = loginModule;
+    }
+
+    public List<String> getPropertiesList() {
+        return propertiesList;
+    }
+
+    public void setPropertiesList(List<String> propertiesList) {
+        this.propertiesList = propertiesList;
+    }
+
+    private boolean checkIfClassExists(String loginModule) {
+        try {
+            this.getClass().getClassLoader().loadClass(loginModule);
+            return true;
+        } catch (ClassNotFoundException e) {
+            return false;
+        }
+    }
+}
diff --git a/jaas/command/src/main/java/org/apache/karaf/jaas/command/RealmAddCommand.java b/jaas/command/src/main/java/org/apache/karaf/jaas/command/RealmAddCommand.java
new file mode 100644
index 0000000..b70655b
--- /dev/null
+++ b/jaas/command/src/main/java/org/apache/karaf/jaas/command/RealmAddCommand.java
@@ -0,0 +1,95 @@
+/*
+ * 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.karaf.jaas.command;
+
+import org.apache.karaf.jaas.command.completers.LoginModuleNameCompleter;
+import org.apache.karaf.jaas.command.completers.RealmCompleter;
+import org.apache.karaf.jaas.config.JaasRealm;
+import org.apache.karaf.jaas.config.impl.Config;
+import org.apache.karaf.jaas.config.impl.Module;
+import org.apache.karaf.jaas.modules.BackingEngine;
+import org.apache.karaf.shell.api.action.Argument;
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.Completion;
+import org.apache.karaf.shell.api.action.lifecycle.Reference;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.osgi.framework.BundleContext;
+
+import java.util.List;
+
+@Command(scope = "jaas", name = "realm-add", description = "Add a realm")
+@Service
+public class RealmAddCommand extends JaasCommandSupport {
+
+    @Reference
+    private BundleContext context;
+
+    @Argument(index = 0, name = "realmname", description = "Realm Name", required = true, multiValued = false)
+    @Completion(RealmCompleter.class)
+    private String realmname;
+
+    @Argument(index = 1, name = "loginModule", description = "Class Name of Login Module", required = true, multiValued = false)
+    @Completion(LoginModuleNameCompleter.class)
+    private String loginModule;
+
+    @Argument(index = 2, name = "properties", description = "Pair of Properties (key value)", required = false, multiValued = true)
+    private List<String> propertiesList;
+
+    @Override
+    protected Object doExecute(BackingEngine engine) throws Exception {
+        return null;
+    }
+
+    @Override
+    public Object execute() throws Exception {
+        Module initialModule = ModuleAddCommand.createModuleFromCmdParameters(loginModule, propertiesList);
+        if (initialModule == null) {
+            // If we could not identify a Module we cannot create a realm => exit (sys.err was already written)
+            return null;
+        }
+        // Create realm
+        Config realm = new Config();
+        realm.setName(realmname);
+        realm.setModules(new Module[]{initialModule});
+        realm.setBundleContext(context);
+
+        context.registerService(JaasRealm.class, realm, null);
+        return null;
+    }
+
+    public BundleContext getContext() {
+        return context;
+    }
+
+    public void setContext(BundleContext context) {
+        this.context = context;
+    }
+
+    public String getRealmname() {
+        return realmname;
+    }
+
+    public void setRealmname(String realmname) {
+        this.realmname = realmname;
+    }
+
+    @Override
+    public String toString() {
+        return "RealmAddCommand{" +
+            "realmname='" + realmname + '\'' +
+            '}';
+    }
+}
diff --git a/jaas/command/src/test/java/org/apache/karaf/jaas/command/ManageRealmCommandTest.java b/jaas/command/src/test/java/org/apache/karaf/jaas/command/ManageRealmCommandTest.java
index 71397a1..206220c 100644
--- a/jaas/command/src/test/java/org/apache/karaf/jaas/command/ManageRealmCommandTest.java
+++ b/jaas/command/src/test/java/org/apache/karaf/jaas/command/ManageRealmCommandTest.java
@@ -16,21 +16,27 @@
 package org.apache.karaf.jaas.command;
 
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.Properties;
 
 import org.apache.karaf.jaas.config.impl.Config;
 import org.apache.karaf.jaas.config.impl.Module;
 import org.apache.karaf.shell.api.console.Session;
+import org.easymock.Capture;
 import org.junit.Test;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleContext;
 
 import static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.capture;
 import static org.easymock.EasyMock.createMock;
 import static org.easymock.EasyMock.eq;
 import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.newCapture;
 import static org.easymock.EasyMock.replay;
 import static org.easymock.EasyMock.verify;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
 
 public class ManageRealmCommandTest {
 
@@ -52,6 +58,74 @@
         doVerifyIndex(cmd, 2, realms);
     }
 
+    @Test
+    public void testRealmAdd() throws Exception {
+        RealmAddCommand cmd = new RealmAddCommand();
+        cmd.setRealmname("myDummyRealm");
+
+        // prepare mocks
+        Session session = createMock(Session.class);
+        BundleContext bundleContext = createMock(BundleContext.class);
+        Bundle bundle = createMock(Bundle.class);
+
+        // prepare command
+        cmd.setContext(bundleContext);
+        cmd.setSession(session);
+
+        Object[] mocks = { session, bundleContext, bundle };
+
+        expect(bundleContext.registerService(anyObject(Class.class),
+            (Object)anyObject(), anyObject())).andReturn(null).anyTimes();
+
+        replay(mocks);
+        cmd.execute();
+        verify(mocks);
+    }
+
+    @Test
+    public void testModuleAdd() throws Exception {
+        RealmAddCommand cmd = new RealmAddCommand();
+        cmd.setRealmname("myDummyRealm");
+
+        ModuleAddCommand addCmd = new ModuleAddCommand();
+        addCmd.setLoginModule(DummyClass.class.getName());
+        addCmd.setPropertiesList(Collections.emptyList());
+
+        // prepare mocks
+        Session session = createMock(Session.class);
+        BundleContext bundleContext = createMock(BundleContext.class);
+        Bundle bundle = createMock(Bundle.class);
+
+        // prepare command
+        cmd.setContext(bundleContext);
+        cmd.setSession(session);
+        addCmd.setSession(session);
+
+        Object[] mocks = { session, bundleContext, bundle };
+
+        expect(session.get(ManageRealmCommand.JAAS_ENTRY)).andReturn(null).anyTimes();
+        expect(session.get(ManageRealmCommand.JAAS_CMDS)).andReturn(null).anyTimes();
+        expect(bundleContext.getBundle()).andReturn(bundle).anyTimes();
+        expect(bundle.getBundleId()).andReturn(4711L).anyTimes();
+
+        Capture<Object> captureSingleArgument = newCapture();
+        expect(bundleContext.registerService(anyObject(Class.class),
+            (Object)capture(captureSingleArgument), anyObject())).andReturn(null).anyTimes();
+        expect(session.get(ManageRealmCommand.JAAS_REALM)).andAnswer(() -> captureSingleArgument.getValue()).anyTimes();
+
+        replay(mocks);
+        cmd.execute();
+        addCmd.execute();
+        verify(mocks);
+
+        assertNotNull((Config) captureSingleArgument.getValue());
+
+        // Now check if two modules are installed (1 initial + 1 addon)
+        assertEquals(2, ((Config) captureSingleArgument.getValue()).getModules().length);
+    }
+
+    public static class DummyClass {}
+
     /**
      * Verify that command selects the correct realm, given some index.
      *
diff --git a/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/BackingEngine.java b/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/BackingEngine.java
index b5d7275..fa9ea1f 100644
--- a/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/BackingEngine.java
+++ b/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/BackingEngine.java
@@ -21,6 +21,7 @@
 import org.apache.karaf.jaas.boot.principal.GroupPrincipal;
 import org.apache.karaf.jaas.boot.principal.RolePrincipal;
 import org.apache.karaf.jaas.boot.principal.UserPrincipal;
+import org.apache.karaf.jaas.config.impl.Module;
 
 public interface BackingEngine {
 
diff --git a/manual/src/main/asciidoc/user-guide/security.adoc b/manual/src/main/asciidoc/user-guide/security.adoc
index 23ca535..a0209cf 100644
--- a/manual/src/main/asciidoc/user-guide/security.adoc
+++ b/manual/src/main/asciidoc/user-guide/security.adoc
@@ -64,6 +64,29 @@
 
 You can manage an existing realm, login module, or create your own realm using the `jaas:realm-manage` command.
 
+===== Adding Realms or Login Modules
+
+If a new realm should be added Apache Karaf provides an easy way to create a new Realm (although with limited flexibility compared to other approaches like blueprint or directly via DS).
+The `jaas:realm-add` command can be used for that purpose.
+Note, that it takes at least 2 Parameters (name of the realm and class name of the initial Login Module) or even more, if the Login Module needs parameters.
+For example, a new realm `myrealm` which uses the `PropertiesLoginModule` with a `users` file located in `/tmp/users` can be added by the command
+
+----
+jaas:realm-add myrealm org.apache.karaf.jaas.modules.properties.PropertiesLoginModule users "/tmp/users"
+----
+
+To add a new Login Module to an existing realm the `jaas:module-add` command can be used. It is similar in semantics than the `jaas:realm-add` command except that it takes no realm name.
+Instead, the realm to modify has to be selected via `jaas:realm-manage` previously.
+Note that the options for the Login Module are given as a list of parameters which is interpreted as a map in the order
+"key1 value1 key2 value2".
+So the command
+
+----
+jaas:module-add org.apache.karaf.jaas.modules.properties.PropertiesLoginModule users "/tmp/users"
+----
+
+will add a `PropertiesLoginModule` with parameters map `users -> /tmp/users` to the selected realm.
+
 ==== Users, groups, roles, and passwords
 
 As we saw, by default, Apache Karaf uses a PropertiesLoginModule.