Example #1
0
    def __send_push(self, factor, state_token):
        ''' Send push re: Okta Verify '''
        url = factor['_links']['verify']['href']
        headers = {
            'Accept': 'application/json',
            'Content-Type': 'application/json',
            'Cache-Control': '"no-cache'
        }
        payload = {
            'stateToken': state_token
        }

        response_data = None
        response = requests.post(url, data=json.dumps(payload), headers=headers)
        if response.status_code == requests.codes.ok:  # pylint: disable=E1101
            response_data = response.json()
        else:
            response.raise_for_status()

        Common.echo(message='Push notification sent; waiting for your response', new_line=False)

        status = response_data['status']
        if status == 'MFA_CHALLENGE':
            if 'factorResult' in response_data and response_data['factorResult'] == 'WAITING':
                return self.__check_push_result(
                    state_token=state_token,
                    push_response=response_data
                )
Example #2
0
    def choose_idp_role_tuple(self):
        ''' Determine the role options the user can choose from '''
        idp_role_tuples = self.__discover_role_idp_tuples()

        # throw an error if no roles are provided
        #  (defensive coding only - this should not be impossible)
        if not idp_role_tuples:
            Common.dump_err(
                message='No AWS Role was assigned to this application!',
                exit_code=4,
                verbose=self.verbose)

        # use the one prvided if there is only one
        if len(idp_role_tuples) == 1:
            role_arn = idp_role_tuples[0][2]
            if self.role_preference and role_arn != self.role_preference:
                Common.echo(
                    message='Your cofigured role was not found; using {role}'.
                    format(role=role_arn))
            else:
                Common.echo(message='Using the configured role {role}'.format(
                    role=role_arn))
            return idp_role_tuples[0]

        # use the configured role if it matches one from the the SAML assertion
        for tup in idp_role_tuples:
            if tup[2] == self.role_preference:
                return tup

        # make the user choose
        return self.__choose_tuple(idp_role_tuples=idp_role_tuples)
    def generate_creds(self, role):
        """
        :param role: the AWS role the user wants to assume
        :type role: AwsRole
        """

        client = boto3.client('sts')
        # Try for a 12 hour session.  If it fails, try for shorter periods
        assumed_role_credentials = None
        durations = [43200, 14400, 3600]
        for duration in durations:
            try:
                assumed_role_credentials = client.assume_role_with_saml(
                    RoleArn=role.role_arn,
                    PrincipalArn=role.idp_arn,
                    SAMLAssertion=self.saml_assertion,
                    DurationSeconds=duration)
                if duration == 3600:
                    Common.echo(message='YOUR SESSION WILL ONLY LAST ONE HOUR')
                break
            except ClientError as e:
                # If we get a validation error and we have shorter durations to try, try a shorter duration
                if e.response['Error'][
                        'Code'] != 'ValidationError' or duration == durations[
                            -1]:
                    raise

        self.clokta_config.apply_credentials(
            credentials=assumed_role_credentials)
        self.bash_file = self.__write_sourceable_file(
            credentials=assumed_role_credentials)
        self.docker_file = self.__write_dockerenv_file(
            credentials=assumed_role_credentials)
Example #4
0
    def __check_push_result(self, state_token, push_response):
        ''' Wait for push response acknowledgement '''
        url = push_response['_links']['next']['href']
        headers = {
            'Accept': 'application/json',
            'Content-Type': 'application/json',
            'Cache-Control': '"no-cache'
        }
        payload = {
            'stateToken': state_token
        }

        wait_for = 60
        timeout = time.time() + wait_for
        response_data = None
        while True:
            Common.echo(message='.', new_line=False)
            response = requests.post(url, data=json.dumps(payload), headers=headers)
            if response.status_code == requests.codes.ok:  # pylint: disable=E1101
                response_data = response.json()
            else:
                response.raise_for_status()

            if 'sessionToken' in response_data or time.time() > timeout:
                Common.echo(message='Session confirmed')
                break
            time.sleep(3)

        if response_data:
            return response_data['sessionToken']
        else:
            msg = 'Timeout expired ({} seconds)'.format(wait_for)
            Common.dump_err(message=msg, exit_code=3)
