Skip to content

Commit 2b9f9bc

Browse files
Feature/valet key with blob storage (#36)
1 parent 60a77d3 commit 2b9f9bc

22 files changed

+1558
-47
lines changed

TEMPLATE_PARAMETERS_USAGE.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Azure APIM templateParameters Support
2+
3+
The Python APIMTypes now support optional `templateParameters` for API operations. This resolves the issue where APIM operations with template parameters in their URL templates (like `/{blob-name}`) would fail with the error:
4+
5+
> "All template parameters used in the UriTemplate must be defined in the Operation"
6+
7+
## What was changed
8+
9+
1. **Bicep module** (`shared/bicep/modules/apim/v1/api.bicep`): Added support for optional `templateParameters` using the safe access operator:
10+
```bicep
11+
templateParameters: op.?templateParameters ?? []
12+
```
13+
14+
2. **Python APIMTypes** (`shared/python/apimtypes.py`): Added optional `templateParameters` field to:
15+
- `APIOperation` base class
16+
- `GET_APIOperation2` class (most commonly used for operations with parameters)
17+
18+
## Usage Example
19+
20+
### Before (would cause deployment error):
21+
```python
22+
blob_get = GET_APIOperation2('GET', 'GET', '/{blob-name}', 'Gets the blob access valet key', blob_get_policy_xml)
23+
```
24+
25+
### After (with templateParameters):
26+
```python
27+
# Define template parameters for the {blob-name} parameter
28+
blob_name_template_params = [
29+
{
30+
"name": "blob-name",
31+
"description": "The name of the blob to access",
32+
"type": "string",
33+
"required": True
34+
}
35+
]
36+
37+
# Create operation with template parameters
38+
blob_get = GET_APIOperation2(
39+
name='GET',
40+
displayName='GET',
41+
urlTemplate='/{blob-name}',
42+
description='Gets the blob access valet key',
43+
policyXml=blob_get_policy_xml,
44+
templateParameters=blob_name_template_params
45+
)
46+
```
47+
48+
## Template Parameter Format
49+
50+
Template parameters should be a list of dictionaries with the following properties:
51+
52+
```python
53+
{
54+
"name": "parameter-name", # Required: matches the parameter in URL template
55+
"description": "Parameter description", # Optional: human-readable description
56+
"type": "string", # Optional: parameter type (string, int, etc.)
57+
"required": True # Optional: whether parameter is required
58+
}
59+
```
60+
61+
## Common Use Cases
62+
63+
1. **Blob/File Access**: `/{file-name}`, `/{blob-name}`
64+
2. **Resource IDs**: `/{user-id}`, `/{product-id}`
65+
3. **Nested Resources**: `/{category}/{item-id}`
66+
67+
## Backward Compatibility
68+
69+
The `templateParameters` field is optional, so existing code will continue to work without changes. However, operations with template parameters in their URL templates should add the `templateParameters` field to avoid deployment errors.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
@startuml "Secure Blob Access Architecture"
2+
3+
!include ./base.puml
4+
!includeurl AzurePuml/Storage/AzureBlobStorage.puml
5+
!includeurl AzurePuml/Identity/AzureManagedIdentity.puml
6+
7+
title Secure Blob Access with JWT Authentication & Valet Key Pattern
8+
9+
' Main components
10+
actor "Client\n(HR Member)" as client #LightBlue
11+
AzureAPIManagement(apim, "API Management", "JWT Auth + Valet Key")
12+
AzureManagedIdentity(mi, "Managed Identity", "APIM Identity")
13+
AzureBlobStorage(storage, "Blob Storage", "Private Container")
14+
AzureApplicationInsights(insights, "Application Insights", "Monitoring")
15+
16+
' Authentication flow
17+
note over client, apim
18+
**Authentication Flow**
19+
1. Client sends JWT with HR Member role
20+
2. APIM validates JWT signature & role claims
21+
3. APIM authorizes based on role ID
22+
end note
23+
24+
' Valet key flow
25+
note over apim, storage
26+
**Valet Key Generation**
27+
4. APIM uses managed identity
28+
5. Generates time-limited SAS token (5 min)
29+
6. Returns secure URL to client
30+
end note
31+
32+
' Direct access
33+
note over client, storage
34+
**Direct Access**
35+
7. Client accesses blob directly
36+
8. No bandwidth through APIM
37+
end note
38+
39+
' Define the relationships
40+
client --> apim : "1. Request blob access\n(JWT with HR Member role)"
41+
apim --> apim : "2. Validate JWT &\nauthorize role"
42+
apim --> mi : "3. Use managed identity"
43+
mi --> storage : "4. Generate SAS token\n(Storage Blob Data Reader)"
44+
storage --> apim : "5. Return SAS URL"
45+
apim --> client : "6. Return secure URL\n(time-limited)"
46+
client --> storage : "7. Direct blob access\n(using SAS URL)"
47+
apim --> insights : "Log requests\n& telemetry"
48+
49+
@enduml
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Example usage of templateParameters support in APIMTypes.
4+
5+
This example demonstrates how to create API operations with template parameters
6+
that match URL template variables used in APIM operations.
7+
"""
8+
9+
import sys
10+
import os
11+
12+
# Add the shared python module to the path
13+
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'shared', 'python'))
14+
15+
from apimtypes import API, APIOperation, GET_APIOperation2, HTTP_VERB
16+
17+
def create_blob_access_api_example():
18+
"""
19+
Example: Create an API with a blob access operation that uses template parameters.
20+
21+
This matches the secure-blob-access sample where we need:
22+
- URL Template: /blobs/{blob-name}
23+
- Template Parameter: blob-name (required, string)
24+
"""
25+
26+
# Define template parameters for the blob name
27+
blob_template_parameters = [
28+
{
29+
"name": "blob-name",
30+
"description": "The name of the blob to access",
31+
"type": "string",
32+
"required": True
33+
}
34+
]
35+
36+
# Read the policy XML (in a real scenario, you'd read from the actual file)
37+
# For this example, we'll use a simple mock policy
38+
blob_policy_xml = """<policies>
39+
<inbound>
40+
<base />
41+
<set-backend-service base-url="https://yourstorageaccount.blob.core.windows.net/container" />
42+
<rewrite-uri template="/{{blob-name}}" />
43+
</inbound>
44+
<backend>
45+
<base />
46+
</backend>
47+
<outbound>
48+
<base />
49+
</outbound>
50+
<on-error>
51+
<base />
52+
</on-error>
53+
</policies>"""
54+
55+
# Create the GET operation with template parameters
56+
blob_get_operation = GET_APIOperation2(
57+
name="get-blob",
58+
displayName="Get Blob",
59+
urlTemplate="/blobs/{blob-name}",
60+
description="Retrieve a blob by name",
61+
policyXml=blob_policy_xml,
62+
templateParameters=blob_template_parameters
63+
)
64+
# Create the API with the operation (use explicit policy to avoid file path issues)
65+
simple_policy = """<policies>
66+
<inbound>
67+
<base />
68+
</inbound>
69+
<backend>
70+
<base />
71+
</backend>
72+
<outbound>
73+
<base />
74+
</outbound>
75+
<on-error>
76+
<base />
77+
</on-error>
78+
</policies>"""
79+
80+
blob_api = API(
81+
name="blob-access-api",
82+
displayName="Blob Access API",
83+
path="/blob",
84+
description="API for secure blob access",
85+
policyXml=simple_policy,
86+
operations=[blob_get_operation]
87+
)
88+
89+
return blob_api
90+
91+
def create_user_management_api_example():
92+
"""
93+
Example: Create a user management API with multiple template parameters.
94+
"""
95+
96+
# Template parameters for user operations
97+
user_template_parameters = [
98+
{
99+
"name": "user-id",
100+
"description": "The unique identifier of the user",
101+
"type": "string",
102+
"required": True
103+
},
104+
{
105+
"name": "department",
106+
"description": "The department code",
107+
"type": "string",
108+
"required": False
109+
}
110+
]
111+
# Create a user operation using the base APIOperation class (with explicit policy)
112+
user_policy = """<policies>
113+
<inbound>
114+
<base />
115+
</inbound>
116+
<backend>
117+
<base />
118+
</backend>
119+
<outbound>
120+
<base />
121+
</outbound>
122+
<on-error>
123+
<base />
124+
</on-error>
125+
</policies>"""
126+
127+
get_user_operation = APIOperation(
128+
name="get-user-by-dept",
129+
displayName="Get User by Department",
130+
urlTemplate="/users/{user-id}/department/{department}",
131+
method=HTTP_VERB.GET,
132+
description="Get user information filtered by department",
133+
policyXml=user_policy,
134+
templateParameters=user_template_parameters
135+
)
136+
137+
# Create the API (with explicit policy)
138+
user_api = API(
139+
name="user-management-api",
140+
displayName="User Management API",
141+
path="/users",
142+
description="API for user management operations",
143+
policyXml=user_policy,
144+
operations=[get_user_operation]
145+
)
146+
147+
return user_api
148+
149+
def main():
150+
"""
151+
Demonstrate the usage of templateParameters in API operations.
152+
"""
153+
154+
print("=== APIMTypes templateParameters Usage Examples ===\n")
155+
156+
# Example 1: Blob access API
157+
print("1. Blob Access API Example:")
158+
blob_api = create_blob_access_api_example()
159+
blob_dict = blob_api.to_dict()
160+
161+
print(f" API Name: {blob_dict['name']}")
162+
print(f" API Path: {blob_dict['path']}")
163+
164+
if blob_dict['operations']:
165+
operation = blob_dict['operations'][0]
166+
print(f" Operation: {operation['name']}")
167+
print(f" URL Template: {operation['urlTemplate']}")
168+
print(f" Template Parameters: {len(operation.get('templateParameters', []))}")
169+
170+
for param in operation.get('templateParameters', []):
171+
print(f" - {param['name']}: {param['description']} (required: {param.get('required', False)})")
172+
173+
print()
174+
175+
# Example 2: User management API
176+
print("2. User Management API Example:")
177+
user_api = create_user_management_api_example()
178+
user_dict = user_api.to_dict()
179+
180+
print(f" API Name: {user_dict['name']}")
181+
print(f" API Path: {user_dict['path']}")
182+
183+
if user_dict['operations']:
184+
operation = user_dict['operations'][0]
185+
print(f" Operation: {operation['name']}")
186+
print(f" URL Template: {operation['urlTemplate']}")
187+
print(f" Template Parameters: {len(operation.get('templateParameters', []))}")
188+
189+
for param in operation.get('templateParameters', []):
190+
print(f" - {param['name']}: {param['description']} (required: {param.get('required', False)})")
191+
192+
print("\n=== Conversion to Bicep-compatible format ===")
193+
print("These API objects can now be serialized and passed to Bicep templates")
194+
print("that support the templateParameters property for APIM operations.")
195+
196+
if __name__ == "__main__":
197+
main()

requirements.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@ pandas
66
matplotlib
77
pyjwt
88
pytest
9-
pytest-cov
9+
pytest-cov
10+
azure.storage.blob
11+
azure.identity

samples/authX-pro/create.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@
127127
" PolicyFragment('Http-Response-200', pf_http_response_200_xml, 'Returns a 200 OK response for the current HTTP method.')\n",
128128
"]\n",
129129
"\n",
130-
"# 5) Define the Products (NEW: moved authentication to product level)\n",
130+
"# 5) Define the Products\n",
131131
"\n",
132132
"# HR Product with authentication policy, including authorization via a required claim check for HR member role\n",
133133
"hr_product_xml = utils.read_policy_xml('./hr_product.xml').format(\n",

samples/authX-pro/main.bicep

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ resource apimService 'Microsoft.ApiManagement/service@2024-06-01-preview' existi
3535
}
3636

3737
// APIM Named Values
38-
module namedValue '../../shared/bicep/modules/apim/v1/named-value.bicep' = [for nv in namedValues: {
38+
module namedValueModule '../../shared/bicep/modules/apim/v1/named-value.bicep' = [for nv in namedValues: {
3939
name: 'nv-${nv.name}'
4040
params:{
4141
apimName: apimName
@@ -46,7 +46,7 @@ module namedValue '../../shared/bicep/modules/apim/v1/named-value.bicep' = [for
4646
}]
4747

4848
// APIM Policy Fragments
49-
module policyFragment '../../shared/bicep/modules/apim/v1/policy-fragment.bicep' = [for pf in policyFragments: {
49+
module policyFragmentModule '../../shared/bicep/modules/apim/v1/policy-fragment.bicep' = [for pf in policyFragments: {
5050
name: 'pf-${pf.name}'
5151
params:{
5252
apimName: apimName
@@ -55,12 +55,12 @@ module policyFragment '../../shared/bicep/modules/apim/v1/policy-fragment.bicep'
5555
policyFragmentValue: pf.policyXml
5656
}
5757
dependsOn: [
58-
namedValue
58+
namedValueModule
5959
]
6060
}]
6161

6262
// APIM Products
63-
module productHr '../../shared/bicep/modules/apim/v1/product.bicep' = [for product in products: {
63+
module productModule '../../shared/bicep/modules/apim/v1/product.bicep' = [for product in products: {
6464
name: 'product-${product.name}'
6565
params: {
6666
apimName: apimName
@@ -73,8 +73,8 @@ module productHr '../../shared/bicep/modules/apim/v1/product.bicep' = [for produ
7373
policyXml: product.policyXml
7474
}
7575
dependsOn: [
76-
namedValue
77-
policyFragment
76+
namedValueModule
77+
policyFragmentModule
7878
]
7979
}]
8080

@@ -90,9 +90,9 @@ module apisModule '../../shared/bicep/modules/apim/v1/api.bicep' = [for api in a
9090
productNames: api.productNames ?? []
9191
}
9292
dependsOn: [
93-
namedValue // ensure all named values are created before APIs
94-
policyFragment // ensure all policy fragments are created before APIs
95-
productHr // ensure all products are fully created before APIs
93+
namedValueModule // ensure all named values are created before APIs
94+
policyFragmentModule // ensure all policy fragments are created before APIs
95+
productModule // ensure all products are fully created before APIs
9696
]
9797
}]
9898

0 commit comments

Comments
 (0)