Skip to content
Merged
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ All requirements for running the API are packaged and uploaded to AWS as a lambd

For convenience:
* `hatch run lambda:prepare` will run steps 2-5 in a single command.
* `hatch run lambda:deploy-no-dnssec` or `hatch run lambda:deploy-dnssec` will clean the environment and then run steps 2-6 with DNSSEC validation enabled or disabled respectively.
* `hatch run lambda:deploy-dnssec` or `hatch run lambda:deploy-no-dnssec` will clean the environment and then run steps 2-6 with DNSSEC validation enabled or disabled respectively.

Note: the above commands do not run `tofu init`. During first time environment setup this will need to be run in the `open-tofu` dir for these commands to work.

Expand All @@ -45,7 +45,7 @@ The above sample must be run from the root directory of a deployed Open MPIC aws

The API is compliant with the [Open MPIC Specification](https://github.com/open-mpic/open-mpic-specification).

There is [documentation based on the API specification used in this version](https://open-mpic.org/documentation.html?commit=65f7409f102995747b966e4cb0c86bfd7f621211).
There is [documentation based on the API specification used in this version](https://open-mpic.org/documentation.html?commit=150a21d8c8e1c4758494f75d4e6811a1c6d05058).

## Development
Code changes can easily be deployed by editing the .py files and then rezipping the project via `./zip-all.sh` and `./2-package.sh` in the `layer` directory. Then, running `tofu apply` run from the open-tofu directory will update only on the required resources and leave the others unchanged. If any `.tf.template` files are changed or `config.yaml` is edited, `hatch run ./configure.py` must be rerun followed by `tofu apply` in the open-tofu directory.
Expand Down
3 changes: 3 additions & 0 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,6 @@ caa-domains:
absolute-max-attempts: 3

log-level: INFO
http-client-timeout-seconds: 20
dns-timeout-seconds: 5
dns-resolution-lifetime-seconds: 11
36 changes: 26 additions & 10 deletions configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,7 @@ def main(raw_args=None):
# Set the source path for the lambda functions.
main_tf_string = main_tf_string.replace("{{source-path}}", f"{config['source-path']}")

# Set log level if present.
if "log-level" in config:
main_tf_string = main_tf_string.replace("{{log-level-with-key}}", f"log_level = \"{config['log-level']}\"")
else:
main_tf_string = main_tf_string.replace("{{log-level-with-key}}", "")
main_tf_string = set_common_env_configuration(main_tf_string, config)

# Derive the out file from the input file name.
if not args.main_tf_template.endswith(".tf.template"):
Expand Down Expand Up @@ -152,11 +148,7 @@ def main(raw_args=None):
# Set the source path for the lambda functions.
aws_perspective_tf_region = aws_perspective_tf_region.replace("{{source-path}}", f"{config['source-path']}")

# Set log level if present.
if "log-level" in config:
aws_perspective_tf_region = aws_perspective_tf_region.replace("{{log-level-with-key}}", f"log_level = \"{config['log-level']}\"")
else:
aws_perspective_tf_region = aws_perspective_tf_region.replace("{{log-level-with-key}}", "")
aws_perspective_tf_region = set_common_env_configuration(aws_perspective_tf_region, config)

if not args.aws_perspective_tf_template.endswith(".tf.template"):
print(f"Error: invalid tf template name: {args.aws_perspective_tf_template}. Make sure all tf template files end in '.tf.template'.")
Expand All @@ -167,6 +159,30 @@ def main(raw_args=None):
out_stream.write(aws_perspective_tf_region)


def set_common_env_configuration(tf_string: str, config: dict) -> str:
# set log level if present
if "log-level" in config:
tf_string = tf_string.replace("{{log-level-with-key}}", f"log_level = \"{config['log-level']}\"")
else:
tf_string = tf_string.replace("{{log-level-with-key}}", "")

# set timeouts if present
if "http-client-timeout-seconds" in config:
tf_string = tf_string.replace("{{http-client-timeout-with-key}}", f"http_client_timeout_seconds = {config['http-client-timeout-seconds']}")
else:
tf_string = tf_string.replace("{{http-client-timeout-with-key}}", "")
if "dns-timeout-seconds" in config:
tf_string = tf_string.replace("{{dns-timeout-with-key}}", f"dns_timeout_seconds = {config['dns-timeout-seconds']}")
else:
tf_string = tf_string.replace("{{dns-timeout-with-key}}", "")
if "dns-resolution-lifetime-seconds" in config:
tf_string = tf_string.replace("{{dns-resolution-lifetime-with-key}}", f"dns_resolution_lifetime_seconds = {config['dns-resolution-lifetime-seconds']}")
else:
tf_string = tf_string.replace("{{dns-resolution-lifetime-with-key}}", "")

return tf_string


# Main module init for direct invocation.
if __name__ == '__main__':
main()
5 changes: 5 additions & 0 deletions open-tofu/aws-perspective.tf.template
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,9 @@ resource "aws_lambda_function" "mpic_dcv_checker_lambda_{{region}}" {
environment {
variables = {
{{log-level-with-key}}
{{http-client-timeout-with-key}}
{{dns-timeout-with-key}}
{{dns-resolution-lifetime-with-key}}
}
}
}
Expand Down Expand Up @@ -254,6 +257,8 @@ resource "aws_lambda_function" "mpic_caa_checker_lambda_{{region}}" {
variables = {
default_caa_domains = {{default-caa-domains}}
{{log-level-with-key}}
{{dns-timeout-with-key}}
{{dns-resolution-lifetime-with-key}}
}
}
}
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ dependencies = [
"requests==2.32.4",
"dnspython==2.7.0",
"pydantic==2.11.7",
"aiohttp==3.12.13",
"aiohttp==3.12.14",
"aws-lambda-powertools[parser]==3.15.1",
"open-mpic-core==5.10.0",
"open-mpic-core==6.1.0",
"aioboto3~=14.3.0",
"black==25.1.0",
]
Expand All @@ -56,7 +56,7 @@ Source = "https://github.com/open-mpic/aws-lambda-python"
#virtual = ".hatch"

[tool.api]
spec_version = "3.5.0"
spec_version = "3.6.0"
spec_repository = "https://github.com/open-mpic/open-mpic-specification"

[tool.hatch]
Expand Down
2 changes: 1 addition & 1 deletion src/aws_lambda_mpic/__about__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "1.3.0"
__version__ = "1.5.0"
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,24 @@ class MpicCaaCheckerLambdaHandler:
def __init__(self):
self.default_caa_domain_list = os.environ["default_caa_domains"].split("|")
self.log_level = os.environ["log_level"] if "log_level" in os.environ else None
self.dns_timeout_seconds = (
float(os.environ["dns_timeout_seconds"]) if "dns_timeout_seconds" in os.environ else None
)
self.dns_resolution_lifetime_seconds = (
float(os.environ["dns_resolution_lifetime_seconds"])
if "dns_resolution_lifetime_seconds" in os.environ
else None
)

self.logger = logger.getChild(self.__class__.__name__)
if self.log_level:
self.logger.setLevel(self.log_level)

self.caa_checker = MpicCaaChecker(
default_caa_domain_list=self.default_caa_domain_list, log_level=self.logger.level
default_caa_domain_list=self.default_caa_domain_list,
log_level=self.logger.level,
dns_timeout=self.dns_timeout_seconds,
dns_resolution_lifetime=self.dns_resolution_lifetime_seconds,
)

def process_invocation(self, caa_request: CaaCheckRequest):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
import asyncio
import aioboto3

from asyncio import Queue
from collections import defaultdict
from importlib import resources
from pydantic import TypeAdapter, ValidationError, BaseModel
from aws_lambda_powertools.utilities.parser import event_parser, envelopes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,30 @@
class MpicDcvCheckerLambdaHandler:
def __init__(self):
self.log_level = os.environ["log_level"] if "log_level" in os.environ else None
self.http_client_timeout_seconds = (
float(os.environ["http_client_timeout_seconds"])
if "http_client_timeout_seconds" in os.environ and float(os.environ["http_client_timeout_seconds"])
else 30
)
self.dns_timeout_seconds = (
float(os.environ["dns_timeout_seconds"]) if "dns_timeout_seconds" in os.environ else None
)
self.dns_resolution_lifetime_seconds = (
float(os.environ["dns_resolution_lifetime_seconds"])
if "dns_resolution_lifetime_seconds" in os.environ
else None
)

self.logger = logger.getChild(self.__class__.__name__)
if self.log_level:
self.logger.setLevel(self.log_level)

self.dcv_checker = MpicDcvChecker(reuse_http_client=False, log_level=self.logger.level)
self.dcv_checker = MpicDcvChecker(
log_level=self.logger.level,
http_client_timeout=self.http_client_timeout_seconds,
dns_timeout=self.dns_timeout_seconds,
dns_resolution_lifetime=self.dns_resolution_lifetime_seconds,
)

def process_invocation(self, dcv_request: DcvCheckRequest):
self.logger.debug("(debug log) Processing DCV check request: %s", dcv_request)
Expand Down
72 changes: 60 additions & 12 deletions tests/integration/test_deployed_mpic_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
DcvAcmeDns01ValidationParameters,
DcvDnsChangeValidationParameters,
)
from open_mpic_core import DcvCheckParameters
from open_mpic_core import CertificateType, CheckType, DnsRecordType
from open_mpic_core import MpicCaaRequest, MpicDcvRequest, MpicResponse, PerspectiveResponse
from open_mpic_core import MpicRequestOrchestrationParameters
Expand Down Expand Up @@ -50,12 +49,16 @@ def api_should_return_200_and_passed_corroboration_given_successful_caa_check(se

print("\nRequest:\n", json.dumps(request.model_dump(), indent=4)) # pretty print request body
response = api_client.post(MPIC_REQUEST_PATH, json.dumps(request.model_dump()))
# response_body_as_json = response.json()
assert response.status_code == 200
# assert response body has a list of perspectives with length 2, and each element has response code 200
mpic_response = self.mpic_response_adapter.validate_json(response.text)
print("\nResponse:\n", json.dumps(mpic_response.model_dump(), indent=4)) # pretty print response body

if response.status_code != 200:
print("\nResponse:\n", response.text)

assert response.status_code == 200
assert mpic_response.is_valid is True
print("\nResponse:\n", json.dumps(mpic_response.model_dump(), indent=4)) # pretty print response body

# assert response body has a list of perspectives with length 2, and each element has response code 200
perspectives_list: list[PerspectiveResponse] = mpic_response.perspectives
assert len(perspectives_list) == request.orchestration_parameters.perspective_count
assert (
Expand Down Expand Up @@ -106,6 +109,24 @@ def api_should_return_is_valid_false_for_all_tests_in_do_not_issue_caa_test_suit
mpic_response = self.mpic_response_adapter.validate_json(response.text)
assert mpic_response.is_valid is False

def api_should_return_is_valid_true_for_caa_lookup_failure_if_allow_lookup_failure_flag_is_true(
self, api_client
):
request = MpicCaaRequest(
domain_or_ip_target="servfail.caatestsuite-dnssec.com",
orchestration_parameters=MpicRequestOrchestrationParameters(perspective_count=3, quorum_count=2),
caa_check_parameters=CaaCheckParameters(
certificate_type=CertificateType.TLS_SERVER,
caa_domains=["example.com"],
allow_lookup_failure=True,
),
)
response = api_client.post(MPIC_REQUEST_PATH, json.dumps(request.model_dump()))
mpic_response = self.mpic_response_adapter.validate_json(response.text)
if mpic_response.is_valid is False:
print("\nResponse:\n", response.text)
assert mpic_response.is_valid is True

# NOTE: Open MPIC AWS-Lambda-Python currently is not able to communicate with an IPv6 only nameserver.
# This case is handled in a compliant manner as it is treated as a lookup failure.
# The test for proper communication with an IPv6 nameserver can be enabled with the following additional parameter to the list below.
Expand Down Expand Up @@ -142,6 +163,8 @@ def api_should_return_is_valid_true_for_valid_tests_in_caa_test_suite_when_caa_d
)
response = api_client.post(MPIC_REQUEST_PATH, json.dumps(request.model_dump()))
mpic_response = self.mpic_response_adapter.validate_json(response.text)
if mpic_response.is_valid is False:
print("\nResponse:\n", response.text)
assert mpic_response.is_valid is True

@pytest.mark.skip(reason="Behavior not required in RFC 8659")
Expand Down Expand Up @@ -185,9 +208,12 @@ def api_should_return_200_given_valid_dns_01_validation(self, api_client, domain

print("\nRequest:\n", json.dumps(request.model_dump(), indent=4)) # pretty print request body
response = api_client.post(MPIC_REQUEST_PATH, json.dumps(request.model_dump()))

if response.status_code != 200:
print("\nResponse:\n", response.text)

assert response.status_code == 200
mpic_response = self.mpic_response_adapter.validate_json(response.text)

assert mpic_response.is_valid is True

# fmt: off
Expand All @@ -211,9 +237,12 @@ def api_should_return_200_is_valid_false_given_invalid_dns_01_validation(

print("\nRequest:\n", json.dumps(request.model_dump(), indent=4)) # pretty print request body
response = api_client.post(MPIC_REQUEST_PATH, json.dumps(request.model_dump()))

if response.status_code != 200:
print("\nResponse:\n", response.text)

assert response.status_code == 200
mpic_response = self.mpic_response_adapter.validate_json(response.text)

assert mpic_response.is_valid is False

# fmt: off
Expand All @@ -233,9 +262,12 @@ def api_should_return_200_given_valid_http_01_validation(
)
print("\nRequest:\n", json.dumps(request.model_dump(), indent=4)) # pretty print request body
response = api_client.post(MPIC_REQUEST_PATH, json.dumps(request.model_dump()))

if response.status_code != 200:
print("\nResponse:\n", response.text)

assert response.status_code == 200
mpic_response = self.mpic_response_adapter.validate_json(response.text)

assert mpic_response.is_valid is True

# fmt: off
Expand All @@ -258,9 +290,12 @@ def api_should_return_200_given_invalid_dns_01_validation(

print("\nRequest:\n", json.dumps(request.model_dump(), indent=4)) # pretty print request body
response = api_client.post(MPIC_REQUEST_PATH, json.dumps(request.model_dump()))

if response.status_code != 200:
print("\nResponse:\n", response.text)

assert response.status_code == 200
mpic_response = self.mpic_response_adapter.validate_json(response.text)

assert mpic_response.is_valid is False

# fmt: off
Expand All @@ -282,9 +317,12 @@ def api_should_return_200_given_valid_website_change_validation(
)
print("\nRequest:\n", json.dumps(request.model_dump(), indent=4)) # pretty print request body
response = api_client.post(MPIC_REQUEST_PATH, json.dumps(request.model_dump()))

if response.status_code != 200:
print("\nResponse:\n", response.text)

assert response.status_code == 200
mpic_response = self.mpic_response_adapter.validate_json(response.text)

assert mpic_response.is_valid is True

# fmt: off
Expand All @@ -309,6 +347,10 @@ def api_should_return_200_is_valid_true_given_valid_dns_change_validation(
print("\nRequest:\n", json.dumps(request.model_dump(), indent=4)) # pretty print request body
response = api_client.post(MPIC_REQUEST_PATH, json.dumps(request.model_dump()))
print("\nResponse:\n", json.dumps(json.loads(response.text), indent=4)) # pretty print request body

if response.status_code != 200:
print("\nResponse:\n", response.text)

assert response.status_code == 200
mpic_response = self.mpic_response_adapter.validate_json(response.text)
assert mpic_response.is_valid is True
Expand All @@ -321,6 +363,10 @@ def api_should_return_200_and_failed_corroboration_given_failed_dcv_check(self,

print("\nRequest:\n", json.dumps(request.model_dump(), indent=4)) # pretty print request body
response = api_client.post(MPIC_REQUEST_PATH, json.dumps(request.model_dump()))

if response.status_code != 200:
print("\nResponse:\n", response.text)

assert response.status_code == 200
response_body = json.loads(response.text)
print("\nResponse:\n", json.dumps(response_body, indent=4)) # pretty print response body
Expand All @@ -338,9 +384,10 @@ def api_should_return_400_given_invalid_orchestration_parameters_in_request(self

print("\nRequest:\n", json.dumps(request.model_dump(), indent=4)) # pretty print request body
response = api_client.post(MPIC_REQUEST_PATH, json.dumps(request.model_dump()))
assert response.status_code == 400
response_body = json.loads(response.text)
print("\nResponse:\n", json.dumps(response_body, indent=4)) # pretty print response body

assert response.status_code == 400
assert response_body["error"] == MpicRequestValidationMessages.REQUEST_VALIDATION_FAILED.key
assert any(
issue["issue_type"] == MpicRequestValidationMessages.INVALID_QUORUM_COUNT.key
Expand All @@ -359,7 +406,8 @@ def api_should_return_400_given_invalid_check_type_in_request(self, api_client):

print("\nRequest:\n", json.dumps(request.model_dump(), indent=4)) # pretty print request body
response = api_client.post(MPIC_REQUEST_PATH, json.dumps(request.model_dump()))
assert response.status_code == 400
response_body = json.loads(response.text)
print("\nResponse:\n", json.dumps(response_body, indent=4))

assert response.status_code == 400
assert response_body["error"] == MpicRequestValidationMessages.REQUEST_VALIDATION_FAILED.key
Loading