Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
181 changes: 181 additions & 0 deletions src/Cli.Tests/ConfigureOptionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1094,5 +1094,186 @@ private void SetupFileSystemWithInitialConfig(string jsonConfig)
Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(jsonConfig, out RuntimeConfig? config));
Assert.IsNotNull(config.Runtime);
}

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

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

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

/// <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.
/// This method verifies that the database-audience can be updated while preserving the enabled setting.
/// </summary>
[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);
}
}
}
103 changes: 103 additions & 0 deletions src/Cli.Tests/UserDelegatedAuthRuntimeParsingTests.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
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. 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; }

Expand Down
31 changes: 30 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;
UserDelegatedAuthConfig? userDelegatedAuthConfig = runtimeConfig.DataSource.UserDelegatedAuth;

if (options.DataSourceDatabaseType is not null)
{
Expand Down Expand Up @@ -714,8 +715,36 @@ 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;

// Create or update user-delegated-auth config
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;
Expand Down
Loading