diff --git a/bless/aws_lambda/bless_lambda.py b/bless/aws_lambda/bless_lambda.py index f04e894a..98ee00a5 100644 --- a/bless/aws_lambda/bless_lambda.py +++ b/bless/aws_lambda/bless_lambda.py @@ -3,11 +3,11 @@ :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. """ -from bless.aws_lambda.bless_lambda_user import lambda_handler_user +from bless.aws_lambda.bless_lambda_lyft_host import lambda_lyft_host_handler def lambda_handler(*args, **kwargs): """ - Wrapper around lambda_handler_user for backwards compatibility + Wrapper to redirect to Lyft version of bless_lambda_host """ - return lambda_handler_user(*args, **kwargs) + return lambda_lyft_host_handler(*args, **kwargs) diff --git a/bless/aws_lambda/bless_lambda_lyft_host.py b/bless/aws_lambda/bless_lambda_lyft_host.py new file mode 100644 index 00000000..594e7aa8 --- /dev/null +++ b/bless/aws_lambda/bless_lambda_lyft_host.py @@ -0,0 +1,316 @@ +""" +.. module: bless.aws_lambda.bless_lambda + :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +""" +import base64 +import logging +import time + +import boto3 +import botocore +import os +import kmsauth + +from bless.config.bless_config import BLESS_OPTIONS_SECTION, \ + ENTROPY_MINIMUM_BITS_OPTION, RANDOM_SEED_BYTES_OPTION, \ + BLESS_CA_SECTION, CA_PRIVATE_KEY_FILE_OPTION, LOGGING_LEVEL_OPTION, \ + KMSAUTH_KEY_ID_OPTION +from bless.config.bless_lyft_config import CERTIFICATE_VALIDITY_WINDOW_SEC_OPTION, CERTIFICATE_TYPE_OPTION,\ + KMSAUTH_CONTEXT_OPTION, CROSS_ACCOUNT_ROLE_ARN_OPTION +from bless.config.bless_lyft_config import BlessLyftConfig +from bless.request.bless_request_lyft_host import BlessLyftHostSchema +from bless.request.bless_request_user import BlessUserSchema +from bless.ssh.certificate_authorities.ssh_certificate_authority_factory import \ + get_ssh_certificate_authority +from bless.ssh.certificates.ssh_certificate_builder import SSHCertificateType +from bless.ssh.certificates.ssh_certificate_builder_factory import get_ssh_certificate_builder + +logger = logging.getLogger() + +REGIONS = { + 'iad': 'us-east-1', + 'sfo': 'us-west-1' +} + + +def get_role_name_from_request(request): + if request.onebox_name: + return 'onebox-production-iad' + else: + return '{}-{}-{}'.format( + request.service_name, + request.service_instance, + request.service_region) + + +def get_role_name(instance_id, cross_account_role_arn, aws_region='us-east-1'): + sts_client = boto3.client('sts') + assumed_role_object = sts_client.assume_role( + RoleArn=cross_account_role_arn, + RoleSessionName='AssumedRoleSession' + ) + credentials = assumed_role_object['Credentials'] + ec2_resource = boto3.resource( + 'ec2', + region_name=aws_region, + api_version='2016-11-15', + aws_access_key_id=credentials['AccessKeyId'], + aws_secret_access_key=credentials['SecretAccessKey'], + aws_session_token=credentials['SessionToken'] + ) + + instance = ec2_resource.Instance(instance_id) + try: + role = instance.iam_instance_profile['Arn'].split('/')[1] + except botocore.exceptions.ClientError: + logger.exception( + 'Could not find instance {0}. ' + 'role: {1}, region: {2}'.format(instance_id, + cross_account_role_arn, + aws_region) + ) + role = None + except IndexError: + logger.error( + 'Could not find the role associated with {0}. ' + 'role: {1}, region: {2}'.format(instance_id, + cross_account_role_arn, + aws_region) + ) + role = None + except Exception: + logger.exception( + 'Failed to lookup role for instance id {0}. ' + 'role: {1}, region: {2}'.format(instance_id, + cross_account_role_arn, + aws_region) + ) + role = None + return role + + +def validate_instance_id(instance_id, request, cross_account_role_arn): + aws_region = REGIONS.get(request.service_region, 'us-east-1') + role = get_role_name(instance_id, cross_account_role_arn, aws_region) + try: + role_split = role.split('-') + role_service_name = role_split[0] + role_service_instance = role_split[1] + role_service_region = role_split[2] + except IndexError: + logger.error( + 'Role is not a valid format {0}.'.format(role) + ) + return False + if (role_service_name in request.service_name + and role_service_instance == request.service_instance + and role_service_region == request.service_region): + return True + else: + return False + + +def get_hostnames(service_name, service_instance, service_region, instance_id, + availability_zone, onebox_name, is_canary): + cluster_name = '{0}-{1}-{2}'.format( + service_name, service_instance, service_region) + az_split = availability_zone.split('-') + az_shortened = az_split[2][-1] # last letter of 3rd block of az + + hostname_prefixes = [] + if instance_id: + # strip 'i' in 'i-12345' + instance_id_stripped = instance_id.split('-')[1] + hostname_prefixes.append(instance_id) + hostname_prefixes.append(instance_id_stripped) + hostname_prefixes.append(cluster_name) + hostname_prefixes.append(service_name) + hostname_prefixes.append('{service_name}-{az_letter}'.format( + service_name=service_name, + az_letter=az_shortened)) + hostname_prefixes.append('{service_name}-{service_region}'.format( + service_name=service_name, + service_region=service_region)) + hostname_prefixes.append('{service_name}-{service_instance}'.format( + service_name=service_name, + service_instance=service_instance)) + if is_canary: + hostname_prefixes.append('{service_name}-canary'.format( + service_name=service_name)) + hostname_prefixes.append('{cluster_name}-canary'.format( + cluster_name=cluster_name)) + if onebox_name: + hostname_prefixes.append('{onebox_name}.onebox'.format( + onebox_name=onebox_name)) + + hostname_suffixes = ['.lyft.net', '.ln'] + if service_name == 'gateway': + hostname_suffixes.append('lyft.com') + hostnames = [] + for prefix in hostname_prefixes: + for suffix in hostname_suffixes: + hostnames.append('{prefix}{suffix}'.format( + prefix=prefix, suffix=suffix)) + + return hostnames + + +def get_certificate_type(certificate_type_option): + if certificate_type_option == 'user': + return 1 + elif certificate_type_option == 'host': + return 2 + else: + raise ValueError('Invalid certificate type option: {}'.format(certificate_type_option)) + + +def lambda_lyft_host_handler( + event, + context=None, + ca_private_key_password=None, + entropy_check=True, + config_file=None): + """ + This is the function that will be called when the lambda function starts. + :param event: Dictionary of the json request. + :param context: AWS LambdaContext Object + http://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html + :param ca_private_key_password: For local testing, if the password is provided, skip the KMS + decrypt. + :param entropy_check: For local testing, if set to false, it will skip checking entropy and + won't try to fetch additional random from KMS + :param config_file: The config file to load the SSH CA private key from, and additional settings + :return: the SSH Certificate that can be written to id_rsa-cert.pub or similar file. + """ + # AWS Region determines configs related to KMS + region = os.environ['AWS_REGION'] + + # Load the deployment config values + if config_file is None: + config_file = os.path.join(os.getcwd(), 'bless_deploy.cfg') + config = BlessLyftConfig(region, config_file=config_file) + + certificate_type = get_certificate_type(config.get(BLESS_OPTIONS_SECTION, CERTIFICATE_TYPE_OPTION)) + logging_level = config.get(BLESS_OPTIONS_SECTION, LOGGING_LEVEL_OPTION) + numeric_level = getattr(logging, logging_level.upper(), None) + if not isinstance(numeric_level, int): + raise ValueError('Invalid log level: {}'.format(logging_level)) + + logger.setLevel(numeric_level) + + certificate_validity_window_seconds = config.getint(BLESS_OPTIONS_SECTION, + CERTIFICATE_VALIDITY_WINDOW_SEC_OPTION) + entropy_minimum_bits = config.getint(BLESS_OPTIONS_SECTION, ENTROPY_MINIMUM_BITS_OPTION) + random_seed_bytes = config.getint(BLESS_OPTIONS_SECTION, RANDOM_SEED_BYTES_OPTION) + cross_account_role_arn = config.get(BLESS_OPTIONS_SECTION, CROSS_ACCOUNT_ROLE_ARN_OPTION) + ca_private_key_file = config.get(BLESS_CA_SECTION, CA_PRIVATE_KEY_FILE_OPTION) + password_ciphertext_b64 = config.getpassword() + kmsauth_key_id = config.get(BLESS_CA_SECTION, KMSAUTH_KEY_ID_OPTION) + kmsauth_context = config.get(BLESS_CA_SECTION, KMSAUTH_CONTEXT_OPTION) + + # read the private key .pem + with open(os.path.join(os.getcwd(), ca_private_key_file), 'r') as f: + ca_private_key = bytes(f.read(), 'ascii') + + # decrypt ca private key password + if ca_private_key_password is None: + kms_client = boto3.client('kms', region_name=region) + ca_password = kms_client.decrypt( + CiphertextBlob=base64.b64decode(password_ciphertext_b64)) + ca_private_key_password = ca_password['Plaintext'] + + # if running as a Lambda, we can check the entropy pool and seed it with KMS if desired + if entropy_check: + with open('/proc/sys/kernel/random/entropy_avail', 'r') as f: + entropy = int(f.read()) + logger.debug(entropy) + if entropy < entropy_minimum_bits: + logger.info( + 'System entropy was {}, which is lower than the entropy_' + 'minimum {}. Using KMS to seed /dev/urandom'.format( + entropy, entropy_minimum_bits)) + if not kms_client: + kms_client = boto3.client('kms', region_name=region) + response = kms_client.generate_random( + NumberOfBytes=random_seed_bytes) + random_seed = response['Plaintext'] + with open('/dev/urandom', 'w') as urandom: + urandom.write(random_seed) + + # Process cert request + if certificate_type == SSHCertificateType.HOST: + schema = BlessLyftHostSchema(strict=True) + else: + schema = BlessUserSchema(strict=True) + request = schema.load(event).data + + # cert values determined only by lambda and its configs + current_time = int(time.time()) + valid_before = current_time + certificate_validity_window_seconds + valid_after = current_time - certificate_validity_window_seconds + + # Authenticate the host with KMS, if key is setup + if kmsauth_key_id: + if request.kmsauth_token: + validator = kmsauth.KMSTokenValidator( + kmsauth_key_id, + kmsauth_key_id, + kmsauth_context, + region + ) + # decrypt_token will raise a TokenValidationError if token doesn't match + role_name = get_role_name_from_request(request) + validator.decrypt_token('2/service/{}'.format(role_name), request.kmsauth_token) + else: + raise ValueError('Invalid request, missing kmsauth token') + + # Build the cert + ca = get_ssh_certificate_authority(ca_private_key, ca_private_key_password) + cert_builder = get_ssh_certificate_builder(ca, certificate_type, + request.public_key_to_sign) + if certificate_type == SSHCertificateType.USER: + cert_builder.add_valid_principal(request.remote_username) + # cert_builder is needed to obtain the SSH public key's fingerprint + key_id = 'request[{}] for[{}] from[{}] command[{}] ssh_key:[{}] ca:[{}] valid_to[{}]'.format( + context.aws_request_id, request.bastion_user, request.bastion_user_ip, request.command, + cert_builder.ssh_public_key.fingerprint, context.invoked_function_arn, + time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(valid_before))) + cert_builder.set_critical_option_source_address(request.bastion_ip) + elif certificate_type == SSHCertificateType.HOST: + if not validate_instance_id(request.instance_id, request, cross_account_role_arn): + request.instance_id = None + remote_hostnames = get_hostnames(request.service_name, + request.service_instance, + request.service_region, + request.instance_id, + request.instance_availability_zone, + request.onebox_name, + request.is_canary) + for remote_hostname in remote_hostnames: + cert_builder.add_valid_principal(remote_hostname) + key_id = 'request[{}] ssh_key:[{}] ca:[{}] valid_to[{}]'.format( + context.aws_request_id, cert_builder.ssh_public_key.fingerprint, + context.invoked_function_arn, time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(valid_before))) + else: + raise ValueError("Unknown certificate type") + + cert_builder.set_valid_before(valid_before) + cert_builder.set_valid_after(valid_after) + + cert_builder.set_key_id(key_id) + cert = cert_builder.get_cert_file() + + if certificate_type == SSHCertificateType.HOST: + remote_name = ', '.join(remote_hostnames) + bastion_ip = None + else: + remote_name = request.remote_username + bastion_ip = request.bastion_ip + + logger.info( + 'Issued a cert to bastion_ip[{}] for the remote_username of [{}] with the key_id[{}] and ' + 'valid_from[{}])'.format( + bastion_ip, remote_name, key_id, + time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(valid_after)))) + return cert diff --git a/bless/config/bless_lyft_config.py b/bless/config/bless_lyft_config.py new file mode 100644 index 00000000..6076366b --- /dev/null +++ b/bless/config/bless_lyft_config.py @@ -0,0 +1,77 @@ +""" +.. module: bless.config.bless_config + :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +""" +import configparser + +from bless.config.bless_config import BLESS_OPTIONS_SECTION + +CERTIFICATE_VALIDITY_WINDOW_SEC_OPTION = 'certificate_validity_seconds' +CERTIFICATE_VALIDITY_SEC_DEFAULT = 60 * 2 + +ENTROPY_MINIMUM_BITS_OPTION = 'entropy_minimum_bits' +ENTROPY_MINIMUM_BITS_DEFAULT = 2048 + +RANDOM_SEED_BYTES_OPTION = 'random_seed_bytes' +RANDOM_SEED_BYTES_DEFAULT = 256 + +CROSS_ACCOUNT_ROLE_ARN_OPTION = 'cross_account_role_arn' +CROSS_ACCOUNT_ROLE_ARN_DEFAULT = None + +LOGGING_LEVEL_OPTION = 'logging_level' +LOGGING_LEVEL_DEFAULT = 'INFO' + +CERTIFICATE_TYPE_OPTION = 'certificate_type' +CERTIFICATE_TYPE_DEFAULT = 'user' + +BLESS_CA_SECTION = 'Bless CA' +CA_PRIVATE_KEY_FILE_OPTION = 'ca_private_key_file' +KMS_KEY_ID_OPTION = 'kms_key_id' + +KMSAUTH_KEY_ID_OPTION = 'kmsauth_key_id' +KMSAUTH_KEY_ID_DEFAULT = None + +KMSAUTH_CONTEXT_OPTION = 'kmsauth_context' +KMSAUTH_CONTEXT_DEFAULT = None + +REGION_PASSWORD_OPTION_SUFFIX = '_password' + + +class BlessLyftConfig(configparser.RawConfigParser, object): + def __init__(self, aws_region, config_file): + """ + Parses the BLESS config file, and provides some reasonable default values if they are + absent from the config file. + The [Bless Options] section is entirely optional, and has defaults. + The [Bless CA] section is required. + :param aws_region: The AWS Region BLESS is deployed to. + :param config_file: Path to the connfig file. + """ + self.aws_region = aws_region + defaults = {CERTIFICATE_VALIDITY_WINDOW_SEC_OPTION: CERTIFICATE_VALIDITY_SEC_DEFAULT, + ENTROPY_MINIMUM_BITS_OPTION: ENTROPY_MINIMUM_BITS_DEFAULT, + RANDOM_SEED_BYTES_OPTION: RANDOM_SEED_BYTES_DEFAULT, + CROSS_ACCOUNT_ROLE_ARN_OPTION: CROSS_ACCOUNT_ROLE_ARN_DEFAULT, + LOGGING_LEVEL_OPTION: LOGGING_LEVEL_DEFAULT, + CERTIFICATE_TYPE_OPTION: CERTIFICATE_TYPE_DEFAULT, + KMSAUTH_KEY_ID_OPTION: KMSAUTH_KEY_ID_DEFAULT, + KMSAUTH_CONTEXT_OPTION: KMSAUTH_CONTEXT_DEFAULT} + configparser.RawConfigParser.__init__(self, defaults=defaults) + self.read(config_file) + + if not self.has_section(BLESS_OPTIONS_SECTION): + self.add_section(BLESS_OPTIONS_SECTION) + + if not self.has_section(BLESS_CA_SECTION): + raise ValueError("Can't read config file at: " + config_file) + + if not self.has_option(BLESS_CA_SECTION, self.aws_region + REGION_PASSWORD_OPTION_SUFFIX): + raise ValueError("No Region Specific Password Provided.") + + def getpassword(self): + """ + Returns the correct encrypted password based off of the aws_region. + :return: A Base64 encoded KMS CiphertextBlob. + """ + return self.get(BLESS_CA_SECTION, self.aws_region + REGION_PASSWORD_OPTION_SUFFIX) diff --git a/bless/request/bless_request_lyft_host.py b/bless/request/bless_request_lyft_host.py new file mode 100644 index 00000000..32cc1803 --- /dev/null +++ b/bless/request/bless_request_lyft_host.py @@ -0,0 +1,93 @@ +""" +.. module: bless.request.bless_request + :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +""" +import ipaddress +import re +from marshmallow import Schema, fields, post_load, ValidationError + +# man 8 useradd +HOSTNAME_PATTERN = re.compile('[a-z0-9_.-]+') + + +def validate_ip(ip): + try: + ipaddress.ip_address(ip) + except ValueError: + raise ValidationError('Invalid IP address.') + + +def validate_host(hostname): + if len(hostname) > 64: + raise ValidationError('Hostname is too long') + if HOSTNAME_PATTERN.match(hostname) is None: + raise ValidationError('Hostname contains invalid characters') + + +class BlessLyftSchema(Schema): + public_key_to_sign = fields.Str() + + @post_load + def make_bless_request(self, data): + return BlessLyftRequest(**data) + + +class BlessLyftHostSchema(BlessLyftSchema): + service_name = fields.Str() + service_instance = fields.Str() + service_region = fields.Str() + kmsauth_token = fields.Str() + instance_id = fields.Str() + instance_availability_zone = fields.Str() + onebox_name = fields.Str(allow_none=True) + is_canary = fields.Bool() + + @post_load + def make_bless_request(self, data): + return BlessLyftHostRequest(**data) + + +class BlessLyftRequest: + def __init__(self, public_key_to_sign): + """ + A BlessRequest must have the following key value pairs to be valid. + :param public_key_to_sign: The id_rsa.pub that will be used in the SSH request. This is + enforced in the issued certificate. + """ + + self.public_key_to_sign = public_key_to_sign + + def __eq__(self, other): + return self.__dict__ == other.__dict__ + + +class BlessLyftHostRequest(BlessLyftRequest): + def __init__(self, public_key_to_sign, service_name, service_instance, + service_region, kmsauth_token, instance_id, + instance_availability_zone, onebox_name=None, is_canary=False): + """ + A BlessRequest must have the following key value pairs to be valid. + :param public_key_to_sign: The id_rsa.pub that will be used in the SSH request. This is + enforced in the issued certificate. + :param service_name: The service name. This is used to generate hostnames and verify kmsauth. + :param service_instance: The service instance name. This is used to generate hostnames + and verify kmsauth. (e.g. staging, production) + :param service_region: The service region name. This is used to generate hostnames + and verify kmsauth. (e.g. iad, sfo) + :param kmsauth_token: KMS auth token to authenticate the host + :param instance_id: The instance id of the host + :param instance_availability_zone: The availability zone of the host + :param onebox_name: The name of the onebox (or None if it is not a onebox) + :param is_canary: Whether the instance is a canary instance + """ + + self.public_key_to_sign = public_key_to_sign + self.service_name = service_name + self.service_instance = service_instance + self.service_region = service_region + self.kmsauth_token = kmsauth_token + self.instance_id = instance_id + self.instance_availability_zone = instance_availability_zone + self.onebox_name = onebox_name + self.is_canary = is_canary diff --git a/tests/aws_lambda/lyft-full.cfg b/tests/aws_lambda/lyft-full.cfg new file mode 100644 index 00000000..47aec53f --- /dev/null +++ b/tests/aws_lambda/lyft-full.cfg @@ -0,0 +1,16 @@ +[Bless Options] +# The default values are sane, these are not. +certificate_validity_seconds = 604800 +entropy_minimum_bits = 2 +random_seed_bytes = 3 +logging_level = DEBUG +certificate_type = host +cross_account_role_arn = arn:aws:iam:1234567:role/something + +[Bless CA] +us-east-1_password = pretend_password +us-west-2_password = another_pretend_password +ca_private_key_file = tests/aws_lambda/only-use-for-unit-tests.pem +ca_private_key_compression = zlib +kmsauth_key_id = alias/authnz-iad, alias/authnz-sfo +kmsauth_context = secret-context diff --git a/tests/aws_lambda/test_bless_lambda_lyft_host.py b/tests/aws_lambda/test_bless_lambda_lyft_host.py new file mode 100644 index 00000000..330e643d --- /dev/null +++ b/tests/aws_lambda/test_bless_lambda_lyft_host.py @@ -0,0 +1,46 @@ +import os +import pytest + +from bless.aws_lambda.bless_lambda_lyft_host import lambda_lyft_host_handler +from tests.ssh.vectors import EXAMPLE_RSA_PUBLIC_KEY, RSA_CA_PRIVATE_KEY_PASSWORD + + +class Context(object): + aws_request_id = 'bogus aws_request_id' + invoked_function_arn = 'bogus invoked_function_arn' + + +VALID_TEST_REQUEST = { + "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, + "hostnames": "thisthat.com", + 'service_name': 'testy', + 'service_instance': 'testing-staging-iad-03ab45f10397e780a.lyft.net', + 'service_region': 'us-east-1', + 'kmsauth_token': 'AABB', + 'instance_id': 'testing-staging-iad-03ab45f10397e780a', + 'instance_availability_zone': 'us-east-1' +} + +VALID_TEST_REQUEST_MULTIPLE_HOSTS = { + "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, + "hostnames": "thisthat.com,thatthis.com", +} + +INVALID_TEST_REQUEST = { + "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, + "hostname": "thisthat.com", # Wrong key name +} + +os.environ['AWS_REGION'] = 'us-west-2' + + +def test_basic_local_request(mocker): + mocker.patch("kmsauth.KMSTokenValidator.decrypt_token") + # Below involves assuming a role on AWS + mocker.patch('bless.aws_lambda.bless_lambda_lyft_host.validate_instance_id', return_value=True) + output = lambda_lyft_host_handler(VALID_TEST_REQUEST, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), 'lyft-full.cfg')) + + assert output diff --git a/tests/aws_lambda/test_bless_lambda_user.py b/tests/aws_lambda/test_bless_lambda_user.py index 40dce14f..71376edb 100644 --- a/tests/aws_lambda/test_bless_lambda_user.py +++ b/tests/aws_lambda/test_bless_lambda_user.py @@ -163,14 +163,6 @@ class Context(object): } -def test_basic_local_request_with_wrapper(): - output = lambda_handler(VALID_TEST_REQUEST, context=Context, - ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, - entropy_check=False, - config_file=os.path.join(os.path.dirname(__file__), 'bless-test.cfg')) - assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') - - def test_basic_local_request(): output = lambda_handler_user(VALID_TEST_REQUEST, context=Context, ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, diff --git a/tests/config/full-lyft.cfg b/tests/config/full-lyft.cfg new file mode 100644 index 00000000..b9a14fc9 --- /dev/null +++ b/tests/config/full-lyft.cfg @@ -0,0 +1,16 @@ +[Bless Options] +# The default values are sane, these are not. +certificate_validity_seconds = 604800 +entropy_minimum_bits = 2 +random_seed_bytes = 3 +logging_level = DEBUG +certificate_type = host +cross_account_role_arn = arn:aws:iam:1234567:role/something + +[Bless CA] +us-east-1_password = pretend_password +us-west-2_password = another_pretend_password +ca_private_key_file = secrets +ca_private_key_compression = zlib +kmsauth_key_id = secret-sauce +kmsauth_context = secret-context diff --git a/tests/config/test_bless_lyft_config.py b/tests/config/test_bless_lyft_config.py new file mode 100644 index 00000000..fdd24382 --- /dev/null +++ b/tests/config/test_bless_lyft_config.py @@ -0,0 +1,23 @@ +import os + +import pytest + +from bless.config.bless_lyft_config import BlessLyftConfig + + +def test_empty_config(): + with pytest.raises(ValueError): + BlessLyftConfig('us-west-2', config_file='') + + +def test_config_no_password(): + with pytest.raises(ValueError) as e: + BlessLyftConfig('bogus-region', + config_file=os.path.join(os.path.dirname(__file__), 'full.cfg')) + assert 'No Region Specific Password Provided.' == str(e.value) + + +def test_getpassword(): + config = BlessLyftConfig('us-east-1', config_file=(os.path.join(os.path.dirname(__file__), 'full-lyft.cfg'))) + + assert config.getpassword() == 'pretend_password' diff --git a/tests/request/test_bless_request_lyft_host.py b/tests/request/test_bless_request_lyft_host.py new file mode 100644 index 00000000..b2efb9e9 --- /dev/null +++ b/tests/request/test_bless_request_lyft_host.py @@ -0,0 +1,35 @@ +import pytest +from bless.request.bless_request_lyft_host import BlessLyftHostSchema, validate_host +from bless.request.bless_request_host import HOSTNAME_VALIDATION_OPTIONS +from marshmallow import ValidationError + + +@pytest.mark.parametrize("test_input", [ + 'thisthat', + 'this.that', + '10.1.1.1' +]) +def test_validate_hostnames(test_input): + validate_host(test_input) + + +@pytest.mark.parametrize("test_input", [ + '%&&&&', + '', + 'carzylongkdjfldksjfkldsfjlkdsjflkdsjflkdsjfkldjfkldjfdlkjfdkljdflkjslkdfjkldfjldk', +]) +def test_invalid_host(test_input): + with pytest.raises(ValidationError) as e: + validate_host(test_input) + + +@pytest.mark.parametrize("test_input", [ + 'this..that', + 'this!that.com', + 'this,that' +]) +def test_invalid_hostnames_with_disabled(test_input): + validate_host(test_input) + + +