diff --git a/pyproject.toml b/pyproject.toml index ddde265..eec9556 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ z3-solver = "*" pydantic = "*" typer = "*" python-dateutil = "^2.8.2" +pytest-bdd = "^6.0.1" [tool.poetry.dev-dependencies] ipython = "*" diff --git a/tests/features/explicit-deny.feature b/tests/features/explicit-deny.feature new file mode 100644 index 0000000..7503cb1 --- /dev/null +++ b/tests/features/explicit-deny.feature @@ -0,0 +1,38 @@ +Feature: Explicit Deny + Background: + Given I'm using arn:aws:iam::111111111111:role/source-deny with the policy: + [{ + "Effect": "Deny", + "Action": "*", + "Resource": "*" + }] + + Scenario: Same account root ARN trust + Given I have the resource arn:aws:iam::111111111111:role/target with the trust policy: + [{ + "Effect": "Allow", + "Principal": { "AWS": "arn:aws:iam::111111111111:root" }, + "Action": "sts:AssumeRole" + }] + When I call sts:AssumeRole on the resource + Then Access should be denied + + Scenario: Same account explicit ARN trust + Given I have the resource arn:aws:iam::111111111111:role/target with the trust policy: + [{ + "Effect": "Allow", + "Principal": { "AWS": "arn:aws:iam::111111111111:role/source-deny" }, + "Action": "sts:AssumeRole" + }] + When I call sts:AssumeRole on the resource + Then Access should be denied + + Scenario: Cross account explicit ARN trust + Given I have the resource arn:aws:iam::999999999999:role/target with the trust policy: + [{ + "Effect": "Allow", + "Principal": { "AWS": "arn:aws:iam::111111111111:role/source-deny" }, + "Action": "sts:AssumeRole" + }] + When I call sts:AssumeRole on the resource + Then Access should be denied \ No newline at end of file diff --git a/tests/features/root-trust.feature b/tests/features/root-trust.feature new file mode 100644 index 0000000..7d03801 --- /dev/null +++ b/tests/features/root-trust.feature @@ -0,0 +1,18 @@ +Feature: Root Trust + Background: + Given I'm using arn:aws:iam::111111111111:role/source with the policy: + [{ + "Effect": "Allow", + "Action": "sts:AssumeRole", + "Resource": "*" + }] + + Scenario: Same account AssumeRole request. + Given I have the resource arn:aws:iam::111111111111:role/target with the trust policy: + [{ + "Effect": "Allow", + "Principal": { "AWS": "arn:aws:iam::111111111111:root" }, + "Action": "sts:AssumeRole" + }] + When I call sts:AssumeRole on the resource + Then Access should be allowed \ No newline at end of file diff --git a/tests/fixtures/role.py b/tests/fixtures/role.py new file mode 100644 index 0000000..0e58ea9 --- /dev/null +++ b/tests/fixtures/role.py @@ -0,0 +1,17 @@ +import pytest + +from iamspy import Model + + +@pytest.fixture +def model() -> Model: + return Model() + + +@pytest.fixture +def req(): + return { + "source": "", + "action": "", + "resource": "", + } diff --git a/tests/test_bdd.py b/tests/test_bdd.py new file mode 100644 index 0000000..cda06aa --- /dev/null +++ b/tests/test_bdd.py @@ -0,0 +1,70 @@ +import json +from datetime import datetime + +from iamspy.iam import RoleDetail, Document, Policy, RoleLastUse, ResourcePolicy +from iamspy.parse import _parse_role, parse_resource_policy +from pytest_bdd import scenarios, given, when, then, parsers +from pytest_bdd.parsers import parse + +from .fixtures.role import * + +scenarios("features") + +@pytest.fixture +def source() -> 'RoleDetail': + return RoleDetail( + Path="/", + RoleName="source", + CreateDate=datetime.now(), + RoleLastUsed=RoleLastUse(LastUsedDate=datetime.now(), Region="us-east-1"), + RoleId="AORAXXXXXXXXXXXXXXXXX", + Arn=f"arn:aws:iam::111111111111:role/source", + AssumeRolePolicyDocument=Document(), + InstanceProfileList=[], + RolePolicyList=[], + ) + + +@given(parsers.parse("I'm using {arn} with the policy:\n{policy}")) # Step alias +def set_source(model, req, source, arn, policy): + source.Arn = arn + source.RoleName = arn.split(':')[5].split("/")[-1] + source.RolePolicyList.append(Policy( + PolicyName="identity_policy", + PolicyDocument=Document(Statement=json.loads(policy)), + )) + + +@pytest.fixture +def resource(): + return ResourcePolicy( + Resource="target", + Policy=Document(), + Account="111111111111", + ) + + +@given(parse("I have the resource {arn} with the trust policy:\n{policy}")) +def set_resource(resource, arn, policy): + resource.Resource = arn + resource.Account = arn.split(':')[4] + resource.Policy = Document(Statement=json.loads(policy)) + + +@when(parsers.parse("I call {action}")) +def call(req, model, source, action, resource): + model.solver.add(*_parse_role(None, source)) + model.solver.add(*parse_resource_policy(resource.Resource, resource.Policy, resource.Account)) + req["source"] = source.Arn + req["action"] = action + req["resource"] = resource.Resource + + +@then(parsers.parse("Access should be allowed")) +def allowed(model, req): + assert model.can_i(**req) + + +@then(parsers.parse("Access should be denied")) +def denied(model, req): + assert not model.can_i(**req, strict_conditions=True) \ No newline at end of file