New AAA CLI standalone JAR to create users and set passwords 72/48372/12
authorMichael Vorburger <vorburger@redhat.com>
Tue, 15 Nov 2016 18:04:37 +0000 (19:04 +0100)
committerMichael Vorburger <vorburger@redhat.com>
Thu, 24 Nov 2016 10:25:05 +0000 (11:25 +0100)
This creates a (new) "executable fat JAR", which is NOT an OSGi bundle,
allowing installation tools such as the one used by Tim Rozet for OPNFV,
to create users and set passwords, without requiring ODL REST API to
run, and (more importantly) without knowing the current password.

As discussed and agreed with Ryan Goulding and others during the weekly
"Kernel call" on Tuesday Nov 15th this is still secure, as it's based on
physical access to the database file.

https://wiki.opendaylight.org/view/AAA:Changing_Account_Passwords has
end-user facing documentation (which may be updated as this gets
merged, perhaps later packaged, etc.)

Change-Id: I0f9f991520128b53460b3ee80dbbe0b4b824ca5b
Signed-off-by: Michael Vorburger <vorburger@redhat.com>
aaa-authn-api/src/main/java/org/opendaylight/aaa/api/StoreBuilder.java
aaa-cli-jar/pom.xml [new file with mode: 0644]
aaa-cli-jar/src/main/java/org/opendaylight/aaa/cli/jar/AbstractMain.java [new file with mode: 0644]
aaa-cli-jar/src/main/java/org/opendaylight/aaa/cli/jar/Main.java [new file with mode: 0644]
aaa-cli-jar/src/main/java/org/opendaylight/aaa/cli/jar/StandaloneCommandLineInterface.java [new file with mode: 0644]
aaa-cli-jar/src/test/java/org/opendaylight/aaa/cli/jar/AbstractMainTest.java [new file with mode: 0644]
aaa-cli-jar/src/test/java/org/opendaylight/aaa/cli/jar/FilesUtils.java [new file with mode: 0644]
aaa-cli-jar/src/test/java/org/opendaylight/aaa/cli/jar/MainIT.java [new file with mode: 0644]
aaa-cli-jar/src/test/java/org/opendaylight/aaa/cli/jar/MainIntegrationTest.java [new file with mode: 0644]
aaa-cli-jar/src/test/java/org/opendaylight/aaa/cli/jar/StandaloneCommandLineInterfaceTest.java [new file with mode: 0644]
pom.xml

