Skip to content

Commit e842175

Browse files
committed
Enhance Connection String
1 parent 15d8863 commit e842175

File tree

7 files changed

+146
-16
lines changed

7 files changed

+146
-16
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using Microsoft.Extensions.Options;
2+
using Microsoft.VisualStudio.TestTools.UnitTesting;
3+
using Moq;
4+
using RentalManagement.Services;
5+
using System;
6+
using System.Threading.Tasks;
7+
8+
namespace RentalManagement.Test
9+
{
10+
[TestClass]
11+
public class GetConnectionStringTests
12+
{
13+
[TestMethod]
14+
public async Task TestConnectionStringWithoutKeyVaultAndProtocol()
15+
{
16+
var keyVaultReaderMock = new Mock<IKeyVaultReader>();
17+
keyVaultReaderMock.Setup(framework => framework.IsAvailable).Returns(false);
18+
19+
var dbServerOptions = new DbServerOptions
20+
{
21+
Server = "s",
22+
Database = "d",
23+
DbPassword = "p"
24+
};
25+
var optionsMock = new Mock<IOptions<DbServerOptions>>();
26+
optionsMock.Setup(framework => framework.Value).Returns(dbServerOptions);
27+
28+
var da = new DataAccess(keyVaultReaderMock.Object, optionsMock.Object);
29+
Assert.AreEqual("Data Source=s;Initial Catalog=d;User ID=demo;Password=p", await da.GetConnectionString());
30+
}
31+
32+
[TestMethod]
33+
public async Task TestConnectionStringWithoutKeyVaultAndWithProtocol()
34+
{
35+
var keyVaultReaderMock = new Mock<IKeyVaultReader>();
36+
keyVaultReaderMock.Setup(framework => framework.IsAvailable).Returns(false);
37+
38+
var dbServerOptions = new DbServerOptions
39+
{
40+
Server = "s",
41+
Database = "d",
42+
DbPassword = "p",
43+
Protocol = "tcp",
44+
TcpPort = 1433
45+
};
46+
var optionsMock = new Mock<IOptions<DbServerOptions>>();
47+
optionsMock.Setup(framework => framework.Value).Returns(dbServerOptions);
48+
49+
var da = new DataAccess(keyVaultReaderMock.Object, optionsMock.Object);
50+
Assert.AreEqual("Data Source=tcp:s,1433;Initial Catalog=d;User ID=demo;Password=p", await da.GetConnectionString());
51+
}
52+
53+
[TestMethod]
54+
public async Task TestConnectionStringWithKeyVaultAndProtocol()
55+
{
56+
var keyVaultReaderMock = new Mock<IKeyVaultReader>();
57+
keyVaultReaderMock.Setup(framework => framework.IsAvailable).Returns(true);
58+
keyVaultReaderMock.Setup(framework => framework.GetSecretAsync(It.IsAny<string>())).Returns(Task.FromResult("p"));
59+
60+
var dbServerOptions = new DbServerOptions
61+
{
62+
Server = "s",
63+
Database = "d",
64+
Protocol = "tcp"
65+
};
66+
var optionsMock = new Mock<IOptions<DbServerOptions>>();
67+
optionsMock.Setup(framework => framework.Value).Returns(dbServerOptions);
68+
69+
var da = new DataAccess(keyVaultReaderMock.Object, optionsMock.Object);
70+
Assert.AreEqual("Data Source=tcp:s,1433;Initial Catalog=d;User ID=demo;Password=p", await da.GetConnectionString());
71+
}
72+
}
73+
}

MicroservicesWorkshop/RentalManagement/RentalManagement/Services/DataAccess.cs

+37-11
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
1-
using Microsoft.Azure.KeyVault;
2-
using Microsoft.Extensions.Options;
3-
using Microsoft.IdentityModel.Clients.ActiveDirectory;
4-
using System;
5-
using System.Collections.Generic;
1+
using Microsoft.Extensions.Options;
62
using System.Data.SqlClient;
7-
using System.Diagnostics;
8-
using System.Linq;
9-
using System.Threading;
3+
using System.Text;
104
using System.Threading.Tasks;
115

126
namespace RentalManagement.Services
@@ -39,10 +33,42 @@ public async Task<string> GetDbNameAsync()
3933
}
4034
}
4135

