Skip to content

Commit

Permalink
Merge pull request getmoto#1721 from sepulworld/adding_secretsmanager…
Browse files Browse the repository at this point in the history
…_random_password

Added SecretsManager get_random_password mock
  • Loading branch information
spulec authored Jul 19, 2018
2 parents de88ae8 + 6c7a22c commit a1d095c
Show file tree
Hide file tree
Showing 5 changed files with 253 additions and 6 deletions.
14 changes: 14 additions & 0 deletions moto/secretsmanager/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,17 @@ def __init__(self):
"ResourceNotFoundException",
"Secrets Manager can't find the specified secret"
)


class ClientError(SecretsManagerClientError):
def __init__(self, message):
super(ClientError, self).__init__(
'InvalidParameterValue',
message)


class InvalidParameterException(SecretsManagerClientError):
def __init__(self, message):
super(InvalidParameterException, self).__init__(
'InvalidParameterException',
message)
42 changes: 36 additions & 6 deletions moto/secretsmanager/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
import boto3

from moto.core import BaseBackend, BaseModel
from .exceptions import ResourceNotFoundException
from .exceptions import (
ResourceNotFoundException,
InvalidParameterException,
ClientError
)
from .utils import random_password, secret_arn


class SecretsManager(BaseModel):
Expand Down Expand Up @@ -40,7 +45,7 @@ def get_secret_value(self, secret_id, version_id, version_stage):
raise ResourceNotFoundException()

