Skip to content

Commit 60a77d3

Browse files
Add required product association for APIs
1 parent ad609e8 commit 60a77d3

File tree

5 files changed

+193
-8
lines changed

5 files changed

+193
-8
lines changed

samples/authX-pro/create.ipynb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -148,15 +148,15 @@
148148
"hremployees_api_path = f'/{api_prefix}employees'\n",
149149
"hremployees_get = GET_APIOperation('Gets the employees', utils.read_policy_xml('./hr_get.xml'))\n",
150150
"hremployees_post = POST_APIOperation('Creates a new employee', utils.read_policy_xml('./hr_post.xml'))\n",
151-
"hremployees = API(f'{api_prefix}Employees', 'Employees Pro', hremployees_api_path, 'This is a Human Resources API for employee information', utils.read_policy_xml(DEFAULT_XML_POLICY_PATH), \n",
152-
" operations = [hremployees_get, hremployees_post], tags = tags, productNames = [hr_product_name], subscriptionRequired = True)\n",
151+
"hremployees = API(f'{api_prefix}Employees', 'Employees Pro', hremployees_api_path, 'This is a Human Resources API for employee information', utils.read_policy_xml(REQUIRE_PRODUCT_XML_POLICY_PATH), \n",
152+
" operations = [hremployees_get, hremployees_post], tags = tags, productNames = [hr_product_name], subscriptionRequired = False)\n",
153153
"\n",
154154
"# Benefits (HR)\n",
155155
"hrbenefits_api_path = f'/{api_prefix}benefits'\n",
156156
"hrbenefits_get = GET_APIOperation('Gets employee benefits', utils.read_policy_xml('./hr_get.xml'))\n",
157157
"hrbenefits_post = POST_APIOperation('Creates employee benefits', utils.read_policy_xml('./hr_post.xml'))\n",
158-
"hrbenefits = API(f'{api_prefix}Benefits', 'Benefits Pro', hrbenefits_api_path, 'This is a Human Resources API for employee benefits', utils.read_policy_xml(DEFAULT_XML_POLICY_PATH), \n",
159-
" operations = [hrbenefits_get, hrbenefits_post], tags = tags, productNames = [hr_product_name], subscriptionRequired = True)\n",
158+
"hrbenefits = API(f'{api_prefix}Benefits', 'Benefits Pro', hrbenefits_api_path, 'This is a Human Resources API for employee benefits', utils.read_policy_xml(REQUIRE_PRODUCT_XML_POLICY_PATH), \n",
159+
" operations = [hrbenefits_get, hrbenefits_post], tags = tags, productNames = [hr_product_name], subscriptionRequired = False)\n",
160160
"\n",
161161
"# APIs Array\n",
162162
"apis: List[API] = [hremployees, hrbenefits]\n",
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<!--
2+
Policy that ensures the API is called with a product subscription.
3+
This policy validates that the request includes a valid product context.
4+
Returns 403 Unauthorized with JSON error response if no product subscription is found.
5+
-->
6+
<policies>
7+
<inbound>
8+
<base />
9+
<!-- Validate that a product subscription is present -->
10+
<choose>
11+
<when condition="@(context.Product == null)">
12+
<return-response>
13+
<set-status code="403" reason="Unauthorized" />
14+
<set-body>"This API requires a valid product subscription to access."</set-body>
15+
</return-response>
16+
</when>
17+
</choose>
18+
</inbound>
19+
<backend>
20+
<base />
21+
</backend>
22+
<outbound>
23+
<base />
24+
</outbound>
25+
<on-error>
26+
<base />
27+
</on-error>
28+
</policies>

shared/bicep/modules/apim/v1/api.bicep

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,12 @@ resource apimApi 'Microsoft.ApiManagement/service/apis@2024-06-01-preview' = {
6060
path: api.path
6161
protocols: [
6262
'https'
63-
]
63+
]
6464
subscriptionKeyParameterNames: {
6565
header: 'api-key'
6666
query: 'api-key'
6767
}
68-
subscriptionRequired: false
68+
subscriptionRequired: api.?subscriptionRequired ?? false
6969
type: 'http'
7070
}
7171
}

