marketplace/vm-solution/common/password.py (56 lines of code) (raw):

# Lint as: python3 # Copyright 2015 Google Inc. All rights reserved. # # 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. """A DM template that generates password as an output, namely "password". An example YAML showing how this template can be used: resources: - name: generated-password type: password.py properties: length: 8 includeSymbols: true - name: main-template type: main-template.jinja properties: password: $(ref.generated-password.password) Input properties to this template: - length: the length of the generated password. At least 8. Default 8. - includeSymbols: true/false whether to include symbol chars. Default false. The generated password satisfies the following requirements: - The length is as specified, - Containing letters and numbers, and optionally symbols if specified, - Starting with a letter, - Containing characters from at least 3 of the 4 categories: uppercases, lowercases, numbers, and symbols. """ import random import six import yaml PROPERTY_LENGTH = 'length' PROPERTY_INCLUDE_SYMBOLS = 'includeSymbols' # Note the omission of some hard to distinguish characters like I, l, 0, and O. UPPERS = 'ABCDEFGHJKLMNPQRSTUVWXYZ' LOWERS = 'abcdefghijkmnopqrstuvwxyz' ALPHABET = UPPERS + LOWERS DIGITS = '123456789' ALPHANUMS = ALPHABET + DIGITS # Including only symbols that can be passed around easily in shell scripts. SYMBOLS = '*-+.' CANDIDATES_WITH_SYMBOLS = ALPHANUMS + SYMBOLS CANDIDATES_WITHOUT_SYMBOLS = ALPHANUMS CATEGORIES_WITH_SYMBOLS = [UPPERS, LOWERS, DIGITS, SYMBOLS] CATEGORIES_WITHOUT_SYMBOLS = [UPPERS, LOWERS, DIGITS] MIN_LENGTH = 8 class InputError(Exception): """Raised when input properties are unexpected.""" def GenerateConfig(context): """Entry function to generate the DM config.""" if six.PY2: raise Exception('Use Python 3 when When using password generation') props = context.properties length = props.setdefault(PROPERTY_LENGTH, MIN_LENGTH) include_symbols = props.setdefault(PROPERTY_INCLUDE_SYMBOLS, False) if not isinstance(include_symbols, bool): raise InputError('%s must be a boolean' % PROPERTY_INCLUDE_SYMBOLS) content = { 'resources': [], 'outputs': [{ 'name': 'password', 'value': GeneratePassword(length, include_symbols) }] } return yaml.dump(content) def GeneratePassword(length=8, include_symbols=False): """Generates a random password.""" if length < MIN_LENGTH: raise InputError('Password length must be at least %d' % MIN_LENGTH) if include_symbols: candidates = CANDIDATES_WITH_SYMBOLS categories = CATEGORIES_WITH_SYMBOLS else: candidates = CANDIDATES_WITHOUT_SYMBOLS categories = CATEGORIES_WITHOUT_SYMBOLS # Generates up to the specified length minus the number of categories. # Then inserts one character for each category, ensuring that the character # satisfy the category if the generated string hasn't already. generated = ([random.choice(ALPHABET)] + [random.choice(candidates) for _ in range(length - 1 - len(categories))]) for category in categories: _InsertAndEnsureSatisfaction(generated, category, candidates) return ''.join(generated) def _InsertAndEnsureSatisfaction(generated, required, all_candidates): """Inserts 1 char into generated, satisfying required if not already. If the required characters are not already in the generated string, one will be inserted. If any required character is already in the generated string, a random character from all_candidates will be inserted. The insertion happens at a random location but not at the beginning. Args: generated: the string to be modified. required: list of required characters to check for. all_candidates: list of characters to choose from if the required characters are already satisfied. """ if set(generated).isdisjoint(required): # Not yet satisfied. Insert a required candidate. _InsertInto(generated, required) else: # Already satisfied. Insert any candidate. _InsertInto(generated, all_candidates) def _InsertInto(generated, candidates): """Inserts a random candidate into a random non-zero index of generated.""" # Avoids inserting at index 0, since the first character follows its own rule. generated.insert(random.randint(1, len(generated) - 1), random.choice(candidates))