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: + *

+ * 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"); } }