Skip to content

Commit be1359c

Browse files
committed
Add credential provider chain (DPAPI, env vars, XML) with --setup-secrets
1 parent bfca175 commit be1359c

10 files changed

Lines changed: 247 additions & 16 deletions

File tree

SPOtoSQL-Net8/ConsoleApp1Net8/ConsoleApp1Net8.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.8" />
4141
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.0.8" />
4242
<PackageReference Include="System.Data.SqlClient" Version="4.8.6" />
43+
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" />
4344
</ItemGroup>
4445

4546
<ItemGroup>
@@ -74,6 +75,11 @@
7475
<Compile Include="..\..\SPOtoSQL-Snapshots\ConsoleApp1\Sharepoint\SPOUser.cs" />
7576
<Compile Include="..\..\SPOtoSQL-Snapshots\ConsoleApp1\Sharepoint\TimesheetDQ.cs" />
7677
<Compile Include="..\..\SPOtoSQL-Snapshots\ConsoleApp1\Sharepoint\TwoFactorAuth.cs" />
78+
<Compile Include="..\..\SPOtoSQL-Snapshots\ConsoleApp1\Security\CredentialManager.cs" />
79+
<Compile Include="..\..\SPOtoSQL-Snapshots\ConsoleApp1\Security\DpapiCredentialProvider.cs" />
80+
<Compile Include="..\..\SPOtoSQL-Snapshots\ConsoleApp1\Security\EnvCredentialProvider.cs" />
81+
<Compile Include="..\..\SPOtoSQL-Snapshots\ConsoleApp1\Security\ICredentialProvider.cs" />
82+
<Compile Include="..\..\SPOtoSQL-Snapshots\ConsoleApp1\Security\XmlCredentialProvider.cs" />
7783
<Compile Include="..\..\SPOtoSQL-Snapshots\ConsoleApp1\XmlConfig\ConfigHelper.cs" />
7884
<Compile Include="..\..\SPOtoSQL-Snapshots\ConsoleApp1\ConsoleLogger\Logger.cs" />
7985
<Compile Include="..\..\SPOtoSQL-Snapshots\ConsoleApp1\AssemblyInfo.cs" />

SPOtoSQL-Net8/ConsoleApp1Net8/GlobalUsings.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@
1212
global using Bring.SPODataQuality;
1313
global using Bring.Sharepoint;
1414
global using Bring.Sqlserver;
15+
global using Bring.Security;
1516
global using Bring.XmlConfig;

SPOtoSQL-Snapshots/ConsoleApp1/ConsoleApp1.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@
101101
<Compile Include="Sharepoint\SPOUser.cs" />
102102
<Compile Include="Sharepoint\TimesheetDQ.cs" />
103103
<Compile Include="Sharepoint\TwoFactorAuth.cs" />
104+
<Compile Include="Security\CredentialManager.cs" />
105+
<Compile Include="Security\DpapiCredentialProvider.cs" />
106+
<Compile Include="Security\EnvCredentialProvider.cs" />
107+
<Compile Include="Security\ICredentialProvider.cs" />
108+
<Compile Include="Security\XmlCredentialProvider.cs" />
104109
<Compile Include="XmlConfig\ConfigHelper.cs" />
105110
<Compile Include="ConsoleLogger\Logger.cs" />
106111
<Compile Include="AssemblyInfo.cs" />

SPOtoSQL-Snapshots/ConsoleApp1/SPODataQuality/RefreshSPOLists.cs

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using Bring.Sharepoint;
1+
using Bring.Security;
2+
using Bring.Sharepoint;
23
using Bring.Sqlserver;
34
using Bring.XmlConfig;
45
using Microsoft.SharePoint.Client;
@@ -20,6 +21,13 @@ private static void Main(string[] args)
2021
try
2122
{
2223
InitializeApplication(args);
24+
25+
if (args.Any(a => a.Equals("--setup-secrets", StringComparison.OrdinalIgnoreCase)))
26+
{
27+
RunSetupSecrets();
28+
return;
29+
}
30+
2331
RunMainWorkflow();
2432
}
2533
catch (Exception ex)
@@ -28,6 +36,48 @@ private static void Main(string[] args)
2836
}
2937
}
3038

