Skip to content

Commit a70bda5

Browse files
Add option 5 for named value by API (#189)
1 parent 3b5acdf commit a70bda5

8 files changed

Lines changed: 398 additions & 285 deletions

.github/copilot-instructions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,7 @@ Samples that require administrative or operational endpoints (cache loading, con
496496
- **Production security**: Subscription keys are a baseline gate but are shared secrets, not identity-based auth. Production deployments should layer JWT validation (`validate-azure-ad-token` or `validate-jwt`) on top of subscription keys. See the `authX` and `authX-pro` samples for implementation patterns.
497497
- **Naming**: Use kebab-case operation paths that describe the action (e.g. `/load-cache`, `/clear-cache`, `/refresh-config`).
498498
- **Tags**: Include the sample's tags so the admin API is grouped with its sibling APIs in the APIM portal.
499-
- **Documentation**: The admin API's display name should start with the phase or sample context (e.g. `Phase 3 Admin`) so its purpose is clear in the APIM portal.
499+
- **Documentation**: The admin API's display name should start with the option or sample context (e.g. `Option 3 Admin`) so its purpose is clear in the APIM portal.
500500

501501
### API Management Policy XML Instructions
502502

samples/dynamic-cors/README.md

Lines changed: 47 additions & 38 deletions
Large diffs are not rendered by default.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<!--
2+
API-level policy for Dynamic CORS with per-API Named Value.
3+
4+
Sets the 'allowedOriginsJson' context variable from a per-API Named Value,
5+
then includes the specified CORS policy fragment. The outbound section includes
6+
the DynamicCorsOutbound fragment to add the Access-Control-Allow-Origin header
7+
to actual (non-preflight) responses. This pattern allows the same fragments to
8+
serve every API and environment - only the Named Value content differs per
9+
deployment.
10+
11+
Placeholders (replaced at deployment time):
12+
{allowed_origins_nv} - The Named Value reference (e.g. {{CorsOrigins-my-api-id}})
13+
-->
14+
<policies>
15+
<inbound>
16+
<base />
17+
<set-variable name="allowedOriginsJson" value="{allowed_origins_nv}" />
18+
<include-fragment fragment-id="DynamicCorsNvPerApi" />
19+
</inbound>
20+
<backend>
21+
<base />
22+
</backend>
23+
<outbound>
24+
<base />
25+
<include-fragment fragment-id="DynamicCorsOutbound" />
26+
</outbound>
27+
<on-error>
28+
<base />
29+
</on-error>
30+
</policies>

samples/dynamic-cors/cors-api-policy.xml

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
- For OPTIONS preflight requests, returns an immediate CORS response.
77
- For all other methods, sets the 'corsOrigin' context variable.
88
9-
The outbound section demonstrates how to add the Access-Control-Allow-Origin header
10-
for APIs with real backends. In this sample, the mock APIs use return-response in the
11-
operation policy (which bypasses outbound), so the CORS header is included directly
12-
in the operation's return-response instead.
9+
The outbound section includes the DynamicCorsOutbound fragment to add the
10+
Access-Control-Allow-Origin header to actual (non-preflight) responses. For mock
11+
APIs that use return-response in the operation policy, outbound is bypassed, so
12+
the fragment has no effect.
1313
-->
1414
<policies>
1515
<inbound>
@@ -21,19 +21,7 @@
2121
</backend>
2222
<outbound>
2323
<base />
24-
<!--
25-
For APIs with real backends, uncomment the block below to add CORS headers to actual responses.
26-
The 'corsOrigin' context variable is set by the included policy fragment.
27-
-->
28-
<!--
29-
<choose>
30-
<when condition="@(!string.IsNullOrEmpty(context.Variables.GetValueOrDefault<string>("corsOrigin", "")))">
31-
<set-header name="Access-Control-Allow-Origin" exists-action="override">
32-
<value>@((string)context.Variables["corsOrigin"])</value>
33-
</set-header>
34-
</when>
35-
</choose>
36-
-->
24+
<include-fragment fragment-id="DynamicCorsOutbound" />
3725
</outbound>
3826
<on-error>
3927
<base />

samples/dynamic-cors/cors-baseline-native.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
fails for Analytics (dashboard origin is missing) and Public Data (no wildcard support
77
without adding '*' which would open all APIs).
88
9-
This is the "before" state that motivates the dynamic CORS approach in Phases 1 and 2.
9+
This is the "before" state that motivates the dynamic CORS approach in Options 1 and 2.
1010
-->
1111
<policies>
1212
<inbound>

samples/dynamic-cors/create.ipynb

Lines changed: 215 additions & 228 deletions
Large diffs are not rendered by default.
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<!--
2+
Policy fragment: Dynamic CORS (Named Value, per-API)
3+
4+
Evaluates the Origin header against a JSON array passed in via a context variable.
5+
The calling API policy must set 'allowedOriginsJson' before including this fragment,
6+
typically by referencing a per-API Named Value:
7+
8+
<set-variable name="allowedOriginsJson" value="{{CorsOrigins-my-api-id}}" />
9+
<include-fragment fragment-id="DynamicCorsNvPerApi" />
10+
11+
Expected context variable format:
12+
allowedOriginsJson = '["https://origin-a.com","https://origin-b.com"]'
13+
14+
This pattern makes the fragment completely environment-agnostic. The same fragment
15+
works across all environments; only the Named Value content differs per deployment.
16+
Each API controls its own origins independently, with no character-limit concerns
17+
from a shared mapping object.
18+
19+
Context variables expected:
20+
allowedOriginsJson - JSON array of allowed origins (set by the calling API policy).
21+
22+
Context variables set:
23+
corsOrigin - The validated origin if allowed, or empty string if not.
24+
-->
25+
<fragment>
26+
<!-- Evaluate the Origin header against the per-API origin array from the Named Value -->
27+
<set-variable name="corsOrigin" value="@{
28+
var origin = context.Request.Headers.GetValueOrDefault("Origin", "");
29+
if (string.IsNullOrEmpty(origin)) { return ""; }
30+
31+
var allowedOriginsJson = context.Variables.GetValueOrDefault<string>("allowedOriginsJson", "[]");
32+
var allowedOrigins = JArray.Parse(allowedOriginsJson);
33+
34+
if (allowedOrigins.Any(o => o.ToString() == "*")) { return origin; }
35+
if (allowedOrigins.Any(o => o.ToString() == origin)) { return origin; }
36+
37+
return "";
38+
}" />
39+
<choose>
40+
<when condition="@(context.Request.Method == "OPTIONS")">
41+
<choose>
42+
<when condition="@(!string.IsNullOrEmpty(context.Variables.GetValueOrDefault<string>("corsOrigin", "")))">
43+
<return-response>
44+
<set-status code="200" reason="OK" />
45+
<set-header name="Access-Control-Allow-Origin" exists-action="override">
46+
<value>@((string)context.Variables["corsOrigin"])</value>
47+
</set-header>
48+
<set-header name="Access-Control-Allow-Methods" exists-action="override">
49+
<value>GET, POST, OPTIONS</value>
50+
</set-header>
51+
<set-header name="Access-Control-Allow-Headers" exists-action="override">
52+
<value>Content-Type, Authorization, X-Requested-With</value>
53+
</set-header>
54+
<set-header name="Access-Control-Max-Age" exists-action="override">
55+
<value>86400</value>
56+
</set-header>
57+
</return-response>
58+
</when>
59+
<otherwise>
60+
<trace source="DynamicCorsNvPerApi" severity="information">
61+
<message>CORS origin rejected by per-API Named Value mapping.</message>
62+
<metadata name="apiId" value="@(context.Api.Id)" />
63+
<metadata name="operationId" value="@(context.Operation.Id)" />
64+
<metadata name="origin" value="@(context.Request.Headers.GetValueOrDefault("Origin", ""))" />
65+
</trace>
66+
<return-response>
67+
<set-status code="403" reason="CORS Origin Not Allowed" />
68+
<set-header name="Content-Type" exists-action="override">
69+
<value>application/json</value>
70+
</set-header>
71+
<set-body>{"error": "CORS origin not allowed for this API"}</set-body>
72+
</return-response>
73+
</otherwise>
74+
</choose>
75+
</when>
76+
</choose>
77+
</fragment>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<!--
2+
Policy fragment: Dynamic CORS Outbound
3+
4+
Adds the Access-Control-Allow-Origin header to actual (non-preflight) responses
5+
when 'corsOrigin' has been set by one of the inbound CORS fragments.
6+
7+
This fragment is intended for APIs with real backends. Mock APIs that use
8+
return-response in the operation policy bypass the outbound section entirely,
9+
so including this fragment is harmless but has no effect for mock responses.
10+
11+
Context variables expected:
12+
corsOrigin - The validated origin (set by the inbound CORS fragment).
13+
-->
14+
<fragment>
15+
<choose>
16+
<when condition="@(!string.IsNullOrEmpty(context.Variables.GetValueOrDefault<string>("corsOrigin", "")))">
17+
<set-header name="Access-Control-Allow-Origin" exists-action="override">
18+
<value>@((string)context.Variables["corsOrigin"])</value>
19+
</set-header>
20+
</when>
21+
</choose>
22+
</fragment>

0 commit comments

Comments
 (0)