Beispiel #1
0
def delete_policy(
    name: str, role_name: str, account_number: str, conn: Dict[str, Any]
) -> None:
    """Deletes the specified IAM Role inline policy.

    Args:
        name (string)
        role (Role object)
        account_number (string)
        conn (dict)

    Returns:
        error (string) or None
    """
    LOGGER.info(
        "Deleting policy with name {} from {} in account {}".format(
            name, role_name, account_number
        )
    )
    try:
        delete_role_policy(RoleName=role_name, PolicyName=name, **conn)
    except botocore.exceptions.ClientError as e:
        raise IAMError(
            f"Error deleting policy: {name} from role: {role_name} in account {account_number}"
        ) from e
Beispiel #2
0
def delete_policy(name, role, account_number, conn):
    """Deletes the specified IAM Role inline policy.

    Args:
        name (string)
        role (Role object)
        account_number (string)
        conn (dict)

    Returns:
        error (string) or None
    """
    LOGGER.info("Deleting policy with name {} from {} in account {}".format(
        name, role.role_name, account_number))
    try:
        delete_role_policy(RoleName=role.role_name, PolicyName=name, **conn)
    except botocore.exceptions.ClientError as e:
        return "Error deleting policy: {} from role: {} in account {}.  Exception: {}".format(
            name, role.role_name, account_number, e)
Beispiel #3
0
def _rollback_role(account_number,
                   role_name,
                   dynamo_table,
                   config,
                   hooks,
                   selection=None,
                   commit=None):
    """
    Display the historical policy versions for a roll as a numbered list.  Restore to a specific version if selected.
    Indicate changes that will be made and then actually make them if commit is selected.

    Args:
        account_number (string)
        role_name (string)
        selection (int): which policy version in the list to rollback to
        commit (bool): actually make the change

    Returns:
        errors (list): if any
    """
    errors = []

    role_id = find_role_in_cache(dynamo_table, account_number, role_name)
    if not role_id:
        message = "Could not find role with name {} in account {}".format(
            role_name, account_number)
        errors.append(message)
        LOGGER.warning(message)
        return errors
    else:
        role = Role(get_role_data(dynamo_table, role_id))

    # no option selected, display a table of options
    if not selection:
        headers = ["Number", "Source", "Discovered", "Permissions", "Services"]
        rows = []
        for index, policies_version in enumerate(role.policies):
            policy_permissions, _ = roledata._get_permissions_in_policy(
                policies_version["Policy"])
            rows.append([
                index,
                policies_version["Source"],
                policies_version["Discovered"],
                len(policy_permissions),
                roledata._get_services_in_permissions(policy_permissions),
            ])
        print(tabulate(rows, headers=headers))
        return

    conn = config["connection_iam"]
    conn["account_number"] = account_number

    current_policies = get_role_inline_policies(role.as_dict(), **conn)

    if selection:
        pp = pprint.PrettyPrinter()

        print("Will restore the following policies:")
        pp.pprint(role.policies[int(selection)]["Policy"])

        print("Current policies:")
        pp.pprint(current_policies)

        current_permissions, _ = roledata._get_permissions_in_policy(
            role.policies[-1]["Policy"])
        selected_permissions, _ = roledata._get_permissions_in_policy(
            role.policies[int(selection)]["Policy"])
        restored_permissions = selected_permissions - current_permissions

        print("\nResore will return these permissions:")
        print("\n".join([perm for perm in sorted(restored_permissions)]))

    if not commit:
        return False

    # if we're restoring from a version with fewer policies than we have now, we need to remove them to
    # complete the restore.  To do so we'll store all the policy names we currently have and remove them
    # from the list as we update.  Any policy names left need to be manually removed
    policies_to_remove = current_policies.keys()

    for policy_name, policy in role.policies[int(selection)]["Policy"].items():
        try:
            LOGGER.info(
                f"Pushing cached policy: {policy_name} (role: {role.role_name} account {account_number})"
            )

            put_role_policy(
                RoleName=role.role_name,
                PolicyName=policy_name,
                PolicyDocument=json.dumps(policy, indent=2, sort_keys=True),
                **conn,
            )

        except botocore.exceptions.ClientError as e:
            message = "Unable to push policy {}.  Error: {} (role: {} account {})".format(
                policy_name, e.message, role.role_name, account_number)
            LOGGER.error(message, exc_info=True)
            errors.append(message)

        else:
            # remove the policy name if it's in the list
            try:
                policies_to_remove.remove(policy_name)
            except Exception:  # nosec
                pass

    if policies_to_remove:
        for policy_name in policies_to_remove:
            try:
                LOGGER.info(
                    f"Deleting policy {policy_name} for rollback (role: {role.role_name} account {account_number})"
                )
                delete_role_policy(RoleName=role.role_name,
                                   PolicyName=policy_name,
                                   **conn)

            except botocore.excpetions.ClientError as e:
                message = "Unable to delete policy {}.  Error: {} (role: {} account {})".format(
                    policy_name, e.message, role.role_name, account_number)
                LOGGER.error(message, exc_info=True)
                errors.append(message)

    partial_update_role_data(
        role,
        dynamo_table,
        account_number,
        config,
        conn,
        hooks,
        source="Restore",
        add_no_repo=False,
    )

    if not errors:
        LOGGER.info(
            f"Successfully restored selected version {selection} of role policies (role: {role.role_name} "
            f"account: {account_number}")
    return errors