39+
private static void RunSetupSecrets()
40+
{
41+
Console.WriteLine("=== Secure Credential Setup ===");
42+
Console.WriteLine("Credentials will be encrypted and stored at:");
43+
Console.WriteLine(" %APPDATA%\\SPO2SQL\\credentials.enc");
44+
Console.WriteLine();
45+
46+
Console.Write("SharePoint Username: ");
47+
string username = Console.ReadLine()?.Trim();
48+
Console.Write("SharePoint Password: ");
49+
string password = Console.ReadLine()?.Trim();
50+
Console.Write("SQL Connection String: ");
51+
string connString = Console.ReadLine()?.Trim();
52+
53+
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
54+
{
55+
Console.WriteLine("ERROR: Username and password are required.");
56+
Environment.Exit(1);
57+
}
58+
59+
try
60+
{
61+
var dpapi = new DpapiCredentialProvider();
62+
dpapi.SaveCredentials(username, password, connString ?? "");
63+
Console.WriteLine("Credentials saved securely.");
64+
}
65+
catch (PlatformNotSupportedException)
66+
{
67+
Console.WriteLine("ERROR: DPAPI encryption is not available on this platform.");
68+
Console.WriteLine("Use environment variables instead:");
69+
Console.WriteLine(" set SPO_USERNAME=" + username);
70+
Console.WriteLine(" set SPO_PASSWORD=...");
71+
Console.WriteLine(" set SQL_CONNECTION_STRING=...");
72+
Environment.Exit(1);
73+
}
74+
catch (Exception ex)
75+
{
76+
Console.WriteLine("ERROR: Failed to save credentials: " + ex.Message);
77+
Environment.Exit(1);
78+
}
79+
}
80+
3181
private static void InitializeApplication(string[] args)
3282
{
3383
Logger.Log(1, "DEBUG: Using the Default config");
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace Bring.Security
5+
{
6+
public static class CredentialManager
7+
{
8+
private static readonly List<ICredentialProvider> Providers;
9+
private static bool _initialized;
10+
11+
static CredentialManager()
12+
{
13+
Providers = new List<ICredentialProvider>();
14+
}
15+
16+
private static void EnsureInitialized()
17+
{
18+
if (_initialized) return;
19+
_initialized = true;
20+
21+
Providers.Add(new EnvCredentialProvider());
22+
Providers.Add(new DpapiCredentialProvider());
23+
}
24+
25+
public static void RegisterProvider(ICredentialProvider provider)
26+
{
27+
EnsureInitialized();
28+
Providers.Insert(0, provider);
29+
}
30+
31+
public static (string Username, string Password) GetSharePointCredentials()
32+
{
33+
EnsureInitialized();
34+
35+
foreach (var provider in Providers)
36+
{
37+
var result = provider.GetCredentials();
38+
if (result != null && !string.IsNullOrEmpty(result.Value.Username) && !string.IsNullOrEmpty(result.Value.Password))
39+
{
40+
Logger.Log(2, $"[CredentialManager] Using provider: {provider.GetType().Name}");
41+
return (result.Value.Username, result.Value.Password);
42+
}
43+
}
44+
45+
throw new InvalidOperationException("No credential provider returned valid SharePoint credentials.");
46+
}
47+
48+
public static string GetSqlConnectionString()
49+
{
50+
EnsureInitialized();
51+
52+
foreach (var provider in Providers)
53+
{
54+
var result = provider.GetCredentials();
55+
if (result != null && !string.IsNullOrEmpty(result.Value.SqlConnectionString))
56+
{
57+
Logger.Log(2, $"[CredentialManager] Using provider: {provider.GetType().Name}");
58+
return result.Value.SqlConnectionString;
59+
}
60+
}
61+
62+
throw new InvalidOperationException("No credential provider returned a valid SQL connection string.");
63+
}
64+
}
65+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using System;
2+
using System.IO;
3+
using System.Security.Cryptography;
4+
using System.Text;
5+
6+
namespace Bring.Security
7+
{
8+
public class DpapiCredentialProvider : ICredentialProvider
9+
{
10+
private string FilePath => Path.Combine(
11+
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
12+
"SPO2SQL",
13+
"credentials.enc");
14+
15+
public (string Username, string Password, string SqlConnectionString)? GetCredentials()
16+
{
17+
try
18+
{
19+
if (!File.Exists(FilePath))
20+
return null;
21+
22+
byte[] encrypted = File.ReadAllBytes(FilePath);
23+
byte[] decrypted = ProtectedData.Unprotect(encrypted, null, DataProtectionScope.CurrentUser);
24+
string plaintext = Encoding.UTF8.GetString(decrypted);
25+
26+
string[] parts = plaintext.Split('|');
27+
return parts.Length >= 3
28+
? (parts[0], parts[1], parts[2])
29+
: null;
30+
}
31+
catch
32+
{
33+
return null;
34+
}
35+
}
36+
37+
public void SaveCredentials(string username, string password, string connectionString)
38+
{
39+
string dir = Path.GetDirectoryName(FilePath);
40+
Directory.CreateDirectory(dir);
41+
42+
string plaintext = $"{username}|{password}|{connectionString}";
43+
byte[] data = Encoding.UTF8.GetBytes(plaintext);
44+
byte[] encrypted = ProtectedData.Protect(data, null, DataProtectionScope.CurrentUser);
45+
46+
File.WriteAllBytes(FilePath, encrypted);
47+
}
48+
}
49+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System;
2+
3+
namespace Bring.Security
4+
{
5+
public class EnvCredentialProvider : ICredentialProvider
6+
{
7+
public (string Username, string Password, string SqlConnectionString)? GetCredentials()
8+
{
9+
string user = Environment.GetEnvironmentVariable("SPO_USERNAME");
10+
string pass = Environment.GetEnvironmentVariable("SPO_PASSWORD");
11+
string conn = Environment.GetEnvironmentVariable("SQL_CONNECTION_STRING");
12+
13+
if (!string.IsNullOrEmpty(user) && !string.IsNullOrEmpty(pass))
14+
return (user, pass, conn ?? string.Empty);
15+
16+
return null;
17+
}
18+
}
19+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace Bring.Security
2+
{
3+
public interface ICredentialProvider
4+
{
5+
(string Username, string Password, string SqlConnectionString)? GetCredentials();
6+
}
7+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using Bring.XmlConfig;
2+
3+
namespace Bring.Security
4+
{
5+
public class XmlCredentialProvider : ICredentialProvider
6+
{
7+
public (string Username, string Password, string SqlConnectionString)? GetCredentials()
8+
{
9+
try
10+
{
11+
var (user, pass) = ConfigurationReader.GetSharePointCredentials();
12+
string conn = ConfigurationReader.GetSqlConnectionString();
13+
return (user, pass, conn);
14+
}
15+
catch
16+
{
17+
return null;
18+
}
19+
}
20+
}
21+
}

SPOtoSQL-Snapshots/ConsoleApp1/XmlConfig/ConfigHelper.cs

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Generic;
33
using System.Xml;
44
using Bring.SPODataQuality;
5+
using Bring.Security;
56
using System.Linq;
67

78
namespace Bring.XmlConfig
@@ -183,8 +184,15 @@ public static HashSet<string> GetIgnoredColumns()
183184
/// <exception cref="InvalidOperationException">Thrown when required configuration elements are missing or invalid.</exception>
184185
public static (string Username, string Password) GetSharePointCredentials()
185186
{
186-
LoadConfig();
187+
try
188+
{
189+
return CredentialManager.GetSharePointCredentials();
190+
}
191+
catch
192+
{
193+
}
187194

195+
LoadConfig();
188196
try
189197
{
190198
var spNode = _xmlDoc.SelectSingleNode("//Configuration/SharePoint");
@@ -196,26 +204,25 @@ public static (string Username, string Password) GetSharePointCredentials()
196204

197205
if (usernameNode == null)
198206
throw new InvalidOperationException("Username element not found in SharePoint configuration.");
199-
207+
200208
if (passwordNode == null)
201209
throw new InvalidOperationException("Password element not found in SharePoint configuration.");
202210

203211
var username = usernameNode.InnerText.Trim();
204212
var password = passwordNode.InnerText.Trim();
205213

206-
// Basic validation
207214
if (string.IsNullOrEmpty(username))
208215
throw new InvalidOperationException("Username cannot be empty.");
209-
216+
210217
if (string.IsNullOrEmpty(password))
211218
throw new InvalidOperationException("Password cannot be empty.");
212219

213-
Logger.Log(2, "SharePoint credentials retrieved successfully.");
220+
Logger.Log(2, "SharePoint credentials retrieved successfully from XML config.");
214221
return (username, password);
215222
}
216223
catch (InvalidOperationException)
217224
{
218-
throw; // Re-throw configuration-specific exceptions
225+
throw;
219226
}
220227
catch (Exception ex)
221228
{
@@ -224,16 +231,17 @@ public static (string Username, string Password) GetSharePointCredentials()
224231
}
225232
}
226233

227-
/// <summary>
228-
/// Retrieves the SQL Server connection string from the configuration file.
229-
/// </summary>
230-
/// <returns>The SQL Server connection string.</returns>
231-
/// <exception cref="FileNotFoundException">Thrown when the configuration file is not found.</exception>
232-
/// <exception cref="InvalidOperationException">Thrown when the connection string configuration is missing or invalid.</exception>
233234
public static string GetSqlConnectionString()
234235
{
235-
LoadConfig();
236+
try
237+
{
238+
return CredentialManager.GetSqlConnectionString();
239+
}
240+
catch
241+
{
242+
}
236243

244+
LoadConfig();
237245
try
238246
{
239247
var connNode = _xmlDoc.SelectSingleNode("//Configuration/SQL/ConnectionString");
@@ -244,12 +252,12 @@ public static string GetSqlConnectionString()
244252
if (string.IsNullOrEmpty(connectionString))
245253
throw new InvalidOperationException("SQL connection string cannot be empty.");
246254

247-
Logger.Log(2, "SQL connection string retrieved successfully.");
255+
Logger.Log(2, "SQL connection string retrieved successfully from XML config.");
248256
return connectionString;
249257
}
250258
catch (InvalidOperationException)
251259
{
252-
throw; // Re-throw configuration-specific exceptions
260+
throw;
253261
}
254262
catch (Exception ex)
255263
{

0 commit comments

Comments
 (0)