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/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/providers/profitbricks/src/main/java/org/jclouds/profitbricks/compute/ProfitBricksComputeServiceAdapter.java b/providers/profitbricks/src/main/java/org/jclouds/profitbricks/compute/ProfitBricksComputeServiceAdapter.java index 766b3609a5..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 @@ -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 { @@ -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 b15689d7ac..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.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"); } }