Repository: jclouds
Updated Branches:
  refs/heads/2.0.x f1126a113 -> 222099660


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/22209966
Tree: http://git-wip-us.apache.org/repos/asf/jclouds/tree/22209966
Diff: http://git-wip-us.apache.org/repos/asf/jclouds/diff/22209966

Branch: refs/heads/2.0.x
Commit: 2220996605b2dde9e088135b146f2161df68799f
Parents: f1126a1
Author: Ignasi Barrera <[email protected]>
Authored: Thu Jan 4 01:21:10 2018 +0100
Committer: Ignasi Barrera <[email protected]>
Committed: Mon Jan 8 08:42:12 2018 +0100

----------------------------------------------------------------------
 .../org/jclouds/util/PasswordGenerator.java     | 195 +++++++++++++++++++
 .../org/jclouds/util/PasswordGeneratorTest.java |  80 ++++++++
 .../ProfitBricksComputeServiceAdapter.java      |  10 +-
 ...ProfitBricksComputeServiceContextModule.java |  11 ++
 .../profitbricks/util/Preconditions.java        |   9 +-
 5 files changed, 300 insertions(+), 5 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/jclouds/blob/22209966/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/22209966/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/22209966/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 766b360..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
@@ -59,7 +59,6 @@ import org.jclouds.profitbricks.domain.Server;
 import org.jclouds.profitbricks.domain.Snapshot;
 import org.jclouds.profitbricks.domain.Storage;
 import org.jclouds.profitbricks.features.ServerApi;
-import org.jclouds.profitbricks.util.Passwords;
 import org.jclouds.rest.ResourceNotFoundException;
 
 import com.google.common.base.Function;
@@ -73,6 +72,8 @@ import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.inject.Inject;
 
+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/22209966/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/22209966/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 b15689d..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.profitbricks.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");
    }
 }

Reply via email to