Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add authentication (username & password) to AddMongoDB #5788

Open
wants to merge 27 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
8470f27
Add authentication (username & password) to AddMongoDB as optional pa…
Crazyht Jun 3, 2024
563d5ad
Merge branch 'dotnet:main' into main
Crazyht Jun 3, 2024
ba0b659
Fix Test for ManifestGeneration by adding passord to config (with val…
Crazyht Jun 3, 2024
a4c621d
Merge branch 'main' of https://github.com/Crazyht/aspire
Crazyht Jun 3, 2024
4371472
Merge branch 'dotnet:main' into main
Crazyht Jun 5, 2024
67354be
Remove Uri.DataEncode call
Crazyht Jun 5, 2024
0249def
Merge branch 'main' of https://github.com/Crazyht/aspire
Crazyht Jun 5, 2024
0df7842
Fix test on manifest test after removing EscapeDataString
Crazyht Jun 5, 2024
a78049f
Merge branch 'dotnet:main' into main
Crazyht Jun 7, 2024
bdee17d
Add overload to avoid breaking change according to discussion on PR
Crazyht Jun 7, 2024
80deb50
Add overload to MongoDBServerResource to avoid breaking changes
Crazyht Jun 7, 2024
e66a4f4
Remove special character in password generation as for now we can't e…
Crazyht Jun 7, 2024
92fc5d4
adapt mongo password manifest to include special false
Crazyht Jun 7, 2024
cb0c788
Add authentication mechanism with default to SCRAM-SHA-256 & reformat…
Crazyht Jun 8, 2024
a31c314
Merge branch 'dotnet:main' into main
Crazyht Jun 8, 2024
66e5f58
Change ConnectionString part to internal
Crazyht Jun 8, 2024
77fea15
Merge branch 'main' of https://github.com/Crazyht/aspire
Crazyht Jun 8, 2024
549a251
Update tests/testproject/TestProject.AppHost/TestProgram.cs
Crazyht Jun 8, 2024
ddc38f2
Merge branch 'dotnet:main' into main
Crazyht Jun 11, 2024
4536526
Apply review comments
Crazyht Jun 11, 2024
94f40f8
Merge branch 'main' of https://github.com/Crazyht/aspire
Crazyht Jun 11, 2024
34bfcf3
Merge branch 'dotnet:main' into main
Crazyht Jun 11, 2024
5cb8399
Merge conflit from upstream
Crazyht Jun 16, 2024
fe099e0
Apply eerhardt review
Crazyht Jun 21, 2024
f4d80c2
Merge remote-tracking branch 'upstream/main'
Crazyht Jun 21, 2024
5d27ad5
Merge remote-tracking branch 'upstream/main' into Crazyht/main
eerhardt Sep 19, 2024
5598874
Fix tests and minor code refactoring
eerhardt Sep 19, 2024
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
48 changes: 44 additions & 4 deletions src/Aspire.Hosting.MongoDB/MongoDBBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Globalization;

using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.MongoDB;
using Aspire.Hosting.Utils;
Expand All @@ -16,19 +18,42 @@ public static class MongoDBBuilderExtensions
// Internal port is always 27017.
private const int DefaultContainerPort = 27017;

private const string UserEnvVarName = "MONGO_INITDB_ROOT_USERNAME";
private const string PasswordEnvVarName = "MONGO_INITDB_ROOT_PASSWORD";

/// <summary>
/// Adds a MongoDB resource to the application model. A container is used for local development. This version the package defaults to the 7.0.8 tag of the mongo container image.
/// </summary>
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/>.</param>
/// <param name="name">The name of the resource. This name will be used as the connection string name when referenced in a dependency.</param>
/// <param name="port">The host port for MongoDB.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
public static IResourceBuilder<MongoDBServerResource> AddMongoDB(this IDistributedApplicationBuilder builder, string name, int? port)
{
return AddMongoDB(builder, name, port, null, null);
}

/// <summary>
/// Adds a MongoDB resource to the application model. A container is used for local development. This version the package defaults to the 7.0.8 tag of the mongo container image.
/// </summary>
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/>.</param>
/// <param name="name">The name of the resource. This name will be used as the connection string name when referenced in a dependency.</param>
/// <param name="port">The host port for MongoDB.</param>
/// <param name="userName">A parameter that contains the MongoDb server user name, or <see langword="null"/> to use a default value.</param>
/// <param name="password">A parameter that contains the MongoDb server password, or <see langword="null"/> to use a generated password.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
public static IResourceBuilder<MongoDBServerResource> AddMongoDB(this IDistributedApplicationBuilder builder, string name, int? port = null)
public static IResourceBuilder<MongoDBServerResource> AddMongoDB(this IDistributedApplicationBuilder builder,
string name,
int? port = null,
IResourceBuilder<ParameterResource>? userName = null,
IResourceBuilder<ParameterResource>? password = null)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(name);

var mongoDBContainer = new MongoDBServerResource(name);
var passwordParameter = password?.Resource ?? ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder, $"{name}-password", special: false);