Example #5
0
    def assume_role(self):
        ''' entry point for the cli tool '''
        profile_mgr = ProfileManager(profile_name=self.profile, verbose=self.verbose)
        configuration = profile_mgr.initialize_configuration()
        profile_mgr.update_configuration(profile_configuration=configuration)

        session_token = self.__okta_session_token(
            configuration=configuration
        )
        if self.verbose:
            Common.dump_verbose(message='Okta session token: {}'.format(session_token))

        saml_assertion = self.__saml_assertion_aws(
            session_token=session_token,
            configuration=configuration
        )

        idp_and_role_chooser = RoleChooser(
            saml_assertion=saml_assertion,
            role_preference=configuration.get('okta_aws_role_to_assume'),
            verbose=self.verbose
        )
        idp_role_tuple = idp_and_role_chooser.choose_idp_role_tuple()

        client = boto3.client('sts')
        # Try for a 12 hour session.  If it fails, try for a 1 hour session
        try:
            assumed_role_credentials = client.assume_role_with_saml(
                RoleArn=idp_role_tuple[2],
                PrincipalArn=idp_role_tuple[1],
                SAMLAssertion=saml_assertion,
                DurationSeconds=43200
            )
        except ClientError as e:
            if e.response['Error']['Code'] == 'ValidationError':
                assumed_role_credentials = client.assume_role_with_saml(
                    RoleArn=idp_role_tuple[2],
                    PrincipalArn=idp_role_tuple[1],
                    SAMLAssertion=saml_assertion,
                    DurationSeconds=3600
                )
                Common.echo(message='YOUR SESSION WILL ONLY LAST ONE HOUR')
            else:
                raise

        profile_mgr.apply_credentials(credentials=assumed_role_credentials, echo_message=True)
        bash_file = profile_mgr.write_sourceable_file(credentials=assumed_role_credentials)
        docker_file = profile_mgr.write_dockerenv_file(credentials=assumed_role_credentials)
        Common.echo(
            message='AWS keys generated. To use, run "export AWS_PROFILE={prof}"\nor use generated files {file1} with docker compose or {file2} with shell scripts'.format(
                prof=self.profile, file1=docker_file, file2=bash_file
            )
        )
Example #6
0
 def dump_account_numbers(cls, clokta_config_file):
     clokta_cfg_file = configparser.ConfigParser()
     clokta_cfg_file.read(os.path.expanduser(clokta_config_file))
     section_names = clokta_cfg_file.sections()
     for section_name in section_names:
         if clokta_cfg_file.has_option(section=section_name,
                                       option='aws_account_number'):
             acct_num = clokta_cfg_file.get(section=section_name,
                                            option='aws_account_number')
             if acct_num:
                 Common.echo("{name} = {number}".format(name=section_name,
                                                        number=acct_num))
Example #7
0
    def __wait_for_push_result(self, state_token, push_response):
        """
        A request was sent to Okta querying the status of a push.  Process the response, pull the session
        token from it, and store in self.session_token
        :param state_token: a token received from Okta identifying this authentication attempt session
        :type state_token: str
        :param push_response: the HTTP response from the HTTP request
        :type push_response: json
        :return: SUCCESS if response indicates SUCCESS.  INPUT_ERROR if timed out.
        """
        url = push_response['_links']['next']['href']
        headers = {
            'Accept': 'application/json',
            'Content-Type': 'application/json',
            'Cache-Control': '"no-cache'
        }
        payload = {'stateToken': state_token}

        wait_for = 60
        timeout = time.time() + wait_for
        response_data = None
        while True:
            Common.echo(message='.', new_line=False)
            response = requests.post(url,
                                     data=json.dumps(payload),
                                     headers=headers)
            if response.status_code == requests.codes.ok:  # pylint: disable=E1101
                response_data = response.json()
            else:
                response.raise_for_status()

            if 'sessionToken' in response_data or time.time() > timeout:
                break
            time.sleep(3)

        if response_data and 'sessionToken' in response_data:
            Common.echo(message='Session confirmed')
            self.session_token = response_data['sessionToken']
            return OktaInitiator.Result.SUCCESS
        else:
            msg = 'Timeout expired ({} seconds)'.format(wait_for)
            Common.dump_err(message=msg)
            return OktaInitiator.Result.INPUT_ERROR
