Skip to content

Commit b2b30a3

Browse files
authored
fix: apim and tests (#124)
1 parent 5c3c2a3 commit b2b30a3

File tree

10 files changed

+318
-70
lines changed

10 files changed

+318
-70
lines changed

.github/skills/api-management/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ All routes live inside a single `<choose>` block in the inbound section. Routes
6262

6363
| Route | Path condition | Feature flag |
6464
|---|---|---|
65-
| Key rotation internal endpoint | path ends with `internal/apim-keys` | `key_rotation_enabled` |
65+
| APIM keys internal endpoint | path ends with `internal/apim-keys` | `apim_keys_endpoint_enabled` |
6666
| Tenant info internal endpoint | path ends with `internal/tenant-info` | `tenant_info_enabled` (always `true`) |
6767
| Document Intelligence | path contains `documentintelligence`, `formrecognizer`, or `documentmodels` | `document_intelligence_enabled` |
6868
| OpenAI | path contains `openai` | `openai_enabled` (auto-set when `model_deployments` is non-empty) |

.github/skills/api-management/SKILLS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ All routes live inside a single `<choose>` block in the inbound section. Routes
7777

7878
| Route | Path condition | Feature flag |
7979
|---|---|---|
80-
| Key rotation internal endpoint | path ends with `internal/apim-keys` | `key_rotation_enabled` |
80+
| APIM keys internal endpoint | path ends with `internal/apim-keys` | `apim_keys_endpoint_enabled` |
8181
| Tenant info internal endpoint | path ends with `internal/tenant-info` | `tenant_info_enabled` (always `true`) |
8282
| Document Intelligence | path contains `documentintelligence`, `formrecognizer`, or `documentmodels` | `document_intelligence_enabled` |
8383
| OpenAI | path contains `openai` | `openai_enabled` (auto-set when `model_deployments` is non-empty) |

docs/_pages/apim-internal-endpoints.html

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ <h1>APIM Internal Endpoints</h1>
1919
<div class="card">
2020
<h3 style="margin-top:0;"><code>/internal/apim-keys</code></h3>
2121
<p>Returns both subscription keys and rotation metadata sourced from the centralized hub Key Vault at request time.</p>
22-
<p><strong>Requires:</strong> key rotation enabled (<code>rotation_enabled = true</code> in <code>shared.tfvars</code>).</p>
22+
<p><strong>Requires:</strong> APIM enabled with subscription-key auth. Available for <strong>all</strong> tenants (not gated on per-tenant key rotation).</p>
2323
<a href="#apim-keys">View reference &darr;</a>
2424
</div>
2525
<div class="card">
@@ -51,8 +51,8 @@ <h3>Authentication</h3>
5151
<h3>How It Works</h3>
5252
<ol>
5353
<li>APIM validates the incoming subscription key (standard APIM behavior).</li>
54-
<li>APIM policy uses its <strong>system-assigned managed identity</strong> to read three secrets from the centralized hub Key Vault: <code>{tenant}-apim-primary-key</code>, <code>{tenant}-apim-secondary-key</code>, and <code>{tenant}-apim-rotation-metadata</code>.</li>
55-
<li>Secrets are assembled into a JSON response and returned directly &mdash; no backend service is called.</li>
54+
<li>APIM policy uses its <strong>system-assigned managed identity</strong> to read secrets from the centralized hub Key Vault: <code>{tenant}-apim-primary-key</code> and <code>{tenant}-apim-secondary-key</code> (always), plus <code>{tenant}-apim-rotation-metadata</code> (only for tenants with <code>key_rotation_enabled = true</code>).</li>
55+
<li>Secrets are assembled into a JSON response and returned directly &mdash; no backend service is called. Tenants without rotation enabled receive a default <code>rotation</code> object indicating rotation is not active.</li>
5656
</ol>
5757
</div>
5858

@@ -104,7 +104,7 @@ <h3>Error Responses</h3>
104104
</thead>
105105
<tbody>
106106
<tr><td><code>401</code></td><td>Missing or invalid subscription key.</td><td>Standard APIM 401</td></tr>
107-
<tr><td><code>404</code></td><td>Key rotation is disabled for this environment (<code>rotation_enabled = false</code>); route not present in policy.</td><td><code>NotFound</code></td></tr>
107+
<tr><td><code>404</code></td><td>Tenant does not use subscription-key auth mode; endpoint not present in policy.</td><td><code>NotFound</code></td></tr>
108108
<tr><td><code>405</code></td><td>Non-GET method used.</td><td><code>MethodNotAllowed</code></td></tr>
109109
<tr><td><code>502</code></td><td>APIM managed identity could not read one or more secrets from the hub Key Vault.</td><td><code>KeyVaultReadFailed</code></td></tr>
110110
</tbody>
@@ -113,8 +113,8 @@ <h3>Error Responses</h3>
113113

114114
<div class="card">
115115
<h3>Infrastructure</h3>
116-
<p>Implemented purely in APIM policy. APIM's system-assigned managed identity holds a single <code>Key Vault Secrets User</code> RBAC assignment on the centralized hub Key Vault &mdash; one assignment scales to all tenants. Enabled only when <code>shared.tfvars</code> has <code>key_rotation.rotation_enabled = true</code>.</p>
117-
<p>Source: <code>infra-ai-hub/params/apim/api_policy.xml.tftpl</code> (<code>key_rotation_enabled</code> block).</p>
116+
<p>Implemented purely in APIM policy. APIM's system-assigned managed identity holds a single <code>Key Vault Secrets User</code> RBAC assignment on the centralized hub Key Vault &mdash; one assignment scales to all tenants. Available for all subscription-key tenants when APIM is enabled. Rotation metadata is included only for tenants with <code>key_rotation_enabled = true</code>; other tenants receive a default rotation object indicating rotation is not active.</p>
117+
<p>Source: <code>infra-ai-hub/params/apim/api_policy.xml.tftpl</code> (<code>apim_keys_endpoint_enabled</code> block).</p>
118118
</div>
119119

120120
<!-- ============================================================ -->

infra-ai-hub/modules/key-rotation-function/main.tf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ resource "terraform_data" "image_refresh" {
3030
# Container App Job (cron-triggered)
3131
# ---------------------------------------------------------------------------
3232
resource "azurerm_container_app_job" "rotation" {
33-
name = "${var.name_prefix}-rotation-job"
33+
name = "${var.name_prefix}rotnjob"
3434
resource_group_name = var.resource_group_name
3535
location = var.location
3636
container_app_environment_id = var.container_app_environment_id

infra-ai-hub/params/apim/api_policy.xml.tftpl

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,10 @@
7878
7979
<!-- Path-based routing to appropriate backend service -->
8080
<choose>
81-
%{ if key_rotation_enabled ~}
81+
%{ if apim_keys_endpoint_enabled ~}
8282
<!-- Internal endpoint: fetch APIM subscription keys from Key Vault -->
8383
<!-- Returns both primary and secondary keys + rotation metadata as JSON -->
84+
<!-- Available for ALL subscription-key tenants (keys stored in KV for all) -->
8485
<when condition="@(context.Request.Url.Path.ToLower().EndsWith(&quot;internal/apim-keys&quot;))">
8586
<!-- Only allow GET -->
8687
<choose>
@@ -112,20 +113,21 @@
112113
<value>@("Bearer " + (string)context.Variables["kv-token"])</value>
113114
</set-header>
114115
</send-request>
115-
<!-- Fetch rotation metadata from hub Key Vault -->
116+
%{ if key_rotation_enabled ~}
117+
<!-- Fetch rotation metadata from hub Key Vault (only for rotation-opted tenants) -->
116118
<send-request mode="new" response-variable-name="kvMetadataResp" timeout="10" ignore-error="true">
117119
<set-url>@($"${keyvault_uri}secrets/${tenant_name}-apim-rotation-metadata?api-version=7.4")</set-url>
118120
<set-method>GET</set-method>
119121
<set-header name="Authorization" exists-action="override">
120122
<value>@("Bearer " + (string)context.Variables["kv-token"])</value>
121123
</set-header>
122124
</send-request>
123-
<!-- Return explicit error if any Key Vault call failed -->
125+
%{ endif ~}
126+
<!-- Return explicit error if primary or secondary Key Vault reads failed -->
124127
<choose>
125128
<when condition="@(
126129
((IResponse)context.Variables[&quot;kvPrimaryResp&quot;]) == null || ((IResponse)context.Variables[&quot;kvPrimaryResp&quot;]).StatusCode &gt;= 400 ||
127-
((IResponse)context.Variables[&quot;kvSecondaryResp&quot;]) == null || ((IResponse)context.Variables[&quot;kvSecondaryResp&quot;]).StatusCode &gt;= 400 ||
128-
((IResponse)context.Variables[&quot;kvMetadataResp&quot;]) == null || ((IResponse)context.Variables[&quot;kvMetadataResp&quot;]).StatusCode &gt;= 400
130+
((IResponse)context.Variables[&quot;kvSecondaryResp&quot;]) == null || ((IResponse)context.Variables[&quot;kvSecondaryResp&quot;]).StatusCode &gt;= 400
129131
)">
130132
<return-response>
131133
<set-status code="502" reason="Bad Gateway" />
@@ -135,16 +137,14 @@
135137
<set-body>@{
136138
var primaryResp = ((IResponse)context.Variables["kvPrimaryResp"]);
137139
var secondaryResp = ((IResponse)context.Variables["kvSecondaryResp"]);
138-
var metadataResp = ((IResponse)context.Variables["kvMetadataResp"]);
139140
140141
var result = new JObject(
141142
new JProperty("error", new JObject(
142143
new JProperty("code", "KeyVaultReadFailed"),
143-
new JProperty("message", "Failed to read one or more APIM key secrets from Key Vault."),
144+
new JProperty("message", "Failed to read APIM key secrets from Key Vault."),
144145
new JProperty("details", new JObject(
145146
new JProperty("primary_status", primaryResp == null ? 0 : primaryResp.StatusCode),
146-
new JProperty("secondary_status", secondaryResp == null ? 0 : secondaryResp.StatusCode),
147-
new JProperty("metadata_status", metadataResp == null ? 0 : metadataResp.StatusCode)
147+
new JProperty("secondary_status", secondaryResp == null ? 0 : secondaryResp.StatusCode)
148148
))
149149
))
150150
);
@@ -162,15 +162,22 @@
162162
<set-body>@{
163163
var primaryBody = ((IResponse)context.Variables["kvPrimaryResp"])?.Body?.As<JObject>(preserveContent: true);
164164
var secondaryBody = ((IResponse)context.Variables["kvSecondaryResp"])?.Body?.As<JObject>(preserveContent: true);
165-
var metadataBody = ((IResponse)context.Variables["kvMetadataResp"])?.Body?.As<JObject>(preserveContent: true);
166165
167166
var primaryKey = primaryBody?["value"]?.ToString() ?? "";
168167
var secondaryKey = secondaryBody?["value"]?.ToString() ?? "";
169-
var metadataRaw = metadataBody?["value"]?.ToString() ?? "{}";
170168
169+
%{ if key_rotation_enabled ~}
170+
var metadataBody = ((IResponse)context.Variables["kvMetadataResp"])?.Body?.As<JObject>(preserveContent: true);
171+
var metadataRaw = metadataBody?["value"]?.ToString() ?? "{}";
171172
JObject metadata;
172173
try { metadata = JObject.Parse(metadataRaw); }
173174
catch { metadata = new JObject(); }
175+
%{ else ~}
176+
var metadata = new JObject(
177+
new JProperty("key_rotation_enabled", false),
178+
new JProperty("message", "Automatic key rotation is not enabled for this tenant")
179+
);
180+
%{ endif ~}
174181
175182
var result = new JObject(
176183
new JProperty("tenant", "${tenant_name}"),

infra-ai-hub/params/test/shared.tfvars

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ shared_config = {
6666
# Subscription key rotation (runs as Container App Job — see stacks/key-rotation)
6767
key_rotation = {
6868
rotation_enabled = true # Global toggle on; per-tenant opt-in via key_rotation_enabled
69-
rotation_interval_days = 60 # Must be less than 90 days (APIM max key lifetime)
69+
rotation_interval_days = 2 # Must be less than 90 days (APIM max key lifetime)
7070
}
7171
}
7272

0 commit comments

Comments
 (0)