This is an automated email from the ASF dual-hosted git repository.
vincbeck pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 56c27f8f5a Create CLI commands for AWS auth manager to create AWS
Identity Center related resources (#37407)
56c27f8f5a is described below
commit 56c27f8f5a3c547147389253ea9653a374ad29f1
Author: Vincent <[email protected]>
AuthorDate: Wed Feb 14 11:43:16 2024 -0800
Create CLI commands for AWS auth manager to create AWS Identity Center
related resources (#37407)
---
.../amazon/aws/auth_manager/cli/avp_commands.py | 4 +-
.../amazon/aws/auth_manager/cli/definition.py | 14 ++
.../amazon/aws/auth_manager/cli/idc_commands.py | 148 +++++++++++++++++++++
.../amazon/aws/auth_manager/cli/test_definition.py | 2 +-
.../aws/auth_manager/cli/test_idc_commands.py | 134 +++++++++++++++++++
5 files changed, 299 insertions(+), 3 deletions(-)
diff --git a/airflow/providers/amazon/aws/auth_manager/cli/avp_commands.py
b/airflow/providers/amazon/aws/auth_manager/cli/avp_commands.py
index 24f7907104..fd49cbcb5a 100644
--- a/airflow/providers/amazon/aws/auth_manager/cli/avp_commands.py
+++ b/airflow/providers/amazon/aws/auth_manager/cli/avp_commands.py
@@ -55,7 +55,7 @@ def init_avp(args):
if not is_new_policy_store:
print(
f"Since an existing policy store with description
'{args.policy_store_description}' has been found in Amazon Verified
Permissions, "
- "the CLI nade no changes to this policy store for security
reasons. "
+ "the CLI made no changes to this policy store for security
reasons. "
"Any modification to this policy store must be done manually.",
)
else:
@@ -115,7 +115,7 @@ def _create_policy_store(client: BaseClient, args) ->
tuple[str | None, bool]:
print(f"No policy store with description
'{args.policy_store_description}' found, creating one.")
if args.dry_run:
print(
- "Dry run, not creating the policy store with description
'{args.policy_store_description}'."
+ f"Dry run, not creating the policy store with description
'{args.policy_store_description}'."
)
return None, True
diff --git a/airflow/providers/amazon/aws/auth_manager/cli/definition.py
b/airflow/providers/amazon/aws/auth_manager/cli/definition.py
index 4355dcce9b..846dc56171 100644
--- a/airflow/providers/amazon/aws/auth_manager/cli/definition.py
+++ b/airflow/providers/amazon/aws/auth_manager/cli/definition.py
@@ -35,6 +35,14 @@ ARG_DRY_RUN = Arg(
action="store_true",
)
+# AWS IAM Identity Center
+ARG_INSTANCE_NAME = Arg(("--instance-name",), help="Instance name in Identity
Center", default="Airflow")
+
+ARG_APPLICATION_NAME = Arg(
+ ("--application-name",), help="Application name in Identity Center",
default="Airflow"
+)
+
+
# Amazon Verified Permissions
ARG_POLICY_STORE_DESCRIPTION = Arg(
("--policy-store-description",), help="Policy store description",
default="Airflow"
@@ -47,6 +55,12 @@ ARG_POLICY_STORE_ID = Arg(("--policy-store-id",),
help="Policy store ID")
################
AWS_AUTH_MANAGER_COMMANDS = (
+ ActionCommand(
+ name="init-identity-center",
+ help="Initialize AWS IAM identity Center resources to be used by AWS
manager",
+
func=lazy_load_command("airflow.providers.amazon.aws.auth_manager.cli.idc_commands.init_idc"),
+ args=(ARG_INSTANCE_NAME, ARG_APPLICATION_NAME, ARG_DRY_RUN,
ARG_VERBOSE),
+ ),
ActionCommand(
name="init-avp",
help="Initialize Amazon Verified resources to be used by AWS manager",
diff --git a/airflow/providers/amazon/aws/auth_manager/cli/idc_commands.py
b/airflow/providers/amazon/aws/auth_manager/cli/idc_commands.py
new file mode 100644
index 0000000000..3639dd4b91
--- /dev/null
+++ b/airflow/providers/amazon/aws/auth_manager/cli/idc_commands.py
@@ -0,0 +1,148 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""User sub-commands."""
+from __future__ import annotations
+
+import logging
+from typing import TYPE_CHECKING
+
+import boto3
+from botocore.exceptions import ClientError
+
+from airflow.configuration import conf
+from airflow.exceptions import AirflowOptionalProviderFeatureException
+from airflow.providers.amazon.aws.auth_manager.constants import
CONF_REGION_NAME_KEY, CONF_SECTION_NAME
+from airflow.utils import cli as cli_utils
+
+try:
+ from airflow.utils.providers_configuration_loader import
providers_configuration_loaded
+except ImportError:
+ raise AirflowOptionalProviderFeatureException(
+ "Failed to import avp_commands. This feature is only available in
Airflow "
+ "version >= 2.8.0 where Auth Managers are introduced."
+ )
+
+if TYPE_CHECKING:
+ from botocore.client import BaseClient
+
+log = logging.getLogger(__name__)
+
+
+@cli_utils.action_cli
+@providers_configuration_loaded
+def init_idc(args):
+ """Initialize AWS IAM Identity Center resources."""
+ client = _get_client()
+
+ # Create the instance if needed
+ instance_arn = _create_instance(client, args)
+
+ # Create the application if needed
+ _create_application(client, instance_arn, args)
+
+ if not args.dry_run:
+ print("AWS IAM Identity Center resources created successfully.")
+
+
+def _get_client():
+ """Return AWS IAM Identity Center client."""
+ region_name = conf.get(CONF_SECTION_NAME, CONF_REGION_NAME_KEY)
+ return boto3.client("sso-admin", region_name=region_name)
+
+
+def _create_instance(client: BaseClient, args) -> str | None:
+ """Create if needed AWS IAM Identity Center instance."""
+ instances = client.list_instances()
+
+ if args.verbose:
+ log.debug("Instances found: %s", instances)
+
+ if len(instances["Instances"]) > 0:
+ print(
+ f"There is already an instance configured in AWS IAM Identity
Center: '{instances['Instances'][0]['InstanceArn']}'. "
+ "No need to create a new one."
+ )
+ return instances["Instances"][0]["InstanceArn"]
+ else:
+ print("No instance configured in AWS IAM Identity Center, creating
one.")
+ if args.dry_run:
+ print("Dry run, not creating the instance.")
+ return None
+
+ response = client.create_instance(Name=args.instance_name)
+ if args.verbose:
+ log.debug("Response from create_instance: %s", response)
+
+ print(f"Instance created: '{response['InstanceArn']}'")
+
+ return response["InstanceArn"]
+
+
+def _create_application(client: BaseClient, instance_arn: str | None, args) ->
str | None:
+ """Create if needed AWS IAM identity Center application."""
+ paginator = client.get_paginator("list_applications")
+ pages = paginator.paginate(InstanceArn=instance_arn or "")
+ applications = [application for page in pages for application in
page["Applications"]]
+ existing_applications = [
+ application for application in applications if application["Name"] ==
args.application_name
+ ]
+
+ if args.verbose:
+ log.debug("Applications found: %s", applications)
+ log.debug("Existing applications found: %s", existing_applications)
+
+ if len(existing_applications) > 0:
+ print(
+ f"There is already an application named '{args.application_name}'
in AWS IAM Identity Center: '{existing_applications[0]['ApplicationArn']}'. "
+ "Using this application."
+ )
+ return existing_applications[0]["ApplicationArn"]
+ else:
+ print(f"No application named {args.application_name} found, creating
one.")
+ if args.dry_run:
+ print("Dry run, not creating the application.")
+ return None
+
+ try:
+ response = client.create_application(
+
ApplicationProviderArn="arn:aws:sso::aws:applicationProvider/custom-saml",
+ Description="Application automatically created through the
Airflow CLI. This application is used to access Airflow environment.",
+ InstanceArn=instance_arn,
+ Name=args.application_name,
+ PortalOptions={
+ "SignInOptions": {
+ "Origin": "IDENTITY_CENTER",
+ },
+ "Visibility": "ENABLED",
+ },
+ Status="ENABLED",
+ )
+ if args.verbose:
+ log.debug("Response from create_application: %s", response)
+ except ClientError as e:
+ # This is needed because as of today, the create_application in
AWS Identity Center does not support SAML application
+ # Remove this part when it is supported
+ if "is not supported for this action" in
e.response["Error"]["Message"]:
+ print(
+ "Creation of SAML applications is only supported in AWS
console today. "
+ "Please create the application through the console."
+ )
+ raise
+
+ print(f"Application created: '{response['ApplicationArn']}'")
+
+ return response["ApplicationArn"]
diff --git a/tests/providers/amazon/aws/auth_manager/cli/test_definition.py
b/tests/providers/amazon/aws/auth_manager/cli/test_definition.py
index 079df886f6..5866aa594f 100644
--- a/tests/providers/amazon/aws/auth_manager/cli/test_definition.py
+++ b/tests/providers/amazon/aws/auth_manager/cli/test_definition.py
@@ -21,4 +21,4 @@ from airflow.providers.amazon.aws.auth_manager.cli.definition
import AWS_AUTH_MA
class TestAwsCliDefinition:
def test_aws_auth_manager_cli_commands(self):
- assert len(AWS_AUTH_MANAGER_COMMANDS) == 2
+ assert len(AWS_AUTH_MANAGER_COMMANDS) == 3
diff --git a/tests/providers/amazon/aws/auth_manager/cli/test_idc_commands.py
b/tests/providers/amazon/aws/auth_manager/cli/test_idc_commands.py
new file mode 100644
index 0000000000..9913e1ae7c
--- /dev/null
+++ b/tests/providers/amazon/aws/auth_manager/cli/test_idc_commands.py
@@ -0,0 +1,134 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import importlib
+from unittest.mock import Mock, patch
+
+import pytest
+
+from airflow.cli import cli_parser
+from airflow.providers.amazon.aws.auth_manager.cli.idc_commands import init_idc
+from tests.test_utils.config import conf_vars
+
+mock_boto3 = Mock()
+
+
[email protected]_test
+class TestIdcCommands:
+ def setup_method(self):
+ mock_boto3.reset_mock()
+
+ @classmethod
+ def setup_class(cls):
+ with conf_vars(
+ {
+ (
+ "core",
+ "auth_manager",
+ ):
"airflow.providers.amazon.aws.auth_manager.aws_auth_manager.AwsAuthManager"
+ }
+ ):
+ importlib.reload(cli_parser)
+ cls.arg_parser = cli_parser.get_parser()
+
+ @pytest.mark.parametrize(
+ "dry_run, verbose",
+ [
+ (False, False),
+ (True, True),
+ ],
+ )
+
@patch("airflow.providers.amazon.aws.auth_manager.cli.idc_commands._get_client")
+ def test_init_idc_with_no_existing_resources(self, mock_get_client,
dry_run, verbose):
+ mock_get_client.return_value = mock_boto3
+
+ instance_name = "test-instance"
+ instance_arn = "test-instance-arn"
+ application_name = "test-application"
+ application_arn = "test-application-arn"
+
+ paginator = Mock()
+ paginator.paginate.return_value = []
+
+ mock_boto3.list_instances.return_value = {"Instances": []}
+ mock_boto3.create_instance.return_value = {"InstanceArn": instance_arn}
+ mock_boto3.get_paginator.return_value = paginator
+ mock_boto3.create_application.return_value = {"ApplicationArn":
application_arn}
+
+ with conf_vars({("database", "check_migrations"): "False"}):
+ params = [
+ "aws-auth-manager",
+ "init-identity-center",
+ "--instance-name",
+ instance_name,
+ "--application-name",
+ application_name,
+ ]
+ if dry_run:
+ params.append("--dry-run")
+ if verbose:
+ params.append("--verbose")
+ init_idc(self.arg_parser.parse_args(params))
+
+ mock_boto3.list_instances.assert_called_once_with()
+ if not dry_run:
+
mock_boto3.create_instance.assert_called_once_with(Name=instance_name)
+ mock_boto3.create_application.assert_called_once()
+
+ @pytest.mark.parametrize(
+ "dry_run, verbose",
+ [
+ (False, False),
+ (True, True),
+ ],
+ )
+
@patch("airflow.providers.amazon.aws.auth_manager.cli.idc_commands._get_client")
+ def test_init_idc_with_existing_resources(self, mock_get_client, dry_run,
verbose):
+ mock_get_client.return_value = mock_boto3
+
+ instance_name = "test-instance"
+ instance_arn = "test-instance-arn"
+ application_name = "test-application"
+ application_arn = "test-application-arn"
+
+ paginator = Mock()
+ paginator.paginate.return_value = [
+ {"Applications": [{"Name": application_name, "ApplicationArn":
application_arn}]}
+ ]
+
+ mock_boto3.list_instances.return_value = {"Instances":
[{"InstanceArn": instance_arn}]}
+ mock_boto3.get_paginator.return_value = paginator
+
+ with conf_vars({("database", "check_migrations"): "False"}):
+ params = [
+ "aws-auth-manager",
+ "init-identity-center",
+ "--instance-name",
+ instance_name,
+ "--application-name",
+ application_name,
+ ]
+ if dry_run:
+ params.append("--dry-run")
+ if verbose:
+ params.append("--verbose")
+ init_idc(self.arg_parser.parse_args(params))
+
+ mock_boto3.list_instances.assert_called_once_with()
+ mock_boto3.create_instance.assert_not_called()
+ mock_boto3.create_application.assert_not_called()