42-
private async Task<string> GetConnectionString()
36+
public async Task<string> GetConnectionString()
4337
{
44-
var dbPassword = await keyVaultReader.GetSecretAsync("DbPassword");
45-
return $"Server=tcp:{dbServerOptions.Server},1433;Initial Catalog={dbServerOptions.Database};User ID=demo;Password={dbPassword}";
38+
var builder = new SqlConnectionStringBuilder();
39+
40+
#region Build data source
41+
var dataSouceBuilder = new StringBuilder(dbServerOptions.Server.Length + 10);
42+
if (dbServerOptions.Protocol != null)
43+
{
44+
// Add protocol if configured
45+
dataSouceBuilder.Append(dbServerOptions.Protocol);
46+
dataSouceBuilder.Append(':');
47+
}
48+
49+
dataSouceBuilder.Append(dbServerOptions.Server);
50+
if (dbServerOptions?.Protocol == "tcp")
51+
{
52+
// Add port if configured
53+
dataSouceBuilder.Append(',');
54+
dataSouceBuilder.Append(dbServerOptions.TcpPort ?? 1433);
55+
}
56+
builder.DataSource = dataSouceBuilder.ToString();
57+
58+
builder.InitialCatalog = dbServerOptions.Database;
59+
builder.UserID = dbServerOptions?.DbUser ?? "demo";
60+
#endregion
61+
62+
if (keyVaultReader != null && keyVaultReader.IsAvailable)
63+
{
64+
builder.Password = await keyVaultReader.GetSecretAsync("DbPassword");
65+
}
66+
else
67+
{
68+
builder.Password = dbServerOptions.DbPassword;
69+
}
70+
71+
return builder.ToString();
4672
}
4773
}
4874
}

MicroservicesWorkshop/RentalManagement/RentalManagement/Services/DbServerOptions.cs

+4
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,9 @@ public class DbServerOptions
44
{
55
public string Server { get; set; }
66
public string Database { get; set; }
7+
public string DbPassword { get; set; }
8+
public string DbUser { get; set; }
9+
public string Protocol { get; set; }
10+
public int? TcpPort { get; set; }
711
}
812
}

MicroservicesWorkshop/RentalManagement/RentalManagement/Services/IDataAccess.cs

+1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ namespace RentalManagement.Services
55
public interface IDataAccess
66
{
77
Task<string> GetDbNameAsync();
8+
Task<string> GetConnectionString();
89
}
910
}

MicroservicesWorkshop/RentalManagement/RentalManagement/Services/IKeyVaultReader.cs

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ namespace RentalManagement.Services
44
{
55
public interface IKeyVaultReader
66
{
7+
bool IsAvailable { get; }
78
Task<string> GetSecretAsync(string key);
89
}
910
}
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
using Microsoft.Azure.KeyVault;
22
using Microsoft.Extensions.Options;
33
using Microsoft.IdentityModel.Clients.ActiveDirectory;
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Security;
47
using System.Threading.Tasks;
58

69
namespace RentalManagement.Services
710
{
811
public class KeyVaultReader : IKeyVaultReader
912
{
1013
private KeyVaultOptions keyVaultOptions;
14+
private Dictionary<string, DateTime> lastRefreshTimestamps = new Dictionary<string, DateTime>();
15+
private Dictionary<string, string> secrets = new Dictionary<string, string>();
16+
17+
public bool IsAvailable => keyVaultOptions?.VaultName != null;
1118

1219
public KeyVaultReader(IOptions<KeyVaultOptions> keyVaultOptions)
1320
{
@@ -17,16 +24,28 @@ public KeyVaultReader(IOptions<KeyVaultOptions> keyVaultOptions)
1724
private async Task<string> GetTokenAsync(string authority, string resource, string scope)
1825
{
1926
var authContext = new AuthenticationContext(authority);
20-
var clientCred = new ClientCredential(this.keyVaultOptions.ClientID, this.keyVaultOptions.ClientSecret);
27+
var clientCred = new ClientCredential(keyVaultOptions.ClientID, keyVaultOptions.ClientSecret);
2128
var result = await authContext.AcquireTokenAsync(resource, clientCred);
2229
return result.AccessToken;
2330
}
2431

2532
public async Task<string> GetSecretAsync(string key)
2633
{
27-
var kv = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(GetTokenAsync));
28-
var sec = await kv.GetSecretAsync($"https://{this.keyVaultOptions.VaultName}.vault.azure.net/secrets/{key}");
29-
return sec.Value;
34+
var now = DateTime.UtcNow;
35+
if (lastRefreshTimestamps.ContainsKey(key) && lastRefreshTimestamps[key] > now.AddHours(-1))
36+
{
37+
return secrets[key];
38+
}
39+
else
40+
{
41+
var kv = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(GetTokenAsync));
42+
var sec = await kv.GetSecretAsync($"https://{keyVaultOptions.VaultName}.vault.azure.net/secrets/{key}");
43+
44+
lastRefreshTimestamps[key] = now;
45+
secrets[key] = sec.Value;
46+
47+
return sec.Value;
48+
}
3049
}
3150
}
3251
}

MicroservicesWorkshop/RentalManagement/RentalManagement/appsettings.json

+7-1
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,15 @@
1515
"ApplicationInsights": {
1616
"InstrumentationKey": "6d06bf9d-6953-4d2e-b9aa-e3c27ec5f5b4"
1717
},
18+
"DB2": {
19+
"Server": "(localdb)\\dev",
20+
"Database": "msws",
21+
"DbPassword": "P@ssw0rd!123"
22+
},
1823
"DB": {
1924
"Server": "msws-sql-tlkbaeerqaffc.database.windows.net",
20-
"Database": "msws-db-test"
25+
"Database": "msws-db-test",
26+
"Protocol": "tcp"
2127
},
2228
"KeyVault": {
2329
"VaultName": "secretstestvehqmnkannhhx",

0 commit comments

Comments
 (0)