Skip to content

Commit

Permalink
Code Sign Integration (aws#217)
Browse files Browse the repository at this point in the history
* Release v1.0.0 (aws#2111)

* feat: Use aws-sam-cli docker images (aws#2066)

* Add Source for Docker Build Images (aws#2078)

* chore: Bump AWS SAM CLI Version (aws#2079)

* Version bump (aws#2080)

* chore: Bump AWS SAM CLI Version

* Change SAM CLI Version Number

There is a conflict betweeen PyPi documentation which asks for the
previous style
https://packaging.python.org/guides/distributing-packages-using-setuptools/#pre-release-versioning
and PEP 440 which proposes the style included in this change
https://www.python.org/dev/peps/pep-0440/#pre-releases - our MSI build
scripts failed on the pattern we were using before, this changes the
pattern.

* refactor: Build init.go with -s and -w flags to removed debug info (aws#2083)

* refactor: Bake Rapid into image on the fly (aws#2100)

* refactor: Bake Rapid into image on the fly

* force chmod on init binary in container for windows

* bake go debug bootstrap

Co-authored-by: Jacob Fuss <[email protected]>

* chore: Bump version to RC2 (aws#2104)

* Remove liblzma and libxslt from AL2 build images (aws#2109)

Discovered a regression where on Ruby 2.7, the `nokogiri` dependency
would build without errors, but would not run on local testing or on AWS
Lambda itself.

On further investigation, it appears that `nokogiri` can compile with or
without `liblzma` present, but if it is present in the build
enviornment (pre-change) and it is not present on the invoke
environment (true in AL2 runtimes), you will experience runtime failures
attempting to require `nokogiri`.

I have been able to verify that with these changes, `nokogiri` builds
correctly for Ruby 2.7 and runs both locally and on AWS Lambda.

* Build output dots (aws#2112)

* Use Low-Level Docker Client

Allows us to stream dots as a progress heartbeat. Pending unit tests and
black formatting.

* Get make pr Passing

Co-authored-by: Jacob Fuss <[email protected]>
Co-authored-by: Jacob Fuss <[email protected]>

* chore: Bump aws-lambda-builders and SAM CLI to 1.0.0 (aws#2116)

* fix: Update Python3.8 debug entrypoint (aws#2119)

* chore: readme update with screenshot (aws#2117)

* chore: readme update with screenshot

* chore: remove beta in the title

Co-authored-by: Alex Wood <[email protected]>

* feature: Lambda Code Sign integration for SAM CLI

* feature: Lambda Code Sign integration for SAM CLI (actual signing impl and unit tests)

* Add details to print_deploy_args
Add documentation for missing classes and methods

* Update couple of prompts

* Wording changes requested by UX & Docs Team

Co-authored-by: Alex Wood <[email protected]>
Co-authored-by: Jacob Fuss <[email protected]>
Co-authored-by: Jacob Fuss <[email protected]>
Co-authored-by: Sriram Madapusi Vasudevan <[email protected]>
  • Loading branch information
5 people committed Nov 23, 2020
1 parent ff150ed commit bbccd37
Show file tree
Hide file tree
Showing 28 changed files with 1,123 additions and 67 deletions.
80 changes: 79 additions & 1 deletion samcli/cli/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@


def _generate_match_regex(match_pattern, delim):

"""
Creates a regex string based on a match pattern (also a regex) that is to be
run on a string (which may contain escaped quotes) that is separated by delimiters.
Expand Down Expand Up @@ -272,3 +271,82 @@ def _space_separated_key_value_parser(tag_value):
return False, None
tags_dict = {**tags_dict, **parsed_tag}
return True, tags_dict


class SigningProfilesOptionType(click.ParamType):
"""
Custom parameter type to parse Signing Profile options
Example options could be;
- MyFunctionOrLayerToBeSigned=MySigningProfile
- MyFunctionOrLayerToBeSigned=MySigningProfile:MyProfileOwner
See convert function docs for details
"""

# Note: this is required, otherwise it is failing when running help
name = ""

def convert(self, value, param, ctx):
"""
Converts given Signing Profile options to a dictionary where Function or Layer name would be key,
and signing profile details would be the value.
Since this method is also been used when we are reading the config details from samconfig.toml,
If value is already a dictionary, don't need to do anything, just return it.
If value is an array, then each value will correspond to a function or layer config, and here it is parsed
and converted into a dictionary
"""
result = {}

# Empty tuple
if value == ("",):
return result

# if it is coming from config file, it should be a dictionary, and not need to parse it
if isinstance(value, dict):
return value

# otherwise type should be array, which is coming from command line
# each element inside array corresponds to Function or Layer code sign option
for val in value:
(function_name, signing_profiles) = self._split_function_and_code_sign_params(val)

if not function_name:
return self.fail(
f"{val} is not a valid code sign config, it should look like this 'MyFunction=SigningProfile'",
param,
ctx,
)

(signer_profile_name, signer_profile_owner) = self._split_signer_profile_name_owner(signing_profiles)

if not signer_profile_name:
return self.fail(
f"Signer profile option has invalid format, it should look like this MyFunction=MySigningProfile "
f"or MyFunction=MySigningProfile:MySigningProfileOwner",
param,
ctx,
)

result[function_name] = {"profile_name": signer_profile_name, "profile_owner": signer_profile_owner}

return result

@staticmethod
def _split_function_and_code_sign_params(param_value):
equals_count = param_value.count("=")

if equals_count != 1:
return None, None

return param_value.split("=")

@staticmethod
def _split_signer_profile_name_owner(signing_profile):
equals_count = signing_profile.count(":")

if equals_count > 1:
return None, None
if equals_count == 1:
return signing_profile.split(":")
return signing_profile, ""
55 changes: 36 additions & 19 deletions samcli/commands/_utils/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from click.types import FuncParamType

from samcli.commands._utils.template import get_template_data, TemplateNotFoundException
from samcli.cli.types import CfnParameterOverridesType, CfnMetadataType, CfnTags
from samcli.cli.types import CfnParameterOverridesType, CfnMetadataType, CfnTags, SigningProfilesOptionType
from samcli.commands._utils.custom_options.option_nargs import OptionNargs

_TEMPLATE_OPTION_DEFAULT_VALUE = "template.[yaml|yml]"
Expand Down Expand Up @@ -87,7 +87,7 @@ def guided_deploy_stack_name(ctx, param, provided_value):
option_name=param.name,
ctx=ctx,
message="Missing option '--stack-name', 'sam deploy --guided' can "
"be used to provide and save needed parameters for future deploys.",
"be used to provide and save needed parameters for future deploys.",
)

return provided_value if provided_value else DEFAULT_STACK_NAME
Expand Down Expand Up @@ -153,8 +153,8 @@ def docker_click_options():
"--docker-network",
envvar="SAM_DOCKER_NETWORK",
help="Specifies the name or id of an existing docker network to lambda docker "
"containers should connect to, along with the default bridge network. If not specified, "
"the Lambda containers will only connect to the default bridge docker network.",
"containers should connect to, along with the default bridge network. If not specified, "
"the Lambda containers will only connect to the default bridge docker network.",
),
]

Expand All @@ -166,8 +166,8 @@ def parameter_override_click_option():
type=CfnParameterOverridesType(),
default={},
help="Optional. A string that contains AWS CloudFormation parameter overrides encoded as key=value pairs."
"For example, 'ParameterKey=KeyPairName,ParameterValue=MyKey ParameterKey=InstanceType,"
"ParameterValue=t1.micro' or KeyPairName=MyKey InstanceType=t1.micro",
"For example, 'ParameterKey=KeyPairName,ParameterValue=MyKey ParameterKey=InstanceType,"
"ParameterValue=t1.micro' or KeyPairName=MyKey InstanceType=t1.micro",
)


Expand All @@ -189,6 +189,23 @@ def no_progressbar_option(f):
return no_progressbar_click_option()(f)


def signing_profiles_click_option():
return click.option(
"--signing-profiles",
cls=OptionNargs,
type=SigningProfilesOptionType(),
default={},
help="Optional. A string that contains Code Sign configuration parameters as "
"FunctionOrLayerNameToSign=SigningProfileName:SigningProfileOwner "
"Since signing profile owner is optional, it could also be written as "
"FunctionOrLayerNameToSign=SigningProfileName",
)


def signing_profiles_option(f):
return signing_profiles_click_option()(f)


def metadata_click_option():
return click.option(
"--metadata",
Expand All @@ -208,15 +225,15 @@ def capabilities_click_option():
required=False,
type=FuncParamType(func=_space_separated_list_func_type),
help="A list of capabilities that you must specify"
"before AWS Cloudformation can create certain stacks. Some stack tem-"
"plates might include resources that can affect permissions in your AWS"
"account, for example, by creating new AWS Identity and Access Manage-"
"ment (IAM) users. For those stacks, you must explicitly acknowledge"
"their capabilities by specifying this parameter. The only valid values"
"are CAPABILITY_IAM and CAPABILITY_NAMED_IAM. If you have IAM resources,"
"you can specify either capability. If you have IAM resources with cus-"
"tom names, you must specify CAPABILITY_NAMED_IAM. If you don't specify"
"this parameter, this action returns an InsufficientCapabilities error.",
"before AWS Cloudformation can create certain stacks. Some stack tem-"
"plates might include resources that can affect permissions in your AWS"
"account, for example, by creating new AWS Identity and Access Manage-"
"ment (IAM) users. For those stacks, you must explicitly acknowledge"
"their capabilities by specifying this parameter. The only valid values"
"are CAPABILITY_IAM and CAPABILITY_NAMED_IAM. If you have IAM resources,"
"you can specify either capability. If you have IAM resources with cus-"
"tom names, you must specify CAPABILITY_NAMED_IAM. If you don't specify"
"this parameter, this action returns an InsufficientCapabilities error.",
)


Expand All @@ -231,8 +248,8 @@ def tags_click_option():
type=CfnTags(),
required=False,
help="A list of tags to associate with the stack that is created or updated."
"AWS CloudFormation also propagates these tags to resources "
"in the stack if the resource supports it.",
"AWS CloudFormation also propagates these tags to resources "
"in the stack if the resource supports it.",
)


Expand All @@ -247,8 +264,8 @@ def notification_arns_click_option():
type=FuncParamType(func=_space_separated_list_func_type),
required=False,
help="Amazon Simple Notification Service topic"
"Amazon Resource Names (ARNs) that AWS CloudFormation associates with"
"the stack.",
"Amazon Resource Names (ARNs) that AWS CloudFormation associates with"
"the stack.",
)


Expand Down
66 changes: 66 additions & 0 deletions samcli/commands/deploy/code_signer_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""
Utilities for code signing process
"""

import logging
from click import prompt, STRING

from samcli.lib.providers.sam_function_provider import SamFunctionProvider

LOG = logging.getLogger(__name__)


def prompt_profile_name(profile_name, start_bold, end_bold):
return prompt(f"\t{start_bold}Signing Profile Name{end_bold}", type=STRING, default=profile_name)


def prompt_profile_owner(profile_owner, start_bold, end_bold):
# click requires to have non None value for passing
if not profile_owner:
profile_owner = ""

profile_owner = prompt(
f"\t{start_bold}Signing Profile Owner Account ID (optional){end_bold}",
type=STRING,
default=profile_owner,
show_default=profile_owner != "",
)

return profile_owner


def extract_profile_name_and_owner_from_existing(function_or_layer_name, signing_profiles):
profile_name = None
profile_owner = None
# extract any code sign config that is passed via command line
if function_or_layer_name in signing_profiles:
profile_name = signing_profiles[function_or_layer_name]["profile_name"]
profile_owner = signing_profiles[function_or_layer_name]["profile_owner"]

return profile_name, profile_owner


def signer_config_per_function(parameter_overrides, template_dict):
functions_with_code_sign = set()
layers_with_code_sign = {}

sam_functions = SamFunctionProvider(template_dict=template_dict, parameter_overrides=parameter_overrides)

for sam_function in sam_functions.get_all():
if sam_function.codesign_config_arn:
function_name = sam_function.name
LOG.debug("Found the following function with a code signing config %s", function_name)
functions_with_code_sign.add(function_name)

if sam_function.layers:
for layer in sam_function.layers:
layer_name = layer.name
LOG.debug("Found following layers inside the function %s", layer_name)
if layer_name in layers_with_code_sign:
layers_with_code_sign[layer_name].add(function_name)
else:
functions_that_is_referring_to_function = set()
functions_that_is_referring_to_function.add(function_name)
layers_with_code_sign[layer_name] = functions_that_is_referring_to_function

return functions_with_code_sign, layers_with_code_sign
8 changes: 8 additions & 0 deletions samcli/commands/deploy/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
no_progressbar_option,
tags_override_option,
template_click_option,
signing_profiles_option,
)
from samcli.commands.deploy.utils import sanitize_parameter_overrides
from samcli.lib.telemetry.metrics import track_command
Expand Down Expand Up @@ -140,6 +141,7 @@
@notification_arns_override_option
@tags_override_option
@parameter_override_option
@signing_profiles_option
@no_progressbar_option
@capabilities_override_option
@aws_creds_options
Expand All @@ -166,6 +168,7 @@ def cli(
metadata,
guided,
confirm_changeset,
signing_profiles,
resolve_s3,
config_file,
config_env,
Expand Down Expand Up @@ -193,6 +196,7 @@ def cli(
confirm_changeset,
ctx.region,
ctx.profile,
signing_profiles,
resolve_s3,
config_file,
config_env,
Expand Down Expand Up @@ -220,6 +224,7 @@ def do_cli(
confirm_changeset,
region,
profile,
signing_profiles,
resolve_s3,
config_file,
config_env,
Expand All @@ -240,6 +245,7 @@ def do_cli(
profile=profile,
confirm_changeset=confirm_changeset,
capabilities=capabilities,
signing_profiles=signing_profiles,
parameter_overrides=parameter_overrides,
config_section=CONFIG_SECTION,
config_env=config_env,
Expand Down Expand Up @@ -269,6 +275,7 @@ def do_cli(
on_deploy=True,
region=guided_context.guided_region if guided else region,
profile=profile,
signing_profiles=guided_context.signing_profiles if guided else signing_profiles,
) as package_context:
package_context.run()

Expand All @@ -292,5 +299,6 @@ def do_cli(
region=guided_context.guided_region if guided else region,
profile=profile,
confirm_changeset=guided_context.confirm_changeset if guided else confirm_changeset,
signing_profiles=guided_context.signing_profiles if guided else signing_profiles,
) as deploy_context:
deploy_context.run()
3 changes: 3 additions & 0 deletions samcli/commands/deploy/deploy_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ def __init__(
region,
profile,
confirm_changeset,
signing_profiles,
):
self.template_file = template_file
self.stack_name = stack_name
Expand All @@ -81,6 +82,7 @@ def __init__(
self.s3_uploader = None
self.deployer = None
self.confirm_changeset = confirm_changeset
self.signing_profiles = signing_profiles

def __enter__(self):
return self
Expand Down Expand Up @@ -129,6 +131,7 @@ def run(self):
self.capabilities,
self.parameter_overrides,
self.confirm_changeset,
self.signing_profiles,
)
return self.deploy(
self.stack_name,
Expand Down
Loading

0 comments on commit bbccd37

Please sign in to comment.