JCLOUDS-1362: Better password generation utility

This commit is contained in:
Ignasi Barrera 2018-01-04 01:21:10 +01:00
parent f1126a1131
commit 2220996605
5 changed files with 300 additions and 5 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

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

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

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

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