Skip to content
Draft
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
9 changes: 4 additions & 5 deletions .github/workflows/ci_codebuild-tests.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
name: AWS CodeBuild CI

on:
push:
# Run once a day
schedule:
- cron: "0 0 * * *"
# Add workflow_call trigger
# Only called by pull.yml, push.yml, and daily_ci.yml.
# Its own push and schedule triggers are removed, temporarily, for development on a fork.
workflow_call:
# Define any secrets that need to be passed from the caller
secrets:
Expand All @@ -19,6 +16,8 @@ permissions:
jobs:
codebuild-tests:
name: AWS CodeBuild CI
# Forks have no CI_AWS_ROLE_ARN, nor the CodeBuild project; skip there.
if: github.repository_owner == 'aws'
runs-on: ubuntu-latest
strategy:
matrix:
Expand Down
100 changes: 49 additions & 51 deletions .github/workflows/ci_tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,23 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: true
# To keep CI cheap, only Python 3.12 on Ubuntu is tested.
# Python 3.8, 3.9, and 3.10 are end of life;
# the ubuntu-latest runner no longer provides 3.8 or 3.9.
matrix:
os:
- ubuntu-latest
- windows-latest
- macos-latest
python:
- 3.8
- 3.9
- "3.10"
- "3.11"
- "3.12"
- 3.x
architecture:
- x64
- x86
category:
- local
- accept
- mpllocal
# Offline examples tests (pytest.mark.local); these need no credentials.
- exampleslocal
- mplexampleslocal
# These require credentials.
# Enable them once we sort how to provide them.
# - integ
Expand All @@ -50,48 +48,15 @@ jobs:
- ""
- -mpl
exclude:
# Python <3.11 incompatible with macOS ARM64 runners
# https://github.com/actions/setup-python/issues/948
- python: 3.7
os: macos-latest
- python: 3.8
os: macos-latest
- python: 3.9
os: macos-latest
- python: 3.10
os: macos-latest

# x86 builds are only meaningful for Windows
- os: ubuntu-latest
architecture: x86
- os: macos-latest
architecture: x86

# `cryptography` no longer publishes 32-bit Windows wheels for
# any CPython version, so pip falls back to building from sdist.
# That build is Rust/maturin-based and fails on the hosted
# windows-latest runner because only the x86_64 Rust target is
# installed while the interpreter reports platform `win32`
# (i686), so maturin refuses to use it. Drop x86 on Windows
# entirely; the only path back would be host-side toolchain
# provisioning (i686 Rust target + 32-bit OpenSSL) per cell,
# which isn't worth it for a configuration upstream no longer
# supports.
- os: windows-latest
architecture: x86

# MPL is not supported on <3.11
- python: 3.7
optional_mpl_dependency: -mpl
- python: 3.8
optional_mpl_dependency: -mpl
- python: 3.9
optional_mpl_dependency: -mpl
- python: 3.10
optional_mpl_dependency: -mpl
# mpllocal requires the MPL to be installed
- category: mpllocal
optional_mpl_dependency: ""
# mplexampleslocal requires the MPL to be installed
- category: mplexampleslocal
optional_mpl_dependency: ""
# exampleslocal is the legacy (Master Key Provider) examples, which do not use the MPL
- category: exampleslocal
optional_mpl_dependency: -mpl
steps:
# Support long Dafny filenames (used in MPL and DBESDK repos)
- name: Support longpaths
Expand All @@ -111,23 +76,56 @@ jobs:
env:
TOXENV: ${{ matrix.category }}${{ matrix.optional_mpl_dependency }}
run: tox -- -vv
upstream-py311:
ddblocal:
# Examples tests against DynamoDB Local (pytest -m ddblocal), with the fake KMS;
# no AWS credentials are needed, so this also runs on forks.
runs-on: ubuntu-latest
services:
dynamodb:
# Pinned, so a new DynamoDB Local release cannot change CI unnoticed.
# The image's default command is `-jar DynamoDBLocal.jar -inMemory`.
image: amazon/dynamodb-local:3.3.2
ports:
- 8000:8000
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: "3.12"
- run: |
python -m pip install --upgrade pip
pip install --upgrade -r dev_requirements/ci-requirements.txt
- name: Wait for DynamoDB Local
run: |
for attempt in $(seq 1 30); do
curl -s -o /dev/null http://localhost:8000 && exit 0
sleep 1
done
echo "DynamoDB Local did not start"
exit 1
- name: run test
env:
TOXENV: mplddblocal-mpl
DDB_LOCAL_ENDPOINT: http://localhost:8000
run: tox -- -vv
nocmk:
# Python 3.12 only, as above. test-upstream-requirements-py311 is not run:
# it pins Python 3.11 and frozen 3.11 requirements, with no 3.12 equivalent.
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
category:
- nocmk
- test-upstream-requirements-py311
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: "3.11"
python-version: "3.12"
- run: |
python -m pip install --upgrade pip
pip install --upgrade -r dev_requirements/ci-requirements.txt
- name: run test
env:
TOXENV: ${{ matrix.category }}${{ matrix.optional_mpl_dependency }}
TOXENV: ${{ matrix.category }}
run: tox -- -vv
6 changes: 4 additions & 2 deletions .github/workflows/issue-notification.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,17 @@ permissions:

