Skip to content

Commit c12d550

Browse files
Merge branch 'main' into copilot/fix-autoentities-in-child-configs
2 parents fefffea + 8f3c217 commit c12d550

17 files changed

Lines changed: 697 additions & 30 deletions

src/Azure.DataApiBuilder.Mcp/Core/DynamicCustomTool.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,7 @@ private JsonElement BuildInputSchema()
361361
}
362362

363363
Dictionary<string, object> properties = new();
364+
List<string> requiredParameters = new();
364365
foreach ((string paramName, ParameterDefinition paramDef) in spDefinition.Parameters)
365366
{
366367
Dictionary<string, object> paramSchema = new()
@@ -370,6 +371,13 @@ private JsonElement BuildInputSchema()
370371
};
371372

372373
properties[paramName] = paramSchema;
374+
375+
// A DB metadata parameter is required unless config marks it optional or supplies
376+
// a default the engine applies when the caller omits it.
377+
if (IsParameterRequired(paramDef.Required, paramDef.HasConfigDefault))
378+
{
379+
requiredParameters.Add(paramName);
380+
}
373381
}
374382

375383
Dictionary<string, object> schema = new()
@@ -378,6 +386,11 @@ private JsonElement BuildInputSchema()
378386
["properties"] = properties
379387
};
380388

389+
if (requiredParameters.Count > 0)
390+
{
391+
schema["required"] = requiredParameters;
392+
}
393+
381394
return JsonSerializer.SerializeToElement(schema);
382395
}
383396

@@ -396,6 +409,7 @@ private JsonElement BuildInputSchemaFromConfig()
396409
if (_entity.Source.Parameters != null && _entity.Source.Parameters.Any())
397410
{
398411
Dictionary<string, object> properties = (Dictionary<string, object>)schema["properties"];
412+
List<string> requiredParameters = new();
399413

400414
foreach (ParameterMetadata param in _entity.Source.Parameters)
401415
{
@@ -404,12 +418,40 @@ private JsonElement BuildInputSchemaFromConfig()
404418
["type"] = new[] { "string", "number", "boolean", "null" },
405419
["description"] = param.Description ?? $"Parameter {param.Name}"
406420
};
421+
422+
// A parameter is required unless config marks it optional or supplies a default.
423+
if (IsParameterRequired(param.Required, param.Default is not null))
424+
{
425+
requiredParameters.Add(param.Name);
426+
}
427+
}
428+
429+
if (requiredParameters.Count > 0)
430+
{
431+
schema["required"] = requiredParameters;
407432
}
408433
}
409434

410435
return JsonSerializer.SerializeToElement(schema);
411436
}
412437

438+
/// <summary>
439+
/// Determines whether a stored procedure parameter should be advertised as required in the
440+
/// tool input schema. A parameter is required when configuration does not mark it optional
441+
/// and does not provide a default value the engine can apply when the caller omits it.
442+
/// </summary>
443+
private static bool IsParameterRequired(bool? configuredRequired, bool hasDefault)
444+
{
445+
// If the engine can supply a default when the caller omits the parameter,
446+
// it should not be advertised as required.
447+
if (hasDefault)
448+
{
449+
return false;
450+
}
451+
452+
return configuredRequired ?? true;
453+
}
454+
413455
/// <summary>
414456
/// Maps a .NET System.Type to the appropriate JSON Schema type string.
415457
/// </summary>