Example #8
0
    def __choose_tuple(self, idp_role_tuples):
        ''' Give the user a choice from the intersection of configured and supported factors '''
        index = 1
        for tup in idp_role_tuples:
            slashIndex = tup[2].find('/')
            shortName = tup[2][slashIndex + 1:] if slashIndex >= 0 else tup[2]
            msg = '{index} - {prompt}'.format(index=tup[0], prompt=shortName)
            Common.echo(message=msg, bold=True)
            index += 1

        raw_choice = None
        try:
            raw_choice = click.prompt('Choose a Role ARN to use', type=int)
            choice = raw_choice - 1
        except ValueError:
            Common.echo(message='Please select a valid option: you chose: {}'.
                        format(raw_choice))
            return self.__choose_tuple(idp_role_tuples=idp_role_tuples)

        if len(idp_role_tuples) > choice >= 0:
            pass
        else:
            Common.echo(message='Please select a valid option: you chose: {}'.
                        format(raw_choice))
            return self.__choose_tuple(idp_role_tuples=idp_role_tuples)

        chosen_option = idp_role_tuples[choice]
        if self.verbose:
            Common.dump_verbose(message='Using chosen Role {role} & IDP {idp}'.
                                format(role=tup[2], idp=tup[1]))

        return chosen_option
Example #9
0
    def choose_supported_factor(self):
        ''' Give the user a choice from the intersection of configured and supported factors '''
        index = 1
        for opt in self.option_factors:
            msg = '{index} - {prompt}'.format(index=index, prompt=opt['prompt'])
            Common.echo(message=msg, bold=True)
            index += 1

        raw_choice = None
        try:
            raw_choice = click.prompt('Choose a MFA type to use', type=int)
            choice = raw_choice - 1
        except ValueError:
            Common.echo(message='Please select a valid option: you chose: {}'.format(raw_choice))
            return self.choose_supported_factor()

        if len(self.option_factors) > choice >= 0:
            pass
        else:
            Common.echo(message='Please select a valid option: you chose: {}'.format(raw_choice))
            return self.choose_supported_factor()

        chosen_option = self.option_factors[choice]
        matching_okta_factor = [
            fact for fact in self.okta_factors
            if fact['provider'] == chosen_option['provider'] and
            fact['factorType'] == chosen_option['factor_type']
        ]
        if self.verbose:
            Common.dump_verbose(message='Using chosen factor: {}'.format(chosen_option['prompt']))

        return matching_okta_factor[0]
Example #10
0
    def __do_mfa_with_push(self, factor, state_token):
        """
        Send push re: Okta Verify and wait for response.
        If succesful, session token will be stored in self.session_token
        :param factor: mfa information
        :type factor: dict
        :param state_token: token used in MFA back and forth
        :type: str
        :return: SUCCESS if push reported success.  INPUT_ERROR if user never responded.  Any other
        possibilities will result in an exception
        :rtype: OktaInitiator.Result
        """
        url = factor['_links']['verify']['href']
        headers = {
            'Accept': 'application/json',
            'Content-Type': 'application/json',
            'Cache-Control': '"no-cache'
        }
        payload = {'stateToken': state_token}

        response_data = None
        response = requests.post(url,
                                 data=json.dumps(payload),
                                 headers=headers)
        if response.status_code == requests.codes.ok:  # pylint: disable=E1101
            response_data = response.json()
        else:
            response.raise_for_status()

        Common.echo(
            message='Push notification sent; waiting for your response',
            new_line=False)

        status = response_data['status']
        if status == 'MFA_CHALLENGE':
            if 'factorResult' in response_data and response_data[
                    'factorResult'] == 'WAITING':
                return self.__wait_for_push_result(state_token=state_token,
                                                   push_response=response_data)