jobs:
notify-issue:
if: github.event_name == 'issues'
# Forks have no Slack webhook.
if: github.repository_owner == 'aws' && github.event_name == 'issues'
uses: aws/aws-cryptographic-material-providers-library/.github/workflows/slack-notification.yml@main
with:
message: "New github issue `${{ github.event.issue.title }}`. Link: ${{ github.event.issue.html_url }}"
secrets:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL_GHI }}

notify-comment:
if: github.event_name == 'issue_comment' && !github.event.issue.pull_request
# Forks have no Slack webhook.
if: github.repository_owner == 'aws' && github.event_name == 'issue_comment' && !github.event.issue.pull_request
uses: aws/aws-cryptographic-material-providers-library/.github/workflows/slack-notification.yml@main
with:
message: "New comment on issue `${{ github.event.issue.title }}`. Link: ${{ github.event.comment.html_url }}"
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/pull.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,20 @@ permissions:
jobs:
# Call each workflow with appropriate parameters
codebuild_batch:
# Forks have no CI_AWS_ROLE_ARN, nor the CodeBuild project; skip there.
if: github.repository_owner == 'aws'
uses: ./.github/workflows/ci_codebuild_batch.yml
secrets:
CI_AWS_ROLE_ARN: ${{ secrets.CI_AWS_ROLE_ARN }}
codebuild_tests:
# Forks have no CI_AWS_ROLE_ARN, nor the CodeBuild project; skip there.
if: github.repository_owner == 'aws'
uses: ./.github/workflows/ci_codebuild-tests.yml
secrets:
CI_AWS_ROLE_ARN: ${{ secrets.CI_AWS_ROLE_ARN }}
decrypt_oracle:
# Pins Python 3.9, which the ubuntu-latest runner no longer provides; skip on forks.
if: github.repository_owner == 'aws'
uses: ./.github/workflows/ci_decrypt-oracle.yaml
static_analysis:
uses: ./.github/workflows/ci_static-analysis.yaml
Expand All @@ -48,4 +54,6 @@ jobs:
- name: Verify all required jobs passed
uses: re-actors/alls-green@release/v1
with:
# Skipped on forks; see each job's condition.
allowed-skips: codebuild_batch, codebuild_tests, decrypt_oracle
jobs: ${{ toJSON(needs) }}
6 changes: 6 additions & 0 deletions .github/workflows/push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,22 @@ permissions:
jobs:
# Call each workflow with appropriate parameters
codebuild_batch:
# Forks have no CI_AWS_ROLE_ARN, nor the CodeBuild project; skip there.
if: github.repository_owner == 'aws'
uses: ./.github/workflows/ci_codebuild_batch.yml
secrets:
CI_AWS_ROLE_ARN: ${{ secrets.CI_AWS_ROLE_ARN }}

codebuild_tests:
# Forks have no CI_AWS_ROLE_ARN, nor the CodeBuild project; skip there.
if: github.repository_owner == 'aws'
uses: ./.github/workflows/ci_codebuild-tests.yml
secrets:
CI_AWS_ROLE_ARN: ${{ secrets.CI_AWS_ROLE_ARN }}

decrypt_oracle:
# Pins Python 3.9, which the ubuntu-latest runner no longer provides; skip on forks.
if: github.repository_owner == 'aws'
uses: ./.github/workflows/ci_decrypt-oracle.yaml

static_analysis:
Expand Down
108 changes: 108 additions & 0 deletions examples/src/multi_tenant/cache_guard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
"""The Cache Guard: fails a Hierarchical Keyring's cache lookups for a Denied Branch Key ID.

See negative_caching.md in this directory, Design Question 4.3.

A ``CacheGuard`` is a Cryptographic Materials Cache, one per Tenant Hierarchical Keyring,
wrapping the cache shared by every Tenant. It is passed to the keyring as ``CacheTypeShared``.

While its ``BranchKeyNegativeCache`` has an honored Negative Cache Entry,
``get_cache_entry`` raises the error AWS KMS returned, the same object,
wrapped so that it survives the MPL's conversion to and from Dafny (negative_caching.md 4.3),
before consulting the shared cache; so a cached hit is not returned (Requirement 3).
The MPL treats any error other than ``EntryDoesNotExist`` as a failure,
and the Hierarchical Keyring fails without calling Amazon DynamoDB or AWS KMS (Requirement 1).
Otherwise, every operation is delegated to the shared cache.

The Cache Guard is a lightweight object: it holds a reference to the shared cache
and a reference to its Branch Key ID's negative cache.

This module imports the MPL; ``negative_cache.py`` does not.
"""
from aws_cryptographic_material_providers.mpl.errors import OpaqueWithTextError
from aws_cryptographic_material_providers.mpl.references import ICryptographicMaterialsCache

