Skip to content

Scenario Inheritance

Andy Rea edited this page Oct 18, 2025 · 1 revision

Scenario Inheritance

Reuse and extend test scenarios with the extends: feature to eliminate duplication and create modular test suites.

Table of Contents

Why Use Inheritance

Without inheritance, you duplicate scenarios:

Without inheritance:

# dev-test.yml
vars:
  environment: "dev"
  bucket: "dev-data"
policy_template: "policy.json.tpl"
scp_paths: ["scp/org-policy.json"]
tests:
  - name: "Test GetObject"
    action: "s3:GetObject"
    resource: "arn:aws:s3:::{{.bucket}}/*"
    expect: "allowed"
# prod-test.yml
vars:
  environment: "prod"
  bucket: "prod-data"
policy_template: "policy.json.tpl"  # Duplicated!
scp_paths: ["scp/org-policy.json"]  # Duplicated!
tests:
  - name: "Test GetObject"  # Duplicated!
    action: "s3:GetObject"
    resource: "arn:aws:s3:::{{.bucket}}/*"
    expect: "allowed"

With inheritance:

# base.yml
policy_template: "policy.json.tpl"
scp_paths: ["scp/org-policy.json"]
tests:
  - name: "Test GetObject"
    action: "s3:GetObject"
    resource: "arn:aws:s3:::{{.bucket}}/*"
    expect: "allowed"
# dev-test.yml
extends: "base.yml"
vars:
  environment: "dev"
  bucket: "dev-data"
# prod-test.yml
extends: "base.yml"
vars:
  environment: "prod"
  bucket: "prod-data"

Basic Usage

Step 1: Create Base Scenario

From test/scenarios/14-base-with-variables.yml:

# Base scenario with common configuration
vars_file: "../vars/common-vars.yml"
policy_template: "../policies/s3-templated.json.tpl"
caller_arn: "arn:aws:iam::{{.account_id}}:user/alice"

tests:
  - name: "GetObject allowed with correct department tag"
    action: "s3:GetObject"
    resource: "arn:aws:s3:::{{.bucket_name}}/data.txt"
    context:
      - ContextKeyName: "aws:PrincipalTag/Department"
        ContextKeyType: "string"
        ContextKeyValues: ["{{.department}}"]
    expect: "allowed"

  - name: "PutObject allowed with correct department tag"
    action: "s3:PutObject"
    resource: "arn:aws:s3:::{{.bucket_name}}/upload.txt"
    context:
      - ContextKeyName: "aws:PrincipalTag/Department"
        ContextKeyType: "string"
        ContextKeyValues: ["{{.department}}"]
    expect: "allowed"

Step 2: Extend and Override

From test/scenarios/15-extends-add-scp.yml:

# Child scenario: adds SCP constraints
extends: "14-base-with-variables.yml"

# Add organizational SCP that denies writes
scp_paths:
  - "../scp/deny-s3-write.json"

# Override tests - writes now denied by SCP
tests:
  - name: "GetObject still allowed (SCP allows reads)"
    action: "s3:GetObject"
    resource: "arn:aws:s3:::{{.bucket_name}}/data.txt"
    context:
      - ContextKeyName: "aws:PrincipalTag/Department"
        ContextKeyType: "string"
        ContextKeyValues: ["{{.department}}"]
    expect: "allowed"

  - name: "PutObject now denied by SCP"
    action: "s3:PutObject"
    resource: "arn:aws:s3:::{{.bucket_name}}/upload.txt"
    context:
      - ContextKeyName: "aws:PrincipalTag/Department"
        ContextKeyType: "string"
        ContextKeyValues: ["{{.department}}"]
    expect: "explicitDeny"  # Changed expectation!

How Merging Works

politest merges parent and child scenarios with specific rules:

Maps: Deep Merge

vars and expect maps are deep-merged (child adds/overrides keys):

# parent.yml
vars:
  bucket: "parent-bucket"
  region: "us-east-1"
  department: "Engineering"

# child.yml
extends: "parent.yml"
vars:
  bucket: "child-bucket"  # Overrides
  environment: "prod"     # Adds new

# Result:
# vars:
#   bucket: "child-bucket"      ← Overridden
#   region: "us-east-1"          ← Inherited
#   department: "Engineering"    ← Inherited
#   environment: "prod"          ← Added

Arrays: Complete Replacement

actions, resources, scp_paths, tests arrays are replaced entirely:

