Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use version 2 STS tokens to query opt-in regions #79

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions botocove/cove_decorator.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import functools
import logging
from itertools import chain
from typing import Any, Callable, Dict, List, Optional
from warnings import warn

Expand Down Expand Up @@ -64,26 +65,35 @@ def wrapper(*args: Any, **kwargs: Any) -> CoveOutput:
thread_workers=thread_workers,
)

output = runner.run_cove_function()
function_output = runner.run_cove_function()

# Rewrite dataclasses into untyped dicts to retain current functionality
return CoveOutput(
cove_output = CoveOutput(
Results=[
{k: v for k, v in r.items() if v is not None}
for r in output["Results"]
for r in function_output["Results"]
],
Exceptions=[
{k: v for k, v in e.items() if v is not None}
for e in output["Exceptions"]
for e in function_output["Exceptions"]
if e["AssumeRoleSuccess"] is True
],
FailedAssumeRole=[
{k: v for k, v in f.items() if v is not None}
for f in output["Exceptions"]
for f in function_output["Exceptions"]
if f["AssumeRoleSuccess"] is False
],
)

if regions is None:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not following why this is required - I'm sure I'm missing something obvious but could you explain?

Copy link
Collaborator Author

@iainelder iainelder Oct 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Test: When the region is unspecified, the result has no region key
  • cove_output: {k: v for k, v in r.items() if v is not None}, so region key is excluded when it's None
  • CoveHostAccount.__init__: self.target_regions = [self._session.region_name], so the region is never None

I think self.target_regions = [self._session.region_name] was the easiest way to pass one of the assuming_session tests, but it's unclear now from this big PR.

And it always made me feel uneasy to see that [None] for the regions list. It was a nice hack to add multi-region support in a backwards-compatible way, but it is confusing.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha - makes sense. I don't think we need to maintain this behaviour in a new release: we can just always emit a Region key in the output and bump the sem ver appropriately. I'm not overly worried about adding to output, it's removing things that's more of a place to be wary of previous API contracts.

for result in chain(
cove_output["Results"],
cove_output["Exceptions"],
cove_output["FailedAssumeRole"],
):
del result["Region"]

return cove_output

return wrapper

# Handle both bare decorator and with argument
Expand Down
71 changes: 23 additions & 48 deletions botocove/cove_host_account.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,12 @@
import logging
import re
from functools import lru_cache
from typing import (
Any,
Dict,
Iterable,
List,
Literal,
Optional,
Sequence,
Set,
Tuple,
Union,
)

import boto3
from typing import Dict, Iterable, List, Optional, Sequence, Set, Tuple

from boto3.session import Session
from botocore.config import Config
from botocore.exceptions import ClientError
from mypy_boto3_organizations.client import OrganizationsClient
from mypy_boto3_organizations.type_defs import AccountTypeDef
from mypy_boto3_sts.client import STSClient
from mypy_boto3_sts.type_defs import PolicyDescriptorTypeTypeDef

from botocove.cove_types import CoveSessionInformation
Expand Down Expand Up @@ -52,15 +38,33 @@ def __init__(

self.thread_workers = thread_workers

self.sts_client = self._get_boto3_sts_client(assuming_session)
self.org_client = self._get_boto3_org_client(assuming_session)
if assuming_session:
logger.info(f"Using provided Boto3 session {assuming_session}")
self._session = assuming_session
else:
logger.info("No Boto3 session argument: using credential chain")
self._session = Session()

# Obtain version 2 session tokens valid in all AWS regions.
# https://repost.aws/knowledge-center/iam-validate-access-credentials
self.sts_client = self._session.client(
service_name="sts",
region_name=self._session.region_name,
endpoint_url=f"https://sts.{self._session.region_name}.amazonaws.com",
config=Config(max_pool_connections=self.thread_workers),
)

self.org_client = self._session.client(
service_name="organizations",
config=Config(max_pool_connections=self.thread_workers),
)

caller_id = self.sts_client.get_caller_identity()
self.host_account_id = caller_id["Account"]
self.host_account_partition = caller_id["Arn"].split(":")[1]

if regions is None:
self.target_regions = [None]
self.target_regions = [self._session.region_name]
else:
self.target_regions = regions

Expand Down Expand Up @@ -147,35 +151,6 @@ def _generate_account_sessions(self) -> Iterable[CoveSessionInformation]:
Result=None,
)

def _get_boto3_client(
self,
clientname: Union[Literal["organizations"], Literal["sts"]],
assuming_session: Optional[Session],
) -> Any:
if assuming_session:
logger.info(f"Using provided Boto3 session {assuming_session}")
return assuming_session.client(
service_name=clientname,
config=Config(max_pool_connections=self.thread_workers),
)
logger.info("No Boto3 session argument: using credential chain")
return boto3.client(
service_name=clientname,
config=Config(max_pool_connections=self.thread_workers),
)

def _get_boto3_org_client(
self, assuming_session: Optional[Session]
) -> OrganizationsClient:
client: OrganizationsClient = self._get_boto3_client(
"organizations", assuming_session
)
return client

def _get_boto3_sts_client(self, assuming_session: Optional[Session]) -> STSClient:
client: STSClient = self._get_boto3_client("sts", assuming_session)
return client

def _resolve_target_accounts(self, target_ids: Optional[List[str]]) -> Set[str]:
accounts_to_ignore = self._gather_ignored_accounts()
logger.info(f"Ignoring account IDs: {accounts_to_ignore=}")
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ flake8-print = ">=5.0.0"
mypy = ">=1.5.1"
pre-commit = ">=3.4.0"
flakeheaven = ">=3.3.0"
moto = {extras = ["organizations", "sts"], version = ">=4.2.5"}
moto = {extras = ["organizations", "sts", "ec2"], version = ">=4.2.5"}
boto3-stubs = {extras = ["ec2"], version = "*"}
pytest-randomly = ">=3.15.0"

# These are the last versions compatible with flake8 4. flakeheaven 3.3.0 is
Expand Down
4 changes: 2 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from _pytest.logging import LogCaptureFixture
from _pytest.monkeypatch import MonkeyPatch
from boto3 import Session
from moto import mock_organizations, mock_sts
from moto import mock_ec2, mock_organizations, mock_sts

from tests.moto_mock_org.moto_models import LargeOrg, SmallOrg

Expand All @@ -27,7 +27,7 @@ def _clean_env(monkeypatch: MonkeyPatch) -> None:
@pytest.fixture()
def mock_session() -> Iterator[Session]:
"""Returns a session with mock AWS services."""
with mock_sts(), mock_organizations():
with mock_sts(), mock_organizations(), mock_ec2():
yield Session()


Expand Down
12 changes: 0 additions & 12 deletions tests/moto_mock_org/test_botocove/test_botocove.py

This file was deleted.

180 changes: 0 additions & 180 deletions tests/moto_mock_org/test_host_account/test_resolve_targets.py

This file was deleted.

Loading