src/Core/AuthenticationHelpers/AppServiceAuthenticationInformation.cs

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,19 @@ public static class AppServiceAuthenticationInfo
1515
/// Environment variable key whose value represents whether AppService EasyAuth is enabled ("true" or "false").
1616
/// </summary>
1717
public const string APPSERVICESAUTH_ENABLED_ENVVAR = "WEBSITE_AUTH_ENABLED";
18-
/// <summary>
19-
/// Environment variable key whose value represents Identity Provider such as "AzureActiveDirectory"
20-
/// </summary>
21-
public const string APPSERVICESAUTH_IDENTITYPROVIDER_ENVVAR = "WEBSITE_AUTH_DEFAULT_PROVIDER";
18+
19+
// ── AppService messages ──────────────────────────────────────────────────────────────────────
2220
/// <summary>
2321
/// Error message used when AppService Authentication is configured in production mode in a non AppService Environment.
2422
/// </summary>
25-
public const string APPSERVICE_PROD_MISSING_ENV_CONFIG = "AppService environment not detected while runtime is in production mode.";
23+
public const string APPSERVICE_PROD_MISSING_ENV_CONFIG =
24+
"App Service: Cannot start in production: EasyAuth is configured with host.mode set to production, but the Azure App Service environment could not be detected because the WEBSITE_AUTH_ENABLED environment variable was missing or did not have the value true. DAB requires an Azure App Service EasyAuth proxy in production because it trusts the X-MS-CLIENT-PRINCIPAL header as the authenticated user identity, and without the EasyAuth proxy this header could be forged. If this is a local or non-Azure deployment, set host.mode to development. If this is intended to run in production, deploy it behind Azure App Service, enable EasyAuth, and verify that WEBSITE_AUTH_ENABLED=true is available to the application.";
2625
/// <summary>
2726
/// Warning message logged when AppService environment not detected (applicable to development mode).
2827
/// </summary>
29-
public const string APPSERVICE_DEV_MISSING_ENV_CONFIG = "AppService environment not detected, EasyAuth authentication may not behave as expected.";
28+
public const string APPSERVICE_DEV_MISSING_ENV_CONFIG =
29+
"AppService environment not detected. The X-MS-CLIENT-PRINCIPAL header is not cryptographically " +
30+
"validated; EasyAuth authentication may not behave as expected outside an Azure App Service environment.";
3031

3132
/// <summary>
3233
/// Returns a best guess whether AppService is enabled in the environment by checking for
@@ -39,14 +40,12 @@ public static class AppServiceAuthenticationInfo
3940
/// </summary>
4041
public static bool AreExpectedAppServiceEnvVarsPresent()
4142
{
43+
// WEBSITE_AUTH_ENABLED is the only variable that is reliably injected by the Azure platform
44+
// whenever App Service Authentication (EasyAuth) is enabled, regardless of how many identity
45+
// providers are configured. WEBSITE_AUTH_DEFAULT_PROVIDER is only set when a *single* provider
46+
// is selected; multi-provider configurations leave it unset, so it must not be required here.
4247
string? appServiceEnabled = Environment.GetEnvironmentVariable(APPSERVICESAUTH_ENABLED_ENVVAR);
43-
string? appServiceIdentityProvider = Environment.GetEnvironmentVariable(APPSERVICESAUTH_IDENTITYPROVIDER_ENVVAR);
44-
45-
if (string.IsNullOrEmpty(appServiceEnabled) || string.IsNullOrEmpty(appServiceIdentityProvider))
46-
{
47-
return false;
48-
}
49-
50-
return appServiceEnabled.Equals(value: "true", comparisonType: StringComparison.OrdinalIgnoreCase);
48+
return appServiceEnabled is not null &&
49+
appServiceEnabled.Equals(value: "true", comparisonType: StringComparison.OrdinalIgnoreCase);
5150
}
5251
}

src/Core/AuthenticationHelpers/StaticWebAppsAuthentication.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,31 @@ public class StaticWebAppsClientPrincipal
3333
public IEnumerable<string>? UserRoles { get; set; }
3434
}
3535

36+
/// <summary>
37+
/// Environment variable key set by the Azure platform for every App Service and Static Web Apps
38+
/// hosted site. Its presence is the best-effort signal that the runtime is executing behind an
39+
/// Azure-managed proxy that injects and validates the X-MS-CLIENT-PRINCIPAL header.
40+
/// </summary>
41+
public const string WEBSITE_SITE_NAME_ENVVAR = "WEBSITE_SITE_NAME";
42+
43+
// ── StaticWebApps messages ───────────────────────────────────────────────────────────────────
44+
/// <summary>
45+
/// Error message used when StaticWebApps Authentication is configured in production mode
46+
/// without a detectable Azure-hosted environment.
47+
/// </summary>
48+
public const string SWA_PROD_MISSING_ENV_CONFIG =
49+
"StaticWebApps environment not detected while runtime is in production mode. " +
50+
"The X-MS-CLIENT-PRINCIPAL header is not cryptographically validated by DAB and can be trivially " +
51+
"forged when the service is not hosted behind an Azure Static Web Apps proxy. " +
52+
"Set host.mode to 'development' for local testing, or deploy behind Azure Static Web Apps.";
53+
54+
/// <summary>
55+
/// Warning message logged when StaticWebApps environment not detected (applicable to development mode).
56+
/// </summary>
57+
public const string SWA_DEV_MISSING_ENV_CONFIG =
58+
"StaticWebApps environment not detected. The X-MS-CLIENT-PRINCIPAL header is not cryptographically " +
59+
"validated; EasyAuth authentication may not behave as expected outside an Azure Static Web Apps environment.";
60+
3661
/// <summary>
3762
/// Base64 decodes and deserializes the x-ms-client-principal payload containing
3863
/// SWA token metadata.
@@ -98,4 +123,14 @@ error is NotSupportedException ||
98123