# parent.yml
tests:
  - name: "Test 1"
    action: "s3:GetObject"
    expect: "allowed"
  - name: "Test 2"
    action: "s3:PutObject"
    expect: "allowed"

# child.yml
extends: "parent.yml"
tests:
  - name: "Test 3"
    action: "s3:DeleteObject"
    expect: "implicitDeny"

# Result: Only Test 3 runs (parent tests replaced)

Scalars: Child Overrides

String fields like policy_json, caller_arn, etc. are replaced:

# parent.yml
policy_json: "parent-policy.json"
caller_arn: "arn:aws:iam::111111111111:user/alice"

# child.yml
extends: "parent.yml"
caller_arn: "arn:aws:iam::111111111111:user/bob"

# Result:
#   policy_json: "parent-policy.json"  ← Inherited
#   caller_arn: "...user/bob"          ← Overridden

Real Examples

Example 1: Variable Override

From test/scenarios/16-extends-override-vars.yml:

extends: "14-base-with-variables.yml"

# Override variables for different department
vars:
  department: "Finance"
  bucket_name: "finance-data"

# All tests from parent re-run with Finance department

Result: Same tests, different variables - tests Finance department access instead of Engineering.

Example 2: Add SCPs to Base Scenario

# base-developer.yml
policy_json: "developer-policy.json"
tests:
  - name: "EC2 access"
    action: "ec2:RunInstances"
    resource: "*"
    expect: "allowed"

# production-developer.yml
extends: "base-developer.yml"

# Add production SCP that restricts access
scp_paths:
  - "scp/deny-production-resources.json"

# Override expectations
tests:
  - name: "EC2 dev instances allowed"
    action: "ec2:RunInstances"
    resource: "arn:aws:ec2:*:*:instance/i-dev-*"
    expect: "allowed"

  - name: "EC2 prod instances denied by SCP"
    action: "ec2:RunInstances"
    resource: "arn:aws:ec2:*:*:instance/i-prod-*"
    expect: "explicitDeny"

Example 3: Multi-Level Inheritance

politest supports recursive inheritance (child extends child extends base):

# base.yml - Foundation
policy_template: "policy.json.tpl"
vars_file: "vars/common.yml"

# dev.yml - Add dev-specific settings
extends: "base.yml"
vars:
  environment: "dev"

# dev-alice.yml - Add user-specific settings
extends: "dev.yml"
caller_arn: "arn:aws:iam::111111111111:user/alice"

Merge order: base.yml → dev.yml → dev-alice.yml

Best Practices

1. Create Environment Hierarchies

scenarios/
├── base.yml              # Common policy and tests
├── dev.yml              # extends base, dev vars
├── staging.yml          # extends base, staging vars
└── prod.yml             # extends base, prod vars + stricter SCPs

2. Separate Concerns

# base-policy.yml - Just the policy
policy_template: "policy.json.tpl"

# base-tests.yml - Common tests
extends: "base-policy.yml"
tests:
  - name: "Standard test 1"
    ...

# with-scp.yml - Add organizational constraints
extends: "base-tests.yml"
scp_paths: ["scp/*.json"]

3. Document Inheritance Chains

Add comments to child scenarios:

# Inherits from: base.yml
# Adds: Production SCPs
# Overrides: Stricter expectations
extends: "base.yml"

4. Use Relative Paths

Always use paths relative to the scenario file:

# In scenarios/prod/test.yml
extends: "../base.yml"  # Up to scenarios/, then base.yml
policy_json: "../../policies/policy.json"  # Up two levels

Troubleshooting

"File not found" when loading parent

Problem: extends: "parent.yml" fails

Solution: Use path relative to current scenario file:

# If current file is: scenarios/env/child.yml
# And parent is: scenarios/base.yml
extends: "../base.yml"  # Not "base.yml"

Variables not overriding

Problem: Child vars don't override parent vars

Solution: Check that vars are maps (key-value), not arrays:

# ✓ Correct (map)
vars:
  bucket: "new-value"

# ✗ Wrong (won't work)
vars:
  - bucket: "new-value"

Tests from parent still running

Problem: Expected child tests only, but parent tests also run

Solution: Remember that tests array is replaced, not merged. If you see parent tests, child scenario likely doesn't have a tests: field.

Next Steps


Working examples: test/scenarios/15-extends-add-scp.yml

Clone this wiki locally