This closes #1169
diff --git a/brooklyn-server/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/policy/CreatePasswordSensorIntegrationTest.java b/brooklyn-server/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/policy/CreatePasswordSensorIntegrationTest.java
new file mode 100644
index 0000000..360b705
--- /dev/null
+++ b/brooklyn-server/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/policy/CreatePasswordSensorIntegrationTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.apache.brooklyn.camp.brooklyn.policy;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.sensor.AttributeSensor;
+import org.apache.brooklyn.camp.brooklyn.AbstractYamlTest;
+import org.apache.brooklyn.core.entity.Entities;
+import org.apache.brooklyn.core.sensor.Sensors;
+import org.apache.brooklyn.entity.software.base.EmptySoftwareProcess;
+import org.apache.brooklyn.test.Asserts;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.Iterables;
+
+public class CreatePasswordSensorIntegrationTest extends AbstractYamlTest {
+
+ private static final Logger LOG = LoggerFactory.getLogger(CreatePasswordSensorIntegrationTest.class);
+ AttributeSensor<String> PASSWORD_1 = Sensors.newStringSensor("test.password.1", "Host name as known internally in " +
+ "the subnet where it is running (if different to host.name)");
+ AttributeSensor<String> PASSWORD_2 = Sensors.newStringSensor("test.password.2", "Host name as known internally in " +
+ "the subnet where it is running (if different to host.name)");
+
+ @Test(groups = "Integration")
+ public void testProvisioningProperties() throws Exception {
+ final Entity app = createAndStartApplication(loadYaml("EmptySoftwareProcessWithPassword.yaml"));
+
+ waitForApplicationTasks(app);
+ EmptySoftwareProcess entity = Iterables.getOnlyElement(Entities.descendants(app, EmptySoftwareProcess.class));
+
+ assertPasswordLength(entity, PASSWORD_1, 15);
+
+ assertPasswordOnlyContains(entity, PASSWORD_2, "abc");
+
+ }
+
+ private void assertPasswordOnlyContains(EmptySoftwareProcess entity, AttributeSensor<String> password, String acceptableChars) {
+ String attribute_2 = entity.getAttribute(password);
+ for (char c : attribute_2.toCharArray()) {
+ Asserts.assertTrue(acceptableChars.indexOf(c) != -1);
+ }
+ }
+
+ private void assertPasswordLength(EmptySoftwareProcess entity, AttributeSensor<String> password, int expectedLength) {
+ String attribute_1 = entity.getAttribute(password);
+ Asserts.assertEquals(attribute_1.length(), expectedLength);
+ }
+
+}
diff --git a/brooklyn-server/camp/camp-brooklyn/src/test/resources/EmptySoftwareProcessWithPassword.yaml b/brooklyn-server/camp/camp-brooklyn/src/test/resources/EmptySoftwareProcessWithPassword.yaml
new file mode 100644
index 0000000..2b6acb2
--- /dev/null
+++ b/brooklyn-server/camp/camp-brooklyn/src/test/resources/EmptySoftwareProcessWithPassword.yaml
@@ -0,0 +1,17 @@
+name: example-with-CreatePasswordSensor
+description: |
+ Creates an emptyService and then attaches a password to it
+origin: https://github.com/apache/incubator-brooklyn
+location: localhost
+services:
+- type: org.apache.brooklyn.entity.software.base.EmptySoftwareProcess
+ brooklyn.initializers:
+ - type: org.apache.brooklyn.core.sensor.password.CreatePasswordSensor
+ brooklyn.config:
+ name: test.password.1
+ password.length: 15
+ - type: org.apache.brooklyn.core.sensor.password.CreatePasswordSensor
+ brooklyn.config:
+ name: test.password.2
+ password.chars: abc
+
diff --git a/brooklyn-server/core/src/main/java/org/apache/brooklyn/core/sensor/password/CreatePasswordSensor.java b/brooklyn-server/core/src/main/java/org/apache/brooklyn/core/sensor/password/CreatePasswordSensor.java
new file mode 100644
index 0000000..7b7a908
--- /dev/null
+++ b/brooklyn-server/core/src/main/java/org/apache/brooklyn/core/sensor/password/CreatePasswordSensor.java
@@ -0,0 +1,59 @@
+/*
+ * 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.apache.brooklyn.core.sensor.password;
+
+import java.util.Map;
+
+import org.apache.brooklyn.api.entity.EntityLocal;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.core.effector.AddSensor;
+import org.apache.brooklyn.util.core.config.ConfigBag;
+import org.apache.brooklyn.util.text.Identifiers;
+
+public class CreatePasswordSensor extends AddSensor<String> {
+
+ public static final ConfigKey<Integer> PASSWORD_LENGTH = ConfigKeys.newIntegerConfigKey("password.length", "The length of the password to be created", 12);
+
+ public static final ConfigKey<String> ACCEPTABLE_CHARS = ConfigKeys.newStringConfigKey("password.chars", "The characters allowed in password");
+
+ private Integer passwordLength;
+ private String acceptableChars;
+
+ public CreatePasswordSensor(Map<String, String> params) {
+ this(ConfigBag.newInstance(params));
+ }
+
+ public CreatePasswordSensor(ConfigBag params) {
+ super(params);
+ passwordLength = params.get(PASSWORD_LENGTH);
+ acceptableChars = params.get(ACCEPTABLE_CHARS);
+ }
+
+ @Override
+ public void apply(EntityLocal entity) {
+ super.apply(entity);
+
+ String password = acceptableChars == null
+ ? Identifiers.makeRandomPassword(passwordLength)
+ : Identifiers.makeRandomPassword(passwordLength, acceptableChars);
+
+ entity.sensors().set(sensor, password);
+ }
+}
diff --git a/brooklyn-server/core/src/test/java/org/apache/brooklyn/core/sensor/password/CreatePasswordSensorTest.java b/brooklyn-server/core/src/test/java/org/apache/brooklyn/core/sensor/password/CreatePasswordSensorTest.java
new file mode 100644
index 0000000..0dc1126
--- /dev/null
+++ b/brooklyn-server/core/src/test/java/org/apache/brooklyn/core/sensor/password/CreatePasswordSensorTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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.apache.brooklyn.core.sensor.password;
+
+import org.apache.brooklyn.api.entity.EntitySpec;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.sensor.AttributeSensor;
+import org.apache.brooklyn.core.entity.EntityInternal;
+import org.apache.brooklyn.core.sensor.Sensors;
+import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport;
+import org.apache.brooklyn.core.test.entity.TestEntity;
+import org.apache.brooklyn.test.Asserts;
+import org.apache.brooklyn.util.core.config.ConfigBag;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableList;
+
+public class CreatePasswordSensorTest extends BrooklynAppUnitTestSupport{
+
+ final static AttributeSensor<String> SENSOR_STRING = Sensors.newStringSensor("aString");
+ private EntityInternal entity;
+
+ @BeforeMethod(alwaysRun = true)
+ public void setup() throws Exception {
+ super.setUp();
+
+ entity = app.createAndManageChild(EntitySpec.create(TestEntity.class)
+ .location(app.newLocalhostProvisioningLocation().obtain()));
+ app.start(ImmutableList.<Location>of());
+ }
+
+ @Test
+ public void testCreatePasswordCreatesAPasswordOfDefaultLength() {
+
+ final CreatePasswordSensor sensor = new CreatePasswordSensor(ConfigBag.newInstance()
+ .configure(CreatePasswordSensor.SENSOR_NAME, SENSOR_STRING.getName()));
+ sensor.apply(entity);
+
+ String password = entity.getAttribute(SENSOR_STRING);
+ Asserts.assertEquals(password.length(), 12);
+ }
+}
\ No newline at end of file
diff --git a/brooklyn-server/utils/common/src/main/java/org/apache/brooklyn/util/text/Identifiers.java b/brooklyn-server/utils/common/src/main/java/org/apache/brooklyn/util/text/Identifiers.java
index c2ec4a5..bea6208 100644
--- a/brooklyn-server/utils/common/src/main/java/org/apache/brooklyn/util/text/Identifiers.java
+++ b/brooklyn-server/utils/common/src/main/java/org/apache/brooklyn/util/text/Identifiers.java
@@ -18,16 +18,30 @@
*/
package org.apache.brooklyn.util.text;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
import java.util.Random;
+import java.util.Set;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
public class Identifiers {
private static Random random = new Random();
- /** @see #JAVA_GOOD_PACKAGE_OR_CLASS_REGEX */
- public static final String JAVA_GOOD_START_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_";
- /** @see #JAVA_GOOD_PACKAGE_OR_CLASS_REGEX */
- public static final String JAVA_GOOD_NONSTART_CHARS = JAVA_GOOD_START_CHARS+"1234567890";
+ public static final String UPPER_CASE_ALPHA = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+ public static final String LOWER_CASE_ALPHA = "abcdefghijklmnopqrstuvwxyz";
+ public static final String NUMERIC = "1234567890";
+ public static final String NON_ALPHA_NUMERIC = "!@$%^&*()-_=+[]{};:\\|/?,.<>~";
+
+ /** @see #JAVA_GOOD_PACKAGE_OR_CLASS_REGEX */
+ public static final String JAVA_GOOD_START_CHARS = UPPER_CASE_ALPHA + LOWER_CASE_ALPHA +"_";
+ /** @see #JAVA_GOOD_PACKAGE_OR_CLASS_REGEX */
+ public static final String JAVA_GOOD_NONSTART_CHARS = JAVA_GOOD_START_CHARS+NUMERIC;
/** @see #JAVA_GOOD_PACKAGE_OR_CLASS_REGEX */
public static final String JAVA_GOOD_SEGMENT_REGEX = "["+JAVA_GOOD_START_CHARS+"]"+"["+JAVA_GOOD_NONSTART_CHARS+"]*";
/** regex for a java package or class name using "good" chars, that is no accents or funny unicodes.
@@ -37,15 +51,21 @@
public static final String JAVA_GOOD_PACKAGE_OR_CLASS_REGEX = "("+JAVA_GOOD_SEGMENT_REGEX+"\\."+")*"+JAVA_GOOD_SEGMENT_REGEX;
/** as {@link #JAVA_GOOD_PACKAGE_OR_CLASS_REGEX} but allowing a dollar sign inside a class name (e.g. Foo$1) */
public static final String JAVA_GOOD_BINARY_REGEX = JAVA_GOOD_PACKAGE_OR_CLASS_REGEX+"(\\$["+JAVA_GOOD_NONSTART_CHARS+"]+)*";
-
- public static final String JAVA_GENERATED_IDENTIFIER_START_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
- public static final String JAVA_GENERATED_IDENTIFIERNONSTART_CHARS = JAVA_GENERATED_IDENTIFIER_START_CHARS+"1234567890";
+
+ public static final String JAVA_GENERATED_IDENTIFIER_START_CHARS = UPPER_CASE_ALPHA + LOWER_CASE_ALPHA;
+
+ public static final String JAVA_GENERATED_IDENTIFIERNONSTART_CHARS = JAVA_GENERATED_IDENTIFIER_START_CHARS+NUMERIC;
public static final String BASE64_VALID_CHARS = JAVA_GENERATED_IDENTIFIERNONSTART_CHARS+"+=";
-
- public static final String ID_VALID_START_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
- public static final String ID_VALID_NONSTART_CHARS = ID_VALID_START_CHARS+"1234567890";
-
+
+ public static final String ID_VALID_START_CHARS = UPPER_CASE_ALPHA + LOWER_CASE_ALPHA;
+ public static final String ID_VALID_NONSTART_CHARS = ID_VALID_START_CHARS+ NUMERIC;
+
+ public static final String PASSWORD_VALID_CHARS = NON_ALPHA_NUMERIC + ID_VALID_NONSTART_CHARS;
+
+ // We only create a secure random when it is first used
+ private static Random secureRandom = null;
+
/** makes a random id string (letters and numbers) of the given length;
* starts with letter (upper or lower) so can be used as java-id;
* tests ensure random distribution, so random ID of length 5
@@ -91,15 +111,65 @@
return new String(id);
}
+ /**
+ *
+ * @param length of password to be returned
+ * @return randomly generated password containing at least one of each upper case,
+ * lower case, numeric, and non alpha-numeric characters. Hopefully this is acceptible
+ * for most password schemes.
+ */
+ public static String makeRandomPassword(final int length) {
+ return makeRandomPassword(length, UPPER_CASE_ALPHA, LOWER_CASE_ALPHA, NUMERIC, NON_ALPHA_NUMERIC, PASSWORD_VALID_CHARS);
+ }
+
+ /**
+ * A fairly slow but hopefully secure way to randomly select characters for a password
+ * Takes a pool of acceptible characters using the first set in the pool for the first character,
+ * second set for the second character, ..., nth set for all remaining character.
+ *
+ * @param length length of password
+ * @param passwordValidCharsPool pool of acceptable character sets
+ * @return a randomly generated password
+ */
+ public static String makeRandomPassword(final int length, String... passwordValidCharsPool) {
+ Preconditions.checkState(length >= passwordValidCharsPool.length);
+ int l = 0;
+
+ Character[] password = new Character[length];
+
+ for(int i = 0; i < passwordValidCharsPool.length; i++){
+ password[l++] = pickRandomCharFrom(passwordValidCharsPool[i]);
+ }
+
+ String remainingValidChars = mergeCharacterSets(passwordValidCharsPool);
+
+ while(l < length) {
+ password[l++] = pickRandomCharFrom(remainingValidChars);
+ }
+
+ List<Character> list = Arrays.asList(password);
+ Collections.shuffle(list);
+ return Joiner.on("").join(list);
+ }
+
+ protected static String mergeCharacterSets(String... s) {
+ Set characters = new HashSet<Character>();
+ for (String characterSet : s) {
+ characters.addAll(Arrays.asList(characterSet.split("")));
+ }
+
+ return Joiner.on("").join(characters);
+ }
+
/** creates a short identifier comfortable in java and OS's, given an input hash code
* <p>
- * result is always at least of length 1, shorter if the hash is smaller */
+ * result is always at least of length 1, shorter if the hash is smaller */
public static String makeIdFromHash(long d) {
StringBuffer result = new StringBuffer();
if (d<0) d=-d;
// correction for Long.MIN_VALUE
if (d<0) d=-(d+1000);
-
+
result.append(ID_VALID_START_CHARS.charAt((int)(d % (26+26))));
d /= (26+26);
while (d!=0) {
@@ -108,31 +178,31 @@
}
return result.toString();
}
-
+
/** makes a random id string (letters and numbers) of the given length;
* starts with letter (upper or lower) so can be used as java-id;
- * tests ensure random distribution, so random ID of length 5
- * is about 2^29 possibilities
+ * tests ensure random distribution, so random ID of length 5
+ * is about 2^29 possibilities
* <p>
- * implementation is efficient, uses char array, and
+ * implementation is efficient, uses char array, and
* makes one call to random per 5 chars; makeRandomId(5)
* takes about 4 times as long as a simple Math.random call,
* or about 50 times more than a simple x++ instruction;
* in other words, it's appropriate for contexts where random id's are needed,
- * but use efficiently (ie cache it per object), and
+ * but use efficiently (ie cache it per object), and
* prefer to use a counter where feasible
**/
public static String makeRandomJavaId(int l) {
// copied from Monterey util's com.cloudsoftcorp.util.StringUtils.
// TODO should share code with makeRandomId, just supplying different char sets (though the char sets in fact are the same..)
- //this version is 30-50% faster than the old double-based one,
+ //this version is 30-50% faster than the old double-based one,
//which computed a random every 3 turns --
//takes about 600 ns to do id of len 10, compared to 10000 ns for old version [on 1.6ghz machine]
if (l<=0) return "";
char[] id = new char[l];
int d = random.nextInt( (26+26) * (26+26+10) * (26+26+10) * (26+26+10) * (26+26+10));
- int i = 0;
+ int i = 0;
id[i] = JAVA_GENERATED_IDENTIFIER_START_CHARS.charAt(d % (26+26));
d /= (26+26);
if (++i<l) do {
@@ -151,6 +221,7 @@
public static double randomDouble() {
return random.nextDouble();
}
+
public static long randomLong() {
return random.nextLong();
}
@@ -173,7 +244,6 @@
byte[] buf = new byte[length];
return randomBytes(buf);
}
-
public static String makeRandomBase64Id(int length) {
StringBuilder s = new StringBuilder();
while (length>0) {
@@ -182,6 +252,7 @@
}
return s.toString();
}
+
public static String getBase64IdFromValue(long value) {
return getBase64IdFromValue(value, 10);
}
@@ -210,7 +281,6 @@
idx = idx >> 6;
}
}
-
public static boolean isValidToken(String token, String validStartChars, String validSubsequentChars) {
if (token==null || token.length()==0) return false;
if (validStartChars.indexOf(token.charAt(0))==-1) return false;
@@ -218,4 +288,15 @@
if (validSubsequentChars.indexOf(token.charAt(i))==-1) return false;
return true;
}
+
+ private static char pickRandomCharFrom(String validChars) {
+ return validChars.charAt(getSecureRandom().nextInt(validChars.length()));
+ }
+
+ private static Random getSecureRandom() {
+ if(secureRandom == null) {
+ secureRandom = new SecureRandom();
+ }
+ return secureRandom;
+ }
}
diff --git a/brooklyn-server/utils/common/src/test/java/org/apache/brooklyn/util/text/IdentifiersTest.java b/brooklyn-server/utils/common/src/test/java/org/apache/brooklyn/util/text/IdentifiersTest.java
index 7b4f999..e21bb81 100644
--- a/brooklyn-server/utils/common/src/test/java/org/apache/brooklyn/util/text/IdentifiersTest.java
+++ b/brooklyn-server/utils/common/src/test/java/org/apache/brooklyn/util/text/IdentifiersTest.java
@@ -21,8 +21,6 @@
import java.util.HashSet;
import java.util.Set;
-import org.apache.brooklyn.util.text.Identifiers;
-import org.apache.brooklyn.util.text.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.Assert;
@@ -98,5 +96,23 @@
Assert.assertTrue("foo".matches(Identifiers.JAVA_GOOD_BINARY_REGEX));
Assert.assertTrue("foo.bar.Baz$1".matches(Identifiers.JAVA_GOOD_BINARY_REGEX));
}
+
+ @Test
+ public void testPassword() {
+ String upper = "ABC";
+ String numbers = "123";
+ String symbols = "!£$";
+ String password = Identifiers.makeRandomPassword(4, upper, numbers, symbols, Identifiers.PASSWORD_VALID_CHARS);
+
+ Assert.assertTrue(password.matches(".*[" + upper + "].*"));
+ Assert.assertTrue(password.matches(".*[" + numbers + "].*"));
+ Assert.assertTrue(password.matches(".*[" + symbols + "].*"));
+ }
+
+ @Test
+ public void testCharMerge() {
+ String characters = Identifiers.mergeCharacterSets("abc", "bcd", "ghjj");
+ Assert.assertEquals(characters.indexOf('b'), characters.lastIndexOf('b'));
+ }
}