Skip to content

Commit a1d095c

Browse files
authored
Merge pull request #1721 from sepulworld/adding_secretsmanager_random_password
Added SecretsManager get_random_password mock
2 parents de88ae8 + 6c7a22c commit a1d095c

File tree

5 files changed

+253
-6
lines changed

5 files changed

+253
-6
lines changed

moto/secretsmanager/exceptions.py

+14
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,17 @@ def __init__(self):
1313
"ResourceNotFoundException",
1414
"Secrets Manager can't find the specified secret"
1515
)
16+
17+
18+
class ClientError(SecretsManagerClientError):
19+
def __init__(self, message):
20+
super(ClientError, self).__init__(
21+
'InvalidParameterValue',
22+
message)
23+
24+
25+
class InvalidParameterException(SecretsManagerClientError):
26+
def __init__(self, message):
27+
super(InvalidParameterException, self).__init__(
28+
'InvalidParameterException',
29+
message)

moto/secretsmanager/models.py

+36-6
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66
import boto3
77

88
from moto.core import BaseBackend, BaseModel
9-
from .exceptions import ResourceNotFoundException
9+
from .exceptions import (
10+
ResourceNotFoundException,
11+
InvalidParameterException,
12+
ClientError
13+
)
14+
from .utils import random_password, secret_arn
1015

1116

1217
class SecretsManager(BaseModel):
@@ -40,7 +45,7 @@ def get_secret_value(self, secret_id, version_id, version_stage):
4045
raise ResourceNotFoundException()
4146

