Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
f246618
Initial plan
Copilot Feb 12, 2026
4792325
Add CLI support for user-delegated-auth configuration
Copilot Feb 12, 2026
122f503
Add runtime parsing tests and verify CLI functionality
Copilot Feb 12, 2026
be514c8
Address code review feedback - improve documentation and validation
Copilot Feb 12, 2026
099435a
Merge branch 'main' into copilot/add-cli-support-obo-delegated-identity
anushakolan Feb 23, 2026
b0f98fe
Align CLI implementation with PR #3151 - rename to UserDelegatedAuthO…
Copilot Feb 23, 2026
4424cee
Revert global.json to original version
Copilot Feb 23, 2026
7d74d47
Remove duplicate UserDelegatedAuthOptions.cs and restore base definit…
Copilot Feb 25, 2026
8b5ccfe
Restore Config files to base - no changes needed
Copilot Feb 25, 2026
0792079
Merge branch 'main' into copilot/add-cli-support-obo-delegated-identity
anushakolan Feb 26, 2026
70f32f1
Add JSON structure validation tests for CLI user-delegated-auth commands
Copilot Feb 26, 2026
c0795d3
Refactor: Extract duplicate JSON config to TestHelper constant
Copilot Feb 26, 2026
ae4b942
Merge branch 'main' into copilot/add-cli-support-obo-delegated-identity
Aniruddh25 Feb 26, 2026
1cd2637
Address code review feedback - consolidate tests and update help text
Copilot Feb 26, 2026
c0fdd62
Enhance test assertions to verify default values
Copilot Feb 26, 2026
572e924
Merge branch 'main' into copilot/add-cli-support-obo-delegated-identity
anushakolan Feb 26, 2026
098fbcc
Remove redundant cloud-specific DataRows from tests
Copilot Feb 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions src/Cli.Tests/ConfigureOptionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1094,5 +1094,130 @@ private void SetupFileSystemWithInitialConfig(string jsonConfig)
Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(jsonConfig, out RuntimeConfig? config));
Assert.IsNotNull(config.Runtime);
}

/// <summary>
/// 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"
/// - dab configure --data-source.user-delegated-auth.enabled true --data-source.user-delegated-auth.database-audience "https://database.windows.net"
/// </summary>
[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 void TestAddUserDelegatedAuthConfiguration(bool? enabledValue, string? audienceValue)
{
// Arrange
SetupFileSystemWithInitialConfig(INITIAL_CONFIG);

ConfigureOptions options = new(
dataSourceUserDelegatedAuthEnabled: enabledValue,
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);

// 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);
}

/// <summary>
/// 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
/// </summary>
[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);
}

/// <summary>
/// Tests updating existing user-delegated-auth configuration by changing the database-audience.
/// 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).
/// </summary>
[TestMethod]
public void TestUpdateUserDelegatedAuthDatabaseAudience()
{
// Arrange - Config with existing user-delegated-auth section
SetupFileSystemWithInitialConfig(TestHelper.CONFIG_WITH_USER_DELEGATED_AUTH);

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);
Assert.AreEqual("EntraId", config.DataSource.UserDelegatedAuth.Provider);

// 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);
Assert.AreEqual(newAudience, (string?)userDelegatedAuthSection["database-audience"]);
Assert.AreEqual(true, (bool?)userDelegatedAuthSection["enabled"]);
Assert.AreEqual("EntraId", (string?)userDelegatedAuthSection["provider"]);
}
}
}
40 changes: 40 additions & 0 deletions src/Cli.Tests/TestHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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}}}";

/// <summary>
/// A config json with user-delegated-auth enabled. This is used in tests to verify updating existing
/// user-delegated-auth configuration.
/// </summary>
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"": {
Expand Down
101 changes: 101 additions & 0 deletions src/Cli.Tests/UserDelegatedAuthRuntimeParsingTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// 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);
}

[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);
}
}
}
10 changes: 10 additions & 0 deletions src/Cli/Commands/ConfigureOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -84,6 +86,8 @@ public ConfigureOptions(
DataSourceOptionsSchema = dataSourceOptionsSchema;
DataSourceOptionsSetSessionContext = dataSourceOptionsSetSessionContext;
DataSourceHealthName = dataSourceHealthName;
DataSourceUserDelegatedAuthEnabled = dataSourceUserDelegatedAuthEnabled;
DataSourceUserDelegatedAuthDatabaseAudience = dataSourceUserDelegatedAuthDatabaseAudience;
// GraphQL
DepthLimit = depthLimit;
RuntimeGraphQLEnabled = runtimeGraphQLEnabled;
Expand Down Expand Up @@ -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 and SQL Server. Default: false (boolean).")]
public bool? DataSourceUserDelegatedAuthEnabled { get; }

[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.")]
public int? DepthLimit { get; }

Expand Down
36 changes: 35 additions & 1 deletion src/Cli/ConfigGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,7 @@ private static bool TryUpdateConfiguredDataSourceOptions(
DatabaseType dbType = runtimeConfig.DataSource.DatabaseType;
string dataSourceConnectionString = runtimeConfig.DataSource.ConnectionString;
DatasourceHealthCheckConfig? datasourceHealthCheckConfig = runtimeConfig.DataSource.Health;
UserDelegatedAuthOptions? userDelegatedAuthConfig = runtimeConfig.DataSource.UserDelegatedAuth;

if (options.DataSourceDatabaseType is not null)
{
Expand Down Expand Up @@ -714,8 +715,41 @@ private static bool TryUpdateConfiguredDataSourceOptions(
}
}

// Handle user-delegated-auth options
if (options.DataSourceUserDelegatedAuthEnabled is not null
|| options.DataSourceUserDelegatedAuthDatabaseAudience is not null)
{
// 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;
}

// Get database-audience: use new value if provided, otherwise preserve existing
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 UserDelegatedAuthOptions(
Enabled: enabled,
Provider: provider,
DatabaseAudience: databaseAudience);
}

dbOptions = EnumerableUtilities.IsNullOrEmpty(dbOptions) ? null : dbOptions;
DataSource dataSource = new(dbType, dataSourceConnectionString, dbOptions, datasourceHealthCheckConfig);
DataSource dataSource = new(dbType, dataSourceConnectionString, dbOptions, datasourceHealthCheckConfig)
{
UserDelegatedAuth = userDelegatedAuthConfig
};
runtimeConfig = runtimeConfig with { DataSource = dataSource };

return runtimeConfig != null;
Expand Down