shared/python/apimtypes.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
# These paths are relative to the infrastructure and samples
1515
SHARED_XML_POLICY_BASE_PATH = '../../shared/apim-policies'
1616
DEFAULT_XML_POLICY_PATH = f'{SHARED_XML_POLICY_BASE_PATH}/default.xml'
17+
REQUIRE_PRODUCT_XML_POLICY_PATH = f'{SHARED_XML_POLICY_BASE_PATH}/require-product.xml'
1718
HELLO_WORLD_XML_POLICY_PATH = f'{SHARED_XML_POLICY_BASE_PATH}/hello-world.xml'
1819
REQUEST_HEADERS_XML_POLICY_PATH = f'{SHARED_XML_POLICY_BASE_PATH}/request-headers.xml'
1920
BACKEND_XML_POLICY_PATH = f'{SHARED_XML_POLICY_BASE_PATH}/backend.xml'
@@ -121,11 +122,14 @@ class API:
121122
operations: Optional[List['APIOperation']] = None
122123
tags: Optional[List[str]] = None
123124
productNames: Optional[List[str]] = None
125+
subscriptionRequired: bool = False
126+
124127
# ------------------------------
125128
# CONSTRUCTOR
126129
# ------------------------------
127130

128-
def __init__(self, name: str, displayName: str, path: str, description: str, policyXml: Optional[str] = None, operations: Optional[List['APIOperation']] = None, tags: Optional[List[str]] = None, productNames: Optional[List[str]] = None):
131+
def __init__(self, name: str, displayName: str, path: str, description: str, policyXml: Optional[str] = None, operations: Optional[List['APIOperation']] = None, tags: Optional[List[str]] = None,
132+
productNames: Optional[List[str]] = None, subscriptionRequired: bool = False):
129133
self.name = name
130134
self.displayName = displayName
131135
self.path = path
@@ -134,6 +138,8 @@ def __init__(self, name: str, displayName: str, path: str, description: str, pol
134138
self.operations = operations if operations is not None else []
135139
self.tags = tags if tags is not None else []
136140
self.productNames = productNames if productNames is not None else []
141+
self.subscriptionRequired = subscriptionRequired
142+
137143
# ------------------------------
138144
# PUBLIC METHODS
139145
# ------------------------------
@@ -144,7 +150,8 @@ def to_dict(self) -> dict:
144150
"displayName": self.displayName,
145151
"path": self.path,
146152
"description": self.description,
147-
"operations": [op.to_dict() for op in self.operations] if self.operations else []
153+
"operations": [op.to_dict() for op in self.operations] if self.operations else [],
154+
"subscriptionRequired": self.subscriptionRequired
148155
}
149156

150157
if self.policyXml is not None:

