From f246618bdb6a19f2f309a933df4f6f36c4b6a34c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 22:30:09 +0000 Subject: [PATCH 01/13] Initial plan From 4792325b0cfde036c66b90a3fb2e38d089b2aab2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 22:37:56 +0000 Subject: [PATCH 02/13] Add CLI support for user-delegated-auth configuration Co-authored-by: anushakolan <45540936+anushakolan@users.noreply.github.com> --- src/Cli.Tests/ConfigureOptionsTests.cs | 181 ++++++++++++++++++ src/Cli/Commands/ConfigureOptions.cs | 10 + src/Cli/ConfigGenerator.cs | 28 ++- .../Converters/DataSourceConverterFactory.cs | 17 +- src/Config/ObjectModel/DataSource.cs | 4 +- .../ObjectModel/UserDelegatedAuthConfig.cs | 59 ++++++ 6 files changed, 296 insertions(+), 3 deletions(-) create mode 100644 src/Config/ObjectModel/UserDelegatedAuthConfig.cs diff --git a/src/Cli.Tests/ConfigureOptionsTests.cs b/src/Cli.Tests/ConfigureOptionsTests.cs index b368227a75..8ab024ae0b 100644 --- a/src/Cli.Tests/ConfigureOptionsTests.cs +++ b/src/Cli.Tests/ConfigureOptionsTests.cs @@ -1094,5 +1094,186 @@ private void SetupFileSystemWithInitialConfig(string jsonConfig) Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(jsonConfig, out RuntimeConfig? config)); Assert.IsNotNull(config.Runtime); } + + /// + /// Tests adding user-delegated-auth.enabled to a config that doesn't have user-delegated-auth configured. + /// This method verifies that the user-delegated-auth.enabled property can be set to true for MSSQL database. + /// Command: dab configure --data-source.user-delegated-auth.enabled true + /// + [TestMethod] + public void TestAddUserDelegatedAuthEnabled() + { + // Arrange + SetupFileSystemWithInitialConfig(INITIAL_CONFIG); + + ConfigureOptions options = new( + dataSourceUserDelegatedAuthEnabled: true, + config: TEST_RUNTIME_CONFIG_FILE + ); + + // Act + bool isSuccess = TryConfigureSettings(options, _runtimeConfigLoader!, _fileSystem!); + + // Assert + Assert.IsTrue(isSuccess); + string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? config)); + Assert.IsNotNull(config.DataSource); + Assert.IsNotNull(config.DataSource.UserDelegatedAuth); + Assert.IsTrue(config.DataSource.UserDelegatedAuth.Enabled); + } + + /// + /// Tests adding user-delegated-auth.database-audience to a config that doesn't have user-delegated-auth configured. + /// This method verifies that the database-audience can be set for user-delegated authentication. + /// Command: dab configure --data-source.user-delegated-auth.database-audience "https://database.windows.net" + /// + [TestMethod] + public void TestAddUserDelegatedAuthDatabaseAudience() + { + // Arrange + SetupFileSystemWithInitialConfig(INITIAL_CONFIG); + string audienceValue = "https://database.windows.net"; + + ConfigureOptions options = new( + dataSourceUserDelegatedAuthDatabaseAudience: audienceValue, + config: TEST_RUNTIME_CONFIG_FILE + ); + + // Act + bool isSuccess = TryConfigureSettings(options, _runtimeConfigLoader!, _fileSystem!); + + // Assert + Assert.IsTrue(isSuccess); + string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? config)); + Assert.IsNotNull(config.DataSource); + Assert.IsNotNull(config.DataSource.UserDelegatedAuth); + Assert.AreEqual(audienceValue, config.DataSource.UserDelegatedAuth.DatabaseAudience); + } + + /// + /// Tests adding both user-delegated-auth.enabled and database-audience in a single command. + /// This method verifies that both properties can be set together. + /// Command: dab configure --data-source.user-delegated-auth.enabled true --data-source.user-delegated-auth.database-audience "https://database.windows.net" + /// + [DataTestMethod] + [DataRow("https://database.windows.net", DisplayName = "Azure SQL Database (public cloud)")] + [DataRow("https://database.usgovcloudapi.net", DisplayName = "Azure Government Cloud")] + [DataRow("https://database.chinacloudapi.cn", DisplayName = "Azure China Cloud")] + [DataRow("https://myinstance.abc123.database.windows.net", DisplayName = "Azure SQL Managed Instance")] + public void TestAddUserDelegatedAuthEnabledAndDatabaseAudience(string audienceValue) + { + // Arrange + SetupFileSystemWithInitialConfig(INITIAL_CONFIG); + + ConfigureOptions options = new( + dataSourceUserDelegatedAuthEnabled: true, + dataSourceUserDelegatedAuthDatabaseAudience: audienceValue, + config: TEST_RUNTIME_CONFIG_FILE + ); + + // Act + bool isSuccess = TryConfigureSettings(options, _runtimeConfigLoader!, _fileSystem!); + + // Assert + Assert.IsTrue(isSuccess); + string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? config)); + Assert.IsNotNull(config.DataSource); + Assert.IsNotNull(config.DataSource.UserDelegatedAuth); + Assert.IsTrue(config.DataSource.UserDelegatedAuth.Enabled); + Assert.AreEqual(audienceValue, config.DataSource.UserDelegatedAuth.DatabaseAudience); + } + + /// + /// Tests that enabling user-delegated-auth on a non-MSSQL database fails. + /// This method verifies that user-delegated-auth is only allowed for MSSQL database type. + /// Command: dab configure --data-source.database-type postgresql --data-source.user-delegated-auth.enabled true + /// + [DataTestMethod] + [DataRow("postgresql", DisplayName = "Fail when enabling user-delegated-auth on PostgreSQL")] + [DataRow("mysql", DisplayName = "Fail when enabling user-delegated-auth on MySQL")] + [DataRow("cosmosdb_nosql", DisplayName = "Fail when enabling user-delegated-auth on CosmosDB")] + public void TestFailureWhenEnablingUserDelegatedAuthOnNonMSSQLDatabase(string dbType) + { + // Arrange + SetupFileSystemWithInitialConfig(INITIAL_CONFIG); + + ConfigureOptions options = new( + dataSourceDatabaseType: dbType, + dataSourceUserDelegatedAuthEnabled: true, + config: TEST_RUNTIME_CONFIG_FILE + ); + + // Act + bool isSuccess = TryConfigureSettings(options, _runtimeConfigLoader!, _fileSystem!); + + // Assert + Assert.IsFalse(isSuccess); + } + + /// + /// Tests updating existing user-delegated-auth configuration by changing the database-audience. + /// This method verifies that the database-audience can be updated while preserving the enabled setting. + /// + [TestMethod] + public void TestUpdateUserDelegatedAuthDatabaseAudience() + { + // Arrange - Config with existing user-delegated-auth section + string configWithUserDelegatedAuth = @" + { + ""$schema"": ""test"", + ""data-source"": { + ""database-type"": ""mssql"", + ""connection-string"": ""testconnectionstring"", + ""user-delegated-auth"": { + ""enabled"": true, + ""database-audience"": ""https://database.windows.net"" + } + }, + ""runtime"": { + ""rest"": { + ""enabled"": true, + ""path"": ""/api"" + }, + ""graphql"": { + ""enabled"": true, + ""path"": ""/graphql"", + ""allow-introspection"": true + }, + ""host"": { + ""mode"": ""development"", + ""cors"": { + ""origins"": [], + ""allow-credentials"": false + }, + ""authentication"": { + ""provider"": ""StaticWebApps"" + } + } + }, + ""entities"": {} + }"; + SetupFileSystemWithInitialConfig(configWithUserDelegatedAuth); + + string newAudience = "https://database.usgovcloudapi.net"; + ConfigureOptions options = new( + dataSourceUserDelegatedAuthDatabaseAudience: newAudience, + config: TEST_RUNTIME_CONFIG_FILE + ); + + // Act + bool isSuccess = TryConfigureSettings(options, _runtimeConfigLoader!, _fileSystem!); + + // Assert + Assert.IsTrue(isSuccess); + string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? config)); + Assert.IsNotNull(config.DataSource); + Assert.IsNotNull(config.DataSource.UserDelegatedAuth); + Assert.IsTrue(config.DataSource.UserDelegatedAuth.Enabled); + Assert.AreEqual(newAudience, config.DataSource.UserDelegatedAuth.DatabaseAudience); + } } } diff --git a/src/Cli/Commands/ConfigureOptions.cs b/src/Cli/Commands/ConfigureOptions.cs index 14234d24d7..069a2d3b6a 100644 --- a/src/Cli/Commands/ConfigureOptions.cs +++ b/src/Cli/Commands/ConfigureOptions.cs @@ -29,6 +29,8 @@ public ConfigureOptions( string? dataSourceOptionsSchema = null, bool? dataSourceOptionsSetSessionContext = null, string? dataSourceHealthName = null, + bool? dataSourceUserDelegatedAuthEnabled = null, + string? dataSourceUserDelegatedAuthDatabaseAudience = null, int? depthLimit = null, bool? runtimeGraphQLEnabled = null, string? runtimeGraphQLPath = null, @@ -84,6 +86,8 @@ public ConfigureOptions( DataSourceOptionsSchema = dataSourceOptionsSchema; DataSourceOptionsSetSessionContext = dataSourceOptionsSetSessionContext; DataSourceHealthName = dataSourceHealthName; + DataSourceUserDelegatedAuthEnabled = dataSourceUserDelegatedAuthEnabled; + DataSourceUserDelegatedAuthDatabaseAudience = dataSourceUserDelegatedAuthDatabaseAudience; // GraphQL DepthLimit = depthLimit; RuntimeGraphQLEnabled = runtimeGraphQLEnabled; @@ -160,6 +164,12 @@ public ConfigureOptions( [Option("data-source.health.name", Required = false, HelpText = "Identifier for data source in health check report.")] public string? DataSourceHealthName { get; } + [Option("data-source.user-delegated-auth.enabled", Required = false, HelpText = "Enable user-delegated authentication (OBO) for Azure SQL. Default: false (boolean).")] + public bool? DataSourceUserDelegatedAuthEnabled { get; } + + [Option("data-source.user-delegated-auth.database-audience", Required = false, HelpText = "Azure SQL resource identifier for token acquisition (e.g., https://database.windows.net).")] + public string? DataSourceUserDelegatedAuthDatabaseAudience { get; } + [Option("runtime.graphql.depth-limit", Required = false, HelpText = "Max allowed depth of the nested query. Allowed values: (0,2147483647] inclusive. Default is infinity. Use -1 to remove limit.")] public int? DepthLimit { get; } diff --git a/src/Cli/ConfigGenerator.cs b/src/Cli/ConfigGenerator.cs index 9a3401a55a..0f77004835 100644 --- a/src/Cli/ConfigGenerator.cs +++ b/src/Cli/ConfigGenerator.cs @@ -643,6 +643,7 @@ private static bool TryUpdateConfiguredDataSourceOptions( DatabaseType dbType = runtimeConfig.DataSource.DatabaseType; string dataSourceConnectionString = runtimeConfig.DataSource.ConnectionString; DatasourceHealthCheckConfig? datasourceHealthCheckConfig = runtimeConfig.DataSource.Health; + UserDelegatedAuthConfig? userDelegatedAuthConfig = runtimeConfig.DataSource.UserDelegatedAuth; if (options.DataSourceDatabaseType is not null) { @@ -714,8 +715,33 @@ private static bool TryUpdateConfiguredDataSourceOptions( } } + // Handle user-delegated-auth options + if (options.DataSourceUserDelegatedAuthEnabled is not null + || options.DataSourceUserDelegatedAuthDatabaseAudience is not null) + { + // Validate that user-delegated-auth is only used with MSSQL + if (options.DataSourceUserDelegatedAuthEnabled == true && !DatabaseType.MSSQL.Equals(dbType)) + { + _logger.LogError("user-delegated-auth is only supported for database-type 'mssql'."); + return false; + } + + // Create or update user-delegated-auth config + bool enabled = options.DataSourceUserDelegatedAuthEnabled + ?? userDelegatedAuthConfig?.Enabled + ?? false; + string? databaseAudience = options.DataSourceUserDelegatedAuthDatabaseAudience + ?? userDelegatedAuthConfig?.DatabaseAudience; + + userDelegatedAuthConfig = new UserDelegatedAuthConfig( + Enabled: enabled, + DatabaseAudience: databaseAudience, + DisableConnectionPooling: userDelegatedAuthConfig?.DisableConnectionPooling, + TokenCacheDurationMinutes: userDelegatedAuthConfig?.TokenCacheDurationMinutes); + } + dbOptions = EnumerableUtilities.IsNullOrEmpty(dbOptions) ? null : dbOptions; - DataSource dataSource = new(dbType, dataSourceConnectionString, dbOptions, datasourceHealthCheckConfig); + DataSource dataSource = new(dbType, dataSourceConnectionString, dbOptions, datasourceHealthCheckConfig, userDelegatedAuthConfig); runtimeConfig = runtimeConfig with { DataSource = dataSource }; return runtimeConfig != null; diff --git a/src/Config/Converters/DataSourceConverterFactory.cs b/src/Config/Converters/DataSourceConverterFactory.cs index 1788ebf2b4..3f697061b2 100644 --- a/src/Config/Converters/DataSourceConverterFactory.cs +++ b/src/Config/Converters/DataSourceConverterFactory.cs @@ -51,12 +51,13 @@ public DataSourceConverter(DeserializationVariableReplacementSettings? replaceme string connectionString = string.Empty; DatasourceHealthCheckConfig? health = null; Dictionary? datasourceOptions = null; + UserDelegatedAuthConfig? userDelegatedAuth = null; while (reader.Read()) { if (reader.TokenType is JsonTokenType.EndObject) { - return new DataSource(databaseType, connectionString, datasourceOptions, health); + return new DataSource(databaseType, connectionString, datasourceOptions, health, userDelegatedAuth); } if (reader.TokenType is JsonTokenType.PropertyName) @@ -91,6 +92,20 @@ public DataSourceConverter(DeserializationVariableReplacementSettings? replaceme } } + break; + case "user-delegated-auth": + if (reader.TokenType is not JsonTokenType.Null) + { + try + { + userDelegatedAuth = JsonSerializer.Deserialize(ref reader, options); + } + catch (Exception e) + { + throw new JsonException($"Error while deserializing DataSource user-delegated-auth: {e.Message}"); + } + } + break; case "options": if (reader.TokenType is not JsonTokenType.Null) diff --git a/src/Config/ObjectModel/DataSource.cs b/src/Config/ObjectModel/DataSource.cs index d1a2456ef9..769e109a43 100644 --- a/src/Config/ObjectModel/DataSource.cs +++ b/src/Config/ObjectModel/DataSource.cs @@ -14,11 +14,13 @@ namespace Azure.DataApiBuilder.Config.ObjectModel; /// Connection string to access the database. /// Custom options for the specific database. If there are no options, this could be null. /// Health check configuration for the datasource. +/// User-delegated authentication configuration (OBO). Optional. public record DataSource( DatabaseType DatabaseType, string ConnectionString, Dictionary? Options = null, - DatasourceHealthCheckConfig? Health = null) + DatasourceHealthCheckConfig? Health = null, + UserDelegatedAuthConfig? UserDelegatedAuth = null) { [JsonIgnore] public bool IsDatasourceHealthEnabled => diff --git a/src/Config/ObjectModel/UserDelegatedAuthConfig.cs b/src/Config/ObjectModel/UserDelegatedAuthConfig.cs new file mode 100644 index 0000000000..3fd55e3d2f --- /dev/null +++ b/src/Config/ObjectModel/UserDelegatedAuthConfig.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Azure.DataApiBuilder.Config.ObjectModel; + +/// +/// Configuration for user-delegated authentication (OBO - On-Behalf-Of). +/// Enables per-user Entra ID access token authentication to Azure SQL. +/// +/// Whether user-delegated authentication is enabled. +/// The Azure SQL resource identifier for token acquisition. +/// Explicitly control connection pooling behavior. Default: true for safety. +/// In-memory cache duration for OBO tokens per user. Default: 50 minutes. +public record UserDelegatedAuthConfig( + bool Enabled = false, + string? DatabaseAudience = null, + bool? DisableConnectionPooling = null, + int? TokenCacheDurationMinutes = null) +{ + /// + /// Default value for token cache duration in minutes. + /// Must be less than typical token lifetime (60 min). + /// + public const int DEFAULT_TOKEN_CACHE_DURATION_MINUTES = 50; + + /// + /// Default value for connection pooling (disabled for safety in MVP). + /// + public const bool DEFAULT_DISABLE_CONNECTION_POOLING = true; + + /// + /// Minimum allowed token cache duration in minutes. + /// + public const int MIN_TOKEN_CACHE_DURATION_MINUTES = 1; + + /// + /// Maximum allowed token cache duration in minutes. + /// Must be less than typical token lifetime (60 min). + /// + public const int MAX_TOKEN_CACHE_DURATION_MINUTES = 59; + + /// + /// Gets the effective token cache duration value. + /// Returns the configured value or the default if not specified. + /// + [JsonIgnore] + public int EffectiveTokenCacheDurationMinutes => + TokenCacheDurationMinutes ?? DEFAULT_TOKEN_CACHE_DURATION_MINUTES; + + /// + /// Gets the effective connection pooling setting. + /// Returns the configured value or the default if not specified. + /// + [JsonIgnore] + public bool EffectiveDisableConnectionPooling => + DisableConnectionPooling ?? DEFAULT_DISABLE_CONNECTION_POOLING; +} From 122f503e512170b4af0a48ce232aa1aa6dac0b52 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 22:42:51 +0000 Subject: [PATCH 03/13] Add runtime parsing tests and verify CLI functionality Co-authored-by: anushakolan <45540936+anushakolan@users.noreply.github.com> --- .../UserDelegatedAuthRuntimeParsingTests.cs | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 src/Cli.Tests/UserDelegatedAuthRuntimeParsingTests.cs diff --git a/src/Cli.Tests/UserDelegatedAuthRuntimeParsingTests.cs b/src/Cli.Tests/UserDelegatedAuthRuntimeParsingTests.cs new file mode 100644 index 0000000000..7a463a845c --- /dev/null +++ b/src/Cli.Tests/UserDelegatedAuthRuntimeParsingTests.cs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Cli.Tests +{ + [TestClass] + public class UserDelegatedAuthRuntimeParsingTests + { + [TestMethod] + public void TestRuntimeCanParseUserDelegatedAuthConfig() + { + // Arrange + string configJson = @"{ + ""$schema"": ""test"", + ""data-source"": { + ""database-type"": ""mssql"", + ""connection-string"": ""testconnectionstring"", + ""user-delegated-auth"": { + ""enabled"": true, + ""database-audience"": ""https://database.windows.net"" + } + }, + ""runtime"": { + ""rest"": { + ""enabled"": true, + ""path"": ""/api"" + }, + ""graphql"": { + ""enabled"": true, + ""path"": ""/graphql"", + ""allow-introspection"": true + }, + ""host"": { + ""mode"": ""development"", + ""cors"": { + ""origins"": [], + ""allow-credentials"": false + }, + ""authentication"": { + ""provider"": ""StaticWebApps"" + } + } + }, + ""entities"": {} + }"; + + // Act + bool success = RuntimeConfigLoader.TryParseConfig(configJson, out RuntimeConfig? config); + + // Assert + Assert.IsTrue(success); + Assert.IsNotNull(config); + Assert.IsNotNull(config.DataSource.UserDelegatedAuth); + Assert.IsTrue(config.DataSource.UserDelegatedAuth.Enabled); + Assert.AreEqual("https://database.windows.net", config.DataSource.UserDelegatedAuth.DatabaseAudience); + Assert.AreEqual(50, config.DataSource.UserDelegatedAuth.EffectiveTokenCacheDurationMinutes); + Assert.IsTrue(config.DataSource.UserDelegatedAuth.EffectiveDisableConnectionPooling); + } + + [TestMethod] + public void TestRuntimeCanParseConfigWithoutUserDelegatedAuth() + { + // Arrange + string configJson = @"{ + ""$schema"": ""test"", + ""data-source"": { + ""database-type"": ""mssql"", + ""connection-string"": ""testconnectionstring"" + }, + ""runtime"": { + ""rest"": { + ""enabled"": true, + ""path"": ""/api"" + }, + ""graphql"": { + ""enabled"": true, + ""path"": ""/graphql"", + ""allow-introspection"": true + }, + ""host"": { + ""mode"": ""development"", + ""cors"": { + ""origins"": [], + ""allow-credentials"": false + }, + ""authentication"": { + ""provider"": ""StaticWebApps"" + } + } + }, + ""entities"": {} + }"; + + // Act + bool success = RuntimeConfigLoader.TryParseConfig(configJson, out RuntimeConfig? config); + + // Assert + Assert.IsTrue(success); + Assert.IsNotNull(config); + Assert.IsNull(config.DataSource.UserDelegatedAuth); + } + } +} From be514c865898c441002a56cffa63105bac1f0e06 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 22:53:11 +0000 Subject: [PATCH 04/13] Address code review feedback - improve documentation and validation Co-authored-by: anushakolan <45540936+anushakolan@users.noreply.github.com> --- src/Cli/ConfigGenerator.cs | 15 +++++++++------ src/Config/ObjectModel/UserDelegatedAuthConfig.cs | 2 +- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Cli/ConfigGenerator.cs b/src/Cli/ConfigGenerator.cs index 0f77004835..7e5b6c400e 100644 --- a/src/Cli/ConfigGenerator.cs +++ b/src/Cli/ConfigGenerator.cs @@ -719,20 +719,23 @@ private static bool TryUpdateConfiguredDataSourceOptions( if (options.DataSourceUserDelegatedAuthEnabled is not null || options.DataSourceUserDelegatedAuthDatabaseAudience is not null) { - // Validate that user-delegated-auth is only used with MSSQL - if (options.DataSourceUserDelegatedAuthEnabled == true && !DatabaseType.MSSQL.Equals(dbType)) + // Determine the enabled state: use new value if provided, otherwise preserve existing + bool enabled = options.DataSourceUserDelegatedAuthEnabled + ?? userDelegatedAuthConfig?.Enabled + ?? false; + + // Validate that user-delegated-auth is only used with MSSQL when enabled=true + if (enabled && !DatabaseType.MSSQL.Equals(dbType)) { _logger.LogError("user-delegated-auth is only supported for database-type 'mssql'."); return false; } - // Create or update user-delegated-auth config - bool enabled = options.DataSourceUserDelegatedAuthEnabled - ?? userDelegatedAuthConfig?.Enabled - ?? false; + // Get database-audience: use new value if provided, otherwise preserve existing string? databaseAudience = options.DataSourceUserDelegatedAuthDatabaseAudience ?? userDelegatedAuthConfig?.DatabaseAudience; + // Create or update user-delegated-auth config userDelegatedAuthConfig = new UserDelegatedAuthConfig( Enabled: enabled, DatabaseAudience: databaseAudience, diff --git a/src/Config/ObjectModel/UserDelegatedAuthConfig.cs b/src/Config/ObjectModel/UserDelegatedAuthConfig.cs index 3fd55e3d2f..ee086a4053 100644 --- a/src/Config/ObjectModel/UserDelegatedAuthConfig.cs +++ b/src/Config/ObjectModel/UserDelegatedAuthConfig.cs @@ -11,7 +11,7 @@ namespace Azure.DataApiBuilder.Config.ObjectModel; /// /// Whether user-delegated authentication is enabled. /// The Azure SQL resource identifier for token acquisition. -/// Explicitly control connection pooling behavior. Default: true for safety. +/// Explicitly control connection pooling behavior. Default: true (disabled) for safety. Connection pooling is disabled by default in OBO scenarios to prevent token reuse across different user contexts. /// In-memory cache duration for OBO tokens per user. Default: 50 minutes. public record UserDelegatedAuthConfig( bool Enabled = false, From b0f98fe913965652c2ab32768717bf6f6d3fa082 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 20:48:55 +0000 Subject: [PATCH 05/13] Align CLI implementation with PR #3151 - rename to UserDelegatedAuthOptions and add provider field Co-authored-by: anushakolan <45540936+anushakolan@users.noreply.github.com> --- global.json | 2 +- .../UserDelegatedAuthRuntimeParsingTests.cs | 2 - src/Cli/ConfigGenerator.cs | 17 ++++-- .../Converters/DataSourceConverterFactory.cs | 9 ++- src/Config/ObjectModel/DataSource.cs | 11 +++- .../ObjectModel/UserDelegatedAuthConfig.cs | 59 ------------------- .../ObjectModel/UserDelegatedAuthOptions.cs | 45 ++++++++++++++ 7 files changed, 71 insertions(+), 74 deletions(-) delete mode 100644 src/Config/ObjectModel/UserDelegatedAuthConfig.cs create mode 100644 src/Config/ObjectModel/UserDelegatedAuthOptions.cs diff --git a/global.json b/global.json index 17811390a4..1bdb496ef0 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.418", + "version": "8.0.417", "rollForward": "latestFeature" } } diff --git a/src/Cli.Tests/UserDelegatedAuthRuntimeParsingTests.cs b/src/Cli.Tests/UserDelegatedAuthRuntimeParsingTests.cs index 7a463a845c..29110a5a7c 100644 --- a/src/Cli.Tests/UserDelegatedAuthRuntimeParsingTests.cs +++ b/src/Cli.Tests/UserDelegatedAuthRuntimeParsingTests.cs @@ -53,8 +53,6 @@ public void TestRuntimeCanParseUserDelegatedAuthConfig() Assert.IsNotNull(config.DataSource.UserDelegatedAuth); Assert.IsTrue(config.DataSource.UserDelegatedAuth.Enabled); Assert.AreEqual("https://database.windows.net", config.DataSource.UserDelegatedAuth.DatabaseAudience); - Assert.AreEqual(50, config.DataSource.UserDelegatedAuth.EffectiveTokenCacheDurationMinutes); - Assert.IsTrue(config.DataSource.UserDelegatedAuth.EffectiveDisableConnectionPooling); } [TestMethod] diff --git a/src/Cli/ConfigGenerator.cs b/src/Cli/ConfigGenerator.cs index 7e5b6c400e..6c51f002b7 100644 --- a/src/Cli/ConfigGenerator.cs +++ b/src/Cli/ConfigGenerator.cs @@ -643,7 +643,7 @@ private static bool TryUpdateConfiguredDataSourceOptions( DatabaseType dbType = runtimeConfig.DataSource.DatabaseType; string dataSourceConnectionString = runtimeConfig.DataSource.ConnectionString; DatasourceHealthCheckConfig? datasourceHealthCheckConfig = runtimeConfig.DataSource.Health; - UserDelegatedAuthConfig? userDelegatedAuthConfig = runtimeConfig.DataSource.UserDelegatedAuth; + UserDelegatedAuthOptions? userDelegatedAuthConfig = runtimeConfig.DataSource.UserDelegatedAuth; if (options.DataSourceDatabaseType is not null) { @@ -735,16 +735,21 @@ private static bool TryUpdateConfiguredDataSourceOptions( string? databaseAudience = options.DataSourceUserDelegatedAuthDatabaseAudience ?? userDelegatedAuthConfig?.DatabaseAudience; + // Get provider: preserve existing or use default "EntraId" + string? provider = userDelegatedAuthConfig?.Provider ?? "EntraId"; + // Create or update user-delegated-auth config - userDelegatedAuthConfig = new UserDelegatedAuthConfig( + userDelegatedAuthConfig = new UserDelegatedAuthOptions( Enabled: enabled, - DatabaseAudience: databaseAudience, - DisableConnectionPooling: userDelegatedAuthConfig?.DisableConnectionPooling, - TokenCacheDurationMinutes: userDelegatedAuthConfig?.TokenCacheDurationMinutes); + Provider: provider, + DatabaseAudience: databaseAudience); } dbOptions = EnumerableUtilities.IsNullOrEmpty(dbOptions) ? null : dbOptions; - DataSource dataSource = new(dbType, dataSourceConnectionString, dbOptions, datasourceHealthCheckConfig, userDelegatedAuthConfig); + DataSource dataSource = new(dbType, dataSourceConnectionString, dbOptions, datasourceHealthCheckConfig) + { + UserDelegatedAuth = userDelegatedAuthConfig + }; runtimeConfig = runtimeConfig with { DataSource = dataSource }; return runtimeConfig != null; diff --git a/src/Config/Converters/DataSourceConverterFactory.cs b/src/Config/Converters/DataSourceConverterFactory.cs index 3f697061b2..f83e7799cb 100644 --- a/src/Config/Converters/DataSourceConverterFactory.cs +++ b/src/Config/Converters/DataSourceConverterFactory.cs @@ -51,13 +51,16 @@ public DataSourceConverter(DeserializationVariableReplacementSettings? replaceme string connectionString = string.Empty; DatasourceHealthCheckConfig? health = null; Dictionary? datasourceOptions = null; - UserDelegatedAuthConfig? userDelegatedAuth = null; + UserDelegatedAuthOptions? userDelegatedAuth = null; while (reader.Read()) { if (reader.TokenType is JsonTokenType.EndObject) { - return new DataSource(databaseType, connectionString, datasourceOptions, health, userDelegatedAuth); + return new DataSource(databaseType, connectionString, datasourceOptions, health) + { + UserDelegatedAuth = userDelegatedAuth + }; } if (reader.TokenType is JsonTokenType.PropertyName) @@ -98,7 +101,7 @@ public DataSourceConverter(DeserializationVariableReplacementSettings? replaceme { try { - userDelegatedAuth = JsonSerializer.Deserialize(ref reader, options); + userDelegatedAuth = JsonSerializer.Deserialize(ref reader, options); } catch (Exception e) { diff --git a/src/Config/ObjectModel/DataSource.cs b/src/Config/ObjectModel/DataSource.cs index 769e109a43..93982b5bd5 100644 --- a/src/Config/ObjectModel/DataSource.cs +++ b/src/Config/ObjectModel/DataSource.cs @@ -14,13 +14,11 @@ namespace Azure.DataApiBuilder.Config.ObjectModel; /// Connection string to access the database. /// Custom options for the specific database. If there are no options, this could be null. /// Health check configuration for the datasource. -/// User-delegated authentication configuration (OBO). Optional. public record DataSource( DatabaseType DatabaseType, string ConnectionString, Dictionary? Options = null, - DatasourceHealthCheckConfig? Health = null, - UserDelegatedAuthConfig? UserDelegatedAuth = null) + DatasourceHealthCheckConfig? Health = null) { [JsonIgnore] public bool IsDatasourceHealthEnabled => @@ -42,6 +40,13 @@ public int DatasourceThresholdMs } } + /// + /// Configuration for user-delegated authentication (OBO) against the + /// configured database. + /// + [JsonPropertyName("user-delegated-auth")] + public UserDelegatedAuthOptions? UserDelegatedAuth { get; init; } + /// /// Converts the Options dictionary into a typed options object. /// May return null if the dictionary is null. diff --git a/src/Config/ObjectModel/UserDelegatedAuthConfig.cs b/src/Config/ObjectModel/UserDelegatedAuthConfig.cs deleted file mode 100644 index ee086a4053..0000000000 --- a/src/Config/ObjectModel/UserDelegatedAuthConfig.cs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Text.Json.Serialization; - -namespace Azure.DataApiBuilder.Config.ObjectModel; - -/// -/// Configuration for user-delegated authentication (OBO - On-Behalf-Of). -/// Enables per-user Entra ID access token authentication to Azure SQL. -/// -/// Whether user-delegated authentication is enabled. -/// The Azure SQL resource identifier for token acquisition. -/// Explicitly control connection pooling behavior. Default: true (disabled) for safety. Connection pooling is disabled by default in OBO scenarios to prevent token reuse across different user contexts. -/// In-memory cache duration for OBO tokens per user. Default: 50 minutes. -public record UserDelegatedAuthConfig( - bool Enabled = false, - string? DatabaseAudience = null, - bool? DisableConnectionPooling = null, - int? TokenCacheDurationMinutes = null) -{ - /// - /// Default value for token cache duration in minutes. - /// Must be less than typical token lifetime (60 min). - /// - public const int DEFAULT_TOKEN_CACHE_DURATION_MINUTES = 50; - - /// - /// Default value for connection pooling (disabled for safety in MVP). - /// - public const bool DEFAULT_DISABLE_CONNECTION_POOLING = true; - - /// - /// Minimum allowed token cache duration in minutes. - /// - public const int MIN_TOKEN_CACHE_DURATION_MINUTES = 1; - - /// - /// Maximum allowed token cache duration in minutes. - /// Must be less than typical token lifetime (60 min). - /// - public const int MAX_TOKEN_CACHE_DURATION_MINUTES = 59; - - /// - /// Gets the effective token cache duration value. - /// Returns the configured value or the default if not specified. - /// - [JsonIgnore] - public int EffectiveTokenCacheDurationMinutes => - TokenCacheDurationMinutes ?? DEFAULT_TOKEN_CACHE_DURATION_MINUTES; - - /// - /// Gets the effective connection pooling setting. - /// Returns the configured value or the default if not specified. - /// - [JsonIgnore] - public bool EffectiveDisableConnectionPooling => - DisableConnectionPooling ?? DEFAULT_DISABLE_CONNECTION_POOLING; -} diff --git a/src/Config/ObjectModel/UserDelegatedAuthOptions.cs b/src/Config/ObjectModel/UserDelegatedAuthOptions.cs new file mode 100644 index 0000000000..fb3b3154e3 --- /dev/null +++ b/src/Config/ObjectModel/UserDelegatedAuthOptions.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Azure.DataApiBuilder.Config.ObjectModel; + +/// +/// Configuration for user-delegated authentication (OBO - On-Behalf-Of). +/// Enables per-user Entra ID access token authentication to Azure SQL. +/// +/// Whether user-delegated authentication is enabled. +/// Identity provider for user-delegated authentication. +/// The Azure SQL resource identifier for token acquisition. +public record UserDelegatedAuthOptions( + [property: JsonPropertyName("enabled")] bool Enabled = false, + [property: JsonPropertyName("provider")] string? Provider = null, + [property: JsonPropertyName("database-audience")] string? DatabaseAudience = null) +{ + /// + /// Default duration, in minutes, to cache tokens for a given delegated identity. + /// With a 5-minute early refresh buffer, tokens are refreshed at the 40-minute mark. + /// + public const int DEFAULT_TOKEN_CACHE_DURATION_MINUTES = 45; + + /// + /// Environment variable name for OBO App Registration client ID. + /// Uses DAB-specific prefix to avoid conflict with AZURE_CLIENT_ID which is + /// interpreted by DefaultAzureCredential/ManagedIdentityCredential as a + /// User-Assigned Managed Identity ID. + /// + public const string AZURE_CLIENT_ID_ENV_VAR = "DAB_OBO_CLIENT_ID"; + + /// + /// Environment variable name for OBO App Registration client secret. + /// Used for On-Behalf-Of token exchange. + /// + public const string AZURE_CLIENT_SECRET_ENV_VAR = "DAB_OBO_CLIENT_SECRET"; + + /// + /// Environment variable name for OBO tenant ID. + /// Uses DAB-specific prefix for consistency with OBO client ID. + /// + public const string AZURE_TENANT_ID_ENV_VAR = "DAB_OBO_TENANT_ID"; +} From 4424cee335e7eb77564900f5dfb6ebfb7b7ddea0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 20:49:29 +0000 Subject: [PATCH 06/13] Revert global.json to original version Co-authored-by: anushakolan <45540936+anushakolan@users.noreply.github.com> --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 1bdb496ef0..17811390a4 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.417", + "version": "8.0.418", "rollForward": "latestFeature" } } From 7d74d474a2c0937616f0eb40bec1e9705b84e559 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 00:51:08 +0000 Subject: [PATCH 07/13] Remove duplicate UserDelegatedAuthOptions.cs and restore base definitions - Delete separate UserDelegatedAuthOptions.cs file (already defined in DataSource.cs in base branch) - Add missing IsUserDelegatedAuthEnabled property to DataSource - Add missing UserDelegatedAuthOptions record definition to DataSource.cs - Restore DataSourceConverterFactory.cs to base version (already has user-delegated-auth support) Co-authored-by: anushakolan <45540936+anushakolan@users.noreply.github.com> --- src/Config/ObjectModel/DataSource.cs | 71 +++++++++++++++++++ .../ObjectModel/UserDelegatedAuthOptions.cs | 45 ------------ 2 files changed, 71 insertions(+), 45 deletions(-) delete mode 100644 src/Config/ObjectModel/UserDelegatedAuthOptions.cs diff --git a/src/Config/ObjectModel/DataSource.cs b/src/Config/ObjectModel/DataSource.cs index 93982b5bd5..e04acdfa37 100644 --- a/src/Config/ObjectModel/DataSource.cs +++ b/src/Config/ObjectModel/DataSource.cs @@ -47,6 +47,13 @@ public int DatasourceThresholdMs [JsonPropertyName("user-delegated-auth")] public UserDelegatedAuthOptions? UserDelegatedAuth { get; init; } + /// + /// Indicates whether user-delegated authentication is enabled for this data source. + /// + [JsonIgnore] + public bool IsUserDelegatedAuthEnabled => + UserDelegatedAuth is not null && UserDelegatedAuth.Enabled; + /// /// Converts the Options dictionary into a typed options object. /// May return null if the dictionary is null. @@ -118,3 +125,67 @@ public record CosmosDbNoSQLDataSourceOptions(string? Database, string? Container /// Options for MsSql database. /// public record MsSqlOptions(bool SetSessionContext = true) : IDataSourceOptions; + +/// +/// Options for user-delegated authentication (OBO) for a data source. +/// +/// When OBO is NOT enabled (default): DAB connects to the database using a single application principal, +/// either via Managed Identity or credentials supplied in the connection string. All requests execute +/// under the same database identity regardless of which user made the API call. +/// +/// When OBO IS enabled: DAB exchanges the calling user's JWT for a database access token using the +/// On-Behalf-Of flow. This allows DAB to connect to the database as the actual user, enabling +/// Row-Level Security (RLS) filtering based on user identity. +/// +/// OBO requires an Azure AD App Registration (separate from the DAB service's Managed Identity). +/// The operator deploying DAB must set the following environment variables for the OBO App Registration, +/// which DAB reads at startup via Environment.GetEnvironmentVariable(): +/// - DAB_OBO_CLIENT_ID: The Application (client) ID of the OBO App Registration +/// - DAB_OBO_TENANT_ID: The Directory (tenant) ID where the OBO App Registration is registered +/// - DAB_OBO_CLIENT_SECRET: The client secret of the OBO App Registration (not a user secret) +/// +/// These credentials belong to the OBO App Registration, which acts as a confidential client to exchange +/// the incoming user JWT for a database access token. The user provides only their JWT; DAB uses the +/// App Registration credentials to perform the OBO token exchange on their behalf. +/// +/// These can be set in the hosting environment (e.g., Azure Container Apps secrets, Kubernetes secrets, +/// Docker environment variables, or local shell environment). +/// +/// Note: DAB-specific prefixes (DAB_OBO_*) are used instead of AZURE_* to avoid conflict with +/// DefaultAzureCredential, which interprets AZURE_CLIENT_ID as a User-Assigned Managed Identity ID. +/// At startup (when no user context is available), DAB falls back to Managed Identity for metadata operations. +/// +/// Whether user-delegated authentication is enabled. +/// The authentication provider (currently only EntraId is supported). +/// Audience used when acquiring database tokens on behalf of the user. +public record UserDelegatedAuthOptions( + [property: JsonPropertyName("enabled")] bool Enabled = false, + [property: JsonPropertyName("provider")] string? Provider = null, + [property: JsonPropertyName("database-audience")] string? DatabaseAudience = null) +{ + /// + /// Default duration, in minutes, to cache tokens for a given delegated identity. + /// With a 5-minute early refresh buffer, tokens are refreshed at the 40-minute mark. + /// + public const int DEFAULT_TOKEN_CACHE_DURATION_MINUTES = 45; + + /// + /// Environment variable name for OBO App Registration client ID. + /// Uses DAB-specific prefix to avoid conflict with AZURE_CLIENT_ID which is + /// interpreted by DefaultAzureCredential/ManagedIdentityCredential as a + /// User-Assigned Managed Identity ID. + /// + public const string DAB_OBO_CLIENT_ID_ENV_VAR = "DAB_OBO_CLIENT_ID"; + + /// + /// Environment variable name for OBO App Registration client secret. + /// Used for On-Behalf-Of token exchange. + /// + public const string DAB_OBO_CLIENT_SECRET_ENV_VAR = "DAB_OBO_CLIENT_SECRET"; + + /// + /// Environment variable name for OBO tenant ID. + /// Uses DAB-specific prefix for consistency with OBO client ID. + /// + public const string DAB_OBO_TENANT_ID_ENV_VAR = "DAB_OBO_TENANT_ID"; +} diff --git a/src/Config/ObjectModel/UserDelegatedAuthOptions.cs b/src/Config/ObjectModel/UserDelegatedAuthOptions.cs deleted file mode 100644 index fb3b3154e3..0000000000 --- a/src/Config/ObjectModel/UserDelegatedAuthOptions.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Text.Json.Serialization; - -namespace Azure.DataApiBuilder.Config.ObjectModel; - -/// -/// Configuration for user-delegated authentication (OBO - On-Behalf-Of). -/// Enables per-user Entra ID access token authentication to Azure SQL. -/// -/// Whether user-delegated authentication is enabled. -/// Identity provider for user-delegated authentication. -/// The Azure SQL resource identifier for token acquisition. -public record UserDelegatedAuthOptions( - [property: JsonPropertyName("enabled")] bool Enabled = false, - [property: JsonPropertyName("provider")] string? Provider = null, - [property: JsonPropertyName("database-audience")] string? DatabaseAudience = null) -{ - /// - /// Default duration, in minutes, to cache tokens for a given delegated identity. - /// With a 5-minute early refresh buffer, tokens are refreshed at the 40-minute mark. - /// - public const int DEFAULT_TOKEN_CACHE_DURATION_MINUTES = 45; - - /// - /// Environment variable name for OBO App Registration client ID. - /// Uses DAB-specific prefix to avoid conflict with AZURE_CLIENT_ID which is - /// interpreted by DefaultAzureCredential/ManagedIdentityCredential as a - /// User-Assigned Managed Identity ID. - /// - public const string AZURE_CLIENT_ID_ENV_VAR = "DAB_OBO_CLIENT_ID"; - - /// - /// Environment variable name for OBO App Registration client secret. - /// Used for On-Behalf-Of token exchange. - /// - public const string AZURE_CLIENT_SECRET_ENV_VAR = "DAB_OBO_CLIENT_SECRET"; - - /// - /// Environment variable name for OBO tenant ID. - /// Uses DAB-specific prefix for consistency with OBO client ID. - /// - public const string AZURE_TENANT_ID_ENV_VAR = "DAB_OBO_TENANT_ID"; -} From 8b5ccfe34ccbd550cda027c39bd59a5e63016143 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 00:52:52 +0000 Subject: [PATCH 08/13] Restore Config files to base - no changes needed - DataSourceConverterFactory.cs already has user-delegated-auth support in base - DataApiBuilderException.cs no changes needed Co-authored-by: anushakolan <45540936+anushakolan@users.noreply.github.com> --- .../Converters/DataSourceConverterFactory.cs | 28 +++++++++---------- src/Config/DataApiBuilderException.cs | 11 +++++++- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/Config/Converters/DataSourceConverterFactory.cs b/src/Config/Converters/DataSourceConverterFactory.cs index f83e7799cb..62d37c55e2 100644 --- a/src/Config/Converters/DataSourceConverterFactory.cs +++ b/src/Config/Converters/DataSourceConverterFactory.cs @@ -95,20 +95,6 @@ public DataSourceConverter(DeserializationVariableReplacementSettings? replaceme } } - break; - case "user-delegated-auth": - if (reader.TokenType is not JsonTokenType.Null) - { - try - { - userDelegatedAuth = JsonSerializer.Deserialize(ref reader, options); - } - catch (Exception e) - { - throw new JsonException($"Error while deserializing DataSource user-delegated-auth: {e.Message}"); - } - } - break; case "options": if (reader.TokenType is not JsonTokenType.Null) @@ -154,6 +140,20 @@ public DataSourceConverter(DeserializationVariableReplacementSettings? replaceme datasourceOptions = optionsDict; } + break; + case "user-delegated-auth": + if (reader.TokenType != JsonTokenType.Null) + { + try + { + userDelegatedAuth = JsonSerializer.Deserialize(ref reader, options); + } + catch (Exception e) + { + throw new JsonException($"Error while deserializing DataSource user-delegated-auth: {e.Message}"); + } + } + break; default: throw new JsonException($"Unexpected property {propertyName} while deserializing DataSource."); diff --git a/src/Config/DataApiBuilderException.cs b/src/Config/DataApiBuilderException.cs index b7696c4deb..95fe916c75 100644 --- a/src/Config/DataApiBuilderException.cs +++ b/src/Config/DataApiBuilderException.cs @@ -20,6 +20,11 @@ public class DataApiBuilderException : Exception public const string GRAPHQL_MUTATION_FIELD_AUTHZ_FAILURE = "Unauthorized due to one or more fields in this mutation."; public const string GRAPHQL_GROUPBY_FIELD_AUTHZ_FAILURE = "Access forbidden to field '{0}' referenced in the groupBy argument."; public const string GRAPHQL_AGGREGATION_FIELD_AUTHZ_FAILURE = "Access forbidden to field '{0}' referenced in the aggregation function '{1}'."; + public const string OBO_IDENTITY_CLAIMS_MISSING = "User-delegated authentication failed: Neither 'oid' nor 'sub' claim found in the access token."; + public const string OBO_TENANT_CLAIM_MISSING = "User-delegated authentication failed: 'tid' (tenant id) claim not found in the access token."; + public const string OBO_TOKEN_ACQUISITION_FAILED = "User-delegated authentication failed: Unable to acquire database access token on behalf of the user."; + public const string OBO_MISSING_USER_CONTEXT = "User-delegated authentication failed: Missing or invalid 'Authorization: Bearer ' header. OBO requires a valid user token to exchange for database access."; + public const string OBO_MISSING_DATABASE_AUDIENCE = "User-delegated authentication failed: 'database-audience' is not configured in the data source's user-delegated-auth settings."; public enum SubStatusCodes { @@ -127,7 +132,11 @@ public enum SubStatusCodes /// /// Error due to client input validation failure. /// - DatabaseInputError + DatabaseInputError, + /// + /// User-delegated (OBO) authentication failed due to missing identity claims. + /// + OboAuthenticationFailure } public HttpStatusCode StatusCode { get; } From 70f32f13bac2854a425f93266607a9ee4eafb99f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Feb 2026 01:59:13 +0000 Subject: [PATCH 09/13] Add JSON structure validation tests for CLI user-delegated-auth commands - Add test to verify correct JSON structure in data-source.user-delegated-auth section - Add test to verify JSON fields are updated correctly when modifying config - Add test to verify disabling user-delegated-auth updates JSON properly - All 15 tests passing (12 original + 3 new JSON validation tests) Co-authored-by: anushakolan <45540936+anushakolan@users.noreply.github.com> --- src/Cli.Tests/ConfigureOptionsTests.cs | 185 +++++++++++++++++++++++++ 1 file changed, 185 insertions(+) diff --git a/src/Cli.Tests/ConfigureOptionsTests.cs b/src/Cli.Tests/ConfigureOptionsTests.cs index 8ab024ae0b..46cf812618 100644 --- a/src/Cli.Tests/ConfigureOptionsTests.cs +++ b/src/Cli.Tests/ConfigureOptionsTests.cs @@ -1275,5 +1275,190 @@ public void TestUpdateUserDelegatedAuthDatabaseAudience() Assert.IsTrue(config.DataSource.UserDelegatedAuth.Enabled); Assert.AreEqual(newAudience, config.DataSource.UserDelegatedAuth.DatabaseAudience); } + + /// + /// Tests that CLI commands write the correct JSON structure to dab-config.json. + /// Verifies that user-delegated-auth section is properly nested under data-source with correct property names. + /// + [TestMethod] + public void TestUserDelegatedAuthCreatesCorrectJsonStructure() + { + // Arrange + SetupFileSystemWithInitialConfig(INITIAL_CONFIG); + string audienceValue = "https://database.windows.net"; + + ConfigureOptions options = new( + dataSourceUserDelegatedAuthEnabled: true, + dataSourceUserDelegatedAuthDatabaseAudience: audienceValue, + config: TEST_RUNTIME_CONFIG_FILE + ); + + // Act + bool isSuccess = TryConfigureSettings(options, _runtimeConfigLoader!, _fileSystem!); + + // Assert + Assert.IsTrue(isSuccess); + string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); + + // Verify JSON structure using JObject + JObject configJson = JObject.Parse(updatedConfig); + + // Verify data-source exists + Assert.IsNotNull(configJson["data-source"]); + + // Verify user-delegated-auth section exists under data-source + JToken? userDelegatedAuthSection = configJson["data-source"]?["user-delegated-auth"]; + Assert.IsNotNull(userDelegatedAuthSection); + + // Verify correct properties with correct values + Assert.AreEqual(true, (bool?)userDelegatedAuthSection["enabled"]); + Assert.AreEqual("EntraId", (string?)userDelegatedAuthSection["provider"]); + Assert.AreEqual(audienceValue, (string?)userDelegatedAuthSection["database-audience"]); + + // Verify no unexpected properties + JObject userDelegatedAuthObj = (JObject)userDelegatedAuthSection; + Assert.AreEqual(3, userDelegatedAuthObj.Properties().Count()); + } + + /// + /// Tests that CLI correctly updates the JSON structure when modifying existing user-delegated-auth configuration. + /// Verifies that only the specified field is updated while others are preserved. + /// + [TestMethod] + public void TestUserDelegatedAuthUpdatesCorrectJsonFields() + { + // Arrange - Start with config that has user-delegated-auth + string configWithUserDelegatedAuth = @" + { + ""$schema"": ""test"", + ""data-source"": { + ""database-type"": ""mssql"", + ""connection-string"": ""testconnectionstring"", + ""user-delegated-auth"": { + ""enabled"": true, + ""provider"": ""EntraId"", + ""database-audience"": ""https://database.windows.net"" + } + }, + ""runtime"": { + ""rest"": { + ""enabled"": true, + ""path"": ""/api"" + }, + ""graphql"": { + ""enabled"": true, + ""path"": ""/graphql"", + ""allow-introspection"": true + }, + ""host"": { + ""mode"": ""development"", + ""cors"": { + ""origins"": [], + ""allow-credentials"": false + }, + ""authentication"": { + ""provider"": ""StaticWebApps"" + } + } + }, + ""entities"": {} + }"; + SetupFileSystemWithInitialConfig(configWithUserDelegatedAuth); + + string newAudience = "https://database.chinacloudapi.cn"; + ConfigureOptions options = new( + dataSourceUserDelegatedAuthDatabaseAudience: newAudience, + config: TEST_RUNTIME_CONFIG_FILE + ); + + // Act + bool isSuccess = TryConfigureSettings(options, _runtimeConfigLoader!, _fileSystem!); + + // Assert + Assert.IsTrue(isSuccess); + string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); + + // Verify JSON structure + JObject configJson = JObject.Parse(updatedConfig); + JToken? userDelegatedAuthSection = configJson["data-source"]?["user-delegated-auth"]; + Assert.IsNotNull(userDelegatedAuthSection); + + // Verify database-audience was updated + Assert.AreEqual(newAudience, (string?)userDelegatedAuthSection["database-audience"]); + + // Verify other fields were preserved + Assert.AreEqual(true, (bool?)userDelegatedAuthSection["enabled"]); + Assert.AreEqual("EntraId", (string?)userDelegatedAuthSection["provider"]); + } + + /// + /// Tests that disabling user-delegated-auth updates the JSON structure correctly. + /// Verifies that the enabled field can be set to false while preserving other settings. + /// + [TestMethod] + public void TestUserDelegatedAuthDisableUpdatesJsonCorrectly() + { + // Arrange - Start with enabled config + string configWithUserDelegatedAuth = @" + { + ""$schema"": ""test"", + ""data-source"": { + ""database-type"": ""mssql"", + ""connection-string"": ""testconnectionstring"", + ""user-delegated-auth"": { + ""enabled"": true, + ""provider"": ""EntraId"", + ""database-audience"": ""https://database.windows.net"" + } + }, + ""runtime"": { + ""rest"": { + ""enabled"": true, + ""path"": ""/api"" + }, + ""graphql"": { + ""enabled"": true, + ""path"": ""/graphql"", + ""allow-introspection"": true + }, + ""host"": { + ""mode"": ""development"", + ""cors"": { + ""origins"": [], + ""allow-credentials"": false + }, + ""authentication"": { + ""provider"": ""StaticWebApps"" + } + } + }, + ""entities"": {} + }"; + SetupFileSystemWithInitialConfig(configWithUserDelegatedAuth); + + ConfigureOptions options = new( + dataSourceUserDelegatedAuthEnabled: false, + config: TEST_RUNTIME_CONFIG_FILE + ); + + // Act + bool isSuccess = TryConfigureSettings(options, _runtimeConfigLoader!, _fileSystem!); + + // Assert + Assert.IsTrue(isSuccess); + string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); + + // Verify JSON structure + JObject configJson = JObject.Parse(updatedConfig); + JToken? userDelegatedAuthSection = configJson["data-source"]?["user-delegated-auth"]; + Assert.IsNotNull(userDelegatedAuthSection); + + // Verify enabled was set to false + Assert.AreEqual(false, (bool?)userDelegatedAuthSection["enabled"]); + + // Verify other fields were preserved + Assert.AreEqual("EntraId", (string?)userDelegatedAuthSection["provider"]); + Assert.AreEqual("https://database.windows.net", (string?)userDelegatedAuthSection["database-audience"]); + } } } From c0795d3a323a275e2d4f0ae458b4e1a94fd1d8e7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Feb 2026 02:05:19 +0000 Subject: [PATCH 10/13] Refactor: Extract duplicate JSON config to TestHelper constant - Add CONFIG_WITH_USER_DELEGATED_AUTH constant to TestHelper - Use constant in test methods to reduce duplication - Improves test maintainability Co-authored-by: anushakolan <45540936+anushakolan@users.noreply.github.com> --- src/Cli.Tests/ConfigureOptionsTests.cs | 110 +------------------------ src/Cli.Tests/TestHelper.cs | 40 +++++++++ 2 files changed, 43 insertions(+), 107 deletions(-) diff --git a/src/Cli.Tests/ConfigureOptionsTests.cs b/src/Cli.Tests/ConfigureOptionsTests.cs index 46cf812618..a82af009a9 100644 --- a/src/Cli.Tests/ConfigureOptionsTests.cs +++ b/src/Cli.Tests/ConfigureOptionsTests.cs @@ -1221,41 +1221,7 @@ public void TestFailureWhenEnablingUserDelegatedAuthOnNonMSSQLDatabase(string db public void TestUpdateUserDelegatedAuthDatabaseAudience() { // Arrange - Config with existing user-delegated-auth section - string configWithUserDelegatedAuth = @" - { - ""$schema"": ""test"", - ""data-source"": { - ""database-type"": ""mssql"", - ""connection-string"": ""testconnectionstring"", - ""user-delegated-auth"": { - ""enabled"": true, - ""database-audience"": ""https://database.windows.net"" - } - }, - ""runtime"": { - ""rest"": { - ""enabled"": true, - ""path"": ""/api"" - }, - ""graphql"": { - ""enabled"": true, - ""path"": ""/graphql"", - ""allow-introspection"": true - }, - ""host"": { - ""mode"": ""development"", - ""cors"": { - ""origins"": [], - ""allow-credentials"": false - }, - ""authentication"": { - ""provider"": ""StaticWebApps"" - } - } - }, - ""entities"": {} - }"; - SetupFileSystemWithInitialConfig(configWithUserDelegatedAuth); + SetupFileSystemWithInitialConfig(TestHelper.CONFIG_WITH_USER_DELEGATED_AUTH); string newAudience = "https://database.usgovcloudapi.net"; ConfigureOptions options = new( @@ -1328,42 +1294,7 @@ public void TestUserDelegatedAuthCreatesCorrectJsonStructure() public void TestUserDelegatedAuthUpdatesCorrectJsonFields() { // Arrange - Start with config that has user-delegated-auth - string configWithUserDelegatedAuth = @" - { - ""$schema"": ""test"", - ""data-source"": { - ""database-type"": ""mssql"", - ""connection-string"": ""testconnectionstring"", - ""user-delegated-auth"": { - ""enabled"": true, - ""provider"": ""EntraId"", - ""database-audience"": ""https://database.windows.net"" - } - }, - ""runtime"": { - ""rest"": { - ""enabled"": true, - ""path"": ""/api"" - }, - ""graphql"": { - ""enabled"": true, - ""path"": ""/graphql"", - ""allow-introspection"": true - }, - ""host"": { - ""mode"": ""development"", - ""cors"": { - ""origins"": [], - ""allow-credentials"": false - }, - ""authentication"": { - ""provider"": ""StaticWebApps"" - } - } - }, - ""entities"": {} - }"; - SetupFileSystemWithInitialConfig(configWithUserDelegatedAuth); + SetupFileSystemWithInitialConfig(TestHelper.CONFIG_WITH_USER_DELEGATED_AUTH); string newAudience = "https://database.chinacloudapi.cn"; ConfigureOptions options = new( @@ -1399,42 +1330,7 @@ public void TestUserDelegatedAuthUpdatesCorrectJsonFields() public void TestUserDelegatedAuthDisableUpdatesJsonCorrectly() { // Arrange - Start with enabled config - string configWithUserDelegatedAuth = @" - { - ""$schema"": ""test"", - ""data-source"": { - ""database-type"": ""mssql"", - ""connection-string"": ""testconnectionstring"", - ""user-delegated-auth"": { - ""enabled"": true, - ""provider"": ""EntraId"", - ""database-audience"": ""https://database.windows.net"" - } - }, - ""runtime"": { - ""rest"": { - ""enabled"": true, - ""path"": ""/api"" - }, - ""graphql"": { - ""enabled"": true, - ""path"": ""/graphql"", - ""allow-introspection"": true - }, - ""host"": { - ""mode"": ""development"", - ""cors"": { - ""origins"": [], - ""allow-credentials"": false - }, - ""authentication"": { - ""provider"": ""StaticWebApps"" - } - } - }, - ""entities"": {} - }"; - SetupFileSystemWithInitialConfig(configWithUserDelegatedAuth); + SetupFileSystemWithInitialConfig(TestHelper.CONFIG_WITH_USER_DELEGATED_AUTH); ConfigureOptions options = new( dataSourceUserDelegatedAuthEnabled: false, diff --git a/src/Cli.Tests/TestHelper.cs b/src/Cli.Tests/TestHelper.cs index 8224a079d4..1cd7852de9 100644 --- a/src/Cli.Tests/TestHelper.cs +++ b/src/Cli.Tests/TestHelper.cs @@ -279,6 +279,46 @@ public static Process ExecuteDabCommand(string command, string flags) public const string CONFIG_WITH_DISABLED_GLOBAL_REST_GRAPHQL = $"{{{SAMPLE_SCHEMA_DATA_SOURCE},{RUNTIME_SECTION_WITH_DISABLED_REST_GRAPHQL}}}"; + /// + /// A config json with user-delegated-auth enabled. This is used in tests to verify updating existing + /// user-delegated-auth configuration. + /// + public const string CONFIG_WITH_USER_DELEGATED_AUTH = @" + { + ""$schema"": """ + DAB_DRAFT_SCHEMA_TEST_PATH + @""", + ""data-source"": { + ""database-type"": ""mssql"", + ""connection-string"": """ + SAMPLE_TEST_CONN_STRING + @""", + ""user-delegated-auth"": { + ""enabled"": true, + ""provider"": ""EntraId"", + ""database-audience"": ""https://database.windows.net"" + } + }, + ""runtime"": { + ""rest"": { + ""enabled"": true, + ""path"": ""/api"" + }, + ""graphql"": { + ""enabled"": true, + ""path"": ""/graphql"", + ""allow-introspection"": true + }, + ""host"": { + ""mode"": ""development"", + ""cors"": { + ""origins"": [], + ""allow-credentials"": false + }, + ""authentication"": { + ""provider"": ""StaticWebApps"" + } + } + }, + ""entities"": {} + }"; + public const string SINGLE_ENTITY = @" { ""entities"": { From 1cd2637c6ff2b809322482c95e3ae471e28c3a15 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Feb 2026 17:04:36 +0000 Subject: [PATCH 11/13] Address code review feedback - consolidate tests and update help text - Update help text to mention SQL Server on-premises (not just Azure SQL) - Consolidate three separate tests into one parameterized test with DataRows - Remove redundant JSON validation tests (parsing tests already verify properties) - Add JSON structure validation to TestUpdateUserDelegatedAuthDatabaseAudience - Reduced from 15 to 12 tests while maintaining coverage Co-authored-by: anushakolan <45540936+anushakolan@users.noreply.github.com> --- src/Cli.Tests/ConfigureOptionsTests.cs | 204 ++++--------------------- src/Cli/Commands/ConfigureOptions.cs | 4 +- 2 files changed, 32 insertions(+), 176 deletions(-) diff --git a/src/Cli.Tests/ConfigureOptionsTests.cs b/src/Cli.Tests/ConfigureOptionsTests.cs index a82af009a9..b1bf3ba303 100644 --- a/src/Cli.Tests/ConfigureOptionsTests.cs +++ b/src/Cli.Tests/ConfigureOptionsTests.cs @@ -1096,46 +1096,27 @@ private void SetupFileSystemWithInitialConfig(string jsonConfig) } /// - /// Tests adding user-delegated-auth.enabled to a config that doesn't have user-delegated-auth configured. - /// This method verifies that the user-delegated-auth.enabled property can be set to true for MSSQL database. - /// Command: dab configure --data-source.user-delegated-auth.enabled true + /// Tests adding user-delegated-auth configuration options individually or together. + /// Verifies that enabled and database-audience properties can be set independently or combined. + /// Commands: + /// - dab configure --data-source.user-delegated-auth.enabled true + /// - dab configure --data-source.user-delegated-auth.database-audience "https://database.windows.net" + /// - dab configure --data-source.user-delegated-auth.enabled true --data-source.user-delegated-auth.database-audience "https://database.windows.net" /// - [TestMethod] - public void TestAddUserDelegatedAuthEnabled() - { - // Arrange - SetupFileSystemWithInitialConfig(INITIAL_CONFIG); - - ConfigureOptions options = new( - dataSourceUserDelegatedAuthEnabled: true, - config: TEST_RUNTIME_CONFIG_FILE - ); - - // Act - bool isSuccess = TryConfigureSettings(options, _runtimeConfigLoader!, _fileSystem!); - - // Assert - Assert.IsTrue(isSuccess); - string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? config)); - Assert.IsNotNull(config.DataSource); - Assert.IsNotNull(config.DataSource.UserDelegatedAuth); - Assert.IsTrue(config.DataSource.UserDelegatedAuth.Enabled); - } - - /// - /// Tests adding user-delegated-auth.database-audience to a config that doesn't have user-delegated-auth configured. - /// This method verifies that the database-audience can be set for user-delegated authentication. - /// Command: dab configure --data-source.user-delegated-auth.database-audience "https://database.windows.net" - /// - [TestMethod] - public void TestAddUserDelegatedAuthDatabaseAudience() + [DataTestMethod] + [DataRow(true, null, DisplayName = "Set enabled=true only")] + [DataRow(null, "https://database.windows.net", DisplayName = "Set database-audience only")] + [DataRow(true, "https://database.windows.net", DisplayName = "Set both enabled and database-audience (public cloud)")] + [DataRow(true, "https://database.usgovcloudapi.net", DisplayName = "Set both enabled and database-audience (gov cloud)")] + [DataRow(true, "https://database.chinacloudapi.cn", DisplayName = "Set both enabled and database-audience (china cloud)")] + [DataRow(true, "https://myinstance.abc123.database.windows.net", DisplayName = "Set both enabled and database-audience (managed instance)")] + public void TestAddUserDelegatedAuthConfiguration(bool? enabledValue, string? audienceValue) { // Arrange SetupFileSystemWithInitialConfig(INITIAL_CONFIG); - string audienceValue = "https://database.windows.net"; ConfigureOptions options = new( + dataSourceUserDelegatedAuthEnabled: enabledValue, dataSourceUserDelegatedAuthDatabaseAudience: audienceValue, config: TEST_RUNTIME_CONFIG_FILE ); @@ -1149,41 +1130,21 @@ public void TestAddUserDelegatedAuthDatabaseAudience() Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? config)); Assert.IsNotNull(config.DataSource); Assert.IsNotNull(config.DataSource.UserDelegatedAuth); - Assert.AreEqual(audienceValue, config.DataSource.UserDelegatedAuth.DatabaseAudience); - } - /// - /// Tests adding both user-delegated-auth.enabled and database-audience in a single command. - /// This method verifies that both properties can be set together. - /// Command: dab configure --data-source.user-delegated-auth.enabled true --data-source.user-delegated-auth.database-audience "https://database.windows.net" - /// - [DataTestMethod] - [DataRow("https://database.windows.net", DisplayName = "Azure SQL Database (public cloud)")] - [DataRow("https://database.usgovcloudapi.net", DisplayName = "Azure Government Cloud")] - [DataRow("https://database.chinacloudapi.cn", DisplayName = "Azure China Cloud")] - [DataRow("https://myinstance.abc123.database.windows.net", DisplayName = "Azure SQL Managed Instance")] - public void TestAddUserDelegatedAuthEnabledAndDatabaseAudience(string audienceValue) - { - // Arrange - SetupFileSystemWithInitialConfig(INITIAL_CONFIG); - - ConfigureOptions options = new( - dataSourceUserDelegatedAuthEnabled: true, - dataSourceUserDelegatedAuthDatabaseAudience: audienceValue, - config: TEST_RUNTIME_CONFIG_FILE - ); + // Verify enabled value + if (enabledValue.HasValue) + { + Assert.AreEqual(enabledValue.Value, config.DataSource.UserDelegatedAuth.Enabled); + } - // Act - bool isSuccess = TryConfigureSettings(options, _runtimeConfigLoader!, _fileSystem!); + // Verify database-audience value + if (audienceValue is not null) + { + Assert.AreEqual(audienceValue, config.DataSource.UserDelegatedAuth.DatabaseAudience); + } - // Assert - Assert.IsTrue(isSuccess); - string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? config)); - Assert.IsNotNull(config.DataSource); - Assert.IsNotNull(config.DataSource.UserDelegatedAuth); - Assert.IsTrue(config.DataSource.UserDelegatedAuth.Enabled); - Assert.AreEqual(audienceValue, config.DataSource.UserDelegatedAuth.DatabaseAudience); + // Verify provider is set to default + Assert.AreEqual("EntraId", config.DataSource.UserDelegatedAuth.Provider); } /// @@ -1216,6 +1177,7 @@ public void TestFailureWhenEnablingUserDelegatedAuthOnNonMSSQLDatabase(string db /// /// Tests updating existing user-delegated-auth configuration by changing the database-audience. /// This method verifies that the database-audience can be updated while preserving the enabled setting. + /// Also verifies that the JSON structure is correct with proper nesting under data-source. /// [TestMethod] public void TestUpdateUserDelegatedAuthDatabaseAudience() @@ -1240,121 +1202,15 @@ public void TestUpdateUserDelegatedAuthDatabaseAudience() Assert.IsNotNull(config.DataSource.UserDelegatedAuth); Assert.IsTrue(config.DataSource.UserDelegatedAuth.Enabled); Assert.AreEqual(newAudience, config.DataSource.UserDelegatedAuth.DatabaseAudience); - } - - /// - /// Tests that CLI commands write the correct JSON structure to dab-config.json. - /// Verifies that user-delegated-auth section is properly nested under data-source with correct property names. - /// - [TestMethod] - public void TestUserDelegatedAuthCreatesCorrectJsonStructure() - { - // Arrange - SetupFileSystemWithInitialConfig(INITIAL_CONFIG); - string audienceValue = "https://database.windows.net"; + Assert.AreEqual("EntraId", config.DataSource.UserDelegatedAuth.Provider); - ConfigureOptions options = new( - dataSourceUserDelegatedAuthEnabled: true, - dataSourceUserDelegatedAuthDatabaseAudience: audienceValue, - config: TEST_RUNTIME_CONFIG_FILE - ); - - // Act - bool isSuccess = TryConfigureSettings(options, _runtimeConfigLoader!, _fileSystem!); - - // Assert - Assert.IsTrue(isSuccess); - string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - - // Verify JSON structure using JObject - JObject configJson = JObject.Parse(updatedConfig); - - // Verify data-source exists - Assert.IsNotNull(configJson["data-source"]); - - // Verify user-delegated-auth section exists under data-source - JToken? userDelegatedAuthSection = configJson["data-source"]?["user-delegated-auth"]; - Assert.IsNotNull(userDelegatedAuthSection); - - // Verify correct properties with correct values - Assert.AreEqual(true, (bool?)userDelegatedAuthSection["enabled"]); - Assert.AreEqual("EntraId", (string?)userDelegatedAuthSection["provider"]); - Assert.AreEqual(audienceValue, (string?)userDelegatedAuthSection["database-audience"]); - - // Verify no unexpected properties - JObject userDelegatedAuthObj = (JObject)userDelegatedAuthSection; - Assert.AreEqual(3, userDelegatedAuthObj.Properties().Count()); - } - - /// - /// Tests that CLI correctly updates the JSON structure when modifying existing user-delegated-auth configuration. - /// Verifies that only the specified field is updated while others are preserved. - /// - [TestMethod] - public void TestUserDelegatedAuthUpdatesCorrectJsonFields() - { - // Arrange - Start with config that has user-delegated-auth - SetupFileSystemWithInitialConfig(TestHelper.CONFIG_WITH_USER_DELEGATED_AUTH); - - string newAudience = "https://database.chinacloudapi.cn"; - ConfigureOptions options = new( - dataSourceUserDelegatedAuthDatabaseAudience: newAudience, - config: TEST_RUNTIME_CONFIG_FILE - ); - - // Act - bool isSuccess = TryConfigureSettings(options, _runtimeConfigLoader!, _fileSystem!); - - // Assert - Assert.IsTrue(isSuccess); - string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - - // Verify JSON structure + // Verify JSON structure using JObject to ensure correct nesting JObject configJson = JObject.Parse(updatedConfig); JToken? userDelegatedAuthSection = configJson["data-source"]?["user-delegated-auth"]; Assert.IsNotNull(userDelegatedAuthSection); - - // Verify database-audience was updated Assert.AreEqual(newAudience, (string?)userDelegatedAuthSection["database-audience"]); - - // Verify other fields were preserved Assert.AreEqual(true, (bool?)userDelegatedAuthSection["enabled"]); Assert.AreEqual("EntraId", (string?)userDelegatedAuthSection["provider"]); } - - /// - /// Tests that disabling user-delegated-auth updates the JSON structure correctly. - /// Verifies that the enabled field can be set to false while preserving other settings. - /// - [TestMethod] - public void TestUserDelegatedAuthDisableUpdatesJsonCorrectly() - { - // Arrange - Start with enabled config - SetupFileSystemWithInitialConfig(TestHelper.CONFIG_WITH_USER_DELEGATED_AUTH); - - ConfigureOptions options = new( - dataSourceUserDelegatedAuthEnabled: false, - config: TEST_RUNTIME_CONFIG_FILE - ); - - // Act - bool isSuccess = TryConfigureSettings(options, _runtimeConfigLoader!, _fileSystem!); - - // Assert - Assert.IsTrue(isSuccess); - string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - - // Verify JSON structure - JObject configJson = JObject.Parse(updatedConfig); - JToken? userDelegatedAuthSection = configJson["data-source"]?["user-delegated-auth"]; - Assert.IsNotNull(userDelegatedAuthSection); - - // Verify enabled was set to false - Assert.AreEqual(false, (bool?)userDelegatedAuthSection["enabled"]); - - // Verify other fields were preserved - Assert.AreEqual("EntraId", (string?)userDelegatedAuthSection["provider"]); - Assert.AreEqual("https://database.windows.net", (string?)userDelegatedAuthSection["database-audience"]); - } } } diff --git a/src/Cli/Commands/ConfigureOptions.cs b/src/Cli/Commands/ConfigureOptions.cs index 069a2d3b6a..262cbc9145 100644 --- a/src/Cli/Commands/ConfigureOptions.cs +++ b/src/Cli/Commands/ConfigureOptions.cs @@ -164,10 +164,10 @@ public ConfigureOptions( [Option("data-source.health.name", Required = false, HelpText = "Identifier for data source in health check report.")] public string? DataSourceHealthName { get; } - [Option("data-source.user-delegated-auth.enabled", Required = false, HelpText = "Enable user-delegated authentication (OBO) for Azure SQL. Default: false (boolean).")] + [Option("data-source.user-delegated-auth.enabled", Required = false, HelpText = "Enable user-delegated authentication (OBO) for Azure SQL and SQL Server. Default: false (boolean).")] public bool? DataSourceUserDelegatedAuthEnabled { get; } - [Option("data-source.user-delegated-auth.database-audience", Required = false, HelpText = "Azure SQL resource identifier for token acquisition (e.g., https://database.windows.net).")] + [Option("data-source.user-delegated-auth.database-audience", Required = false, HelpText = "Database resource identifier for token acquisition (e.g., https://database.windows.net for Azure SQL).")] public string? DataSourceUserDelegatedAuthDatabaseAudience { get; } [Option("runtime.graphql.depth-limit", Required = false, HelpText = "Max allowed depth of the nested query. Allowed values: (0,2147483647] inclusive. Default is infinity. Use -1 to remove limit.")] From c0fdd623f139af74533e7cd903991962af354c2a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Feb 2026 17:08:38 +0000 Subject: [PATCH 12/13] Enhance test assertions to verify default values - Add assertions to verify default Enabled=false when only database-audience is set - Add assertions to verify default DatabaseAudience=null when only enabled is set - Improve method documentation to clarify JSON structure validation Co-authored-by: anushakolan <45540936+anushakolan@users.noreply.github.com> --- src/Cli.Tests/ConfigureOptionsTests.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Cli.Tests/ConfigureOptionsTests.cs b/src/Cli.Tests/ConfigureOptionsTests.cs index b1bf3ba303..e18b10468e 100644 --- a/src/Cli.Tests/ConfigureOptionsTests.cs +++ b/src/Cli.Tests/ConfigureOptionsTests.cs @@ -1098,6 +1098,7 @@ private void SetupFileSystemWithInitialConfig(string jsonConfig) /// /// Tests adding user-delegated-auth configuration options individually or together. /// Verifies that enabled and database-audience properties can be set independently or combined. + /// Also verifies default values for properties not explicitly set. /// Commands: /// - dab configure --data-source.user-delegated-auth.enabled true /// - dab configure --data-source.user-delegated-auth.database-audience "https://database.windows.net" @@ -1131,17 +1132,25 @@ public void TestAddUserDelegatedAuthConfiguration(bool? enabledValue, string? au Assert.IsNotNull(config.DataSource); Assert.IsNotNull(config.DataSource.UserDelegatedAuth); - // Verify enabled value + // Verify enabled value (if set, use provided value; otherwise defaults to false) if (enabledValue.HasValue) { Assert.AreEqual(enabledValue.Value, config.DataSource.UserDelegatedAuth.Enabled); } + else + { + Assert.IsFalse(config.DataSource.UserDelegatedAuth.Enabled); + } // Verify database-audience value if (audienceValue is not null) { Assert.AreEqual(audienceValue, config.DataSource.UserDelegatedAuth.DatabaseAudience); } + else + { + Assert.IsNull(config.DataSource.UserDelegatedAuth.DatabaseAudience); + } // Verify provider is set to default Assert.AreEqual("EntraId", config.DataSource.UserDelegatedAuth.Provider); @@ -1176,8 +1185,9 @@ public void TestFailureWhenEnablingUserDelegatedAuthOnNonMSSQLDatabase(string db /// /// Tests updating existing user-delegated-auth configuration by changing the database-audience. - /// This method verifies that the database-audience can be updated while preserving the enabled setting. - /// Also verifies that the JSON structure is correct with proper nesting under data-source. + /// Verifies that the database-audience can be updated while preserving the enabled setting. + /// Also validates JSON structure: verifies user-delegated-auth is correctly nested under data-source + /// with proper JSON property names (enabled, provider, database-audience). /// [TestMethod] public void TestUpdateUserDelegatedAuthDatabaseAudience() From 098fbcc6e2a9a9ef70bab8d4b125c0321d6e5eac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 01:09:04 +0000 Subject: [PATCH 13/13] Remove redundant cloud-specific DataRows from tests Removed duplicate DataRows for gov/china/managed instance clouds since they just test string assignment and don't actually validate cloud endpoints. Kept essential test cases: enabled only, audience only, and both together. Test count reduced from 12 to 9 tests while maintaining full coverage. Co-authored-by: anushakolan <45540936+anushakolan@users.noreply.github.com> --- src/Cli.Tests/ConfigureOptionsTests.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Cli.Tests/ConfigureOptionsTests.cs b/src/Cli.Tests/ConfigureOptionsTests.cs index e18b10468e..a1355679fb 100644 --- a/src/Cli.Tests/ConfigureOptionsTests.cs +++ b/src/Cli.Tests/ConfigureOptionsTests.cs @@ -1107,10 +1107,7 @@ private void SetupFileSystemWithInitialConfig(string jsonConfig) [DataTestMethod] [DataRow(true, null, DisplayName = "Set enabled=true only")] [DataRow(null, "https://database.windows.net", DisplayName = "Set database-audience only")] - [DataRow(true, "https://database.windows.net", DisplayName = "Set both enabled and database-audience (public cloud)")] - [DataRow(true, "https://database.usgovcloudapi.net", DisplayName = "Set both enabled and database-audience (gov cloud)")] - [DataRow(true, "https://database.chinacloudapi.cn", DisplayName = "Set both enabled and database-audience (china cloud)")] - [DataRow(true, "https://myinstance.abc123.database.windows.net", DisplayName = "Set both enabled and database-audience (managed instance)")] + [DataRow(true, "https://database.windows.net", DisplayName = "Set both enabled and database-audience")] public void TestAddUserDelegatedAuthConfiguration(bool? enabledValue, string? audienceValue) { // Arrange