mirror of https://github.com/apache/jclouds.git
JCLOUDS-1362: Better password generation utility
This commit is contained in:
parent
11640b6c2e
commit
9fef6ed06b
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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]+$"));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue