You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jclouds.apache.org by na...@apache.org on 2018/01/08 07:39:41 UTC

jclouds git commit: JCLOUDS-1362: Better password generation utility

Repository: jclouds
Updated Branches:
  refs/heads/master 11640b6c2 -> 9fef6ed06


JCLOUDS-1362: Better password generation utility


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

Branch: refs/heads/master
Commit: 9fef6ed06b027c1525579c88b58aa9f5833687fd
Parents: 11640b6
Author: Ignasi Barrera <na...@apache.org>
Authored: Thu Jan 4 01:21:10 2018 +0100
Committer: Ignasi Barrera <na...@apache.org>
Committed: Mon Jan 8 08:39:20 2018 +0100

----------------------------------------------------------------------
 .../org/jclouds/util/PasswordGenerator.java     | 195 +++++++++++++++++++
 .../main/java/org/jclouds/util/Passwords.java   |  70 -------
 .../org/jclouds/util/PasswordGeneratorTest.java |  80 ++++++++
 .../java/org/jclouds/util/PasswordsTest.java    |  63 ------
 .../ProfitBricksComputeServiceAdapter.java      |  10 +-
 ...ProfitBricksComputeServiceContextModule.java |  11 ++
 .../profitbricks/util/Preconditions.java        |   9 +-
 7 files changed, 300 insertions(+), 138 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/jclouds/blob/9fef6ed0/core/src/main/java/org/jclouds/util/PasswordGenerator.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/jclouds/util/PasswordGenerator.java b/core/src/main/java/org/jclouds/util/PasswordGenerator.java