tests/python/test_apimtypes.py

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,3 +531,153 @@ def test_product_repr():
531531
assert "Product" in result
532532
assert "hr" in result
533533
assert "Human Resources" in result
534+
535+
@pytest.mark.unit
536+
def test_api_subscription_required_default():
537+
"""Test that API object has subscriptionRequired defaulting to False."""
538+
api = apimtypes.API(
539+
name = EXAMPLE_NAME,
540+
displayName = EXAMPLE_DISPLAY_NAME,
541+
path = EXAMPLE_PATH,
542+
description = EXAMPLE_DESCRIPTION,
543+
policyXml = EXAMPLE_POLICY_XML,
544+
operations = None
545+
)
546+
assert api.subscriptionRequired == False
547+
548+
@pytest.mark.unit
549+
def test_api_subscription_required_explicit_false():
550+
"""Test creation of API object with explicit subscriptionRequired=False."""
551+
api = apimtypes.API(
552+
name = EXAMPLE_NAME,
553+
displayName = EXAMPLE_DISPLAY_NAME,
554+
path = EXAMPLE_PATH,
555+
description = EXAMPLE_DESCRIPTION,
556+
policyXml = EXAMPLE_POLICY_XML,
557+
operations = None,
558+
subscriptionRequired = False
559+
)
560+
assert api.subscriptionRequired == False
561+
562+
@pytest.mark.unit
563+
def test_api_subscription_required_explicit_true():
564+
"""Test creation of API object with explicit subscriptionRequired=True."""
565+
api = apimtypes.API(
566+
name = EXAMPLE_NAME,
567+
displayName = EXAMPLE_DISPLAY_NAME,
568+
path = EXAMPLE_PATH,
569+
description = EXAMPLE_DESCRIPTION,
570+
policyXml = EXAMPLE_POLICY_XML,
571+
operations = None,
572+
subscriptionRequired = True
573+
)
574+
assert api.subscriptionRequired == True
575+
576+
@pytest.mark.unit
577+
def test_api_to_dict_includes_subscription_required_when_true():
578+
"""Test that to_dict includes subscriptionRequired when True."""
579+
api = apimtypes.API(
580+
name = EXAMPLE_NAME,
581+
displayName = EXAMPLE_DISPLAY_NAME,
582+
path = EXAMPLE_PATH,
583+
description = EXAMPLE_DESCRIPTION,
584+
policyXml = EXAMPLE_POLICY_XML,
585+
operations = None,
586+
subscriptionRequired = True
587+
)
588+
d = api.to_dict()
589+
assert "subscriptionRequired" in d
590+
assert d["subscriptionRequired"] == True
591+
592+
@pytest.mark.unit
593+
def test_api_to_dict_includes_subscription_required_when_false():
594+
"""Test that to_dict includes subscriptionRequired when explicitly False."""
595+
api = apimtypes.API(
596+
name = EXAMPLE_NAME,
597+
displayName = EXAMPLE_DISPLAY_NAME,
598+
path = EXAMPLE_PATH,
599+
description = EXAMPLE_DESCRIPTION,
600+
policyXml = EXAMPLE_POLICY_XML,
601+
operations = None,
602+
subscriptionRequired = False
603+
)
604+
d = api.to_dict()
605+
assert "subscriptionRequired" in d
606+
assert d["subscriptionRequired"] == False
607+
608+
@pytest.mark.unit
609+
def test_api_equality_with_subscription_required():
610+
"""Test equality comparison for API objects with different subscriptionRequired values."""
611+
api1 = apimtypes.API(
612+
name = EXAMPLE_NAME,
613+
displayName = EXAMPLE_DISPLAY_NAME,
614+
path = EXAMPLE_PATH,
615+
description = EXAMPLE_DESCRIPTION,
616+
policyXml = EXAMPLE_POLICY_XML,
617+
operations = None,
618+
subscriptionRequired = True
619+
)
620+
api2 = apimtypes.API(
621+
name = EXAMPLE_NAME,
622+
displayName = EXAMPLE_DISPLAY_NAME,
623+
path = EXAMPLE_PATH,
624+
description = EXAMPLE_DESCRIPTION,
625+
policyXml = EXAMPLE_POLICY_XML,
626+
operations = None,
627+
subscriptionRequired = True
628+
)
629+
api3 = apimtypes.API(
630+
name = EXAMPLE_NAME,
631+
displayName = EXAMPLE_DISPLAY_NAME,
632+
path = EXAMPLE_PATH,
633+
description = EXAMPLE_DESCRIPTION,
634+
policyXml = EXAMPLE_POLICY_XML,
635+
operations = None,
636+
subscriptionRequired = False
637+
)
638+
639+
# Same subscriptionRequired values should be equal
640+
assert api1 == api2
641+
642+
# Different subscriptionRequired values should not be equal
643+
assert api1 != api3
644+
645+
@pytest.mark.unit
646+
def test_api_with_all_properties():
647+
"""Test creation of API object with all properties including subscriptionRequired."""
648+
tags = ["tag1", "tag2"]
649+
product_names = ["product1", "product2"]
650+
api = apimtypes.API(
651+
name = EXAMPLE_NAME,
652+
displayName = EXAMPLE_DISPLAY_NAME,
653+
path = EXAMPLE_PATH,
654+
description = EXAMPLE_DESCRIPTION,
655+
policyXml = EXAMPLE_POLICY_XML,
656+
operations = None,
657+
tags = tags,
658+
productNames = product_names,
659+
subscriptionRequired = True
660+
)
661+
662+
assert api.name == EXAMPLE_NAME
663+
assert api.displayName == EXAMPLE_DISPLAY_NAME
664+
assert api.path == EXAMPLE_PATH
665+
assert api.description == EXAMPLE_DESCRIPTION
666+
assert api.policyXml == EXAMPLE_POLICY_XML
667+
assert api.operations == []
668+
assert api.tags == tags
669+
assert api.productNames == product_names
670+
assert api.subscriptionRequired == True
671+
672+
d = api.to_dict()
673+
assert d["name"] == EXAMPLE_NAME
674+
assert d["displayName"] == EXAMPLE_DISPLAY_NAME
675+
assert d["path"] == EXAMPLE_PATH
676+
assert d["description"] == EXAMPLE_DESCRIPTION
677+
assert d["policyXml"] == EXAMPLE_POLICY_XML
678+
assert d["tags"] == tags
679+
assert d["productNames"] == product_names
680+
assert d["subscriptionRequired"] == True
681+
682+
683+
# ------------------------------

0 commit comments

Comments
 (0)