var mongoDBContainer = new MongoDBServerResource(name, userName?.Resource, passwordParameter);

string? connectionString = null;

Expand All @@ -50,6 +75,11 @@ public static IResourceBuilder<MongoDBServerResource> AddMongoDB(this IDistribut
.WithEndpoint(port: port, targetPort: DefaultContainerPort, name: MongoDBServerResource.PrimaryEndpointName)
.WithImage(MongoDBContainerImageTags.Image, MongoDBContainerImageTags.Tag)
.WithImageRegistry(MongoDBContainerImageTags.Registry)
.WithEnvironment(context =>
{
context.EnvironmentVariables[UserEnvVarName] = mongoDBContainer.UserNameReference;
context.EnvironmentVariables[PasswordEnvVarName] = mongoDBContainer.PasswordParameter!;
})
.WithHealthCheck(healthCheckKey);
}

Expand Down Expand Up @@ -163,9 +193,19 @@ public static IResourceBuilder<MongoDBServerResource> WithInitBindMount(this IRe

private static void ConfigureMongoExpressContainer(EnvironmentCallbackContext context, MongoDBServerResource resource)
{
// Mongo Exporess assumes Mongo is being accessed over a default Aspire container network and hardcodes the resource address
// Mongo Express assumes Mongo is being accessed over a default Aspire container network and hardcodes the resource address
// This will need to be refactored once updated service discovery APIs are available
context.EnvironmentVariables.Add("ME_CONFIG_MONGODB_URL", $"mongodb://{resource.Name}:{resource.PrimaryEndpoint.TargetPort}/?directConnection=true");
context.EnvironmentVariables.Add("ME_CONFIG_MONGODB_SERVER", resource.Name);
var targetPort = resource.PrimaryEndpoint.TargetPort;
if (targetPort is int targetPortValue)
{
context.EnvironmentVariables.Add("ME_CONFIG_MONGODB_PORT", targetPortValue.ToString(CultureInfo.InvariantCulture));
}
context.EnvironmentVariables.Add("ME_CONFIG_BASICAUTH", "false");
if (resource.PasswordParameter is not null)
{
context.EnvironmentVariables.Add("ME_CONFIG_MONGODB_ADMINUSERNAME", resource.UserNameReference);
context.EnvironmentVariables.Add("ME_CONFIG_MONGODB_ADMINPASSWORD", resource.PasswordParameter);
}
}
}
2 changes: 1 addition & 1 deletion src/Aspire.Hosting.MongoDB/MongoDBDatabaseResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public MongoDBDatabaseResource(string name, string databaseName, MongoDBServerRe
/// Gets the connection string expression for the MongoDB database.
/// </summary>
public ReferenceExpression ConnectionStringExpression
=> ReferenceExpression.Create($"{Parent}/{DatabaseName}");
=> ReferenceExpression.Create($"{Parent.ConnectionStringWithoutOptionsExpression}/{DatabaseName}{Parent.ConnectionStringOptionsExpression}");

/// <summary>
/// Gets the parent MongoDB container resource.
Expand Down
70 changes: 67 additions & 3 deletions src/Aspire.Hosting.MongoDB/MongoDBServerResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,84 @@ namespace Aspire.Hosting.ApplicationModel;
public class MongoDBServerResource(string name) : ContainerResource(name), IResourceWithConnectionString
{
internal const string PrimaryEndpointName = "tcp";
private const string DefaultUserName = "admin";
private const string DefaultAuthenticationDatabase = "admin";
private const string DefaultAuthenticationMechanism = "SCRAM-SHA-256";

private EndpointReference? _primaryEndpoint;

/// <summary>
/// Initialize a resource that represents a MongoDB container.
/// </summary>
/// <param name="name">The name of the resource.</param>
/// <param name="userNameParameter">A parameter that contains the MongoDb server user name, or <see langword="null"/> to use a default value.</param>
/// <param name="passwordParameter">A parameter that contains the MongoDb server password.</param>
public MongoDBServerResource(string name, ParameterResource? userNameParameter, ParameterResource? passwordParameter) : this(name)
{
UserNameParameter = userNameParameter;
PasswordParameter = passwordParameter;
}

/// <summary>
/// Gets the primary endpoint for the MongoDB server.
/// </summary>
public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName);

/// <summary>
/// Gets the parameter that contains the MongoDb server password.
/// </summary>
public ParameterResource? PasswordParameter { get; }

/// <summary>
/// Gets the parameter that contains the MongoDb server username.
/// </summary>
public ParameterResource? UserNameParameter { get; }

internal ReferenceExpression UserNameReference =>
UserNameParameter is not null ?
ReferenceExpression.Create($"{UserNameParameter}") :
ReferenceExpression.Create($"{DefaultUserName}");

/// <summary>
/// Gets the connection string for the MongoDB server.
/// </summary>
public ReferenceExpression ConnectionStringExpression =>
ReferenceExpression.Create(
$"mongodb://{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)}");
public ReferenceExpression ConnectionStringExpression => ReferenceExpression.Create($"{ConnectionStringWithoutOptionsExpression}{ConnectionStringOptionsExpression}");

/// <summary>
/// Gets the connection string for the MongoDB server without options parameters.
/// </summary>
internal ReferenceExpression ConnectionStringWithoutOptionsExpression
{
get
{
if (PasswordParameter is null)
{
return ReferenceExpression.Create($"mongodb://{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)}");
}
else
{
return ReferenceExpression.Create($"mongodb://{UserNameReference}:{PasswordParameter}@{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)}");
}
}
}

/// <summary>
/// Gets the options parameters for connection string of the MongoDB server.
/// </summary>
internal ReferenceExpression ConnectionStringOptionsExpression
{
get
{
if (PasswordParameter is null)
{
return ReferenceExpression.Create($"");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't love this pattern. We have a ReferenceExpressionBuilder that could clean up some of this I think. We could have a single method that took a builder and some arguments and returned an expression.

}
else
{
return ReferenceExpression.Create($"?authSource={DefaultAuthenticationDatabase}&authMechanism={DefaultAuthenticationMechanism}");
}
}
}

private readonly Dictionary<string, string> _databases = new Dictionary<string, string>(StringComparers.ResourceName);

Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Hosting.MongoDB/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Aspire.Hosting.MongoDB.MongoExpressContainerResource
Aspire.Hosting.MongoDB.MongoExpressContainerResource.MongoExpressContainerResource(string! name) -> void
Aspire.Hosting.MongoDBBuilderExtensions
static Aspire.Hosting.MongoDBBuilderExtensions.AddDatabase(this Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.MongoDBServerResource!>! builder, string! name, string? databaseName = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.MongoDBDatabaseResource!>!
static Aspire.Hosting.MongoDBBuilderExtensions.AddMongoDB(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, int? port = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.MongoDBServerResource!>!
static Aspire.Hosting.MongoDBBuilderExtensions.AddMongoDB(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, int? port) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.MongoDBServerResource!>!
static Aspire.Hosting.MongoDBBuilderExtensions.WithDataBindMount(this Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.MongoDBServerResource!>! builder, string! source, bool isReadOnly = false) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.MongoDBServerResource!>!
static Aspire.Hosting.MongoDBBuilderExtensions.WithDataVolume(this Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.MongoDBServerResource!>! builder, string? name = null, bool isReadOnly = false) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.MongoDBServerResource!>!
static Aspire.Hosting.MongoDBBuilderExtensions.WithHostPort(this Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.MongoDB.MongoExpressContainerResource!>! builder, int? port) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.MongoDB.MongoExpressContainerResource!>!
Expand Down
4 changes: 4 additions & 0 deletions src/Aspire.Hosting.MongoDB/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
#nullable enable
Aspire.Hosting.ApplicationModel.MongoDBServerResource.MongoDBServerResource(string! name, Aspire.Hosting.ApplicationModel.ParameterResource? userNameParameter, Aspire.Hosting.ApplicationModel.ParameterResource? passwordParameter) -> void
Aspire.Hosting.ApplicationModel.MongoDBServerResource.PasswordParameter.get -> Aspire.Hosting.ApplicationModel.ParameterResource?
Aspire.Hosting.ApplicationModel.MongoDBServerResource.UserNameParameter.get -> Aspire.Hosting.ApplicationModel.ParameterResource?
static Aspire.Hosting.MongoDBBuilderExtensions.AddMongoDB(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, int? port = null, Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.ParameterResource!>? userName = null, Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.ParameterResource!>? password = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.MongoDBServerResource!>!

45 changes: 32 additions & 13 deletions tests/Aspire.Hosting.MongoDB.Tests/AddMongoDBTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,11 @@ public async Task MongoDBCreatesConnectionString()
var connectionStringResource = dbResource as IResourceWithConnectionString;
Assert.NotNull(connectionStringResource);
var connectionString = await connectionStringResource.GetConnectionStringAsync();

Assert.Equal("mongodb://localhost:27017", await serverResource.GetConnectionStringAsync());
Assert.Equal("mongodb://{mongodb.bindings.tcp.host}:{mongodb.bindings.tcp.port}", serverResource.ConnectionStringExpression.ValueExpression);
Assert.Equal("mongodb://localhost:27017/mydatabase", connectionString);
Assert.Equal("{mongodb.connectionString}/mydatabase", connectionStringResource.ConnectionStringExpression.ValueExpression);
Assert.Equal($"mongodb://admin:{dbResource.Parent.PasswordParameter?.Value}@localhost:27017?authSource=admin&authMechanism=SCRAM-SHA-256", await serverResource.GetConnectionStringAsync());
Assert.Equal("mongodb://admin:{mongodb-password.value}@{mongodb.bindings.tcp.host}:{mongodb.bindings.tcp.port}?authSource=admin&authMechanism=SCRAM-SHA-256", serverResource.ConnectionStringExpression.ValueExpression);
Assert.Equal($"mongodb://admin:{dbResource.Parent.PasswordParameter?.Value}@localhost:27017/mydatabase?authSource=admin&authMechanism=SCRAM-SHA-256", connectionString);
Assert.Equal("mongodb://admin:{mongodb-password.value}@{mongodb.bindings.tcp.host}:{mongodb.bindings.tcp.port}/mydatabase?authSource=admin&authMechanism=SCRAM-SHA-256", connectionStringResource.ConnectionStringExpression.ValueExpression);
}

[Fact]
Expand Down Expand Up @@ -151,13 +151,28 @@ public async Task WithMongoExpressUsesContainerHost()
Assert.Collection(env,
e =>
{
Assert.Equal("ME_CONFIG_MONGODB_URL", e.Key);
Assert.Equal($"mongodb://mongo:27017/?directConnection=true", e.Value);
Assert.Equal("ME_CONFIG_MONGODB_SERVER", e.Key);
Assert.Equal("mongo", e.Value);
},
e =>
{
Assert.Equal("ME_CONFIG_MONGODB_PORT", e.Key);
Assert.Equal("27017", e.Value);
},
e =>
{
Assert.Equal("ME_CONFIG_BASICAUTH", e.Key);
Assert.Equal("false", e.Value);
},
e =>
{
Assert.Equal("ME_CONFIG_MONGODB_ADMINUSERNAME", e.Key);
Assert.Equal("admin", e.Value);
},
e =>
{
Assert.Equal("ME_CONFIG_MONGODB_ADMINPASSWORD", e.Key);
Assert.NotEmpty(e.Value);
});
}

Expand All @@ -184,8 +199,12 @@ public async Task VerifyManifest()
var expectedManifest = $$"""
{
"type": "container.v0",
"connectionString": "mongodb://{mongo.bindings.tcp.host}:{mongo.bindings.tcp.port}",
"connectionString": "mongodb://admin:{mongo-password.value}@{mongo.bindings.tcp.host}:{mongo.bindings.tcp.port}?authSource=admin\u0026authMechanism=SCRAM-SHA-256",
"image": "{{MongoDBContainerImageTags.Registry}}/{{MongoDBContainerImageTags.Image}}:{{MongoDBContainerImageTags.Tag}}",
"env": {
"MONGO_INITDB_ROOT_USERNAME": "admin",
"MONGO_INITDB_ROOT_PASSWORD": "{mongo-password.value}"
},
"bindings": {
"tcp": {
"scheme": "tcp",
Expand All @@ -201,7 +220,7 @@ public async Task VerifyManifest()
expectedManifest = """
{
"type": "value.v0",
"connectionString": "{mongo.connectionString}/mydb"
"connectionString": "mongodb://admin:{mongo-password.value}@{mongo.bindings.tcp.host}:{mongo.bindings.tcp.port}/mydb?authSource=admin\u0026authMechanism=SCRAM-SHA-256"
}
""";
Assert.Equal(expectedManifest, dbManifest.ToString());
Expand Down Expand Up @@ -243,8 +262,8 @@ public void CanAddDatabasesWithDifferentNamesOnSingleServer()
Assert.Equal("customers1", db1.Resource.DatabaseName);
Assert.Equal("customers2", db2.Resource.DatabaseName);

Assert.Equal("{mongo1.connectionString}/customers1", db1.Resource.ConnectionStringExpression.ValueExpression);
Assert.Equal("{mongo1.connectionString}/customers2", db2.Resource.ConnectionStringExpression.ValueExpression);
Assert.Equal("mongodb://admin:{mongo1-password.value}@{mongo1.bindings.tcp.host}:{mongo1.bindings.tcp.port}/customers1?authSource=admin&authMechanism=SCRAM-SHA-256", db1.Resource.ConnectionStringExpression.ValueExpression);
Assert.Equal("mongodb://admin:{mongo1-password.value}@{mongo1.bindings.tcp.host}:{mongo1.bindings.tcp.port}/customers2?authSource=admin&authMechanism=SCRAM-SHA-256", db2.Resource.ConnectionStringExpression.ValueExpression);
}

[Fact]
Expand All @@ -261,7 +280,7 @@ public void CanAddDatabasesWithTheSameNameOnMultipleServers()
Assert.Equal("imports", db1.Resource.DatabaseName);
Assert.Equal("imports", db2.Resource.DatabaseName);

Assert.Equal("{mongo1.connectionString}/imports", db1.Resource.ConnectionStringExpression.ValueExpression);
Assert.Equal("{mongo2.connectionString}/imports", db2.Resource.ConnectionStringExpression.ValueExpression);
Assert.Equal("mongodb://admin:{mongo1-password.value}@{mongo1.bindings.tcp.host}:{mongo1.bindings.tcp.port}/imports?authSource=admin&authMechanism=SCRAM-SHA-256", db1.Resource.ConnectionStringExpression.ValueExpression);
Assert.Equal("mongodb://admin:{mongo2-password.value}@{mongo2.bindings.tcp.host}:{mongo2.bindings.tcp.port}/imports?authSource=admin&authMechanism=SCRAM-SHA-256", db2.Resource.ConnectionStringExpression.ValueExpression);
}
}
6 changes: 5 additions & 1 deletion tests/Aspire.Hosting.MongoDB.Tests/MongoDbFunctionalTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume)
{
using var builder1 = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry(testOutputHelper);
var mongodb1 = builder1.AddMongoDB("mongodb");
var password = mongodb1.Resource.PasswordParameter!.Value;
var db1 = mongodb1.AddDatabase(dbName);

if (useVolume)
Expand Down Expand Up @@ -168,7 +169,10 @@ await pipeline.ExecuteAsync(async token =>
}

using var builder2 = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry(testOutputHelper);
var mongodb2 = builder2.AddMongoDB("mongodb");
var passwordParameter2 = builder2.AddParameter("pwd");
builder2.Configuration["Parameters:pwd"] = password;

var mongodb2 = builder2.AddMongoDB("mongodb", password: passwordParameter2);
var db2 = mongodb2.AddDatabase(dbName);

if (useVolume)
Expand Down
Loading
Loading