def test_get_config(ssl, protocol): """If a base url is specified in the environment, we use it.""" with mock.patch.object(config, '_CONFIG', None): with mock.patch.dict(os.environ, {}, clear=True): token = utils.uuid4() use_https = 'True' if protocol == 'https' else 'False' account_number = int(time.time()) cloudtrail_prefix = random.choice([ 'aardvark', 'aardvark-', 'flying-aardvark-', '42', utils.uuid4(), ]) bucket_name = 'flying-aardvark-s3' os.environ['CLOUDIGRADE_TOKEN'] = token os.environ['CLOUDIGRADE_BASE_URL'] = 'example.com' os.environ['AWS_S3_BUCKET_NAME'] = bucket_name os.environ['CLOUDIGRADE_ROLE_CUSTOMER1'] = '{}:{}:{}'.format( utils.uuid4(), account_number, utils.uuid4()) os.environ['CLOUDTRAIL_PREFIX'] = cloudtrail_prefix os.environ['AWS_ACCESS_KEY_ID_CUSTOMER1'] = utils.uuid4() os.environ['USE_HTTPS'] = use_https os.environ['SSL_VERIFY'] = 'True' if ssl else 'False' cfg = config.get_config() assert cfg['superuser_token'] == token assert cfg['base_url'] == 'example.com' assert cfg['scheme'] == protocol assert cfg['ssl-verify'] == ssl assert cfg['api_version'] == 'v1' assert len(cfg['aws_profiles']) == 1 assert cfg['aws_profiles'][0]['name'] == 'CUSTOMER1' assert cfg['aws_profiles'][0]['cloudtrail_name'] == ( f'{cloudtrail_prefix}{account_number}') assert cfg['cloudigrade_s3_bucket'] == bucket_name
def test_negative_super_user_creation_fails(): """Test that if super user creation fails, we get the right exception. If no super user token is provided, then config.get_config will call injector.make_super_user. If this fails, we want config.get_config to bail out with as informative of a message as possible, so tests can fail more gracefully and we can know what happened. This test ensures that if injector.make_super_user throws a RuntimeError, that config.get_config raises an exceptions.MissingConfigurationError so so that integrade.tests.conftest.check_superuser can catch that and let the user know what happened. """ with mock.patch.object(config, '_CONFIG', None): with mock.patch.dict(os.environ, {}, clear=True): account_number = int(time.time()) os.environ['CLOUDIGRADE_USER'] = '******' os.environ['CLOUDIGRADE_PASSWORD'] = '******' os.environ['CLOUDIGRADE_BASE_URL'] = 'example.com' os.environ['CLOUDIGRADE_ROLE_CUSTOMER1'] = '{}:{}:{}'.format( utils.uuid4(), account_number, utils.uuid4()) os.environ['AWS_ACCESS_KEY_ID_CUSTOMER1'] = utils.uuid4() with mock.patch.object(injector, 'make_super_user') as make_super_user: make_super_user.side_effect = RuntimeError() with pytest.raises(exceptions.MissingConfigurationError): config.get_config()
def test_token_auth(): """Test TokenAuth generates proper request header.""" token = uuid4() header_format = uuid4() auth = api.TokenAuth(token, header_format) request = Mock() request.headers = {} changed_request = auth(request) assert changed_request is request assert 'Authorization' in request.headers assert request.headers['Authorization'] == f'{header_format} {token}'
def test_raise_exception_missing_aws_image_config(): """Test that when no aws_image_config file is present, an error is raised. This only occurs when the config.get_aws_image_config() function is called. """ with mock.patch.object(config, '_AWS_CONFIG', None): with mock.patch.object(xdg.BaseDirectory, 'load_config_paths') as lcp: lcp.return_value = ('fake_path', ) with mock.patch.object(os.path, 'isfile') as isfile: isfile.return_value = False with pytest.raises(exceptions.ConfigFileNotFoundError): # pylint:disable=protected-access config._get_config_file_path(utils.uuid4(), utils.uuid4()) assert isfile.call_count == 1
def test_negative_get_config_missing(): """If a base url is specified in the environment, we use it.""" with mock.patch.object(config, '_CONFIG', None): with mock.patch.dict(os.environ, {}, clear=True): with mock.patch.object(injector, 'make_super_user') as make_super_user: make_super_user.return_value = utils.uuid4() os.environ['CLOUDIGRADE_ROLE_CUSTOMER1'] = '{}:{}:{}'.format( utils.uuid4(), '1234', utils.uuid4()) try: config.get_config() except exceptions.MissingConfigurationError as e: msg = str(e) msg.replace('\n', ' ') assert 'AWS access key id' in msg
def create_bucket_for_cloudtrail(aws_profile): """Create an s3 bucket suitable to point cloudtrail to for given aws_profile. :returns: (string) name of the s3 bucket to point the cloudtrail to. """ session = aws_session(aws_profile) s3client = session.client('s3') bucket_name = uuid4() s3client.create_bucket(Bucket=bucket_name, ACL='public-read-write') unique_name1 = uuid4() unique_name2 = uuid4() new_policy = { 'Version': '2012-10-17', 'Statement': [{ 'Sid': f'{unique_name1}', 'Effect': 'Allow', 'Principal': { 'Service': 'cloudtrail.amazonaws.com' }, 'Action': 's3:GetBucketAcl', 'Resource': f'arn:aws:s3:::{bucket_name}' }, { 'Sid': f'{unique_name2}', 'Effect': 'Allow', 'Principal': { 'Service': 'cloudtrail.amazonaws.com' }, 'Action': 's3:PutObject', 'Resource': f'arn:aws:s3:::{bucket_name}/AWSLogs/*', 'Condition': { 'StringEquals': { 's3:x-amz-acl': 'bucket-owner-full-control' } } }] } s3_resource = session.resource('s3') bucket_policy = s3_resource.BucketPolicy(bucket_name) bucket_policy.put(Policy=json.dumps(new_policy)) return bucket_name
def test_create(): """Ensure user accounts can be created with username and password. :id: 5099a61d-7aa6-4c1b-8408-d030f210cd08 :description: Ensure an user account can be created by an super user account with only username and password. :steps: With an authenticated superuser, send a post request to /auth/user/create/ with an username and password. :expectedresults: The server returns a 201 response with the information of the created user. The information should include the information passed as payload to the create request and also an ID should be created. """ create_user_account({ 'email': '', 'password': gen_password(), 'username': uuid4(), })
def test_create_with_email(): """Ensure user accounts can be created with username, email and password. :id: 003ac47f-9946-4ffe-b49d-732dbffe1cfc :description: Ensure an user account can be created by an super user account with username, email and password. :steps: With an authenticated superuser, send a post request to /auth/user/create/ with an username, email and password. :expectedresults: The server returns a 201 response with the information of the created user. The information should include the information passed as payload to the create request and also an ID should be created. """ create_user_account({ 'email': '*****@*****.**', 'username': uuid4(), 'password': gen_password() })
def test_token_negative(endpoint): """Given that we have an invalid token, we cannot make requests. :id: a87f7069-3ee9-4435-a953-fd8664199419 :description: Test that if we have a bad token, we cannot use it to make requests to any of the /api/v1/* endpoints :steps: 1) Send a GET request with a invalid authorization token in the header to all /api/v1/* endpoints. 2) Assert that we get a 401 response for all requests. :expectedresults: The server rejects our invalid token for all /api/v1/* endpoints. """ client = api.Client(response_handler=api.echo_handler) auth = api.TokenAuth(uuid4()) response = client.get(f'/api/v1/{endpoint}', auth=auth) assert response.status_code == 401 assert response.json() == {'detail': 'Invalid token.'}
def create_cloud_account(auth, n, cloudtrails_to_delete=None, name=_SENTINEL): """Create a cloud account based on configured AWS customer info.""" client = api.Client(authenticate=False) cfg = config.get_config() aws_profile = cfg['aws_profiles'][n] acct_arn = aws_profile['arn'] cloud_account = { 'account_arn': acct_arn, 'name': uuid4() if name is _SENTINEL else name, 'resourcetype': 'AwsAccount' } create_response = client.post(urls.CLOUD_ACCOUNT, payload=cloud_account, auth=auth) assert create_response.status_code == 201 if isinstance(cloudtrails_to_delete, list): cloudtrails_to_delete.append( (aws_profile['name'], aws_profile['cloudtrail_name'])) return create_response.json()
def test_create_multiple_cloud_accounts(drop_account_data, cloudtrails_to_delete): """Ensure cloud accounts can be registered to a user. :id: f1db2617-fd15-4270-b9d3-595db001e1e7 :description: Ensure an user can register multiple cloud accounts as long as each ARN is associated with unique cloud accounts. :steps: 1) Create a user and authenticate with their password 2) Send POSTS with each of the cloud account's information to 'api/v1/account/' 3) Send a GET to 'api/v1/account/' to get a list of the cloud accounts :expectedresults: The server returns a 201 response with the information of the created accounts. """ client = api.Client(authenticate=False) auth = get_auth() cfg = config.get_config() accts = [] for profile in cfg['aws_profiles']: arn = profile['arn'] cloud_account = { 'account_arn': arn, 'name': uuid4(), 'resourcetype': 'AwsAccount' } create_response = client.post(urls.CLOUD_ACCOUNT, payload=cloud_account, auth=auth) assert create_response.status_code == 201 cloudtrails_to_delete.append( (profile['name'], profile['cloudtrail_name'])) accts.append(create_response.json()) # list cloud accounts associated with this user list_response = client.get(urls.CLOUD_ACCOUNT, auth=auth) for acct in accts: assert acct in list_response.json()['results']
def test_uuid4(): """Test we get a unique string each time we call uuid4().""" assert isinstance(uuid4(), str) assert uuid4() != uuid4()
from json import JSONDecodeError from unittest import mock from unittest.mock import Mock, patch from urllib.parse import urljoin import pytest import requests from integrade import api, config, exceptions from integrade.utils import uuid4 VALID_CONFIG = { 'superuser': '******', 'superuser_pass': uuid4(), 'base_url': 'example.com', 'superuser_token': uuid4(), 'scheme': 'http', 'ssl-verify': False, 'api_version': 'v1' } def mock_request(): """Return a mock request to include as attribute of mock response.""" mock_request = mock.Mock( body='{"Test Body"}', path_url='/example/path/', headers={'Authorization': 'authorizationkey'}, text='Some text',
def test_create_cloud_account(drop_account_data, cloudtrails_to_delete): """Ensure cloud accounts can be registered to a user. :id: f7a9225b-83af-4567-b59a-a8bb62d612c9 :description: Ensure an user can register a cloud account by specifying the role ARN. :steps: 1) Create a user and authenticate with their password 2) Send a POST with the cloud account information to 'api/v1/account/' 3) Send a GET to 'api/v1/account/' to get a list of the cloud accounts 4) Attempt to create a duplicate and expect it to be rejected 5) Attempt to delete the account and expect to succeed :expectedresults: 1) The server returns a 201 response with the information of the created account. 2) The account cannot be duplicated, and attempts to do so receive a 400 status code. 3) The account can be deleted and we get a 200 response. """ auth = get_auth() client = api.Client(authenticate=False) cfg = config.get_config() aws_profile = cfg['aws_profiles'][0] profile_name = aws_profile['name'] acct_arn = aws_profile['arn'] cloud_account = { 'account_arn': acct_arn, 'name': uuid4(), 'resourcetype': 'AwsAccount' } start = time() create_response = client.post(urls.CLOUD_ACCOUNT, payload=cloud_account, auth=auth) end = time() create_data = create_response.json() assert end - start < 7 assert create_response.status_code == 201 assert create_data['account_arn'] == acct_arn assert create_data['aws_account_id'] == aws_profile['account_number'] # Assert that a cloudtrail has been set up in the customer's account cloudtrail_client = aws_utils.aws_session(profile_name).client( 'cloudtrail') trail_names = [ trail['Name'] for trail in cloudtrail_client.describe_trails()['trailList'] ] assert aws_profile['cloudtrail_name'] in trail_names # since account was created, add trail to cleanup cloudtrails_to_delete.append( (profile_name, aws_profile['cloudtrail_name'])) acct = create_response.json() # get specific account account_url = urljoin(urls.CLOUD_ACCOUNT, '{}/'.format(acct['id'])) get_response = client.get(account_url, auth=auth) assert acct == get_response.json() # list cloud accounts associated with this user list_response = client.get(urls.CLOUD_ACCOUNT, auth=auth) assert acct in list_response.json()['results'] # Check if account name can be patched payload = { 'name': 'new_name', 'resourcetype': 'AwsAccount', } response = client.patch(account_url, payload=payload, auth=auth) response = client.get(account_url, auth=auth) assert response.json()['name'] == 'new_name' # Check if an account can be updated payload = { 'account_arn': acct_arn, 'name': 'new_name2', 'resourcetype': 'AwsAccount', } response = client.put(account_url, payload=payload, auth=auth) response = client.get(account_url, auth=auth) assert response.json()['name'] == 'new_name2' # assert we cannot create duplicate client.response_handler = api.echo_handler response = client.post(urls.CLOUD_ACCOUNT, payload=cloud_account, auth=auth) assert response.status_code == 400 assert 'account_arn' in response.json().keys() assert 'already exists' in response.json()['account_arn'][0] # attempt to delete the specific account delete_response = client.delete(urljoin(urls.CLOUD_ACCOUNT, '{}/'.format(acct['id'])), auth=auth) assert delete_response.status_code == 204
def images_data(): """Create test data for the tests on this module. To be able to verify that the image API endpoing works we need: * One super user acccount * Two regular user account * Image data for each user """ utils.drop_image_data() user1 = utils.create_user_account() user2 = utils.create_user_account() auth1 = utils.get_auth(user1) auth2 = utils.get_auth(user2) # user1 will have 2 images images1 = [ { 'ec2_ami_id': str(random.randint(100000, 999999999999)), 'rhel': True, 'rhel_detected': True, 'openshift': False, 'openshift_detected': False, }, { 'ec2_ami_id': str(random.randint(100000, 999999999999)), 'rhel': False, 'rhel_detected': False, 'openshift': True, 'openshift_detected': True, }, ] # user2 will have 3 images images2 = [ { 'ec2_ami_id': str(random.randint(100000, 999999999999)), 'rhel': True, 'rhel_detected': True, 'openshift': False, 'openshift_detected': False, }, { 'ec2_ami_id': str(random.randint(100000, 999999999999)), 'rhel': True, 'rhel_detected': True, 'openshift': True, 'openshift_detected': True, }, { 'ec2_ami_id': str(random.randint(100000, 999999999999)), 'rhel': False, 'rhel_detected': False, 'openshift': True, 'openshift_detected': True, }, ] for user, images in zip((user1, user2), (images1, images2)): account = inject_aws_cloud_account( user['id'], name=uuid4(), ) for image in images: if image['rhel'] and image['openshift']: image_type = 'rhel,openshift' elif image['rhel'] and not image['openshift']: image_type = 'rhel' elif not image['rhel'] and image['openshift']: image_type = 'openshift' else: raise ValueError('Not a valid image type.') image['id'] = inject_instance_data( account['id'], image_type, [random.randint(0, 20)], ec2_ami_id=image['ec2_ami_id'], )['image_id'] return user1, user2, auth1, auth2, images1, images2
def test_challenge_image(superuser, method): """Test if a challenge flags for RHEL and OS can be changed. :id: ec5fe0b6-9852-48db-a2ba-98d01aeaac28 :description: Try to change challenge flags on an image and ensure that change is reflected afterwards. :steps: 1. Create an image in a known account and make sure the challenge flags are false by default. 2. Use both PUT and PATCH forms of the image endpoint to set a flag to true :expectedresults: The image data now reflects this change. """ cfg = config.get_config() user = utils.create_user_account() auth = utils.get_auth(user) client = api.Client(authenticate=False) account = inject_aws_cloud_account( user['id'], name=uuid4(), ) image_type = '' ec2_ami_id = str(random.randint(100000, 999999999999)) image_id = inject_instance_data( account['id'], image_type, [random.randint(0, 20)], ec2_ami_id=ec2_ami_id, )['image_id'] if superuser: auth = api.TokenAuth(cfg.get('superuser_token')) image_url = urljoin(urls.IMAGE, str(image_id)) + '/' # Ensure the image owner can fetch it response = client.get(image_url, auth=auth).json() assert response['rhel_challenged'] is False assert response['openshift_challenged'] is False for tag in ('rhel', 'openshift'): if method == 'put': response[f'{tag}_challenged'] = True response = client.put(image_url, response, auth=auth).json() elif method == 'patch': data = { 'resourcetype': 'AwsMachineImage', f'{tag}_challenged': True, } response = client.patch(image_url, data, auth=auth).json() else: pytest.fail(f'Unknown method "{method}"') assert response[f'{tag}_challenged'] is True # Make sure the change is reflected in new responses response = client.get(image_url, auth=auth).json() response[f'{tag}_challenged'] = True # Ensure any other user can't fetch it response = client.get(image_url, auth=auth) assert response.status_code == 200 assert response.json()[f'{tag}_challenged']
def get_config(create_superuser=True, need_base_url=True): """Return a copy of the global config dictionary. This method makes use of a cache. If the cache is empty, the configuration file is parsed and the cache is populated. Otherwise, a copy of the cached configuration object is returned. :returns: A copy of the global integrade configuration object. """ global _CONFIG # pylint:disable=global-statement if _CONFIG is None: _CONFIG = {} _CONFIG['api_version'] = os.getenv('CLOUDIGRADE_API_VERSION', 'v1') _CONFIG['cloudigrade_s3_bucket'] = os.getenv('AWS_S3_BUCKET_NAME') ref_slug = os.environ.get('CI_COMMIT_REF_SLUG', '') cloudtrail_prefix = os.getenv('CLOUDTRAIL_PREFIX', f'review-{ref_slug}') # The location of the API endpoints and UI may be configured directly # with `CLOUDIGRADE_BASE_URL` -OR- we can determine a location based # on `CI_COMMIT_REF_SLUG` which comes from Gitlab CI and is our # current branch name. _CONFIG['base_url'] = os.getenv( 'CLOUDIGRADE_BASE_URL', f'review-{ref_slug}.5a9f.insights-dev.openshiftapps.com', ) _CONFIG['openshift_prefix'] = os.getenv( 'OPENSHIFT_PREFIX', f'c-review-{ref_slug[:29]}-', ) # pull all customer roles out of environ def is_role(string): return string.startswith('CLOUDIGRADE_ROLE_') def profile_name(string): return string.replace( 'CLOUDIGRADE_ROLE_', '') profiles = [{'arn': os.environ.get(role), 'name': profile_name(role)} for role in filter(is_role, os.environ.keys()) ] profiles.sort(key=lambda p: p['name']) _CONFIG['aws_profiles'] = profiles missing_config_errors = [] try: aws_image_config = get_aws_image_config() except exceptions.ConfigFileNotFoundError: aws_image_config = {} for i, profile in enumerate(_CONFIG['aws_profiles']): profile_name = profile['name'].upper() acct_arn = profile['arn'] acct_num = [ num for num in filter( str.isdigit, acct_arn.split(':'))][0] profile['account_number'] = acct_num profile['cloudtrail_name'] = f'{cloudtrail_prefix}{acct_num}' profile['access_key_id'] = os.environ.get( f'AWS_ACCESS_KEY_ID_{profile_name}') profile['images'] = aws_image_config.get('profiles', {}).get( profile_name, {}).get('images', []) if i == 0: if not profile['access_key_id']: missing_config_errors.append( f'Could not find AWS access key id for {profile_name}') if _CONFIG['base_url'] == '' and need_base_url: missing_config_errors.append( 'Could not find $CLOUDIGRADE_BASE_URL set in in' ' your environment.' ) if os.environ.get('USE_HTTPS', 'false').lower() == 'true': _CONFIG['scheme'] = 'https' else: _CONFIG['scheme'] = 'http' if os.environ.get('SSL_VERIFY', 'false').lower() == 'true': _CONFIG['ssl-verify'] = True else: _CONFIG['ssl-verify'] = False if missing_config_errors: raise exceptions.MissingConfigurationError( '\n'.join(missing_config_errors) ) super_username = os.environ.get( 'CLOUDIGRADE_USER', utils.uuid4() ) _CONFIG['super_user_name'] = super_username super_password = os.environ.get( 'CLOUDIGRADE_PASSWORD', utils.gen_password() ) _CONFIG['super_user_password'] = super_password token = os.environ.get('CLOUDIGRADE_TOKEN', False) if not token and create_superuser: try: token = injector.make_super_user( super_username, super_password) except RuntimeError as e: raise exceptions.MissingConfigurationError( 'Could not create a super user or token, error:\n' f'{repr(e)}' ) _CONFIG['superuser_token'] = token return deepcopy(_CONFIG)
def test_create_cloud_account(cloudtrails_to_delete, aws_profile, request): """Ensure cloud accounts can be registered to a user. :id: bb8fa2a4-7ff7-43e6-affb-7a2dedaaab74 :description: Ensure a user can create a cloud account. :steps: 1) Log in as a user. 2) Send POST with the cloud account's information to 'api/v2/account/' 3) Send a GET to 'api/v2/account/' to get a list of the cloud accounts 4) Run an instance in that account 5) Check that Cloudigrade and AWS both show same instance_id 6) Delete account 7) Check instance in AWS :expectedresults: The server returns a 201 response with the information of the created account. Cloudigrade and AWS both show same instance_id. Delete account response returns status 204. After deletion, AWS instance_id.state is 'terminated'. """ account_id = 0 arn = aws_profile['arn'] client = api.ClientV2() acct_data_params = { 'account_arn': arn, 'name': uuid4(), 'cloud_type': 'aws', } delete_preexisting_accounts(aws_profile) # POST # Create an account add_acct_response = client.request('post', 'accounts/', data=acct_data_params) assert add_acct_response.status_code == 201 # Check AWS permissions acct_details = add_acct_response.json() permission = acct_details['content_object']['account_arn'] assert 'allow-dev11-cloudigrade-metering' in permission # Start AWS session and cloudtrail client session = aws_utils.aws_session('DEV07CUSTOMER') env_bucket_name = config.get_config()['openshift_prefix'].strip( 'c-review-') aws_cloudtrails = session.client( 'cloudtrail').describe_trails()['trailList'] aws_cloudtrail_found = False aws_cloudtrail_arn = '' cloudtrails_client = session.client('cloudtrail') # Find the cloudtrail for this particular account and check that # it's enabled. for trail in aws_cloudtrails: if env_bucket_name in trail['S3BucketName']: aws_cloudtrail_found = True aws_cloudtrail_arn = trail['TrailARN'] assert aws_cloudtrail_found is True trail_status = cloudtrails_client.get_trail_status(Name=aws_cloudtrail_arn) assert trail_status['IsLogging'] is True # Find the recently added account so we can delete it accounts = fetch_api_accounts() for acct in accounts: if acct['content_object']['account_arn'] == arn: account_id = acct['account_id'] # DELETE # Delete that account endpoint = f'accounts/{account_id}/' delete_acct_response = client.request('delete', endpoint) assert delete_acct_response.status_code == 204 # Check that deleted account is no longer present in cloudigrade accounts = fetch_api_accounts() assert account_id not in accounts # Check that the cloudtrail has been disabled trail_status = cloudtrails_client.get_trail_status(Name=aws_cloudtrail_arn) assert trail_status['IsLogging'] is False # Cleanup: Remove cloudtrail from AWS cloudtrails_to_delete.append( (aws_profile['name'], aws_profile['cloudtrail_name']))