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 0000000000..e0d14ad772
--- /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.
+ *
+ * This class allows to configure the password requirements for:
+ *
+ * - Number of upper case and lower case letters
+ * - Inclusion of numbers
+ * - Inclusion of special characters
+ *
+ * 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.
+ *
+ * It also allows to configure forbidden characters to accommodate the password
+ * requirements for the different clouds.
+ *
+ * Example usage:
+ *
+ * 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();
+ *
+ *
+ */
+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 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);
+ }
+ }
+}
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 cf2d57da2f..0000000000
--- 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 INVALID_CHARS = ImmutableSet.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;
- }
-}
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 0000000000..1e08b0a64a
--- /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]+$"));
+ }
+}
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 d1d5d50ae1..0000000000
--- 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 validPasswords = ImmutableList.of(
- "fKVasTnNm", "84625894", "QQQQQQQQ", "qqqqqqqq", "asdfghjk"
- );
- private final List 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);
- }
-}
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 c6fd08bed8..c92c6a0758 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 {
@@ -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 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();
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 cd60bd7a6f..e83db06d05 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>() {
}).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
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 b0013c497d..28d197b87f 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");
}
}