Example #11
0
    def __prompt_for_role(self, with_set_default_option):
        """
        Give the user a choice from the intersection of configured and supported factors
        :param with_set_default_option: if True will add an option for setting a default role
        :type with_set_default_option: bool
        :return: a tuple of what role was chosen and whether it is the new default
        :rtype: AwsRole, bool
        """
        index = 1
        for role in self.possible_roles:
            msg = '{index} - {prompt}'.format(index=index,
                                              prompt=role.role_name)
            Common.echo(message=msg, bold=True)
            index += 1
        if with_set_default_option:
            Common.echo('{index} - set a default role'.format(index=index))
        raw_choice = None
        try:
            raw_choice = click.prompt(text='Choose a Role ARN to use',
                                      type=int,
                                      err=Common.to_std_error())
            choice = raw_choice - 1
        except ValueError:
            Common.echo(message='Please select a valid option: you chose: {}'.
                        format(raw_choice))
            return self.__prompt_for_role()

        if choice == len(self.possible_roles):
            # They want to set a default.  Prompt again (just without the set-default option)
            # and return that chosen role and that it's the new default
            chosen_option, _ = self.__prompt_for_role(
                with_set_default_option=False)
            return chosen_option, True
        if len(self.possible_roles) > choice >= 0:
            pass
        else:
            Common.echo(message='Please select a valid option: you chose: {}'.
                        format(raw_choice))
            return self.__prompt_for_role(
                with_set_default_option=with_set_default_option)

        chosen_option = self.possible_roles[choice]
        if Common.is_debug():
            Common.dump_out(
                message='Using chosen Role {role} & IDP {idp}'.format(
                    role=chosen_option.role_arn, idp=chosen_option.idp_arn))

        return chosen_option, False
Example #12
0
    def write_dockerenv_file(self, credentials):
        '''
        Generates a Docker .env file that can be used with docker compose to inject into the environment.
        '''
        creds = credentials['Credentials']
        output_file_name = '{dir}/{profile}.env'.format(
            dir=os.path.dirname(self.config_location),
            profile=self.profile_name
        )
        lines = [
            'AWS_ACCESS_KEY_ID={}\n'.format(creds['AccessKeyId']),
            'AWS_SECRET_ACCESS_KEY={}\n'.format(creds['SecretAccessKey'])
        ]
        if 'SessionToken' in creds:
            lines.append('AWS_SESSION_TOKEN={}\n'.format(creds['SessionToken']))

        with open(output_file_name, mode='w') as file_handle:
            file_handle.writelines(lines)
        Common.echo(
            message='AWS keys saved to {loc} for use with docker compose'.format(
                loc=output_file_name
            )
        )
Example #13
0
    def write_sourceable_file(self, credentials):
        '''
        Generates a shell script to source in order to apply credentials to the shell environment.
        '''
        creds = credentials['Credentials']
        output_file_name = '{dir}/{profile}.sh'.format(
            dir=os.path.dirname(self.config_location),
            profile=self.profile_name
        )
        lines = [
            'export AWS_ACCESS_KEY_ID={}\n'.format(creds['AccessKeyId']),
            'export AWS_SECRET_ACCESS_KEY={}\n'.format(creds['SecretAccessKey'])
        ]
        if 'SessionToken' in creds:
            lines.append('export AWS_SESSION_TOKEN={}\n'.format(creds['SessionToken']))

        with open(output_file_name, mode='w') as file_handle:
            file_handle.writelines(lines)
        Common.echo(
            message='AWS keys saved to {loc}. To use, `source {loc}`'.format(
                loc=output_file_name
            )
        )
Example #14
0
    def choose_role(self):
        """
        Look for a default role defined and, if not, prompt the user for one
        Allow the user to also specify the role is the default to be used
        from now on
        :return: a tuple of the chosen role and whether it is the
        new default
        :rtype: AwsRole, bool
        """
        # throw an error if no roles are provided
        #  (defensive coding only - this should not be possible)
        if not self.possible_roles:
            Common.dump_err(
                message='No AWS Role was assigned to this application!')
            raise ValueError(
                'Unexpected configuration - No AWS role assigned to Okta login.'
            )

        # use the one provided if there is only one
        if len(self.possible_roles) == 1:
            role = self.possible_roles[0]
            if self.role_preference and role.role_arn != self.role_preference:
                Common.dump_err(
                    message=
                    'Your cofigured role "{notfound}" was not found; using "{found}" role'
                    .format(notfound=self.role_preference,
                            found=role.role_name))
            elif Common.is_debug():
                Common.echo(message="Using default role '{role}'".format(
                    role=role.role_arn))
            return role, False

        # use the configured role if it matches one from the the SAML assertion
        for role in self.possible_roles:
            if role.role_arn == self.role_preference:
                message = "Using default role '{}'".format(role.role_name)
                extra_message = '.  Run "clokta --no-default-role" to override.'
                if Common.get_output_format() == Common.long_out:
                    Common.echo(message + extra_message)
                else:
                    Common.echo(message)
                return role, True

        # make the user choose
        return self.__prompt_for_role(with_set_default_option=True)
