JCLOUDS-1362: Better password generation utility

This commit is contained in:
Ignasi Barrera 2018-01-04 01:21:10 +01:00
parent 11640b6c2e
commit 9fef6ed06b
7 changed files with 300 additions and 138 deletions

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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]+$"));
}
}

View File

@ -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);
}
}

View File

@ -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();

View File

@ -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;
@ -99,6 +100,16 @@ public class ProfitBricksComputeServiceContextModule extends
}).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
@Named(POLL_PREDICATE_DATACENTER)

View File

@ -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;
@ -93,8 +92,14 @@ public final class Preconditions {
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");
}
}