index 6875d5220a455d496ffce5d14463af261141742f..31a591f2d1ae894135dbc6a08c9f5adb0ee518e5 100644 (file)
@@ -9,7 +9,6 @@ package org.opendaylight.aaa.api;
 
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -128,11 +127,8 @@ public class StoreBuilder {
     public void initWithDefaultUsers(String domainID) throws IDMStoreException {
         String newDomainID = initDomainAndRolesWithoutUsers(domainID);
         if (newDomainID != null) {
-            List<String> userAndAdminRoleIDs = getRoleIDs(newDomainID, Arrays.asList("user", "admin"));
-            createUser(newDomainID, "admin", "admin", userAndAdminRoleIDs);
-
-            List<String> userRoleID = getRoleIDs(newDomainID, Collections.singletonList("user"));
-            createUser(newDomainID, "user", "user", userRoleID);
+            createUser(newDomainID, "admin", "admin", true);
+            createUser(newDomainID, "user", "user", false);
         }
     }
 
@@ -187,8 +183,18 @@ public class StoreBuilder {
         return newUserID;
     }
 
-    private void createGrant(String domainID, String userID, String roleID)
+    public String createUser(String domainID, String userName, String password, boolean isAdmin)
             throws IDMStoreException {
+        List<String> roleIDs;
+        if (isAdmin) {
+            roleIDs = getRoleIDs(domainID, Arrays.asList("user", "admin"));
+        } else {
+            roleIDs = getRoleIDs(domainID, Arrays.asList("user"));
+        }
+        return createUser(domainID, userName, password, roleIDs);
+    }
+
+    private void createGrant(String domainID, String userID, String roleID) throws IDMStoreException {
         Grant grant = new Grant();
         grant.setDomainid(domainID);
         grant.setUserid(userID);
diff --git a/aaa-cli-jar/pom.xml b/aaa-cli-jar/pom.xml
new file mode 100644 (file)
index 0000000..fb2c80c
--- /dev/null
@@ -0,0 +1,149 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (c) 2016 Red Hat, Inc. and others. All rights reserved.
+
+This program and the accompanying materials are made available under the
+terms of the Eclipse Public License v1.0 which accompanies this distribution,
+and is available at http://www.eclipse.org/legal/epl-v10.html INTERNAL
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.opendaylight.aaa</groupId>
+    <artifactId>aaa-parent</artifactId>
+    <version>0.5.0-SNAPSHOT</version>
+    <relativePath>../parent</relativePath>
+  </parent>
+
+  <artifactId>aaa-cli-jar</artifactId>
+  <packaging>jar</packaging>
+
+  <dependencies>
+    <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>net.sf.jopt-simple</groupId>
+      <artifactId>jopt-simple</artifactId>
+      <version>5.0.3</version>
+    </dependency>
+    <dependency>
+      <groupId>org.opendaylight.aaa</groupId>
+      <artifactId>aaa-h2-store</artifactId>
+      <exclusions>
+        <exclusion>
+          <!-- Completely disable transitive dependencies -->
+          <groupId>*</groupId>
+          <artifactId>*</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <!-- Now repeat the few really needed dependencies which we would normally get transitively -->
+    <dependency>
+      <groupId>org.opendaylight.aaa</groupId>
+      <artifactId>aaa-authn-api</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.h2database</groupId>
+      <artifactId>h2</artifactId>
+    </dependency>
+
+    <!-- Now for the FAT JAR we need to fix up some <scope>provided to be <scope>compile -->
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-api</artifactId>
+      <scope>compile</scope> <!-- Not provided -->
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-simple</artifactId>
+      <scope>compile</scope> <!-- Not test -->
+    </dependency>
+
+    <!-- Testing Dependencies -->
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.google.truth</groupId>
+      <artifactId>truth</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-core</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.opendaylight.yangtools</groupId>
+      <artifactId>testutils</artifactId>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <artifactId>maven-failsafe-plugin</artifactId>
+        <!-- TODO Remove when https://git.opendaylight.org/gerrit/#/c/48400/ is merged -->
+        <executions>
+          <execution>
+            <id>integration-test</id>
+            <goals>
+              <goal>integration-test</goal>
+            </goals>
+          </execution>
+          <execution>
+            <id>verify</id>
+            <goals>
+              <goal>verify</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <!-- TODO Use maven-shade-plugin with its interesting minimizeJar property... -->
+        <artifactId>maven-assembly-plugin</artifactId>
+        <version>2.6</version>
+        <configuration>
+          <descriptorRefs>
+            <descriptorRef>jar-with-dependencies</descriptorRef>
+          </descriptorRefs>
+          <archive>
+            <manifest>
+              <mainClass>org.opendaylight.aaa.cli.jar.Main</mainClass>
+            </manifest>
+          </archive>
+        </configuration>
+        <executions>
+          <execution>
+            <id>make-assembly</id>
+            <phase>package</phase>
+            <goals>
+              <goal>single</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-checkstyle-plugin</artifactId>
+        <configuration>
+          <propertyExpansion>checkstyle.violationSeverity=error</propertyExpansion>
+        </configuration>
+      </plugin>
+<!-- TODO
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>findbugs-maven-plugin</artifactId>
+        <configuration>
+          <failOnError>true</failOnError>
+        </configuration>
+      </plugin>
+  -->
+    </plugins>
+  </build>
+
+</project>
diff --git a/aaa-cli-jar/src/main/java/org/opendaylight/aaa/cli/jar/AbstractMain.java b/aaa-cli-jar/src/main/java/org/opendaylight/aaa/cli/jar/AbstractMain.java
new file mode 100644 (file)
index 0000000..e12b89e
--- /dev/null
@@ -0,0 +1,148 @@
+/*
+ * Copyright (c) 2016 Red Hat, Inc. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.aaa.cli.jar;
+
+import static java.util.Arrays.asList;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import joptsimple.OptionParser;
+import joptsimple.OptionSet;
+import org.opendaylight.aaa.api.IDMStoreException;
+
+/**
+ * Class with main() method and argument parsing etc.
+ * This class ONLY deals with argument parsing etc. and doesn't "do" anything,
+ * yet; this is intentional, and best for true unit test-ability of this class.
+ *
+ * @author Michael Vorburger
+ */
+@SuppressWarnings("checkstyle:RegexpSingleLineJava") // allow System.out / System.err here..
+public abstract class AbstractMain {
+
+    private static final String OPTION_HELP = "h";
+    private static final String OPTION_DB_DIR = "dbd";
+    private static final String OPTION_LIST_USERS = "l";
+    private static final String OPTION_CHANGE_USER = "cu";
+    private static final String OPTION_NEW_USER = "nu";
+    private static final String OPTION_ADMINS = "a";
+    private static final String OPTION_PASS = "p";
+    private static final String OPTION_DEBUG = "X";
+
+    private static final int RETURN_NOT_ENOUGH_ARGS = -1;
+    private static final int RETURN_ABORT_DUE_TO_EXCEPTION = -2;
+    private static final int RETURN_ARGUMENTS_MISMATCHED = -3;
+    protected static final int RETURN_ILLEGAL_ARGUMENTS = -4;
+    private static final int RETURN_ARGUMENTS_INCOMPATIBLE = -5;
+    private static final int RETURN_ARGUMENTS_MISSING = -6;
+
+    @SuppressWarnings({ "unchecked", "checkstyle:IllegalThrows", "checkstyle:IllegalCatch" })
+    public int parseArguments(String[] args) throws Exception {
+        boolean isInDebugLogging = false;
+        try {
+            OptionParser optionParser = getOptionParser();
+            OptionSet optionSet = optionParser.parse(args);
+            if (optionSet.has(OPTION_DEBUG)) {
+                isInDebugLogging = true;
+            }
+            if (!optionSet.nonOptionArguments().isEmpty()) {
+                unrecognizedOptions(optionSet.nonOptionArguments());
+            }
+            if (args.length == 0 || optionSet.has(OPTION_HELP) || !optionSet.nonOptionArguments().isEmpty()) {
+                printHelp(optionParser);
+                return RETURN_NOT_ENOUGH_ARGS;
+            }
+
+            if (optionSet.has(OPTION_CHANGE_USER) && optionSet.has(OPTION_NEW_USER)) {
+                System.err.println("Can't use these options together: -" + OPTION_CHANGE_USER
+                        + ", -" + OPTION_NEW_USER);
+                return RETURN_ARGUMENTS_INCOMPATIBLE;
+            } else if (optionSet.has(OPTION_PASS) && !optionSet.has(OPTION_CHANGE_USER)
+                    && !optionSet.has(OPTION_NEW_USER)) {
+                System.err.println("If passwords are specificied, then must use one or the other of these options: -"
+                        + OPTION_CHANGE_USER + ", -" + OPTION_NEW_USER);
+                return RETURN_ARGUMENTS_MISSING;
+            }
+
+            List<String> userNames;
+            if (optionSet.has(OPTION_CHANGE_USER)) {
+                userNames = (List<String>) optionSet.valuesOf(OPTION_CHANGE_USER);
+            } else { // optionSet.has(OPTION_NEW_USER))
+                userNames = (List<String>) optionSet.valuesOf(OPTION_NEW_USER);
+            }
+            List<String> passwords = (List<String>) optionSet.valuesOf(OPTION_PASS);
+            if (passwords.size() != userNames.size()) {
+                System.err.println("Must give as many user names as passwords");
+                return RETURN_ARGUMENTS_MISMATCHED;
+            }
+
+            File dbDirectory = (File) optionSet.valueOf(OPTION_DB_DIR);
+            setDbDirectory(dbDirectory);
+
+            if (optionSet.has(OPTION_LIST_USERS)) {
+                listUsers();
+            }
+
+            if (optionSet.has(OPTION_CHANGE_USER)) {
+                return resetPasswords(userNames, passwords);
+            } else { // optionSet.has(OPTION_NEW_USER))
+                boolean areAdmins = optionSet.has(OPTION_ADMINS);
+                return addNewUsers(userNames, passwords, areAdmins);
+            }
+
+        } catch (Throwable t) {
+            if (!isInDebugLogging) {
+                System.err.println("Aborting due to " + t.getClass().getSimpleName()
+                        + " (use -X to see full stack trace): " + t.getMessage());
+                return RETURN_ABORT_DUE_TO_EXCEPTION;
+            } else {
+                // Java will print the full stack trace if we rethrow it
+                throw t;
+            }
+        }
+    }
+
+    private OptionParser getOptionParser() {
+        return new OptionParser() { {
+                acceptsAll(asList(OPTION_HELP, "?" ), "Show help").forHelp();
+                accepts(OPTION_DB_DIR, "databaseDirectory").withRequiredArg().ofType(File.class)
+                        .defaultsTo(new File(".")).describedAs("path");
+                acceptsAll(asList(OPTION_LIST_USERS, "listUsers"), "List all existing users");
+                acceptsAll(asList(OPTION_NEW_USER, "newUser"), "New user to create").withRequiredArg();
+                acceptsAll(asList(OPTION_CHANGE_USER, "changeUser"), "Existing user name to change password")
+                        .withRequiredArg();
+                acceptsAll(asList(OPTION_PASS, "passwd"), "New password").withRequiredArg();
+                accepts(OPTION_ADMINS, "New User(s) added with 'admin' role");
+                // TODO accepts("v", "Display version information").forHelp();
+                acceptsAll(asList(OPTION_DEBUG, "debug"), "Produce execution debug output");
+
+                allowsUnrecognizedOptions();
+            }
+        };
+    }
+
+    protected void unrecognizedOptions(List<?> unrecognizedOptions) {
+        System.err.println("Unrecognized options: " + unrecognizedOptions);
+    }
+
+    protected void printHelp(OptionParser optionParser) throws IOException {
+        optionParser.printHelpOn(System.out);
+    }
+
+    // ----
+
+    protected abstract void setDbDirectory(File dbDirectory) throws IOException, IDMStoreException;
+
+    protected abstract void listUsers() throws IDMStoreException;
+
+    protected abstract int resetPasswords(List<String> userNames, List<String> passwords) throws IDMStoreException;
+
+    protected abstract int addNewUsers(List<String> userNames, List<String> passwords, boolean areAdmins) throws IDMStoreException;
+
+}
diff --git a/aaa-cli-jar/src/main/java/org/opendaylight/aaa/cli/jar/Main.java b/aaa-cli-jar/src/main/java/org/opendaylight/aaa/cli/jar/Main.java
new file mode 100644 (file)
index 0000000..0b86078
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2016 Red Hat, Inc. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.aaa.cli.jar;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import org.opendaylight.aaa.api.IDMStoreException;
+
+/**
+ * TestableMain which actually does something real.
+ *
+ * @author Michael Vorburger
+ */
+@SuppressWarnings("checkstyle:RegexpSingleLineJava") // allow System.out / System.err here..
+public class Main extends AbstractMain {
+
+    private StandaloneCommandLineInterface cli;
+
+    @SuppressWarnings("checkstyle:IllegalThrows")
+    public static void main(String[] args) throws Exception {
+        System.exit(new Main().parseArguments(args));
+    }
+
+    @Override
+    protected void setDbDirectory(File dbDirectory) throws IOException, IDMStoreException {
+        cli = new StandaloneCommandLineInterface(dbDirectory);
+    }
+
+    @Override
+    protected void listUsers() throws IDMStoreException {
+        System.out.println("User names:");
+        List<String> userNames = cli.getAllUserNames();
+        for (String userName : userNames) {
+            System.out.println(userName);
+        }
+    }
+
+    @Override
+    protected int resetPasswords(List<String> userNames, List<String> passwords) throws IDMStoreException {
+        for (int i = 0; i < userNames.size(); i++) {
+            String userName = userNames.get(i);
+            String newPassword = passwords.get(i);
+            boolean isSuccess = cli.resetPassword(userName, newPassword);
+            if (isSuccess) {
+                // Output text shamelessly copy/pasted from org.opendaylight.aaa.cli.ChangeUserPassword
+                System.out.println(userName + "'s password has been changed");
+            } else {
+                System.err.println("User does not exist: " + userName);
+                return RETURN_ILLEGAL_ARGUMENTS;
+            }
+        }
+        return 0;
+    }
+
+    @Override
+    protected int addNewUsers(List<String> userNames, List<String> passwords, boolean areAdmins) throws IDMStoreException {
+        for (int i = 0; i < userNames.size(); i++) {
+            String userName = userNames.get(i);
+            String newPassword = passwords.get(i);
+            cli.createNewUser(userName, newPassword, areAdmins);
+            System.out.print("New user created");
+            if (areAdmins){
+                System.out.print(", as admin");
+            }
+            System.out.println(": " + userName);
+        }
+        return 0;
+    }
+
+}
diff --git a/aaa-cli-jar/src/main/java/org/opendaylight/aaa/cli/jar/StandaloneCommandLineInterface.java b/aaa-cli-jar/src/main/java/org/opendaylight/aaa/cli/jar/StandaloneCommandLineInterface.java
new file mode 100644 (file)
index 0000000..d1373c6
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2016 Red Hat, Inc. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.aaa.cli.jar;
+
+import com.google.common.base.Preconditions;
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.opendaylight.aaa.api.IDMStoreException;
+import org.opendaylight.aaa.api.IIDMStore;
+import org.opendaylight.aaa.api.StoreBuilder;
+import org.opendaylight.aaa.api.model.User;
+import org.opendaylight.aaa.h2.config.IdmLightConfig;
+import org.opendaylight.aaa.h2.config.IdmLightConfigBuilder;
+import org.opendaylight.aaa.h2.config.IdmLightSimpleConnectionProvider;
+import org.opendaylight.aaa.h2.persistence.H2Store;
+
+/**
+ * AAA CLI interface.
+ * This is for a "standalone Java" environment (i.e. plain JSE; non-OSGi, no Karaf).
+ *
+ * @author Michael Vorburger
+ */
+public class StandaloneCommandLineInterface {
+
+    private final IIDMStore identityStore;
+    private final StoreBuilder storeBuilder;
+    private static final String DOMAIN = IIDMStore.DEFAULT_DOMAIN;
+
+    public StandaloneCommandLineInterface(File directoryWithDatabaseFile) throws IOException, IDMStoreException {
+        IdmLightConfigBuilder configBuider = new IdmLightConfigBuilder();
+        configBuider.dbDirectory(directoryWithDatabaseFile.getCanonicalPath());
+        IdmLightConfig config = configBuider.build();
+
+        H2Store h2Store = new H2Store(new IdmLightSimpleConnectionProvider(config));
+        this.identityStore = h2Store;
+
+        this.storeBuilder = new StoreBuilder(h2Store);
+        storeBuilder.initDomainAndRolesWithoutUsers(DOMAIN);
+    }
+
+    public List<String> getAllUserNames() throws IDMStoreException {
+        List<User> users = identityStore.getUsers().getUsers();
+        return users.stream().map(user -> user.getName()).collect(Collectors.toList());
+    }
+
+    public boolean resetPassword(String userIdWithoutDomain, String newPassword) throws IDMStoreException {
+        Preconditions.checkNotNull(userIdWithoutDomain, "userIdWithoutDomain == null");
+        List<User> users = identityStore.getUsers(userIdWithoutDomain, DOMAIN).getUsers();
+        if (users.isEmpty()) {
+                       return false;
+               }
+        if (users.size() > 1) {
+                       throw new IDMStoreException("More than 1 user found: " + userIdWithoutDomain);
+               }
+        User user = users.get(0);
+        user.setPassword(newPassword);
+        identityStore.updateUser(user);
+        return true;
+    }
+
+    public void createNewUser(String userName, String password, boolean isAdmin) throws IDMStoreException {
+        Preconditions.checkNotNull(userName, "userName == null");
+        storeBuilder.createUser(DOMAIN, userName, password, isAdmin);
+    }
+}
diff --git a/aaa-cli-jar/src/test/java/org/opendaylight/aaa/cli/jar/AbstractMainTest.java b/aaa-cli-jar/src/test/java/org/opendaylight/aaa/cli/jar/AbstractMainTest.java
new file mode 100644 (file)
index 0000000..ef20a21
--- /dev/null
@@ -0,0 +1,147 @@
+/*
+ * Copyright (c) 2016 Red Hat, Inc. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.aaa.cli.jar;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.any;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.Collections;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.opendaylight.yangtools.testutils.mockito.MoreAnswers;
+
+/**
+ * Unit Test of Main class with the argument parsing.
+ *
+ * @author Michael Vorburger
+ */
+public class AbstractMainTest {
+
+    private AbstractMain mockedMain() {
+        return Mockito.mock(AbstractMain.class, MoreAnswers.realOrException());
+    }
+
+    @Test
+    public void noArguments() throws Exception {
+        AbstractMain main = Mockito.spy(AbstractMain.class);
+        assertThat(main.parseArguments(new String[] {})).isEqualTo(-1);
+        Mockito.verify(main).printHelp(any());
+    }
+
+    @Test
+    public void unrecognizedArgument() throws Exception {
+        AbstractMain main = Mockito.spy(AbstractMain.class);
+        assertThat(main.parseArguments(new String[] { "saywhat" })).isEqualTo(-1);
+        Mockito.verify(main).unrecognizedOptions(Collections.singletonList("saywhat"));
+        Mockito.verify(main).printHelp(any());
+    }
+
+    /**
+     * Verify that allowsUnrecognizedOptions() is used, and "bad" arguments
+     * print help message instead of causing an UnrecognizedOptionException.
+     */
+    @Test
+    public void parsingError() throws Exception {
+        AbstractMain main = Mockito.spy(AbstractMain.class);
+        assertThat(main.parseArguments(new String[] { "-d" })).isEqualTo(-1);
+        Mockito.verify(main).printHelp(any());
+    }
+
+    @Test
+    public void exceptionWithoutX() throws Exception {
+        AbstractMain main = mockedMain();
+        Mockito.doThrow(new IllegalStateException()).when(main).printHelp(any());
+        assertThat(main.parseArguments(new String[] { "-?" })).isEqualTo(-2);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void exceptionWithX() throws Exception {
+        AbstractMain main = mockedMain();
+        Mockito.doThrow(new IllegalStateException()).when(main).printHelp(any());
+        assertThat(main.parseArguments(new String[] { "-hX" })).isEqualTo(-2);
+    }
+
+    @Test
+    public void onlyAUser() throws Exception {
+        assertThat(mockedMain().parseArguments(new String[] { "-X", "--nu", "admin" })).isEqualTo(-3);
+    }
+
+    @Test
+    public void onlyTwoUsers() throws Exception {
+        assertThat(mockedMain().parseArguments(new String[] { "-X", "-cu", "admin", "-cu", "auser" }))
+                .isEqualTo(-3);
+    }
+
+    @Test
+    public void userOptionWithoutArgument() throws Exception {
+        assertThat(mockedMain().parseArguments(new String[] { "-nu" })).isEqualTo(-2);
+    }
+
+    @Test
+    public void ifPasswordsThenEitherCreateOrChangeUser() throws Exception {
+        assertThat(mockedMain().parseArguments(new String[] { "-X", "-p", "newpass" })).isEqualTo(-6);
+    }
+
+    @Test
+    public void changeUserAndPassword() throws Exception {
+        AbstractMain main = Mockito.spy(AbstractMain.class);
+        assertThat(main.parseArguments(new String[] { "-X", "-cu", "user", "-p", "newpass" })).isEqualTo(0);
+        Mockito.verify(main).setDbDirectory(new File("."));
+        Mockito.verify(main).resetPasswords(Collections.singletonList("user"), Collections.singletonList("newpass"));
+    }
+
+    @Test
+    public void addNewUser() throws Exception {
+        AbstractMain main = Mockito.spy(AbstractMain.class);
+        assertThat(main.parseArguments(new String[] { "-X", "-nu", "user", "-p", "newpass" })).isEqualTo(0);
+        Mockito.verify(main).setDbDirectory(new File("."));
+        Mockito.verify(main).addNewUsers(Collections.singletonList("user"), Collections.singletonList("newpass"), false);
+    }
+
+    @Test
+    public void addNewAdminUser() throws Exception {
+        AbstractMain main = Mockito.spy(AbstractMain.class);
+        assertThat(main.parseArguments(new String[] { "-X", "-nu", "user", "-p", "newpass", "-a" })).isEqualTo(0);
+        Mockito.verify(main).setDbDirectory(new File("."));
+        Mockito.verify(main).addNewUsers(Collections.singletonList("user"), Collections.singletonList("newpass"), true);
+    }
+
+    @Test
+    public void changeUserAndPasswordInNonDefaultDatabase() throws Exception {
+        AbstractMain main = Mockito.spy(AbstractMain.class);
+        assertThat(main.parseArguments(new String[] { "-X", "--dbd", "altDbDir", "-cu", "user", "-p", "newpass" }))
+                .isEqualTo(0);
+        Mockito.verify(main).setDbDirectory(new File("altDbDir"));
+        Mockito.verify(main).resetPasswords(Collections.singletonList("user"), Collections.singletonList("newpass"));
+    }
+
+    @Test
+    public void changeTwoUsersAndPasswords() throws Exception {
+        AbstractMain main = Mockito.spy(AbstractMain.class);
+        assertThat(main.parseArguments(
+                new String[] { "-X", "-cu", "user1", "-p", "newpass1", "-cu", "user2", "-p", "newpass2" }))
+                        .isEqualTo(0);
+        Mockito.verify(main).resetPasswords(Arrays.asList("user1", "user2"), Arrays.asList("newpass1", "newpass2"));
+    }
+
+    @Test
+    public void morePasswordsThanUsers() throws Exception {
+        assertThat(mockedMain().parseArguments(new String[] { "-X", "-cu", "admin", "-p", "newpass1", "-cu", "auser",
+            "-p", "newpass2", "-p", "newpass3" })).isEqualTo(-3);
+    }
+
+    @Test
+    public void cantAddAndChangeUsersTogether() throws Exception {
+        assertThat(
+                mockedMain().parseArguments(new String[] { "-X", "-cu", "admin", "-nu", "admin2", "-p", "newpass1" }))
+                        .isEqualTo(-5);
+    }
+
+}
diff --git a/aaa-cli-jar/src/test/java/org/opendaylight/aaa/cli/jar/FilesUtils.java b/aaa-cli-jar/src/test/java/org/opendaylight/aaa/cli/jar/FilesUtils.java
new file mode 100644 (file)
index 0000000..b570d88
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2016 Red Hat, Inc. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.aaa.cli.jar;
+
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+
+/**
+ * Utilities for Files.
+ *
+ * @author Michael Vorburger
+ */
+public final class FilesUtils {
+    private FilesUtils() {
+    }
+
+    public static void delete(String directory) throws IOException {
+        Path path = Paths.get(directory);
+        if (!path.toFile().exists()) {
+            return;
+        }
+        Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
+            @Override
+            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+                file.toFile().delete();
+                return FileVisitResult.CONTINUE;
+            }
+
+            @Override
+            public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
+                dir.toFile().delete();
+                if (exc != null) {
+                    throw exc;
+                }
+                return FileVisitResult.CONTINUE;
+            }
+        });
+    }
+
+}
diff --git a/aaa-cli-jar/src/test/java/org/opendaylight/aaa/cli/jar/MainIT.java b/aaa-cli-jar/src/test/java/org/opendaylight/aaa/cli/jar/MainIT.java
new file mode 100644 (file)
index 0000000..ad81ed0
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2016 Red Hat, Inc. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.aaa.cli.jar;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.io.File;
+import org.junit.Test;
+
+/**
+ * Integration Test for the built JAR file.
+ *
+ * <p>Note that the maven-failsafe-plugin, not the usual maven-surefire-plugin (for
+ * *Test), runs this *IT AFTER the final "fat" self-executable JAR has been
+ * built.
+ *
+ * @author Michael Vorburger
+ */
+public class MainIT {
+
+    private static final String DIR = "target/" + MainIT.class.getSimpleName();
+
+    @Test
+    public void integrationTestBuildJAR() throws Exception {
+        FilesUtils.delete(DIR);
+
+        // If Output piping to LOG instead of inheritIO() etc. is needed, then
+        // consider using https://github.com/vorburger/MariaDB4j/tree/master/mariaDB4j-core/src/main/java/ch/vorburger/exec
+        Process process = new ProcessBuilder(
+                findJava().getAbsolutePath(),
+                "-jar",
+                findExecutableFatJAR().getAbsolutePath(),
+                "--dbd",
+                DIR,
+                "-a",
+                "--nu",
+                "vorburger",
+                "-p",
+                "nosecret" )
+                .inheritIO()
+                // NO .redirectErrorStream(true)
+                .start();
+        process.waitFor();
+        assertThat(process.exitValue()).isEqualTo(0);
+    }
+
+    private File findExecutableFatJAR() {
+        File targetDirectory = new File(".", "target");
+        File[] jarFiles = targetDirectory.listFiles((dir, name) -> name.endsWith("jar-with-dependencies.jar"));
+        assertThat(jarFiles).named("*jar-with-dependencies.jar files in " + targetDirectory).isNotNull();
+        assertThat(jarFiles).named("*jar-with-dependencies.jar files in " + targetDirectory).hasLength(1);
+        return jarFiles[0];
+    }
+
+    private File findJava() {
+        File javaHome = new File(System.getProperty("java.home"));
+        File javaHomeBin = new File(javaHome, "bin");
+        File javaExecutable = new File(javaHomeBin, "java");
+        return javaExecutable;
+    }
+
+}
diff --git a/aaa-cli-jar/src/test/java/org/opendaylight/aaa/cli/jar/MainIntegrationTest.java b/aaa-cli-jar/src/test/java/org/opendaylight/aaa/cli/jar/MainIntegrationTest.java
new file mode 100644 (file)
index 0000000..2943921
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2016 Red Hat, Inc. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.aaa.cli.jar;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+/**
+ * Test of RealMain (and its dependencies; incl. args parsing, real DB, etc.).
+ * This intentionally only tests a very basic scenario end-to-end; more fine grained cases are covered in the
+ * {@link StandaloneCommandLineInterfaceTest} and the {@link AbstractMainTest}.
+ *
+ * @author Michael Vorburger
+ */
+public class MainIntegrationTest {
+
+    private static final String DIR = "target/" + MainIntegrationTest.class.getSimpleName();
+
+    @Test
+    public void testCLI() throws Exception {
+        FilesUtils.delete(DIR);
+        assertThat(new Main()
+                .parseArguments(new String[] { "-X", "--dbd", DIR, "-a", "-nu", "newuser", "-p", "firstpass" }))
+                .isEqualTo(0);
+        assertThat(new Main()
+                .parseArguments(new String[] { "-X", "--dbd", DIR, "-cu", "newuser", "-p", "newpass" }))
+                .isEqualTo(0);
+    }
+
+}
diff --git a/aaa-cli-jar/src/test/java/org/opendaylight/aaa/cli/jar/StandaloneCommandLineInterfaceTest.java b/aaa-cli-jar/src/test/java/org/opendaylight/aaa/cli/jar/StandaloneCommandLineInterfaceTest.java
new file mode 100644 (file)
index 0000000..8b5fdce
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2016 Red Hat, Inc. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.aaa.cli.jar;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.io.File;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test of StandaloneCommandLineInterface (and its dependencies; incl. real DB).
+ *
+ * @author Michael Vorburger
+ */
+public class StandaloneCommandLineInterfaceTest {
+
+    private static final String DIR = "target/" + StandaloneCommandLineInterfaceTest.class.getSimpleName();
+
+    StandaloneCommandLineInterface cli;
+
+    @Before
+    public void before() throws Exception {
+        FilesUtils.delete(DIR);
+        cli = new StandaloneCommandLineInterface(new File(DIR));
+    }
+
+    @Test
+    public void testInitialEmptyDatabase() throws Exception {
+        assertThat(cli.getAllUserNames()).isEmpty();
+    }
+
+    @Test
+    public void testCreateNewUserAndSetPassword() throws Exception {
+        cli.createNewUser("test", "testpassword", false);
+        assertThat(cli.getAllUserNames()).hasSize(1);
+        assertThat(cli.getAllUserNames().get(0)).isEqualTo("test");
+
+        assertThat(cli.resetPassword("test", "anothertestpassword")).isTrue();
+    }
+
+    @Test
+    public void testSetPasswordOnNonExistingUser() throws Exception {
+        assertThat(cli.resetPassword("noSuchUID", "...")).isFalse();
+    }
+
+}
diff --git a/pom.xml b/pom.xml
index c0066092fdf7aefc680637b3a4022367077bcf25..249e908d8a1314778abb5c5e15bb6b408af16200 100644 (file)
--- a/pom.xml
+++ b/pom.xml
@@ -44,6 +44,7 @@
     <module>aaa-h2-store</module>
     <module>aaa-cert</module>
     <module>aaa-cli</module>
+    <module>aaa-cli-jar</module>
     <module>aaa-filterchain</module>
     <module>aaa-cassandra-store</module>
     <module>artifacts</module>
@@ -77,4 +78,4 @@
     <tag>HEAD</tag>
     <url>https://wiki.opendaylight.org/view/AAA:Main</url>
   </scm>
-</project>
\ No newline at end of file
+</project>