Example #15
0
 def output_instructions(self, docker_file, bash_file):
     if Common.get_output_format() == Common.quiet_out:
         Common.echo(
             message='export AWS_PROFILE={}'.format(self.profile),
             always_stdout=True
         )
     elif Common.get_output_format() == Common.long_out:
         Common.echo(
             message='\nAWS keys generated.\nTo use with docker-compose include\n' +
                     '\tenv_file:\n\t    - {}\n'.format(docker_file) +
                     'To use with shell scripts include\n\tsource {}\n'.format(bash_file) +
                     'to use in the current interactive shell run\n\texport AWS_PROFILE={}\n'.format(self.profile)
         )
     else:
         Common.echo(
             message='Add the "-i" flag for how to use credentials and override defaults or just run:\n\n' +
                     'export AWS_PROFILE={}\n'.format(self.profile)
         )
Example #16
0
    def assume_role(self):
        ''' entry point for the cli tool '''
        profile_mgr = ProfileManager(profile_name=self.profile,
                                     verbose=self.verbose)
        configuration = profile_mgr.initialize_configuration()
        profile_mgr.update_configuration(profile_configuration=configuration)

        # Need a directory to store intermediate files.  Use the same directory that clokta configuration
        # is kept in
        self.data_dir = os.path.dirname(profile_mgr.config_location)

        saml_assertion = self.__saml_assertion_aws(session_token=None,
                                                   configuration=configuration)

        if not saml_assertion:
            if 'okta_password' not in configuration or not configuration[
                    'okta_password']:
                configuration['okta_password'] = profile_mgr.prompt_for(
                    'okta_password')

            session_token = self.__okta_session_token(
                configuration=configuration)
            if self.verbose:
                Common.dump_verbose(
                    message='Okta session token: {}'.format(session_token))

            saml_assertion = self.__saml_assertion_aws(
                session_token=session_token, configuration=configuration)

        idp_and_role_chooser = RoleChooser(
            saml_assertion=saml_assertion,
            role_preference=configuration.get('okta_aws_role_to_assume'),
            verbose=self.verbose)
        idp_role_tuple = idp_and_role_chooser.choose_idp_role_tuple()

        client = boto3.client('sts')
        # Try for a 12 hour session.  If it fails, try for shorter periods
        durations = [43200, 14400, 3600]
        for duration in durations:
            try:
                assumed_role_credentials = client.assume_role_with_saml(
                    RoleArn=idp_role_tuple[2],
                    PrincipalArn=idp_role_tuple[1],
                    SAMLAssertion=saml_assertion,
                    DurationSeconds=duration)
                if duration == 3600:
                    Common.echo(message='YOUR SESSION WILL ONLY LAST ONE HOUR')
                break
            except ClientError as e:
                # If we get a validation error and we have shorter durations to try, try a shorter duration
                if e.response['Error'][
                        'Code'] != 'ValidationError' or duration == durations[
                            -1]:
                    raise

        profile_mgr.apply_credentials(credentials=assumed_role_credentials,
                                      echo_message=True)
        bash_file = profile_mgr.write_sourceable_file(
            credentials=assumed_role_credentials)
        docker_file = profile_mgr.write_dockerenv_file(
            credentials=assumed_role_credentials)
        Common.echo(
            message=
            'AWS keys generated. To use, run "export AWS_PROFILE={prof}"\nor use files {file1} with docker compose or {file2} with shell scripts'
            .format(prof=self.profile, file1=docker_file, file2=bash_file))