def test_get_account_id(self): """ Checks if get_account_id returns the correct account id Returns: None Raises: AssertionError if any of the assert checks fail """ target_account_id = "123456789" mock_sts_client = Mock(name="mock sts client") mock_sts_client.get_caller_identity.return_value.get.return_value = target_account_id self.assertEquals(ef_utils.get_account_id(mock_sts_client), target_account_id)
def __init__(self, profile=None, region=None, # set both for user access mode lambda_context=None, # set if target is 'self' and this is a lambda target_other=False, env=None, service=None, # set env & service if target_other=True verbose=False ): """ Depending on how this is called, access mode (how it logs into AWS) and target (what the various context vars report) will vary ACCESS MODE - how this logs in to AWS user: "******" both 'profile' and 'region' are required is always "operating on something else" (TARGET is never "self") role: "running in AWS EC2 or Lambda with a role credential" do not set profile TARGET - what context is reported self: "this ec2 instance or lambda is initializing itself" assumed for ec2 and lambda, unless target_other=True in the constructor never an option for "user" access mode other: "this local user, ec2 instance, or lambda is configuring something else" always "other" if access mode is "user" if access mode is "role", set target_other=True in the constructor Constructor must also set 'env' and 'service' self: lambda_context must be provided if this is a lambda; leave it unset for ec2 INSTANCE_ID = instance ID if EC2, else None FUNCTION_NAME = function name if lambda, else None ACCOUNT = numeric account this is running in ACCOUNT_ALIAS = named alias of the account this is running in ROLE = role this is running as ENV = the environment this is running in, from role name ENV_SHORT = derived from ENV ENV_FULL = fully qualified environment, same as ENV unless env is a global env (mgmt.* or global.*) SERVICE = the service this is, from role name REGION = region this is running in something else: INSTANCE_ID = None FUNCTION_NAME = None ACCOUNT = the numeric account I'm logged into (look up) ACCOUNT_ALIAS = the alias of the account i'm logged into (look up) ROLE = None ENV = the target's environment, passed in from the constructor ENV_SHORT = derived from ENV ENV_FULL = ENV, with ".<ACCOUNT_ALIAS>" as appropriate SERVICE = the service name, passed in from the constructor REGION = the region I am in (ec2, lambda) or explicitly set (region= in constructor) Collects instance's environment for use in templates: {{ACCOUNT}} - AWS account number CloudFormation can use this or the AWS::AccountID pseudo param {{ACCOUNT_ALIAS}} - AWS account alias {{ENV}} - environment: mgmt, prod, staging, proto<N>, etc. {{ENV_SHORT}} - env with <N> or account trimmed: mgmt, prod, staging, proto, etc. {{ENV_FULL}} - env fully qualified: prod, staging, proto<N>, mgmt.<account_alias>, etc. {{FUNCTION_NAME}} - only for lambdas {{INSTANCE_ID}} - only for ec2 {{REGION}} - the region currently being worked in CloudFormation can use this or the AWS::Region pseudo param {{ROLE}} - the role bound to the ec2 instance or lambda; only for ec2 and lambda CloudFormation: compose role name in template by joining other strings """ # instance vars self.verbose = False # print noisy status if True # resolved tokens - only look up symbols once per session. Protect internal names by declaring self.resolved = { "ACCOUNT": None, "ACCOUNT_ALIAS": None, "ENV": None, "ENV_SHORT": None, "ENV_FULL": None, "FUNCTION_NAME": None, "INSTANCE_ID": None, "REGION": None, "ROLE": None } # template and parameters are populated by the load() method as each template is processed self.template = None # parameters that accompany this template, if any self.parameters = {} # Sets of symbols found in the current template (only) # read back with self.symbols() and self.unresolved_symbols() self.symbols = set() # capture verbosity pref from constructor self.verbose = verbose # determine ACCESS MODE if profile: # accessing as a user target_other = True if not region: fail("'region' is required with 'profile' for user-mode access") where = whereami() # require env and service params init() when target is 'other' if (target_other or where == "virtualbox-kvm") and (env is None or service is None): fail("'env' and 'service' must be set when target is 'other' or running in " + where) if target_other or profile: self.resolved["REGION"] = region # lambda initializing self elif lambda_context: self.resolved["REGION"] = lambda_context.invoked_function_arn.split(":")[3] # ec2 initializing self else: self.resolved["REGION"] = get_metadata_or_fail("placement/availability-zone/")[:-1] # Create clients - if accessing by role, profile should be None clients = [ "cloudformation", "cloudfront", "cognito-identity", "cognito-idp", "ec2", "elbv2", "iam", "kms", "lambda", "route53", "s3", "sts", "waf" ] try: EFTemplateResolver.__CLIENTS = create_aws_clients(self.resolved["REGION"], profile, *clients) except RuntimeError as error: fail("Exception logging in with Session()", error) # Create EFAwsResolver object for interactive lookups EFTemplateResolver.__AWSR = EFAwsResolver(EFTemplateResolver.__CLIENTS) # Create EFConfigResolver object for ef tooling config lookups EFTemplateResolver.__EFCR = EFConfigResolver() # Create EFVersionResolver object for version lookups EFTemplateResolver.__VR = EFVersionResolver(EFTemplateResolver.__CLIENTS) # Set the internal parameter values for aws # self-configuring lambda if (not target_other) and lambda_context: arn_split = lambda_context.invoked_function_arn.split(":") self.resolved["ACCOUNT"] = arn_split[4] self.resolved["FUNCTION_NAME"] = arn_split[6] try: lambda_desc = EFTemplateResolver.__CLIENTS["lambda"].get_function() except: fail("Exception in get_function: ", sys.exc_info()) self.resolved["ROLE"] = lambda_desc["Configuration"]["Role"] env = re.search("^({})-".format(EFConfig.VALID_ENV_REGEX), self.resolved["ROLE"]) if not env: fail("Did not find environment in lambda function name.") self.resolved["ENV"] = env.group(1) parsed_service = re.search(self.resolved["ENV"] + "-(.*?)-lambda", self.resolved["ROLE"]) if parsed_service: self.resolved["SERVICE"] = parsed_service.group(1) # self-configuring EC2 elif (not target_other) and (not lambda_context): self.resolved["INSTANCE_ID"] = get_metadata_or_fail('instance-id') try: instance_desc = EFTemplateResolver.__CLIENTS["ec2"].describe_instances(InstanceIds=[self.resolved["INSTANCE_ID"]]) except: fail("Exception in describe_instances: ", sys.exc_info()) self.resolved["ACCOUNT"] = instance_desc["Reservations"][0]["OwnerId"] arn = instance_desc["Reservations"][0]["Instances"][0]["IamInstanceProfile"]["Arn"] self.resolved["ROLE"] = arn.split(":")[5].split("/")[1] env = re.search("^({})-".format(EFConfig.VALID_ENV_REGEX), self.resolved["ROLE"]) if not env: fail("Did not find environment in role name") self.resolved["ENV"] = env.group(1) self.resolved["SERVICE"] = "-".join(self.resolved["ROLE"].split("-")[1:]) # target is "other" else: try: if whereami() == "ec2": self.resolved["ACCOUNT"] = str(json.loads(http_get_metadata('iam/info'))["InstanceProfileArn"].split(":")[4]) else: self.resolved["ACCOUNT"] = get_account_id(EFTemplateResolver.__CLIENTS["sts"]) except botocore.exceptions.ClientError as error: fail("Exception in get_user()", error) self.resolved["ENV"] = env self.resolved["SERVICE"] = service # ACCOUNT_ALIAS is resolved consistently for access modes and targets other than virtualbox try: self.resolved["ACCOUNT_ALIAS"] = EFTemplateResolver.__CLIENTS["iam"].list_account_aliases()["AccountAliases"][0] except botocore.exceptions.ClientError as error: fail("Exception in list_account_aliases", error) # ENV_SHORT is resolved the same way for all access modes and targets self.resolved["ENV_SHORT"] = self.resolved["ENV"].strip(".0123456789") # ENV_FULL is resolved the same way for all access modes and targets, depending on previously-resolved values if self.resolved["ENV"] in EFConfig.ACCOUNT_SCOPED_ENVS: self.resolved["ENV_FULL"] = "{}.{}".format(self.resolved["ENV"], self.resolved["ACCOUNT_ALIAS"]) else: self.resolved["ENV_FULL"] = self.resolved["ENV"] if self.verbose: print(repr(self.resolved), file=sys.stderr)
def main(): global CONTEXT, CLIENTS, AWS_RESOLVER CONTEXT = handle_args_and_set_context(sys.argv[1:]) if not (CONTEXT.devel or getenv("JENKINS_URL", False)): try: pull_repo() except RuntimeError as error: fail("Error checking or pulling repo", error) else: print("Not refreshing repo because --devel was set or running on Jenkins") # sign on to AWS and create clients and get account ID try: # If running in EC2, always use instance credentials. One day we'll have "lambda" in there too, so use "in" w/ list if CONTEXT.whereami == "ec2": CLIENTS = create_aws_clients(EFConfig.DEFAULT_REGION, None, "ec2", "iam", "kms") CONTEXT.account_id = str(json.loads(http_get_metadata('iam/info'))["InstanceProfileArn"].split(":")[4]) else: # Otherwise, we use local user creds based on the account alias CLIENTS = create_aws_clients(EFConfig.DEFAULT_REGION, CONTEXT.account_alias, "ec2", "iam", "kms", "sts") CONTEXT.account_id = get_account_id(CLIENTS["sts"]) except RuntimeError: fail("Exception creating AWS clients in region {} with profile {}".format( EFConfig.DEFAULT_REGION, CONTEXT.account_alias)) # Instantiate an AWSResolver to lookup AWS resources AWS_RESOLVER = EFAwsResolver(CLIENTS) # Show where we're working if not CONTEXT.commit: print("=== DRY RUN ===\nUse --commit to create roles and security groups\n=== DRY RUN ===") print("env: {}".format(CONTEXT.env)) print("env_full: {}".format(CONTEXT.env_full)) print("env_short: {}".format(CONTEXT.env_short)) print("aws account profile: {}".format(CONTEXT.account_alias)) print("aws account number: {}".format(CONTEXT.account_id)) # Step through all services in the service registry for CONTEXT.service in CONTEXT.service_registry.iter_services(): service_name = CONTEXT.service[0] target_name = "{}-{}".format(CONTEXT.env, service_name) sr_entry = CONTEXT.service[1] service_type = sr_entry['type'] print_if_verbose("service: {} in env: {}".format(service_name, CONTEXT.env)) # Is this service_type handled by this tool? if service_type not in SUPPORTED_SERVICE_TYPES: print_if_verbose("unsupported service type: {}".format(service_type)) continue # Is the env valid for this service? if CONTEXT.env_full not in CONTEXT.service_registry.valid_envs(service_name): print_if_verbose("env: {} not valid for service {}".format(CONTEXT.env_full, service_name)) continue # Is the service_type allowed in 'global'? if CONTEXT.env == "global" and service_type not in GLOBAL_SERVICE_TYPES: print_if_verbose("env: {} not valid for service type {}".format(CONTEXT.env, service_type)) continue # 1. CONDITIONALLY MAKE ROLE AND/OR INSTANCE PROFILE FOR THE SERVICE # If service gets a role, create with either a custom or default AssumeRole policy document conditionally_create_role(target_name, sr_entry) # Instance profiles and security groups are not allowed in the global scope if CONTEXT.env != "global": conditionally_create_profile(target_name, service_type) # 2. SECURITY GROUP(S) FOR THE SERVICE : only some types of services get security groups conditionally_create_security_groups(CONTEXT.env, service_name, service_type) # 3. KMS KEY FOR THE SERVICE : only some types of services get kms keys conditionally_create_kms_key(target_name, service_type) # 4. ATTACH AWS MANAGED POLICIES TO ROLE conditionally_attach_aws_managed_policies(target_name, sr_entry) # 5. ATTACH CUSTOMER MANAGED POLICIES TO ROLE conditionally_attach_customer_managed_policies(target_name, sr_entry) # 6. INLINE SERVICE'S POLICIES INTO ROLE # only eligible service types with "policies" sections in the service registry get policies conditionally_inline_policies(target_name, sr_entry) print("Exit: success")