99124
return identity;
100125
}
126+
127+
/// <summary>
128+
/// Returns a best-effort indication that the runtime is executing inside an Azure Static Web Apps
129+
/// environment by checking for the presence of <see cref="WEBSITE_SITE_NAME_ENVVAR"/>, which is
130+
/// injected by the Azure platform for every SWA-hosted application.
131+
/// </summary>
132+
public static bool AreExpectedSWAEnvVarsPresent()
133+
{
134+
return !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(WEBSITE_SITE_NAME_ENVVAR));
135+
}
101136
}

src/Core/Resolvers/DWSqlQueryBuilder.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -452,12 +452,17 @@ public string Build(SqlUpsertQueryStructure structure)
452452
// Final query to be executed for the given PUT/PATCH operation.
453453
StringBuilder upsertQuery = new(prefixQuery);
454454

455+
// Predicates to scope the UPDATE to the record(s) identified by the PK
456+
// combined with any database policy defined for the update operation.
457+
string updatePredicates = JoinPredicateStrings(pkPredicates, structure.GetDbPolicyForOperation(EntityActionOperation.Update));
458+
455459
// Query to update record (if there exists one for given PK).
456460
StringBuilder updateQuery = new(
457461
$"IF @ROWS_TO_UPDATE = 1 " +
458462
$"BEGIN " +
459463
$"UPDATE {tableName} " +
460-
$"SET {updateOperations} ");
464+
$"SET {updateOperations} " +
465+
$"WHERE {updatePredicates} ");
461466

462467
// End the IF block.
463468
updateQuery.Append("END ");