Beispiel #4
0
def repo_role(account_number, role_name, dynamo_table, config, hooks, commit=False, scheduled=False):
    """
    Calculate what repoing can be done for a role and then actually do it if commit is set
      1) Check that a role exists, it isn't being disqualified by a filter, and that is has fresh AA data
      2) Get the role's current permissions, repoable permissions, and the new policy if it will change
      3) Make the changes if commit is set
    Args:
        account_number (string)
        role_name (string)
        commit (bool)

    Returns:
        None
    """
    errors = []

    role_id = find_role_in_cache(dynamo_table, account_number, role_name)
    # only load partial data that we need to determine if we should keep going
    role_data = get_role_data(dynamo_table, role_id, fields=['DisqualifiedBy', 'AAData', 'RepoablePermissions',
                                                             'RoleName'])
    if not role_data:
        LOGGER.warn('Could not find role with name {}'.format(role_name))
        return
    else:
        role = Role(role_data)

    if len(role.disqualified_by) > 0:
        LOGGER.info('Cannot repo role {} in account {} because it is being disqualified by: {}'.format(
            role_name,
            account_number,
            role.disqualified_by))
        return

    if not role.aa_data:
        LOGGER.warning('ARN not found in Access Advisor: {}'.format(role.arn))
        return

    if not role.repoable_permissions:
        LOGGER.info('No permissions to repo for role {} in account {}'.format(role_name, account_number))
        return

    # if we've gotten to this point, load the rest of the role
    role = Role(get_role_data(dynamo_table, role_id))

    old_aa_data_services = []
    for aa_service in role.aa_data:
        if(datetime.datetime.strptime(aa_service['lastUpdated'], '%a, %d %b %Y %H:%M:%S %Z') <
           datetime.datetime.now() - datetime.timedelta(days=config['repo_requirements']['oldest_aa_data_days'])):
            old_aa_data_services.append(aa_service['serviceName'])

    if old_aa_data_services:
        LOGGER.error('AAData older than threshold for these services: {} (role: {}, account {})'.format(
            old_aa_data_services,
            role_name,
            account_number))
        return

    permissions = roledata._get_role_permissions(role)
    repoable_permissions = roledata._get_repoable_permissions(account_number, role.role_name, permissions, role.aa_data,
                                                              role.no_repo_permissions,
                                                              config['filter_config']['AgeFilter']['minimum_age'],
                                                              hooks)

    # if this is a scheduled repo we need to filter out permissions that weren't previously scheduled
    if scheduled:
        repoable_permissions = roledata._filter_scheduled_repoable_perms(repoable_permissions, role.scheduled_perms)

    repoed_policies, deleted_policy_names = roledata._get_repoed_policy(role.policies[-1]['Policy'],
                                                                        repoable_permissions)

    policies_length = len(json.dumps(repoed_policies))

    if policies_length > MAX_AWS_POLICY_SIZE:
        error = ("Policies would exceed the AWS size limit after repo for role: {} in account {}.  "
                 "Please manually minify.".format(role_name, account_number))
        LOGGER.error(error)
        errors.append(error)
        return

    if not commit:
        for name in deleted_policy_names:
            LOGGER.info('Would delete policy from {} with name {} in account {}'.format(
                role_name,
                name,
                account_number))
        if repoed_policies:
            LOGGER.info('Would replace policies for role {} with: \n{} in account {}'.format(
                role_name,
                json.dumps(repoed_policies, indent=2, sort_keys=True),
                account_number))
        return

    conn = config['connection_iam']
    conn['account_number'] = account_number

    for name in deleted_policy_names:
        LOGGER.info('Deleting policy with name {} from {} in account {}'.format(name, role.role_name, account_number))
        try:
            delete_role_policy(RoleName=role.role_name, PolicyName=name, **conn)
        except botocore.exceptions.ClientError as e:
            error = 'Error deleting policy: {} from role: {} in account {}.  Exception: {}'.format(
                name,
                role.role_name,
                account_number,
                e)
            LOGGER.error(error)
            errors.append(error)

    if repoed_policies:
        LOGGER.info('Replacing Policies With: \n{} (role: {} account: {})'.format(
            json.dumps(repoed_policies, indent=2, sort_keys=True),
            role.role_name,
            account_number))

        for policy_name, policy in repoed_policies.items():
            try:
                put_role_policy(RoleName=role.role_name, PolicyName=policy_name,
                                PolicyDocument=json.dumps(policy, indent=2, sort_keys=True),
                                **conn)

            except botocore.exceptions.ClientError as e:
                error = 'Exception calling PutRolePolicy on {role}/{policy} in account {account}\n{e}\n'.format(
                    role=role.role_name, policy=policy_name, account=account_number, e=str(e))
                LOGGER.error(error)
                errors.append(error)

    current_policies = get_role_inline_policies(role.as_dict(), **conn) or {}
    roledata.add_new_policy_version(dynamo_table, role, current_policies, 'Repo')

    # regardless of whether we're successful we want to unschedule the repo
    set_role_data(dynamo_table, role.role_id, {'RepoScheduled': 0, 'ScheduledPerms': []})

    repokid.hooks.call_hooks(hooks, 'AFTER_REPO', {'role': role})

    if not errors:
        # repos will stay scheduled until they are successful
        set_role_data(dynamo_table, role.role_id, {'Repoed': datetime.datetime.utcnow().isoformat()})
        _update_repoed_description(role.role_name, **conn)
        _update_role_data(role, dynamo_table, account_number, config, conn, hooks, source='Repo', add_no_repo=False)
        LOGGER.info('Successfully repoed role: {} in account {}'.format(role.role_name, account_number))
    return errors