from .negative_cache import BranchKeyNegativeCache


class CacheGuard(ICryptographicMaterialsCache):
"""A Cryptographic Materials Cache that fails lookups for a Denied Branch Key ID.

:param delegate: The cache shared by every Tenant; an MPL cache
(from ``create_cryptographic_materials_cache``) or any ``ICryptographicMaterialsCache``
:type delegate: aws_cryptographic_material_providers.mpl.references.ICryptographicMaterialsCache
:param negative_cache: The negative cache of this keyring's Branch Key ID
:type negative_cache: BranchKeyNegativeCache
:raises TypeError: if ``delegate`` is not an ``ICryptographicMaterialsCache``,
or ``negative_cache`` is not a ``BranchKeyNegativeCache``
"""

def __init__(self, delegate, negative_cache): # pylint: disable=super-init-not-called
"""Validates and stores the shared cache and the negative cache."""
if not isinstance(delegate, ICryptographicMaterialsCache):
raise TypeError("delegate must be an ICryptographicMaterialsCache")
if not isinstance(negative_cache, BranchKeyNegativeCache):
raise TypeError("negative_cache must be a BranchKeyNegativeCache")
self._delegate = delegate
self._negative_cache = negative_cache

@property
def negative_cache(self):
"""The negative cache of this keyring's Branch Key ID.

:rtype: BranchKeyNegativeCache
"""
return self._negative_cache

def get_cache_entry(self, param):
"""Raises the Denial's error, wrapped, while it is honored; otherwise delegates.

:param param: The lookup
:type param: aws_cryptographic_material_providers.mpl.models.GetCacheEntryInput
:rtype: aws_cryptographic_material_providers.mpl.models.GetCacheEntryOutput
:raises aws_cryptographic_material_providers.mpl.errors.OpaqueWithTextError:
while Denied; ``obj`` is the Negative Cache Entry's error, the same object,
and ``obj_message`` is its text, which names the AWS KMS error code
:raises aws_cryptographic_material_providers.mpl.errors.EntryDoesNotExist: from the shared cache, on a miss
"""
# Checked before the shared cache: a Denied Branch Key ID's cached materials MUST NOT be used,
# and a miss in a storm tracking cache would block concurrent callers (negative_caching.md 4.3).
entry = self._negative_cache.get()
if entry is not None:
# OpaqueWithTextError, because it survives the MPL's conversion to and from Dafny intact:
# Dafny's opaque errors carry the Python object itself, so `obj` is the error AWS KMS returned,
# the same object; and `obj_message` keeps its text, which names the KMS error code.
# A ComAmazonawsKms error would lose both: Dafny keeps only its data, and for the KMS errors
# the MPL models (DisabledException, KMSInvalidStateException, NotFoundException,
# IncorrectKeyException) only their message, without the code (negative_caching.md 4.3).
# A new wrapper per raise also keeps the error's own traceback from growing on every raise.
raise OpaqueWithTextError(obj=entry.error, obj_message=str(entry.error))
return self._delegate.get_cache_entry(param)

def put_cache_entry(self, param):
"""Delegates to the shared cache.

:param param: The entry
:type param: aws_cryptographic_material_providers.mpl.models.PutCacheEntryInput
"""
return self._delegate.put_cache_entry(param)

def update_usage_metadata(self, param):
"""Delegates to the shared cache.

:param param: The usage
:type param: aws_cryptographic_material_providers.mpl.models.UpdateUsageMetadataInput
"""
return self._delegate.update_usage_metadata(param)

def delete_cache_entry(self, param):
"""Delegates to the shared cache.

:param param: The entry to delete
:type param: aws_cryptographic_material_providers.mpl.models.DeleteCacheEntryInput
"""
return self._delegate.delete_cache_entry(param)


__all__ = ("CacheGuard", "BranchKeyNegativeCache")
8 changes: 8 additions & 0 deletions examples/src/multi_tenant/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,14 @@ and additionally by their Partition IDs.

### 10.2 The Partition ID

UPDATE: This decision is revisited in
[negative_caching.md 8](./negative_caching.md#8-the-partition-id-revisited).
No part of refreshed credentials is constant,
so a Partition ID derived from them changes on every refresh,
and every Tenant misses the shared cache once per refresh.
The Partition ID SHOULD instead be derived from a stable Tenant identity;
the example will be changed in a later change.

The Partition ID is the SHA-384 digest of the Tenant's secret access key.

**It is distinct per Tenant**:
Expand Down
Loading
Loading