4247
response = json.dumps({
43-
"ARN": self.secret_arn(self.region, self.secret_id),
48+
"ARN": secret_arn(self.region, self.secret_id),
4449
"Name": self.secret_id,
4550
"VersionId": "A435958A-D821-4193-B719-B7769357AER4",
4651
"SecretString": self.secret_string,
@@ -58,16 +63,41 @@ def create_secret(self, name, secret_string, **kwargs):
5863
self.secret_id = name
5964

6065
response = json.dumps({
61-
"ARN": self.secret_arn(self.region, name),
66+
"ARN": secret_arn(self.region, name),
6267
"Name": self.secret_id,
6368
"VersionId": "A435958A-D821-4193-B719-B7769357AER4",
6469
})
6570

6671
return response
6772

68-
def secret_arn(self, region, secret_id):
69-
return "arn:aws:secretsmanager:{0}:1234567890:secret:{1}-rIjad".format(
70-
region, secret_id)
73+
def get_random_password(self, password_length,
74+
exclude_characters, exclude_numbers,
75+
exclude_punctuation, exclude_uppercase,
76+
exclude_lowercase, include_space,
77+
require_each_included_type):
78+
# password size must have value less than or equal to 4096
79+
if password_length > 4096:
80+
raise ClientError(
81+
"ClientError: An error occurred (ValidationException) \
82+
when calling the GetRandomPassword operation: 1 validation error detected: Value '{}' at 'passwordLength' \
83+
failed to satisfy constraint: Member must have value less than or equal to 4096".format(password_length))
84+
if password_length < 4:
85+
raise InvalidParameterException(
86+
"InvalidParameterException: An error occurred (InvalidParameterException) \
87+
when calling the GetRandomPassword operation: Password length is too short based on the required types.")
88+
89+
response = json.dumps({
90+
"RandomPassword": random_password(password_length,
91+
exclude_characters,
92+
exclude_numbers,
93+
exclude_punctuation,
94+
exclude_uppercase,
95+
exclude_lowercase,
96+
include_space,
97+
require_each_included_type)
98+
})
99+
100+
return response
71101

72102

73103
available_regions = (

moto/secretsmanager/responses.py

+21
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,24 @@ def create_secret(self):
2323
name=name,
2424
secret_string=secret_string
2525
)
26+
27+
def get_random_password(self):
28+
password_length = self._get_param('PasswordLength', if_none=32)
29+
exclude_characters = self._get_param('ExcludeCharacters', if_none='')
30+
exclude_numbers = self._get_param('ExcludeNumbers', if_none=False)
31+
exclude_punctuation = self._get_param('ExcludePunctuation', if_none=False)
32+
exclude_uppercase = self._get_param('ExcludeUppercase', if_none=False)
33+
exclude_lowercase = self._get_param('ExcludeLowercase', if_none=False)
34+
include_space = self._get_param('IncludeSpace', if_none=False)
35+
require_each_included_type = self._get_param(
36+
'RequireEachIncludedType', if_none=True)
37+
return secretsmanager_backends[self.region].get_random_password(
38+
password_length=password_length,
39+
exclude_characters=exclude_characters,
40+
exclude_numbers=exclude_numbers,
41+
exclude_punctuation=exclude_punctuation,
42+
exclude_uppercase=exclude_uppercase,
43+
exclude_lowercase=exclude_lowercase,
44+
include_space=include_space,
45+
require_each_included_type=require_each_included_type
46+
)

moto/secretsmanager/utils.py

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from __future__ import unicode_literals
2+
3+
import random
4+
import string
5+
import six
6+
import re
7+
8+
9+
def random_password(password_length, exclude_characters, exclude_numbers,
10+
exclude_punctuation, exclude_uppercase, exclude_lowercase,
11+
include_space, require_each_included_type):
12+
13+
password = ''
14+
required_characters = ''
15+
16+
if not exclude_lowercase and not exclude_uppercase:
17+
password += string.ascii_letters
18+
required_characters += random.choice(_exclude_characters(
19+
string.ascii_lowercase, exclude_characters))
20+
required_characters += random.choice(_exclude_characters(
21+
string.ascii_uppercase, exclude_characters))
22+
elif not exclude_lowercase:
23+
password += string.ascii_lowercase
24+
required_characters += random.choice(_exclude_characters(
25+
string.ascii_lowercase, exclude_characters))
26+
elif not exclude_uppercase:
27+
password += string.ascii_uppercase
28+
required_characters += random.choice(_exclude_characters(
29+
string.ascii_uppercase, exclude_characters))
30+
if not exclude_numbers:
31+
password += string.digits
32+
required_characters += random.choice(_exclude_characters(
33+
string.digits, exclude_characters))
34+
if not exclude_punctuation:
35+
password += string.punctuation
36+
required_characters += random.choice(_exclude_characters(
37+
string.punctuation, exclude_characters))
38+
if include_space:
39+
password += " "
40+
required_characters += " "
41+
42+
password = ''.join(
43+
six.text_type(random.choice(password))
44+
for x in range(password_length))
45+
46+
if require_each_included_type:
47+
password = _add_password_require_each_included_type(
48+
password, required_characters)
49+
50+
password = _exclude_characters(password, exclude_characters)
51+
return password
52+
53+
54+
def secret_arn(region, secret_id):
55+
return "arn:aws:secretsmanager:{0}:1234567890:secret:{1}-rIjad".format(
56+
region, secret_id)
57+
58+
59+
def _exclude_characters(password, exclude_characters):
60+
for c in exclude_characters:
61+
if c in string.punctuation:
62+
# Escape punctuation regex usage
63+
c = "\{0}".format(c)
64+
password = re.sub(c, '', str(password))
65+
return password
66+
67+
68+
def _add_password_require_each_included_type(password, required_characters):
69+
password_with_required_char = password[:-len(required_characters)]
70+
password_with_required_char += required_characters
71+
72+
return password_with_required_char

tests/test_secretsmanager/test_secretsmanager.py

+110
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from moto import mock_secretsmanager
66
from botocore.exceptions import ClientError
77
import sure # noqa
8+
import string
9+
import unittest
810
from nose.tools import assert_raises
911

1012
@mock_secretsmanager
@@ -33,3 +35,111 @@ def test_create_secret():
3335
assert result['Name'] == 'test-secret'
3436
secret = conn.get_secret_value(SecretId='test-secret')
3537
assert secret['SecretString'] == 'foosecret'
38+
39+
@mock_secretsmanager
40+
def test_get_random_password_default_length():
41+
conn = boto3.client('secretsmanager', region_name='us-west-2')
42+
43+
random_password = conn.get_random_password()
44+
assert len(random_password['RandomPassword']) == 32
45+
46+
@mock_secretsmanager
47+
def test_get_random_password_default_requirements():
48+
# When require_each_included_type, default true
49+
conn = boto3.client('secretsmanager', region_name='us-west-2')
50+
51+
random_password = conn.get_random_password()
52+
# Should contain lowercase, upppercase, digit, special character
53+
assert any(c.islower() for c in random_password['RandomPassword'])
54+
assert any(c.isupper() for c in random_password['RandomPassword'])
55+
assert any(c.isdigit() for c in random_password['RandomPassword'])
56+
assert any(c in string.punctuation
57+
for c in random_password['RandomPassword'])
58+
59+
@mock_secretsmanager
60+
def test_get_random_password_custom_length():
61+
conn = boto3.client('secretsmanager', region_name='us-west-2')
62+
63+
random_password = conn.get_random_password(PasswordLength=50)
64+
assert len(random_password['RandomPassword']) == 50
65+
66+
@mock_secretsmanager
67+
def test_get_random_exclude_lowercase():
68+
conn = boto3.client('secretsmanager', region_name='us-west-2')
69+
70+
random_password = conn.get_random_password(PasswordLength=55,
71+
ExcludeLowercase=True)
72+
assert any(c.islower() for c in random_password['RandomPassword']) == False
73+
74+
@mock_secretsmanager
75+
def test_get_random_exclude_uppercase():
76+
conn = boto3.client('secretsmanager', region_name='us-west-2')
77+
78+
random_password = conn.get_random_password(PasswordLength=55,
79+
ExcludeUppercase=True)
80+
assert any(c.isupper() for c in random_password['RandomPassword']) == False
81+
82+
@mock_secretsmanager
83+
def test_get_random_exclude_characters_and_symbols():
84+
conn = boto3.client('secretsmanager', region_name='us-west-2')
85+
86+
random_password = conn.get_random_password(PasswordLength=20,
87+
ExcludeCharacters='xyzDje@?!.')
88+
assert any(c in 'xyzDje@?!.' for c in random_password['RandomPassword']) == False
89+
90+
@mock_secretsmanager
91+
def test_get_random_exclude_numbers():
92+
conn = boto3.client('secretsmanager', region_name='us-west-2')
93+
94+
random_password = conn.get_random_password(PasswordLength=100,
95+
ExcludeNumbers=True)
96+
assert any(c.isdigit() for c in random_password['RandomPassword']) == False
97+
98+
@mock_secretsmanager
99+
def test_get_random_exclude_punctuation():
100+
conn = boto3.client('secretsmanager', region_name='us-west-2')
101+
102+
random_password = conn.get_random_password(PasswordLength=100,
103+
ExcludePunctuation=True)
104+
assert any(c in string.punctuation
105+
for c in random_password['RandomPassword']) == False
106+
107+
@mock_secretsmanager
108+
def test_get_random_include_space_false():
109+
conn = boto3.client('secretsmanager', region_name='us-west-2')
110+
111+
random_password = conn.get_random_password(PasswordLength=300)
112+
assert any(c.isspace() for c in random_password['RandomPassword']) == False
113+
114+
@mock_secretsmanager
115+
def test_get_random_include_space_true():
116+
conn = boto3.client('secretsmanager', region_name='us-west-2')
117+
118+
random_password = conn.get_random_password(PasswordLength=4,
119+
IncludeSpace=True)
120+
assert any(c.isspace() for c in random_password['RandomPassword']) == True
121+
122+
@mock_secretsmanager
123+
def test_get_random_require_each_included_type():
124+
conn = boto3.client('secretsmanager', region_name='us-west-2')
125+
126+
random_password = conn.get_random_password(PasswordLength=4,
127+
RequireEachIncludedType=True)
128+
assert any(c in string.punctuation for c in random_password['RandomPassword']) == True
129+
assert any(c in string.ascii_lowercase for c in random_password['RandomPassword']) == True
130+
assert any(c in string.ascii_uppercase for c in random_password['RandomPassword']) == True
131+
assert any(c in string.digits for c in random_password['RandomPassword']) == True
132+
133+
@mock_secretsmanager
134+
def test_get_random_too_short_password():
135+
conn = boto3.client('secretsmanager', region_name='us-west-2')
136+
137+
with assert_raises(ClientError):
138+
random_password = conn.get_random_password(PasswordLength=3)
139+
140+
@mock_secretsmanager
141+
def test_get_random_too_long_password():
142+
conn = boto3.client('secretsmanager', region_name='us-west-2')
143+
144+
with assert_raises(Exception):
145+
random_password = conn.get_random_password(PasswordLength=5555)

0 commit comments

Comments
 (0)