diff --git a/samples/general/README.md b/samples/general/README.md index 447d215..a2ba8e2 100644 --- a/samples/general/README.md +++ b/samples/general/README.md @@ -12,8 +12,14 @@ Sets up a simple APIM instance with a variety of policies to experiment. 1. Become proficient with how policies operate. 1. Gain confidence in setting up and configuring policies appropriately. +## 🔗 APIs + +| API Name | What does it do? | +|:------------------------------|:----------------------------------------------------------------------------------------------------------------------------------| +| Request Headers | Returns a list of the request headers sent to APIM. Useful for debugging. | +| API ID | Returns the ID of an API as per a predefined standard such as `api-123`. Useful for explicitly identifying an API in telemetry. | + ## ⚙️ Configuration 1. Decide which of the [Infrastructure Architectures](../../README.md#infrastructure-architectures) you wish to use. - 1. If the infrastructure _does not_ yet exist, navigate to the desired [infrastructure](../../infrastructure/) folder and follow its README.md. - 1. If the infrastructure _does_ exist, adjust the `user-defined parameters` in the _Initialize notebook variables_ below. Please ensure that all parameters match your infrastructure. +1. Press `Run All` in this sample's `create.ipynb` notebook. \ No newline at end of file diff --git a/samples/general/create.ipynb b/samples/general/create.ipynb index 81493c6..6a0e540 100644 --- a/samples/general/create.ipynb +++ b/samples/general/create.ipynb @@ -42,25 +42,20 @@ "\n", "# Define the APIs and their operations and policies\n", "\n", - "# API 1\n", - "api1_path = f'{api_prefix}api1'\n", - "api1_get = GET_APIOperation('This is a GET for API 1')\n", - "api1_post = POST_APIOperation('This is a POST for API 1')\n", - "api1 = API(api1_path, 'API 1', api1_path, 'This is API 1', operations = [api1_get, api1_post], tags = tags)\n", - "\n", - "# API 2\n", - "api2_path = f'{api_prefix}api2'\n", - "api2_post = POST_APIOperation('This is a POST for API 2')\n", - "api2 = API(api2_path, 'API 2', api2_path, 'This is API 2', operations = [api2_post], tags = tags)\n", - "\n", - "# API 3: Request Headers\n", + "# API 1: Request Headers\n", "rh_path = f'{api_prefix}request-headers'\n", "pol_request_headers_get = utils.read_policy_xml(REQUEST_HEADERS_XML_POLICY_PATH)\n", "request_headers_get = GET_APIOperation('Gets the request headers for the current request and returns them. Great for troubleshooting.', pol_request_headers_get)\n", "request_headers = API(rh_path, 'Request Headers', rh_path, 'API for request headers', operations = [request_headers_get], tags = tags)\n", "\n", + "# API 2: API ID: Extract and trace API Identifier\n", + "api_id_path = f'{api_prefix}api-id'\n", + "pol_api_id_get = utils.read_policy_xml(API_ID_XML_POLICY_PATH)\n", + "api_id_get = GET_APIOperation('Gets the API identifier for the current request and traces it', pol_api_id_get)\n", + "api_id = API(api_id_path, 'API Identifier (api-42)', api_id_path, 'API for extracting and tracing API identifier', operations = [api_id_get], tags = tags)\n", + "\n", "# APIs Array\n", - "apis: List[API] = [api1, api2, request_headers]\n", + "apis: List[API] = [request_headers, api_id]\n", "\n", "utils.print_ok('Notebook initialized')" ] @@ -121,16 +116,22 @@ "\n", "# Initialize testing framework\n", "tests = ApimTesting(\"General Sample Tests\", sample_folder, nb_helper.deployment)\n", - "api_subscription_key = apim_apis[2]['subscriptionPrimaryKey']\n", "\n", "# Preflight: Check if the infrastructure architecture deployment uses Azure Front Door. If so, assume that APIM is not directly accessible and use the Front Door URL instead.\n", "endpoint_url = utils.test_url_preflight_check(deployment, rg_name, apim_gateway_url)\n", "\n", "# 1) Request Headers\n", + "api_subscription_key = apim_apis[0]['subscriptionPrimaryKey']\n", "reqs = ApimRequests(endpoint_url, api_subscription_key)\n", - "output = reqs.singleGet('/request-headers', msg = 'Calling Request Headers API via Azure Front Door. Expect 200.')\n", + "output = reqs.singleGet('/request-headers', msg = 'Calling the Request Headers API. Expect 200.')\n", "tests.verify('Host:' in output, True)\n", "\n", + "# 2) API ID\n", + "api_subscription_key = apim_apis[1]['subscriptionPrimaryKey']\n", + "reqs = ApimRequests(endpoint_url, api_subscription_key)\n", + "output = reqs.singleGet('/api-id', msg = 'Calling the API ID API. Expect 200.')\n", + "tests.verify(output, 'Extracted API ID: api-42')\n", + "\n", "tests.print_summary()\n", "\n", "utils.print_ok('All done!')" diff --git a/shared/apim-policies/api-id.xml b/shared/apim-policies/api-id.xml new file mode 100644 index 0000000..f7191a0 --- /dev/null +++ b/shared/apim-policies/api-id.xml @@ -0,0 +1,27 @@ + + + + + + + + + @{ + var apiId = context.Variables.GetValueOrDefault("apiId", string.Empty); + return (string.IsNullOrEmpty(apiId)) ? "No API ID extracted." : string.Format("Extracted API ID: {0}", apiId); + } + + + + + + + + + + + + + \ No newline at end of file diff --git a/shared/apim-policies/fragments/pf-api-id.xml b/shared/apim-policies/fragments/pf-api-id.xml new file mode 100644 index 0000000..04bfd2a --- /dev/null +++ b/shared/apim-policies/fragments/pf-api-id.xml @@ -0,0 +1,44 @@ + + + + + + + + @{ + return "Extracted API ID: " + (string)context.Variables["apiId"]; + } + + + + + @((string)context.Variables["apiId"]) + + + + \ No newline at end of file diff --git a/shared/python/apimtesting.py b/shared/python/apimtesting.py index 23304f0..c4babf6 100644 --- a/shared/python/apimtesting.py +++ b/shared/python/apimtesting.py @@ -51,6 +51,7 @@ def verify(self, value: any, expected: any) -> bool: bool: True, if the assertion passes; otherwise, False. """ try: + print(f'🔍 Assert that [{value}] matches [{expected}].') self.total_tests += 1 assert value == expected, f'Value [{value}] does not match expected [{expected}]' self.tests_passed += 1 diff --git a/shared/python/apimtypes.py b/shared/python/apimtypes.py index 355bf9c..d3b0061 100644 --- a/shared/python/apimtypes.py +++ b/shared/python/apimtypes.py @@ -36,10 +36,11 @@ def _get_project_root() -> Path: _SHARED_XML_POLICY_BASE_PATH = _PROJECT_ROOT / 'shared' / 'apim-policies' # Policy file paths (now absolute and platform-independent) -DEFAULT_XML_POLICY_PATH = str(_SHARED_XML_POLICY_BASE_PATH / 'default.xml') -HELLO_WORLD_XML_POLICY_PATH = str(_SHARED_XML_POLICY_BASE_PATH / 'hello-world.xml') -REQUEST_HEADERS_XML_POLICY_PATH = str(_SHARED_XML_POLICY_BASE_PATH / 'request-headers.xml') -BACKEND_XML_POLICY_PATH = str(_SHARED_XML_POLICY_BASE_PATH / 'backend.xml') +DEFAULT_XML_POLICY_PATH = str(_SHARED_XML_POLICY_BASE_PATH / 'default.xml') +HELLO_WORLD_XML_POLICY_PATH = str(_SHARED_XML_POLICY_BASE_PATH / 'hello-world.xml') +REQUEST_HEADERS_XML_POLICY_PATH = str(_SHARED_XML_POLICY_BASE_PATH / 'request-headers.xml') +BACKEND_XML_POLICY_PATH = str(_SHARED_XML_POLICY_BASE_PATH / 'backend.xml') +API_ID_XML_POLICY_PATH = str(_SHARED_XML_POLICY_BASE_PATH / 'api-id.xml') SUBSCRIPTION_KEY_PARAMETER_NAME = 'api-key' SLEEP_TIME_BETWEEN_REQUESTS_MS = 50 diff --git a/shared/python/infrastructures.py b/shared/python/infrastructures.py index d76f25e..b6948da 100644 --- a/shared/python/infrastructures.py +++ b/shared/python/infrastructures.py @@ -59,6 +59,7 @@ def _define_policy_fragments(self) -> List[PolicyFragment]: # The base policy fragments common to all infrastructures self.base_pfs = [ + PolicyFragment('Api-Id', utils.read_policy_xml(utils.determine_shared_policy_path('pf-api-id.xml')), 'Extracts a specific API identifier for tracing.'), PolicyFragment('AuthZ-Match-All', utils.read_policy_xml(utils.determine_shared_policy_path('pf-authz-match-all.xml')), 'Authorizes if all of the specified roles match the JWT role claims.'), PolicyFragment('AuthZ-Match-Any', utils.read_policy_xml(utils.determine_shared_policy_path('pf-authz-match-any.xml')), 'Authorizes if any of the specified roles match the JWT role claims.'), PolicyFragment('Http-Response-200', utils.read_policy_xml(utils.determine_shared_policy_path('pf-http-response-200.xml')), 'Returns a 200 OK response for the current HTTP method.'), diff --git a/shared/python/utils.py b/shared/python/utils.py index 4ece227..c02229c 100644 --- a/shared/python/utils.py +++ b/shared/python/utils.py @@ -473,7 +473,7 @@ def _query_and_select_infrastructure(self) -> tuple[INFRASTRUCTURE | None, int | option_counter += 1 else: print_warning('No existing supported infrastructures found.') - print_info(f'🚀 Automatically proceeding to create new infrastructure: {self.deployment.value}') + print_info(f'Automatically proceeding to create new infrastructure: {self.deployment.value}') # Automatically create the desired infrastructure without user confirmation selected_index = self._get_current_index() diff --git a/tests/python/test_infrastructures.py b/tests/python/test_infrastructures.py index 0afcabf..e1e1639 100644 --- a/tests/python/test_infrastructures.py +++ b/tests/python/test_infrastructures.py @@ -116,7 +116,7 @@ def test_infrastructure_creation_with_custom_policy_fragments(mock_utils, mock_p pfs = infra._define_policy_fragments() # Should have base policy fragments + custom ones - assert len(pfs) == 7 # 5 base + 2 custom + assert len(pfs) == 8 # 6 base + 2 custom assert any(pf.name == 'Test-Fragment-1' for pf in pfs) assert any(pf.name == 'Test-Fragment-2' for pf in pfs) assert any(pf.name == 'AuthZ-Match-All' for pf in pfs) @@ -225,8 +225,8 @@ def test_define_policy_fragments_with_none_input(mock_utils): pfs = infra._define_policy_fragments() # Should only have base policy fragments - assert len(pfs) == 5 - assert all(pf.name in ['AuthZ-Match-All', 'AuthZ-Match-Any', 'Http-Response-200', 'Product-Match-Any', 'Remove-Request-Headers'] for pf in pfs) + assert len(pfs) == 6 + assert all(pf.name in ['Api-Id', 'AuthZ-Match-All', 'AuthZ-Match-Any', 'Http-Response-200', 'Product-Match-Any', 'Remove-Request-Headers'] for pf in pfs) @pytest.mark.unit def test_define_policy_fragments_with_custom_input(mock_utils, mock_policy_fragments): @@ -242,7 +242,7 @@ def test_define_policy_fragments_with_custom_input(mock_utils, mock_policy_fragm pfs = infra._define_policy_fragments() # Should have base + custom policy fragments - assert len(pfs) == 7 # 5 base + 2 custom + assert len(pfs) == 8 # 6 base + 2 custom fragment_names = [pf.name for pf in infra.pfs] assert 'Test-Fragment-1' in fragment_names assert 'Test-Fragment-2' in fragment_names @@ -319,7 +319,7 @@ def test_define_bicep_parameters(mock_utils): assert 'policyFragments' in bicep_params assert isinstance(bicep_params['policyFragments']['value'], list) - assert len(bicep_params['policyFragments']['value']) == 5 # base policy fragments + assert len(bicep_params['policyFragments']['value']) == 6 # base policy fragments # ------------------------------ @@ -769,8 +769,8 @@ def test_infrastructure_end_to_end_simple(mock_utils): # Verify all components are created correctly assert infra.infra == INFRASTRUCTURE.SIMPLE_APIM - assert len(infra.base_pfs) == 5 - assert len(infra.pfs) == 5 + assert len(infra.base_pfs) == 6 + assert len(infra.pfs) == 6 assert len(infra.base_apis) == 1 assert len(infra.apis) == 1 @@ -778,7 +778,7 @@ def test_infrastructure_end_to_end_simple(mock_utils): bicep_params = infra._define_bicep_parameters() assert bicep_params['apimSku']['value'] == 'Developer' assert len(bicep_params['apis']['value']) == 1 - assert len(bicep_params['policyFragments']['value']) == 5 + assert len(bicep_params['policyFragments']['value']) == 6 @pytest.mark.unit def test_infrastructure_with_all_custom_components(mock_utils, mock_policy_fragments, mock_apis): @@ -798,8 +798,8 @@ def test_infrastructure_with_all_custom_components(mock_utils, mock_policy_fragm infra._define_apis() # Verify all components are combined correctly - assert len(infra.base_pfs) == 5 - assert len(infra.pfs) == 7 # 5 base + 2 custom + assert len(infra.base_pfs) == 6 + assert len(infra.pfs) == 8 # 6 base + 2 custom assert len(infra.base_apis) == 1 assert len(infra.apis) == 3 # 1 base + 2 custom @@ -807,7 +807,7 @@ def test_infrastructure_with_all_custom_components(mock_utils, mock_policy_fragm bicep_params = infra._define_bicep_parameters() assert bicep_params['apimSku']['value'] == 'Premium' assert len(bicep_params['apis']['value']) == 3 - assert len(bicep_params['policyFragments']['value']) == 7 + assert len(bicep_params['policyFragments']['value']) == 8 # ------------------------------ @@ -856,7 +856,7 @@ def test_infrastructure_empty_custom_lists(mock_utils): infra._define_apis() # Empty lists should behave the same as None - assert len(infra.pfs) == 5 # Only base policy fragments + assert len(infra.pfs) == 6 # Only base policy fragments assert len(infra.apis) == 1 # Only base APIs @pytest.mark.unit @@ -927,6 +927,7 @@ def test_policy_fragment_creation_robustness(mock_utils): '', '', '', + '', # Added for the new Api-Id policy fragment '' ]