A resource to generate random strings
A common pattern for templates is to define parameters for passwords and specify the password on stack create. For complex templates this can impact template maintainability and makes stack creation more complicated. In addition, a password parameter leaves the temptation of using the same password (or worse, the parameter default) on each stack launch which raises the security risk. This resource allows secrets to be generated in the template and propagated to compute resources as needed. Often the user will never want to see the generated string, but if they do they can put it in a stack output. This resource has been requested by the tripleo project who run trunk heat, so it will be of immediate benefit to them. Implements blueprint random-string-resource Change-Id: I044fc7717133fc7a3520dca774300024925ff2aa
This commit is contained in:
83
heat/engine/resources/random_string.py
Normal file
83
heat/engine/resources/random_string.py
Normal file
@@ -0,0 +1,83 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
#
|
||||
# Licensed 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.
|
||||
|
||||
from heat.db import api as db_api
|
||||
from heat.engine import resource
|
||||
from heat.engine import properties
|
||||
|
||||
import random
|
||||
import string
|
||||
|
||||
|
||||
class RandomString(resource.Resource):
|
||||
'''
|
||||
A resource which generates a random string.
|
||||
|
||||
This is useful for configuring passwords and secrets on services.
|
||||
'''
|
||||
properties_schema = {
|
||||
'length': properties.Schema(
|
||||
properties.INTEGER,
|
||||
_('Length of the string to generate.'),
|
||||
default=32,
|
||||
constraints=[properties.Range(1, 512)]),
|
||||
'sequence': properties.Schema(
|
||||
properties.STRING,
|
||||
_('Sequence of characters to build the random string from.'),
|
||||
default='lettersdigits',
|
||||
constraints=[properties.AllowedValues((
|
||||
'lettersdigits', 'letters', 'lowercase', 'uppercase', 'digits',
|
||||
'hexdigits', 'octdigits'))]),
|
||||
'salt': properties.Schema(
|
||||
properties.STRING,
|
||||
_('Value which can be set or changed on stack update to trigger '
|
||||
'the resource for replacement with a new random string . '
|
||||
'The salt value itself is ignored by the random generator.'))
|
||||
}
|
||||
|
||||
attributes_schema = {
|
||||
'value': _('The random string generated by this resource'),
|
||||
}
|
||||
|
||||
_sequences = {
|
||||
'lettersdigits': string.ascii_letters + string.digits,
|
||||
'letters': string.ascii_letters,
|
||||
'lowercase': string.ascii_lowercase,
|
||||
'uppercase': string.ascii_uppercase,
|
||||
'digits': string.digits,
|
||||
'hexdigits': string.digits + 'ABCDEF',
|
||||
'octdigits': string.octdigits
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _generate_random_string(sequence, length):
|
||||
rand = random.SystemRandom()
|
||||
return ''.join(rand.choice(sequence) for x in xrange(length))
|
||||
|
||||
def handle_create(self):
|
||||
length = self.properties.get('length')
|
||||
sequence = self._sequences[self.properties.get('sequence')]
|
||||
random_string = self._generate_random_string(sequence, length)
|
||||
db_api.resource_data_set(self, 'value', random_string, redact=True)
|
||||
|
||||
def _resolve_attribute(self, name):
|
||||
if name == 'value':
|
||||
return db_api.resource_data_get(self, 'value')
|
||||
|
||||
|
||||
def resource_mapping():
|
||||
return {
|
||||
'OS::Heat::RandomString': RandomString,
|
||||
}
|
||||
114
heat/tests/test_random_string.py
Normal file
114
heat/tests/test_random_string.py
Normal file
@@ -0,0 +1,114 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Licensed 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.
|
||||
|
||||
|
||||
from heat.common import exception
|
||||
from heat.common import template_format
|
||||
from heat.engine import parser
|
||||
from heat.engine.resources.random_string import RandomString
|
||||
|
||||
from heat.tests.common import HeatTestCase
|
||||
from heat.tests import utils
|
||||
|
||||
import testscenarios
|
||||
from testtools.matchers import MatchesRegex
|
||||
from testtools.matchers import HasLength
|
||||
|
||||
load_tests = testscenarios.load_tests_apply_scenarios
|
||||
|
||||
|
||||
class TestRandomString(HeatTestCase):
|
||||
|
||||
template_random_string = '''
|
||||
HeatTemplateFormatVersion: '2012-12-12'
|
||||
Resources:
|
||||
secret1:
|
||||
Type: OS::Heat::RandomString
|
||||
secret2:
|
||||
Type: OS::Heat::RandomString
|
||||
Properties:
|
||||
length: 10
|
||||
secret3:
|
||||
Type: OS::Heat::RandomString
|
||||
Properties:
|
||||
length: 100
|
||||
sequence: octdigits
|
||||
'''
|
||||
|
||||
def setUp(self):
|
||||
super(HeatTestCase, self).setUp()
|
||||
utils.setup_dummy_db()
|
||||
self.ctx = utils.dummy_context()
|
||||
|
||||
def create_stack(self, template):
|
||||
t = template_format.parse(template)
|
||||
self.stack = self.parse_stack(t)
|
||||
self.assertEqual(None, self.stack.create())
|
||||
return self.stack
|
||||
|
||||
def parse_stack(self, t):
|
||||
stack_name = 'test_stack'
|
||||
tmpl = parser.Template(t)
|
||||
stack = parser.Stack(utils.dummy_context(), stack_name, tmpl)
|
||||
stack.validate()
|
||||
stack.store()
|
||||
return stack
|
||||
|
||||
def test_random_string(self):
|
||||
stack = self.create_stack(self.template_random_string)
|
||||
secret1 = stack['secret1']
|
||||
|
||||
random_string = secret1.FnGetAtt('value')
|
||||
self.assertThat(random_string, MatchesRegex('[a-zA-Z0-9]{32}'))
|
||||
self.assertRaises(exception.InvalidTemplateAttribute,
|
||||
secret1.FnGetAtt, 'foo')
|
||||
|
||||
secret2 = stack['secret2']
|
||||
random_string = secret2.FnGetAtt('value')
|
||||
self.assertThat(random_string, MatchesRegex('[a-zA-Z0-9]{10}'))
|
||||
|
||||
secret3 = stack['secret3']
|
||||
random_string = secret3.FnGetAtt('value')
|
||||
self.assertThat(random_string, MatchesRegex('[0-7]{100}'))
|
||||
|
||||
|
||||
class TestGenerateRandomString(HeatTestCase):
|
||||
|
||||
scenarios = [
|
||||
('lettersdigits', dict(
|
||||
length=1, seq='lettersdigits', pattern='[a-zA-Z0-9]')),
|
||||
('letters', dict(
|
||||
length=10, seq='letters', pattern='[a-zA-Z]')),
|
||||
('lowercase', dict(
|
||||
length=100, seq='lowercase', pattern='[a-z]')),
|
||||
('uppercase', dict(
|
||||
length=50, seq='uppercase', pattern='[A-Z]')),
|
||||
('digits', dict(
|
||||
length=512, seq='digits', pattern='[0-9]')),
|
||||
('hexdigits', dict(
|
||||
length=16, seq='hexdigits', pattern='[A-F0-9]')),
|
||||
('octdigits', dict(
|
||||
length=32, seq='octdigits', pattern='[0-7]'))
|
||||
]
|
||||
|
||||
def test_generate_random_string(self):
|
||||
# run each test multiple times to confirm random generator
|
||||
# doesn't generate a matching pattern by chance
|
||||
for i in range(1, 32):
|
||||
sequence = RandomString._sequences[self.seq]
|
||||
r = RandomString._generate_random_string(sequence, self.length)
|
||||
|
||||
self.assertThat(r, HasLength(self.length))
|
||||
regex = '%s{%s}' % (self.pattern, self.length)
|
||||
self.assertThat(r, MatchesRegex(regex))
|
||||
@@ -10,6 +10,7 @@ mock>=1.0
|
||||
mox>=0.5.3
|
||||
testtools>=0.9.32
|
||||
testrepository>=0.0.17
|
||||
testscenarios>=0.4
|
||||
python-glanceclient>=0.9.0
|
||||
sphinx>=1.1.2
|
||||
oslo.sphinx
|
||||
|
||||
Reference in New Issue
Block a user