response = json.dumps({
"ARN": self.secret_arn(self.region, self.secret_id),
"ARN": secret_arn(self.region, self.secret_id),
"Name": self.secret_id,
"VersionId": "A435958A-D821-4193-B719-B7769357AER4",
"SecretString": self.secret_string,
Expand All @@ -58,16 +63,41 @@ def create_secret(self, name, secret_string, **kwargs):
self.secret_id = name

response = json.dumps({
"ARN": self.secret_arn(self.region, name),
"ARN": secret_arn(self.region, name),
"Name": self.secret_id,
"VersionId": "A435958A-D821-4193-B719-B7769357AER4",
})

return response

def secret_arn(self, region, secret_id):
return "arn:aws:secretsmanager:{0}:1234567890:secret:{1}-rIjad".format(
region, secret_id)
def get_random_password(self, password_length,
exclude_characters, exclude_numbers,
exclude_punctuation, exclude_uppercase,
exclude_lowercase, include_space,
require_each_included_type):
# password size must have value less than or equal to 4096
if password_length > 4096:
raise ClientError(
"ClientError: An error occurred (ValidationException) \
when calling the GetRandomPassword operation: 1 validation error detected: Value '{}' at 'passwordLength' \
failed to satisfy constraint: Member must have value less than or equal to 4096".format(password_length))
if password_length < 4:
raise InvalidParameterException(
"InvalidParameterException: An error occurred (InvalidParameterException) \
when calling the GetRandomPassword operation: Password length is too short based on the required types.")

response = json.dumps({
"RandomPassword": random_password(password_length,
exclude_characters,
exclude_numbers,
exclude_punctuation,
exclude_uppercase,
exclude_lowercase,
include_space,
require_each_included_type)
})

return response


available_regions = (
Expand Down
21 changes: 21 additions & 0 deletions moto/secretsmanager/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,24 @@ def create_secret(self):
name=name,
secret_string=secret_string
)

def get_random_password(self):
password_length = self._get_param('PasswordLength', if_none=32)
exclude_characters = self._get_param('ExcludeCharacters', if_none='')
exclude_numbers = self._get_param('ExcludeNumbers', if_none=False)
exclude_punctuation = self._get_param('ExcludePunctuation', if_none=False)
exclude_uppercase = self._get_param('ExcludeUppercase', if_none=False)
exclude_lowercase = self._get_param('ExcludeLowercase', if_none=False)
include_space = self._get_param('IncludeSpace', if_none=False)
require_each_included_type = self._get_param(
'RequireEachIncludedType', if_none=True)
return secretsmanager_backends[self.region].get_random_password(
password_length=password_length,
exclude_characters=exclude_characters,
exclude_numbers=exclude_numbers,
exclude_punctuation=exclude_punctuation,
exclude_uppercase=exclude_uppercase,
exclude_lowercase=exclude_lowercase,
include_space=include_space,
require_each_included_type=require_each_included_type
)
72 changes: 72 additions & 0 deletions moto/secretsmanager/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from __future__ import unicode_literals

import random
import string
import six
import re


def random_password(password_length, exclude_characters, exclude_numbers,
exclude_punctuation, exclude_uppercase, exclude_lowercase,
include_space, require_each_included_type):

password = ''
required_characters = ''

if not exclude_lowercase and not exclude_uppercase:
password += string.ascii_letters
required_characters += random.choice(_exclude_characters(
string.ascii_lowercase, exclude_characters))
required_characters += random.choice(_exclude_characters(
string.ascii_uppercase, exclude_characters))
elif not exclude_lowercase:
password += string.ascii_lowercase
required_characters += random.choice(_exclude_characters(
string.ascii_lowercase, exclude_characters))
elif not exclude_uppercase:
password += string.ascii_uppercase
required_characters += random.choice(_exclude_characters(
string.ascii_uppercase, exclude_characters))
if not exclude_numbers:
password += string.digits
required_characters += random.choice(_exclude_characters(
string.digits, exclude_characters))
if not exclude_punctuation:
password += string.punctuation
required_characters += random.choice(_exclude_characters(
string.punctuation, exclude_characters))
if include_space:
password += " "
required_characters += " "

password = ''.join(
six.text_type(random.choice(password))
for x in range(password_length))

if require_each_included_type:
password = _add_password_require_each_included_type(
password, required_characters)

password = _exclude_characters(password, exclude_characters)
return password


def secret_arn(region, secret_id):
return "arn:aws:secretsmanager:{0}:1234567890:secret:{1}-rIjad".format(
region, secret_id)


def _exclude_characters(password, exclude_characters):
for c in exclude_characters:
if c in string.punctuation:
# Escape punctuation regex usage
c = "\{0}".format(c)
password = re.sub(c, '', str(password))
return password


def _add_password_require_each_included_type(password, required_characters):
password_with_required_char = password[:-len(required_characters)]
password_with_required_char += required_characters

return password_with_required_char
110 changes: 110 additions & 0 deletions tests/test_secretsmanager/test_secretsmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from moto import mock_secretsmanager
from botocore.exceptions import ClientError
import sure # noqa
import string
import unittest
from nose.tools import assert_raises

@mock_secretsmanager
Expand Down Expand Up @@ -33,3 +35,111 @@ def test_create_secret():
assert result['Name'] == 'test-secret'
secret = conn.get_secret_value(SecretId='test-secret')
assert secret['SecretString'] == 'foosecret'

@mock_secretsmanager
def test_get_random_password_default_length():
conn = boto3.client('secretsmanager', region_name='us-west-2')

random_password = conn.get_random_password()
assert len(random_password['RandomPassword']) == 32

@mock_secretsmanager
def test_get_random_password_default_requirements():
# When require_each_included_type, default true
conn = boto3.client('secretsmanager', region_name='us-west-2')

random_password = conn.get_random_password()
# Should contain lowercase, upppercase, digit, special character
assert any(c.islower() for c in random_password['RandomPassword'])
assert any(c.isupper() for c in random_password['RandomPassword'])
assert any(c.isdigit() for c in random_password['RandomPassword'])
assert any(c in string.punctuation
for c in random_password['RandomPassword'])

@mock_secretsmanager
def test_get_random_password_custom_length():
conn = boto3.client('secretsmanager', region_name='us-west-2')

random_password = conn.get_random_password(PasswordLength=50)
assert len(random_password['RandomPassword']) == 50

@mock_secretsmanager
def test_get_random_exclude_lowercase():
conn = boto3.client('secretsmanager', region_name='us-west-2')

random_password = conn.get_random_password(PasswordLength=55,
ExcludeLowercase=True)
assert any(c.islower() for c in random_password['RandomPassword']) == False

@mock_secretsmanager
def test_get_random_exclude_uppercase():
conn = boto3.client('secretsmanager', region_name='us-west-2')

random_password = conn.get_random_password(PasswordLength=55,
ExcludeUppercase=True)
assert any(c.isupper() for c in random_password['RandomPassword']) == False

@mock_secretsmanager
def test_get_random_exclude_characters_and_symbols():
conn = boto3.client('secretsmanager', region_name='us-west-2')

random_password = conn.get_random_password(PasswordLength=20,
ExcludeCharacters='xyzDje@?!.')
assert any(c in 'xyzDje@?!.' for c in random_password['RandomPassword']) == False

@mock_secretsmanager
def test_get_random_exclude_numbers():
conn = boto3.client('secretsmanager', region_name='us-west-2')

random_password = conn.get_random_password(PasswordLength=100,
ExcludeNumbers=True)
assert any(c.isdigit() for c in random_password['RandomPassword']) == False

@mock_secretsmanager
def test_get_random_exclude_punctuation():
conn = boto3.client('secretsmanager', region_name='us-west-2')

random_password = conn.get_random_password(PasswordLength=100,
ExcludePunctuation=True)
assert any(c in string.punctuation
for c in random_password['RandomPassword']) == False

@mock_secretsmanager
def test_get_random_include_space_false():
conn = boto3.client('secretsmanager', region_name='us-west-2')

random_password = conn.get_random_password(PasswordLength=300)
assert any(c.isspace() for c in random_password['RandomPassword']) == False

@mock_secretsmanager
def test_get_random_include_space_true():
conn = boto3.client('secretsmanager', region_name='us-west-2')

random_password = conn.get_random_password(PasswordLength=4,
IncludeSpace=True)
assert any(c.isspace() for c in random_password['RandomPassword']) == True

@mock_secretsmanager
def test_get_random_require_each_included_type():
conn = boto3.client('secretsmanager', region_name='us-west-2')

random_password = conn.get_random_password(PasswordLength=4,
RequireEachIncludedType=True)
assert any(c in string.punctuation for c in random_password['RandomPassword']) == True
assert any(c in string.ascii_lowercase for c in random_password['RandomPassword']) == True
assert any(c in string.ascii_uppercase for c in random_password['RandomPassword']) == True
assert any(c in string.digits for c in random_password['RandomPassword']) == True

@mock_secretsmanager
def test_get_random_too_short_password():
conn = boto3.client('secretsmanager', region_name='us-west-2')

with assert_raises(ClientError):
random_password = conn.get_random_password(PasswordLength=3)

@mock_secretsmanager
def test_get_random_too_long_password():
conn = boto3.client('secretsmanager', region_name='us-west-2')

with assert_raises(Exception):
random_password = conn.get_random_password(PasswordLength=5555)

0 comments on commit a1d095c

Please sign in to comment.