new file mode 100644
index 0000000..e0d14ad
--- /dev/null
+++ b/core/src/main/java/org/jclouds/util/PasswordGenerator.java
@@ -0,0 +1,195 @@
+/*
+ * 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.jclouds.util;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import java.security.SecureRandom;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Strings;
+import com.google.common.primitives.Chars;
+
+/**
+ * Generates random passwords.
+ * <p>
+ * This class allows to configure the password requirements for:
+ * <ul>
+ * <li>Number of upper case and lower case letters</li>
+ * <li>Inclusion of numbers</li>
+ * <li>Inclusion of special characters</li>
+ * </ul>
+ * By default, it will include at least three lower case letters, three upper
+ * case, three numbers and three special characters, and a maximum of five from
+ * each set.
+ * <p>
+ * It also allows to configure forbidden characters to accommodate the password
+ * requirements for the different clouds.
+ * <p>
+ * Example usage:
+ * <pre>
+ * String password = new PasswordGenerator()
+ *    .lower().count(3)  // Exactly three lower case characters
+ *    .upper().count(2)  // Exactly 2 upper case characters 
+ *    .numbers().min(5).exclude("012345".toCharArray()) // At least five numbers, from 6 to 9.
+ *    .symbols().min(6).max(10) // Between 6 and 10 special characters
+ *    .generate();
+ * </pre>
+ *
+ */
+public class PasswordGenerator {
+
+   private static final Random RANDOM = new SecureRandom();
+
+   private final Config lower = new Config("abcdefghijklmnopqrstuvwxyz").min(3).max(5);
+   private final Config upper = new Config("ABCDEFGHIJKLMNOPQRSTUVWXYZ").min(3).max(5);
+   private final Config numbers = new Config("1234567890").min(3).max(5);
+   // Use a small set of symbols that does not break shell commands
+   private final Config symbols = new Config("~@#%*()-_=+:,.?").min(3).max(5);
+
+   /**
+    * Returns the lower case configuration. Allows to configure the presence of lower case characters.
+    */
+   public Config lower() {
+      return lower;
+   }
+
+   /**
+    * Returns the upper case configuration. Allows to configure the presence of upper case characters.
+    */
+   public Config upper() {
+      return upper;
+   }
+
+   /**
+    * Returns the numbers configuration. Allows to configure the presence of numeric characters.
+    */
+   public Config numbers() {
+      return numbers;
+   }
+
+   /**
+    * Returns the special character configuration. Allows to configure the presence of special characters.
+    */
+   public Config symbols() {
+      return symbols;
+   }
+
+   /**
+    * Generates a random password using the configured spec.
+    */
+   public String generate() {
+      StringBuilder sb = new StringBuilder();
+      sb.append(lower.fragment());
+      sb.append(upper.fragment());
+      sb.append(numbers.fragment());
+      sb.append(symbols.fragment());
+      return shuffleAndJoin(sb.toString().toCharArray());
+   }
+
+   private static String shuffleAndJoin(char[] chars) {
+      List<Character> result = Chars.asList(chars);
+      Collections.shuffle(result);
+      return Joiner.on("").join(result);
+   }
+
+   public class Config {
+      private final String characters;
+      private char[] exclusions;
+      private int minLength;
+      private int maxLength;
+
+      private Config(String characters) {
+         checkArgument(!Strings.isNullOrEmpty(characters), "charactets must be a non-empty string");
+         this.characters = characters;
+      }
+
+      public Config exclude(char[] exclusions) {
+         this.exclusions = exclusions;
+         return this;
+      }
+
+      public Config min(int num) {
+         this.minLength = num;
+         return this;
+      }
+
+      public Config max(int num) {
+         this.maxLength = num;
+         return this;
+      }
+
+      public Config count(int num) {
+         min(num);
+         max(num);
+         return this;
+      }
+
+      private String fragment() {
+         int length = minLength + RANDOM.nextInt((maxLength - minLength) + 1);
+         return new Generator(characters, length, exclusions).generate();
+      }
+
+      // Delegate to enclosing class for better fluent generators
+
+      public Config lower() {
+         return PasswordGenerator.this.lower();
+      }
+
+      public Config upper() {
+         return PasswordGenerator.this.upper();
+      }
+
+      public Config numbers() {
+         return PasswordGenerator.this.numbers();
+      }
+
+      public Config symbols() {
+         return PasswordGenerator.this.symbols();
+      }
+
+      public String generate() {
+         return PasswordGenerator.this.generate();
+      }
+   }
+
+   private static class Generator {
+      private final char[] characters;
+      private final int count;
+
+      private Generator(String characters, int count, char[] exclusions) {
+         checkArgument(!Strings.isNullOrEmpty(characters), "charactets must be a non-empty string");
+         this.count = count;
+         if (exclusions == null || exclusions.length == 0) {
+            this.characters = characters.toCharArray();
+         } else {
+            this.characters = new String(characters).replaceAll("[" + new String(exclusions) + "]", "").toCharArray();
+         }
+      }
+
+      public String generate() {
+         char[] selected = new char[count];
+         for (int i = 0; i < count; i++) {
+            selected[i] = characters[RANDOM.nextInt(characters.length)];
+         }
+         return shuffleAndJoin(selected);
+      }
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/9fef6ed0/core/src/main/java/org/jclouds/util/Passwords.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/jclouds/util/Passwords.java b/core/src/main/java/org/jclouds/util/Passwords.java
deleted file mode 100644
index cf2d57d..0000000
--- a/core/src/main/java/org/jclouds/util/Passwords.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * 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.jclouds.util;
-
-import java.util.Random;
-import java.util.regex.Pattern;
-
-import com.google.common.base.Preconditions;
-import com.google.common.collect.ImmutableSet;
-
-public class Passwords {
-
-   private static final Random random = new Random();
-
-   private static final int GENERATE_PASSWORD_LENGTH = 30;
-   private static final int VALID_PASSWORD_MIN_LENGTH = 8;
-   private static final int VALID_PASSWORD_MAX_LENGTH = 50;
-   private static final String PASSWORD_FORMAT = String.format(
-           "[a-zA-Z0-9][^iIloOwWyYzZ10]{%d,%d}", VALID_PASSWORD_MIN_LENGTH - 1, VALID_PASSWORD_MAX_LENGTH);
-   private static final Pattern PASSWORD_PATTERN = Pattern.compile(PASSWORD_FORMAT);
-
-   private static final ImmutableSet<Character> INVALID_CHARS = ImmutableSet.<Character>of(
-           'i', 'I', 'l', 'o', 'O', 'w', 'W', 'y', 'Y', 'z', 'Z', '1', '0');
-
-   public static boolean isValidPassword(String password) {
-      return PASSWORD_PATTERN.matcher(password).matches();
-   }
-
-   public static String generate() {
-      return generate(GENERATE_PASSWORD_LENGTH);
-   }
-
-   public static String generate(int count) {
-      Preconditions.checkArgument(count > 0, "Password length must be a positive number");
-
-      final char[] buffer = new char[count];
-
-      final int start = 'A';
-      final int end = 'z';
-      final int gap = end - start + 1;
-
-      while (count-- != 0) {
-         char ch = (char) (random.nextInt(gap) + start);
-         if ((isBetween(ch, start, 'Z') || isBetween(ch, 'a', end))
-                 && !INVALID_CHARS.contains(ch))
-            buffer[count] = ch;
-         else
-            count++;
-      }
-      return new String(buffer);
-   }
-
-   private static boolean isBetween(char ch, int start, int end) {
-      return ch >= start && ch <= end;
-   }
-}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/9fef6ed0/core/src/test/java/org/jclouds/util/PasswordGeneratorTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/jclouds/util/PasswordGeneratorTest.java b/core/src/test/java/org/jclouds/util/PasswordGeneratorTest.java
new file mode 100644
index 0000000..1e08b0a
--- /dev/null
+++ b/core/src/test/java/org/jclouds/util/PasswordGeneratorTest.java
@@ -0,0 +1,80 @@
+/*
+ * 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.jclouds.util;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+import org.testng.annotations.Test;
+
+@Test(groups = "unit", testName = "PasswordGeneratorTest")
+public class PasswordGeneratorTest {
+   
+   @Test
+   public void emptyPassword() {
+      String password = new PasswordGenerator()
+            .lower().count(0)
+            .upper().count(0)
+            .numbers().count(0)
+            .symbols().count(0)
+            .generate();
+      assertEquals(password, "");
+   }
+
+   @Test
+   public void onlyLowerCase() {
+      String password = new PasswordGenerator()
+            .upper().count(0)
+            .numbers().count(0)
+            .symbols().count(0)
+            .generate();
+      assertTrue(password.matches("^[a-z]+$"));
+   }
+   
+   @Test
+   public void lowerAndUpperWithConstrainedLength() {
+      String password = new PasswordGenerator()
+            .lower().min(2).max(5)
+            .upper().count(3)
+            .numbers().count(0)
+            .symbols().count(0)
+            .generate();
+      assertTrue(password.matches("^[a-zA-Z]+$"));
+      assertTrue(password.replaceAll("[A-Z]", "").matches("[a-z]{2,5}"));
+      assertTrue(password.replaceAll("[a-z]", "").matches("[A-Z]{3}"));
+   }
+   
+   @Test
+   public void defaultGeneratorContainsAll() {
+      String password = new PasswordGenerator().generate();
+      assertTrue(password.matches(".*[a-z].*[a-z].*"));
+      assertTrue(password.matches(".*[A-Z].*[A-Z].*"));
+      assertTrue(password.matches(".*[0-9].*[0-9].*"));
+      assertTrue(password.replaceAll("[a-zA-Z0-9]", "").length() > 0);
+   }
+   
+   @Test
+   public void characterExclusion() {
+      String password = new PasswordGenerator()
+            .lower().count(0)
+            .upper().count(0)
+            .numbers().exclude("012345".toCharArray())
+            .symbols().count(0)
+            .generate();
+      assertTrue(password.matches("^[6-9]+$"));
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/9fef6ed0/core/src/test/java/org/jclouds/util/PasswordsTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/jclouds/util/PasswordsTest.java b/core/src/test/java/org/jclouds/util/PasswordsTest.java
deleted file mode 100644
index d1d5d50..0000000
--- a/core/src/test/java/org/jclouds/util/PasswordsTest.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * 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.jclouds.util;
-
-import com.google.common.collect.ImmutableList;
-import org.testng.annotations.Test;
-
-import java.util.List;
-import java.util.Random;
-
-import static org.jclouds.util.Passwords.isValidPassword;
-import static org.testng.Assert.assertEquals;
-import static org.testng.Assert.assertFalse;
-import static org.testng.Assert.assertTrue;
-
-@Test(groups = "unit", testName = "PasswordsTest")
-public class PasswordsTest {
-
-   private final List<String> validPasswords = ImmutableList.of(
-           "fKVasTnNm", "84625894", "QQQQQQQQ", "qqqqqqqq", "asdfghjk"
-   );
-   private final List<String> invalidPasswords = ImmutableList.of(
-           "", "apachejclouds", "s0merand0mpassw0rd"
-   );
-
-   @Test
-   public void testPasswordValidation() {
-      for (String pwd : validPasswords)
-         assertTrue(isValidPassword(pwd), "Should've been valid: " + pwd);
-
-      for (String pwd : invalidPasswords)
-         assertFalse(isValidPassword(pwd), "Should've been invalid: " + pwd);
-   }
-
-   @Test
-   public void testGeneratorGeneratesValidPassword() {
-      final int times = 50;
-      for (int i = 0; i < times; i++) {
-         String pwd = Passwords.generate();
-         assertTrue(isValidPassword(pwd), "Failed with: " + pwd);
-      }
-   }
-
-   @Test
-   public void testGeneratorGeneratesRequestedLength() {
-      int passwordLength = new Random().nextInt(40) + 10;
-      assertEquals(Passwords.generate(passwordLength).length(), passwordLength);
-   }
-}

http://git-wip-us.apache.org/repos/asf/jclouds/blob/9fef6ed0/providers/profitbricks/src/main/java/org/jclouds/profitbricks/compute/ProfitBricksComputeServiceAdapter.java
----------------------------------------------------------------------
diff --git a/providers/profitbricks/src/main/java/org/jclouds/profitbricks/compute/ProfitBricksComputeServiceAdapter.java b/providers/profitbricks/src/main/java/org/jclouds/profitbricks/compute/ProfitBricksComputeServiceAdapter.java
index c6fd08b..c92c6a0 100644
--- a/providers/profitbricks/src/main/java/org/jclouds/profitbricks/compute/ProfitBricksComputeServiceAdapter.java
+++ b/providers/profitbricks/src/main/java/org/jclouds/profitbricks/compute/ProfitBricksComputeServiceAdapter.java
@@ -71,7 +71,8 @@ import com.google.common.collect.Lists;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.inject.Inject;
-import org.jclouds.util.Passwords;
+
+import org.jclouds.util.PasswordGenerator;
 
 @Singleton
 public class ProfitBricksComputeServiceAdapter implements ComputeServiceAdapter<Server, Hardware, Provisionable, Location> {
@@ -85,6 +86,7 @@ public class ProfitBricksComputeServiceAdapter implements ComputeServiceAdapter<
    private final ListeningExecutorService executorService;
    private final ProvisioningJob.Factory jobFactory;
    private final ProvisioningManager provisioningManager;
+   private final PasswordGenerator.Config passwordGenerator;
 
    private static final Integer DEFAULT_LAN_ID = 1;
 
@@ -93,12 +95,14 @@ public class ProfitBricksComputeServiceAdapter implements ComputeServiceAdapter<
            @Named(POLL_PREDICATE_DATACENTER) Predicate<String> waitDcUntilAvailable,
            @Named(PROPERTY_USER_THREADS) ListeningExecutorService executorService,
            ProvisioningJob.Factory jobFactory,
-           ProvisioningManager provisioningManager) {
+           ProvisioningManager provisioningManager,
+           PasswordGenerator.Config passwordGenerator) {
       this.api = api;
       this.waitDcUntilAvailable = waitDcUntilAvailable;
       this.executorService = executorService;
       this.jobFactory = jobFactory;
       this.provisioningManager = provisioningManager;
+      this.passwordGenerator = passwordGenerator;
    }
    
    @Override
@@ -115,7 +119,7 @@ public class ProfitBricksComputeServiceAdapter implements ComputeServiceAdapter<
 
       TemplateOptions options = template.getOptions();
       final String loginUser = isNullOrEmpty(options.getLoginUser()) ? "root" : options.getLoginUser();
-      final String password = options.hasLoginPassword() ? options.getLoginPassword() : Passwords.generate();
+      final String password = options.hasLoginPassword() ? options.getLoginPassword() : passwordGenerator.generate();
 
       final org.jclouds.compute.domain.Image image = template.getImage();
 

http://git-wip-us.apache.org/repos/asf/jclouds/blob/9fef6ed0/providers/profitbricks/src/main/java/org/jclouds/profitbricks/compute/config/ProfitBricksComputeServiceContextModule.java
----------------------------------------------------------------------
diff --git a/providers/profitbricks/src/main/java/org/jclouds/profitbricks/compute/config/ProfitBricksComputeServiceContextModule.java b/providers/profitbricks/src/main/java/org/jclouds/profitbricks/compute/config/ProfitBricksComputeServiceContextModule.java
index cd60bd7..e83db06 100644
--- a/providers/profitbricks/src/main/java/org/jclouds/profitbricks/compute/config/ProfitBricksComputeServiceContextModule.java
+++ b/providers/profitbricks/src/main/java/org/jclouds/profitbricks/compute/config/ProfitBricksComputeServiceContextModule.java
@@ -57,6 +57,7 @@ import org.jclouds.profitbricks.domain.Provisionable;
 import org.jclouds.profitbricks.domain.ProvisioningState;
 import org.jclouds.profitbricks.domain.Server;
 import org.jclouds.profitbricks.domain.Storage;
+import org.jclouds.util.PasswordGenerator;
 
 import com.google.common.base.Function;
 import com.google.common.base.Predicate;
@@ -98,6 +99,16 @@ public class ProfitBricksComputeServiceContextModule extends
       bind(new TypeLiteral<Function<Hardware, Hardware>>() {
       }).to(Class.class.cast(IdentityFunction.class));
    }
+   
+   @Provides
+   @Singleton
+   protected PasswordGenerator.Config providePasswordGenerator() {
+      return new PasswordGenerator()
+            .lower().min(2).max(10).exclude("ilowyz".toCharArray())
+            .upper().min(2).max(10).exclude("IOWYZ".toCharArray())
+            .numbers().min(2).max(10).exclude("10".toCharArray())
+            .symbols().count(0);
+   }
 
    @Provides
    @Singleton

http://git-wip-us.apache.org/repos/asf/jclouds/blob/9fef6ed0/providers/profitbricks/src/main/java/org/jclouds/profitbricks/util/Preconditions.java
----------------------------------------------------------------------
diff --git a/providers/profitbricks/src/main/java/org/jclouds/profitbricks/util/Preconditions.java b/providers/profitbricks/src/main/java/org/jclouds/profitbricks/util/Preconditions.java
index b0013c4..28d197b 100644
--- a/providers/profitbricks/src/main/java/org/jclouds/profitbricks/util/Preconditions.java
+++ b/providers/profitbricks/src/main/java/org/jclouds/profitbricks/util/Preconditions.java
@@ -21,7 +21,6 @@ import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Strings.isNullOrEmpty;
 import static com.google.common.net.InetAddresses.isInetAddress;
 import static org.jclouds.profitbricks.util.MacAddresses.isMacAddress;
-import static org.jclouds.util.Passwords.isValidPassword;
 
 import java.util.List;
 import java.util.regex.Pattern;
@@ -92,9 +91,15 @@ public final class Preconditions {
    public static void checkSize(Float size) {
       checkArgument(size > 1, "Storage size must be > 1GB");
    }
+   
+   private static final int VALID_PASSWORD_MIN_LENGTH = 8;
+   private static final int VALID_PASSWORD_MAX_LENGTH = 50;
+   private static final String PASSWORD_FORMAT = String.format(
+           "[a-zA-Z0-9][^iIloOwWyYzZ10]{%d,%d}", VALID_PASSWORD_MIN_LENGTH - 1, VALID_PASSWORD_MAX_LENGTH);
+   private static final Pattern PASSWORD_PATTERN = Pattern.compile(PASSWORD_FORMAT);
 
    public static void checkPassword(String password) {
-      checkArgument(isValidPassword(password), "Password must be between 8 and 50 characters, "
+      checkArgument(PASSWORD_PATTERN.matcher(password).matches(), "Password must be between 8 and 50 characters, "
               + "only a-z, A-Z, 0-9 without  characters i, I, l, o, O, w, W, y, Y, z, Z and 1, 0");
    }
 }