Source/NuGetGallery.Operations/Tasks/SanitizeDatabaseTask.cs (75 lines of code) (raw):
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web.Helpers;
using AnglicanGeek.DbExecutor;
namespace NuGetGallery.Operations.Tasks
{
[Command("sanitizedatabase", "Cleans Personally-Identified Information out of a database without destroying data", AltName = "sdb", MinArgs = 0, MaxArgs = 0)]
public class SanitizeDatabaseTask : DatabaseTask
{
private const string SanitizeUsersQuery = @"
UPDATE Users
SET ApiKey = NEWID(),
EmailAddress = [Username] + '@' + @emailDomain,
UnconfirmedEmailAddress = NULL,
HashedPassword = CAST(NEWID() AS NVARCHAR(MAX)),
EmailAllowed = 1,
EmailConfirmationToken = NULL,
PasswordResetToken = NULL,
PasswordResetTokenExpirationDate = NULL,
PasswordHashAlgorithm = 'PBKDF2'
WHERE [Key] NOT IN (SELECT ur.UserKey FROM UserRoles ur INNER JOIN Roles r ON r.[Key] = ur.RoleKey WHERE r.Name = 'Admins')";
private static readonly string[] AllowedPrefixes = new[] {
"Export_" // Only exports can be sanitized
};
[Option("The database name on the server to santize if different from the database identified in the connection string", AltName = "d")]
public string DatabaseName { get; set; }
[Option("Domain name to use for sanitized email addresses, username@[emaildomain]", AltName = "e")]
public string EmailDomain { get; set; }
[Option("Forces the command to run, even against a non-backup/export database", AltName = "f")]
public bool Force { get; set; }
public override void ValidateArguments()
{
base.ValidateArguments();
EmailDomain = String.IsNullOrEmpty(EmailDomain) ?
"example.com" :
EmailDomain;
}
public override void ExecuteCommand()
{
// Coalesce the database name
DatabaseName = String.IsNullOrEmpty(DatabaseName) ?
ConnectionString.InitialCatalog :
DatabaseName;
ConnectionString = new SqlConnectionStringBuilder(ConnectionString.ConnectionString)
{
InitialCatalog = DatabaseName
};
// Verify the name
if (!Force && !AllowedPrefixes.Any(p => ConnectionString.InitialCatalog.StartsWith(p)))
{
Log.Error("Cannot sanitize database named '{0}' without -Force argument", ConnectionString.InitialCatalog);
return;
}
Log.Info("Ready to sanitize {0} on {1}", ConnectionString.InitialCatalog, Util.GetDatabaseServerName(ConnectionString));
// All we need to sanitize is the user table. Package data is public (EVEN unlisted ones) and not PII
if (WhatIf)
{
Log.Trace("Would execute the following SQL:");
Log.Trace(SanitizeUsersQuery);
Log.Trace("With @emailDomain = " + EmailDomain);
}
else
{
using (SqlConnection connection = new SqlConnection(ConnectionString.ConnectionString))
using (SqlExecutor dbExecutor = new SqlExecutor(connection))
{
connection.Open();
try
{
var count = dbExecutor.Execute(SanitizeUsersQuery, new { emailDomain = EmailDomain });
Log.Info("Sanitization complete. {0} Users affected", count);
}
catch (Exception ex)
{
Log.Error(ex.ToString());
}
}
}
}
}
}