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
37 changes: 37 additions & 0 deletions samcli/cli/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -662,3 +662,40 @@ def restore_spaces(self):
text_list[pos] = " "

return "".join(text_list)


class TenantIdType(click.ParamType):
"""
Custom Click parameter type for tenant-id validation.
Validates tenant-id format according to AWS Lambda multi-tenancy requirements.
"""

name = "string"

# Tenant-id validation pattern
TENANT_ID_PATTERN = re.compile(r"^[a-zA-Z0-9\._:\/=+\-@ ]+$")
MIN_LENGTH = 1
MAX_LENGTH = 256

def convert(self, value, param, ctx):
if value is None:
return None

# Check length constraints
if len(value) < self.MIN_LENGTH or len(value) > self.MAX_LENGTH:
raise click.BadParameter(
f"{param.opts[0]} must be between {self.MIN_LENGTH} and {self.MAX_LENGTH} characters"
)

# Check for empty or whitespace-only strings
if not value.strip():
raise click.BadParameter(f"{param.opts[0]} cannot be empty or contain only whitespace")

# Check format pattern
if not self.TENANT_ID_PATTERN.match(value):
raise click.BadParameter(
f"{param.opts[0]} contains invalid characters. "
"Allowed characters are a-z, A-Z, numbers, spaces, and the characters _ . : / = + - @"
)