src/Core/Resolvers/MsSqlQueryExecutor.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,7 @@ public override string GetSessionParamsQuery(HttpContext? httpContext, IDictiona
516516

517517
// Counter to generate different param name for each of the sessionParam.
518518
IncrementingInteger counter = new();
519+
const string SESSION_KEY_NAME = $"{BaseQueryStructure.PARAM_NAME_PREFIX}session_key";
519520
const string SESSION_PARAM_NAME = $"{BaseQueryStructure.PARAM_NAME_PREFIX}session_param";
520521
StringBuilder sessionMapQuery = new();
521522

@@ -528,10 +529,14 @@ public override string GetSessionParamsQuery(HttpContext? httpContext, IDictiona
528529

529530
foreach ((string claimType, string claimValue) in sessionParams)
530531
{
532+
string keyName = $"{SESSION_KEY_NAME}{counter.Current()}";
533+
parameters.Add(keyName, new(claimType));
534+
531535
string paramName = $"{SESSION_PARAM_NAME}{counter.Next()}";
532536
parameters.Add(paramName, new(claimValue));
537+
533538
// Append statement to set read only param value - can be set only once for a connection.
534-
string statementToSetReadOnlyParam = "EXEC sp_set_session_context " + $"'{claimType}', " + paramName + ", @read_only = 0;";
539+
string statementToSetReadOnlyParam = "EXEC sp_set_session_context " + keyName + ", " + paramName + ", @read_only = 0;";
535540
sessionMapQuery = sessionMapQuery.Append(statementToSetReadOnlyParam);
536541
}
537542
}

src/Service.Tests/Authentication/EasyAuthAuthenticationUnitTests.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,6 @@ public async Task TestValidStaticWebAppsEasyAuthTokenWithAnonymousRoleOnly()
325325
DisplayName = "Anonymous role - X-MS-API-ROLE is not honored")]
326326
[DataRow(true, "author",
327327
DisplayName = "Authenticated role - existing X-MS-API-ROLE is honored")]
328-
[TestMethod]
329328
public async Task TestClientRoleHeaderPresence(bool addAuthenticated, string clientRoleHeader)
330329
{
331330
string generatedToken = AuthTestHelper.CreateStaticWebAppsEasyAuthToken(addAuthenticated);

src/Service.Tests/Caching/HealthEndpointCachingTests.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ public class HealthEndpointCachingTests
2222
{
2323
private const string CUSTOM_CONFIG_FILENAME = "custom-config.json";
2424

25+
[TestInitialize]
26+
public void SetupAuthProviderEnvironmentVariables()
27+
{
28+
TestHelper.SetAppServiceEnvironmentVariable();
29+
}
30+
2531
[TestCleanup]
2632
public void CleanupAfterEachTest()
2733
{

src/Service.Tests/Configuration/ConfigurationTests.cs

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -735,6 +735,13 @@ public void CleanupAfterEachTest()
735735
TestHelper.UnsetAllDABEnvironmentVariables();
736736
}
737737

738+
[TestInitialize]
739+
public void SetupAuthProviderEnvironmentVariables()
740+
{
741+
TestHelper.SetAppServiceEnvironmentVariable();
742+
TestHelper.SetStaticWebAppsEnvironmentVariable();
743+
}
744+
738745
/// <summary>
739746
/// When updating config during runtime is possible, then For invalid config the Application continues to
740747
/// accept request with status code of 503.
@@ -3934,23 +3941,24 @@ type Planet @model(name:""PlanetAlias"") {
39343941
/// </summary>
39353942
/// <param name="hostMode">HostMode in Runtime config - Development or Production.</param>
39363943
/// <param name="authType">EasyAuth auth type - AppService or StaticWebApps.</param>
3937-
/// <param name="setEnvVars">Whether to set the AppService host environment variables.</param>
3944+
/// <param name="setAppServiceEnvVars">Whether to set the AppService host environment variables.</param>
3945+
/// <param name="setStaticWebAppsEnvVar">Whether to set the Static Web Apps host environment variable.</param>
39383946
/// <param name="expectError">Whether an error is expected.</param>
39393947
[DataTestMethod]
39403948
[TestCategory(TestCategory.MSSQL)]
3941-
[DataRow(HostMode.Development, EasyAuthType.AppService, false, false, DisplayName = "AppService Dev - No EnvVars - No Error")]
3942-
[DataRow(HostMode.Development, EasyAuthType.AppService, true, false, DisplayName = "AppService Dev - EnvVars - No Error")]
3943-
[DataRow(HostMode.Production, EasyAuthType.AppService, false, false, DisplayName = "AppService Prod - No EnvVars - Error")]
3944-
[DataRow(HostMode.Production, EasyAuthType.AppService, true, false, DisplayName = "AppService Prod - EnvVars - Error")]
3945-
[DataRow(HostMode.Development, EasyAuthType.StaticWebApps, false, false, DisplayName = "SWA Dev - No EnvVars - No Error")]
3946-
[DataRow(HostMode.Development, EasyAuthType.StaticWebApps, true, false, DisplayName = "SWA Dev - EnvVars - No Error")]
3947-
[DataRow(HostMode.Production, EasyAuthType.StaticWebApps, false, false, DisplayName = "SWA Prod - No EnvVars - No Error")]
3948-
[DataRow(HostMode.Production, EasyAuthType.StaticWebApps, true, false, DisplayName = "SWA Prod - EnvVars - No Error")]
3949-
public void TestProductionModeAppServiceEnvironmentCheck(HostMode hostMode, EasyAuthType authType, bool setEnvVars, bool expectError)
3949+
[DataRow(HostMode.Development, EasyAuthType.AppService, false, false, false, DisplayName = "AppService Dev - No EnvVars - No Error")]
3950+
[DataRow(HostMode.Development, EasyAuthType.AppService, true, false, false, DisplayName = "AppService Dev - EnvVars - No Error")]
3951+
[DataRow(HostMode.Production, EasyAuthType.AppService, false, false, true, DisplayName = "AppService Prod - No EnvVars - Error")]
3952+
[DataRow(HostMode.Production, EasyAuthType.AppService, true, false, false, DisplayName = "AppService Prod - EnvVars - No Error")]
3953+
[DataRow(HostMode.Development, EasyAuthType.StaticWebApps, false, false, false, DisplayName = "SWA Dev - No EnvVars - No Error")]
3954+
[DataRow(HostMode.Development, EasyAuthType.StaticWebApps, false, true, false, DisplayName = "SWA Dev - EnvVars - No Error")]
3955+
[DataRow(HostMode.Production, EasyAuthType.StaticWebApps, false, false, true, DisplayName = "SWA Prod - No EnvVars - Error")]
3956+
[DataRow(HostMode.Production, EasyAuthType.StaticWebApps, false, true, false, DisplayName = "SWA Prod - EnvVars - No Error")]
3957+
public void TestProductionModeAppServiceEnvironmentCheck(HostMode hostMode, EasyAuthType authType, bool setAppServiceEnvVars, bool setStaticWebAppsEnvVar, bool expectError)
39503958
{
39513959
// Clears or sets App Service Environment Variables based on test input.
3952-
Environment.SetEnvironmentVariable(AppServiceAuthenticationInfo.APPSERVICESAUTH_ENABLED_ENVVAR, setEnvVars ? "true" : null);
3953-
Environment.SetEnvironmentVariable(AppServiceAuthenticationInfo.APPSERVICESAUTH_IDENTITYPROVIDER_ENVVAR, setEnvVars ? "AzureActiveDirectory" : null);
3960+
Environment.SetEnvironmentVariable(AppServiceAuthenticationInfo.APPSERVICESAUTH_ENABLED_ENVVAR, setAppServiceEnvVars ? "true" : null);
3961+
Environment.SetEnvironmentVariable(StaticWebAppsAuthentication.WEBSITE_SITE_NAME_ENVVAR, setStaticWebAppsEnvVar ? "test-site-name" : null);
39543962
TestHelper.SetupDatabaseEnvironment(TestCategory.MSSQL);
39553963

39563964
FileSystem fileSystem = new();
@@ -3987,7 +3995,16 @@ public void TestProductionModeAppServiceEnvironmentCheck(HostMode hostMode, Easy
39873995
catch (DataApiBuilderException ex)
39883996
{
39893997
Assert.IsTrue(expectError, message: ex.Message);
3990-
Assert.AreEqual(AppServiceAuthenticationInfo.APPSERVICE_PROD_MISSING_ENV_CONFIG, ex.Message);
3998+
Assert.AreEqual(
3999+
expected: authType == EasyAuthType.AppService
4000+
? AppServiceAuthenticationInfo.APPSERVICE_PROD_MISSING_ENV_CONFIG
4001+
: StaticWebAppsAuthentication.SWA_PROD_MISSING_ENV_CONFIG,
4002+
actual: ex.Message);
4003+
}
4004+
finally
4005+
{
4006+
Environment.SetEnvironmentVariable(AppServiceAuthenticationInfo.APPSERVICESAUTH_ENABLED_ENVVAR, null);
4007+
Environment.SetEnvironmentVariable(StaticWebAppsAuthentication.WEBSITE_SITE_NAME_ENVVAR, null);
39914008
}
39924009
}
39934010

src/Service.Tests/Configuration/HealthEndpointRolesTests.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ public class HealthEndpointRolesTests
2222

2323
private const string CUSTOM_CONFIG_FILENAME = "custom-config.json";
2424

25+
[TestInitialize]
26+
public void SetupAuthProviderEnvironmentVariables()
27+
{
28+
TestHelper.SetAppServiceEnvironmentVariable();
29+
}
30+
2531
[TestCleanup]
2632
public void CleanupAfterEachTest()
2733
{

src/Service.Tests/Configuration/HealthEndpointTests.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ public class HealthEndpointTests
3636
private const string CUSTOM_CONFIG_FILENAME = "custom_config.json";
3737
private const string BASE_DAB_URL = "http://localhost:5000";
3838

39+
[TestInitialize]
40+
public void SetupAuthProviderEnvironmentVariables()
41+
{
42+
TestHelper.SetAppServiceEnvironmentVariable();
43+
}
44+
3945
[TestCleanup]
4046
public void CleanupAfterEachTest()
4147
{

0 commit comments

Comments
 (0)