return value
13 changes: 13 additions & 0 deletions samcli/commands/local/invoke/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from samcli.cli.cli_config_file import ConfigProvider, configuration_option, save_params_option
from samcli.cli.main import aws_creds_options, pass_context, print_cmdline_args
from samcli.cli.main import common_options as cli_framework_options
from samcli.cli.types import TenantIdType
from samcli.commands._utils.option_value_processor import process_image_options
from samcli.commands._utils.options import (
hook_name_click_option,
Expand Down Expand Up @@ -74,6 +75,14 @@
help="Lambda runtime used to invoke the function."
+ click.style(f"\n\nRuntimes: {', '.join(get_sorted_runtimes(INIT_RUNTIMES))}", bold=True),
)
@click.option(
"--tenant-id",
type=TenantIdType(),
help="Tenant ID for multi-tenant Lambda functions. "
"Used to ensure compute isolation between different tenants. "
"Must be 1-256 characters, the allowed characters are a-z and A-Z, "
"numbers, spaces, and the characters _ . : / = + - @",
)
@mount_symlinks_option
@invoke_common_options
@local_common_options
Expand Down Expand Up @@ -117,6 +126,7 @@ def cli(
runtime,
mount_symlinks,
no_memory_limit,
tenant_id,
):
"""
`sam local invoke` command entry point
Expand Down Expand Up @@ -150,6 +160,7 @@ def cli(
runtime,
mount_symlinks,
no_memory_limit,
tenant_id,
) # pragma: no cover


Expand Down Expand Up @@ -180,6 +191,7 @@ def do_cli( # pylint: disable=R0914
runtime,
mount_symlinks,
no_mem_limit,
tenant_id,
):
"""
Implementation of the ``cli`` method, just separated out for unit testing purposes
Expand Down Expand Up @@ -236,6 +248,7 @@ def do_cli( # pylint: disable=R0914
context.local_lambda_runner.invoke(
context.function_identifier,
event=event_data,
tenant_id=tenant_id,
stdout=context.stdout,
stderr=context.stderr,
override_runtime=runtime,
Expand Down
1 change: 1 addition & 0 deletions samcli/commands/local/invoke/core/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"add_host",
"invoke_image",
"runtime",
"tenant_id",
"mount_symlinks",
"no_memory_limit",
]
Expand Down
7 changes: 7 additions & 0 deletions samcli/commands/local/lib/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,10 @@ class InvalidHandlerPathError(UserException):
"""
Raises when the handler is in an unexpected format and can't be parsed
"""


class TenantIdValidationError(UserException):
"""
Raised when there's a tenant-id validation error (missing tenant-id for multi-tenant functions
or providing tenant-id for non-multi-tenant functions)
"""
17 changes: 17 additions & 0 deletions samcli/commands/local/lib/local_lambda.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
InvalidIntermediateImageError,
NoPrivilegeException,
OverridesNotWellDefinedError,
TenantIdValidationError,
UnsupportedInlineCodeError,
)
from samcli.lib.providers.provider import Function
Expand Down Expand Up @@ -99,6 +100,7 @@ def invoke(
self,
function_identifier: str,
event: str,
tenant_id: Optional[str] = None,
stdout: Optional[StreamWriter] = None,
stderr: Optional[StreamWriter] = None,
override_runtime: Optional[str] = None,
Expand Down Expand Up @@ -158,6 +160,20 @@ def invoke(
LOG.info("Invoking Container created from %s", function.imageuri)

validate_architecture_runtime(function)

# Validate tenant-id for multi-tenant functions
if function.tenancy_config and isinstance(function.tenancy_config, dict):
if not tenant_id:
raise TenantIdValidationError(
"The invoked function is enabled with tenancy configuration. "
"Add a valid tenant ID in your request and try again."
)
elif tenant_id:
raise TenantIdValidationError(
"The invoked function is not enabled with tenancy configuration. "
"Remove the tenant ID from your request and try again."
)

config = self.get_invoke_config(function, override_runtime)

if (
Expand All @@ -173,6 +189,7 @@ def invoke(
self.local_runtime.invoke(
config,
event,
tenant_id,
debug_context=self.debug_context,
stdout=stdout,
stderr=stderr,
Expand Down
4 changes: 4 additions & 0 deletions samcli/commands/local/start_api/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@
a interpreted language, local changes will be available immediately in Docker container on every invoke. For more
compiled languages or projects requiring complex packing support, it is recommended to run custom building solution
and point AWS SAM CLI to the directory or file containing build artifacts.

For testing multi-tenant functions, pass tenant-id via X-Amz-Tenant-Id header. By default, each request uses a new
container providing tenant isolation. When using --warm-containers, containers are reused and do not
provide tenant isolation like production Lambda.
"""


Expand Down
6 changes: 5 additions & 1 deletion samcli/commands/local/start_lambda/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@
Programmatically invoke your Lambda function locally using the AWS CLI or SDKs.
Start a local endpoint that emulates the AWS Lambda service, and one can run their automated
tests against this local Lambda endpoint. Invokes to this endpoint can be sent using the AWS CLI or
SDK and they will in turn locally execute the Lambda function specified in the request.\n
SDK and they will in turn locally execute the Lambda function specified in the request.

For testing multi-tenant functions, pass tenant-id via the TenantId parameter in Lambda API calls. By default, each
invocation uses a new container providing tenant isolation. When using --warm-containers, containers are reused
and do not provide tenant isolation like production Lambda.
"""


Expand Down
15 changes: 13 additions & 2 deletions samcli/commands/remote/invoke/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from samcli.cli.cli_config_file import ConfigProvider, configuration_option, save_params_option
from samcli.cli.context import Context
from samcli.cli.main import aws_creds_options, common_options, pass_context, print_cmdline_args
from samcli.cli.types import RemoteInvokeOutputFormatType
from samcli.cli.types import RemoteInvokeOutputFormatType, TenantIdType
from samcli.commands._utils.command_exception_handler import command_exception_handler
from samcli.commands._utils.options import remote_invoke_parameter_option
from samcli.commands.remote.invoke.core.command import RemoteInvokeCommand
Expand Down Expand Up @@ -66,6 +66,14 @@
type=click.File("r", encoding="utf-8"),
help="The file that contains the event that will be sent to the resource.",
)
@click.option(
"--tenant-id",
type=TenantIdType(),
help="Tenant ID for multi-tenant Lambda functions. "
"Used to ensure compute isolation between different tenants. "
"Must be 1-256 characters, the allowed characters are a-z and A-Z, "
"numbers, spaces, and the characters _ . : / = + - @",
)
@click.option(
"--test-event-name",
help="Name of the remote test event to send to the resource",
Expand Down Expand Up @@ -94,6 +102,7 @@ def cli(
resource_id: str,
event: str,
event_file: TextIOWrapper,
tenant_id: str,
output: RemoteInvokeOutputFormat,
test_event_name: str,
parameter: dict,
Expand All @@ -110,6 +119,7 @@ def cli(
resource_id,
event,
event_file,
tenant_id,
output,
parameter,
test_event_name,
Expand All @@ -125,6 +135,7 @@ def do_cli(
resource_id: str,
event: str,
event_file: TextIOWrapper,
tenant_id: str,
output: RemoteInvokeOutputFormat,
parameter: dict,
test_event_name: str,
Expand Down Expand Up @@ -187,7 +198,7 @@ def do_cli(
EventTracker.track_event("RemoteInvokeEventType", event_type)

remote_invoke_input = RemoteInvokeExecutionInfo(
payload=event, payload_file=event_file, parameters=parameter, output_format=output
payload=event, payload_file=event_file, tenant_id=tenant_id, parameters=parameter, output_format=output
)

remote_invoke_context.run(remote_invoke_input=remote_invoke_input)
Expand Down
2 changes: 1 addition & 1 deletion samcli/commands/remote/invoke/core/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

INPUT_EVENT_OPTIONS: List[str] = ["event", "event_file", "test_event_name"]

ADDITIONAL_OPTIONS: List[str] = ["parameter", "output"]
ADDITIONAL_OPTIONS: List[str] = ["parameter", "output", "tenant_id"]

AWS_CREDENTIAL_OPTION_NAMES: List[str] = ["region", "profile"]

Expand Down
2 changes: 2 additions & 0 deletions samcli/lib/providers/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ class Function(NamedTuple):
runtime_management_config: Optional[Dict] = None
# LoggingConfig for Advanced logging
logging_config: Optional[Dict] = None
# Function Tenancy Configuration for multi-tenant functions
tenancy_config: Optional[Dict] = None

@property
def full_path(self) -> str:
Expand Down
1 change: 1 addition & 0 deletions samcli/lib/providers/sam_function_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,7 @@ def _build_function_configuration(
runtime_management_config=resource_properties.get("RuntimeManagementConfig"),
function_build_info=function_build_info,
logging_config=resource_properties.get("LoggingConfig"),
tenancy_config=resource_properties.get("TenancyConfig"),
)

@staticmethod
Expand Down
10 changes: 10 additions & 0 deletions samcli/lib/remote_invoke/lambda_invoke_executors.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
LOG = logging.getLogger(__name__)
FUNCTION_NAME = "FunctionName"
PAYLOAD = "Payload"
TENANT_ID = "TenantId"
EVENT_STREAM = "EventStream"
PAYLOAD_CHUNK = "PayloadChunk"
INVOKE_COMPLETE = "InvokeComplete"
Expand All @@ -58,6 +59,15 @@ def __init__(self, lambda_client: LambdaClient, function_name: str, remote_outpu
self._remote_output_format = remote_output_format
self.request_parameters = {"InvocationType": "RequestResponse", "LogType": "Tail"}

def execute(self, remote_invoke_input: RemoteInvokeExecutionInfo) -> RemoteInvokeIterableResponseType:
"""
Override execute to extract tenant_id from RemoteInvokeExecutionInfo for Lambda-specific handling.
"""
if remote_invoke_input.tenant_id:
self.request_parameters[TENANT_ID] = remote_invoke_input.tenant_id

return super().execute(remote_invoke_input)

def validate_action_parameters(self, parameters: dict) -> None:
"""
Validates the input boto parameters and prepares the parameters for calling the API.
Expand Down
4 changes: 4 additions & 0 deletions samcli/lib/remote_invoke/remote_invoke_executors.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ class RemoteInvokeExecutionInfo:

payload: payload string given by the customer
payload_file: if file is given, this points to its location
tenant_id: tenant ID for multi-tenant Lambda functions

response: response object returned from boto3 action
exception: if an exception is thrown, it will be stored here
Expand All @@ -91,6 +92,7 @@ class RemoteInvokeExecutionInfo:
# Request related properties
payload: Optional[Union[str, List, dict]]
payload_file: Optional[TextIOWrapper]
tenant_id: Optional[str]
parameters: dict
output_format: RemoteInvokeOutputFormat

Expand All @@ -103,11 +105,13 @@ def __init__(
self,
payload: Optional[Union[str, List, dict]],
payload_file: Optional[TextIOWrapper],
tenant_id: Optional[str],
parameters: dict,
output_format: RemoteInvokeOutputFormat,
):
self.payload = payload
self.payload_file = payload_file
self.tenant_id = tenant_id
self.parameters = parameters
self.output_format = output_format
self.response = None
Expand Down
15 changes: 12 additions & 3 deletions samcli/local/apigw/local_apigw_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -597,7 +597,9 @@ def _valid_identity_sources(self, request: Request, route: Route) -> bool:

return True

def _invoke_lambda_function(self, lambda_function_name: str, event: dict) -> Union[str, bytes]:
def _invoke_lambda_function(
self, lambda_function_name: str, event: dict, tenant_id: Optional[str] = None
) -> Union[str, bytes]:
"""
Helper method to invoke a function and setup stdout+stderr

Expand All @@ -607,6 +609,8 @@ def _invoke_lambda_function(self, lambda_function_name: str, event: dict) -> Uni
The name of the Lambda function to invoke
event: dict
The event object to pass into the Lambda function
tenant_id: Optional[str]
Tenant ID for multi-tenant Lambda functions

Returns
-------
Expand All @@ -617,7 +621,9 @@ def _invoke_lambda_function(self, lambda_function_name: str, event: dict) -> Uni
event_str = json.dumps(event, sort_keys=True)
stdout_writer = StreamWriter(stdout, auto_flush=True)

self.lambda_runner.invoke(lambda_function_name, event_str, stdout=stdout_writer, stderr=self.stderr)
self.lambda_runner.invoke(
lambda_function_name, event_str, stdout=stdout_writer, stderr=self.stderr, tenant_id=tenant_id
)
lambda_response, is_lambda_user_error_response = LambdaOutputParser.get_lambda_output(stdout)
if is_lambda_user_error_response:
raise LambdaResponseParseException
Expand Down Expand Up @@ -728,8 +734,11 @@ def _request_handler(self, **kwargs):

endpoint_service_error = None
try:
# Extract tenant-id from HTTP request header
tenant_id = request.headers.get("X-Amz-Tenant-Id")

# invoke the route's Lambda function
lambda_response = self._invoke_lambda_function(route.function_name, route_lambda_event)
lambda_response = self._invoke_lambda_function(route.function_name, route_lambda_event, tenant_id)
except FunctionNotFound:
endpoint_service_error = ServiceErrorResponses.lambda_not_found_response()
except UnsupportedInlineCodeError:
Expand Down
Loading
Loading