This change allows users to configure the casing behavior of simple
membership provider. The simple membership provider will by default
generate a database query that normalizes the case of usernames on the
database side.

This comes with the side effect of obviating any index that the user may
have configured for the user name column.

The fix is to make this behavior configurable. With the new option, it
will be possible to turn off casing normalization, and allow the database
to handle it specific to its collation.
This commit is contained in:
Ryan Nowak 2013-11-04 18:32:05 -08:00
Родитель 679d450616
Коммит 256968e02c
6 изменённых файлов: 204 добавлений и 31 удалений

Просмотреть файл

@ -157,7 +157,7 @@ namespace WebMatrix.WebData
get { return "webpages_OAuthMembership"; } get { return "webpages_OAuthMembership"; }
} }
internal static string OAuthTokenTableName internal static string OAuthTokenTableName
{ {
get { return "webpages_OAuthToken"; } get { return "webpages_OAuthToken"; }
} }
@ -187,6 +187,8 @@ namespace WebMatrix.WebData
// REVIEW: we could get this from the primary key of UserTable in the future // REVIEW: we could get this from the primary key of UserTable in the future
public string UserIdColumn { get; set; } public string UserIdColumn { get; set; }
public SimpleMembershipProviderCasingBehavior CasingBehavior { get; set; }
internal DatabaseConnectionInfo ConnectionInfo { get; set; } internal DatabaseConnectionInfo ConnectionInfo { get; set; }
internal bool InitializeCalled { get; set; } internal bool InitializeCalled { get; set; }
@ -297,15 +299,42 @@ namespace WebMatrix.WebData
VerifyInitialized(); VerifyInitialized();
using (var db = ConnectToDatabase()) using (var db = ConnectToDatabase())
{ {
return GetUserId(db, SafeUserTableName, SafeUserNameColumn, SafeUserIdColumn, userName); return GetUserId(db, userName);
} }
} }
internal static int GetUserId(IDatabase db, string userTableName, string userNameColumn, string userIdColumn, string userName) private int GetUserId(IDatabase db, string userName)
{ {
// Casing is normalized in Sql to allow the database to normalize username according to its collation. The common issue return GetUserId(db, SafeUserTableName, SafeUserNameColumn, SafeUserIdColumn, CasingBehavior, userName);
// that can occur here is the 'Turkish i problem', where the uppercase of 'i' is not 'I' in Turkish. }
var result = db.QueryValue(@"SELECT " + userIdColumn + " FROM " + userTableName + " WHERE (UPPER(" + userNameColumn + ") = UPPER(@0))", userName);
internal static int GetUserId(
IDatabase db,
string userTableName,
string userNameColumn,
string userIdColumn,
SimpleMembershipProviderCasingBehavior casingBehavior,
string userName)
{
dynamic result;
if (casingBehavior == SimpleMembershipProviderCasingBehavior.NormalizeCasing)
{
// Casing is normalized in Sql to allow the database to normalize username according to its collation. The common issue
// that can occur here is the 'Turkish i problem', where the uppercase of 'i' is not 'I' in Turkish.
result = db.QueryValue(@"SELECT " + userIdColumn + " FROM " + userTableName + " WHERE (UPPER(" + userNameColumn + ") = UPPER(@0))", userName);
}
else if (casingBehavior == SimpleMembershipProviderCasingBehavior.RelyOnDatabaseCollation)
{
// When this option is supplied we assume the database has been configured with an appropriate casing, and don't normalize
// the user name. This is performant but requires appropriate configuration on the database.
result = db.QueryValue(@"SELECT " + userIdColumn + " FROM " + userTableName + " WHERE (" + userNameColumn + " = @0)", userName);
}
else
{
Debug.Fail("Unexpected enum value");
return -1;
}
if (result != null) if (result != null)
{ {
return (int)result; return (int)result;
@ -429,7 +458,7 @@ namespace WebMatrix.WebData
using (var db = ConnectToDatabase()) using (var db = ConnectToDatabase())
{ {
// Step 1: Check if the user exists in the Users table // Step 1: Check if the user exists in the Users table
int uid = GetUserId(db, SafeUserTableName, SafeUserNameColumn, SafeUserIdColumn, userName); int uid = GetUserId(db, SafeUserTableName, SafeUserNameColumn, SafeUserIdColumn, CasingBehavior, userName);
if (uid == -1) if (uid == -1)
{ {
// User not found // User not found
@ -476,7 +505,7 @@ namespace WebMatrix.WebData
private void CreateUserRow(IDatabase db, string userName, IDictionary<string, object> values) private void CreateUserRow(IDatabase db, string userName, IDictionary<string, object> values)
{ {
// Make sure user doesn't exist // Make sure user doesn't exist
int userId = GetUserId(db, SafeUserTableName, SafeUserNameColumn, SafeUserIdColumn, userName); int userId = GetUserId(db, userName);
if (userId != -1) if (userId != -1)
{ {
throw new MembershipCreateUserException(MembershipCreateStatus.DuplicateUserName); throw new MembershipCreateUserException(MembershipCreateStatus.DuplicateUserName);
@ -575,7 +604,7 @@ namespace WebMatrix.WebData
using (var db = ConnectToDatabase()) using (var db = ConnectToDatabase())
{ {
int userId = GetUserId(db, SafeUserTableName, SafeUserNameColumn, SafeUserIdColumn, username); int userId = GetUserId(db, username);
if (userId == -1) if (userId == -1)
{ {
return false; // User not found return false; // User not found
@ -622,7 +651,7 @@ namespace WebMatrix.WebData
// Due to a bug in v1, GetUser allows passing null / empty values. // Due to a bug in v1, GetUser allows passing null / empty values.
using (var db = ConnectToDatabase()) using (var db = ConnectToDatabase())
{ {
int userId = GetUserId(db, SafeUserTableName, SafeUserNameColumn, SafeUserIdColumn, username); int userId = GetUserId(db, username);
if (userId == -1) if (userId == -1)
{ {
return null; // User not found return null; // User not found
@ -649,7 +678,7 @@ namespace WebMatrix.WebData
using (var db = ConnectToDatabase()) using (var db = ConnectToDatabase())
{ {
int userId = GetUserId(db, SafeUserTableName, SafeUserNameColumn, SafeUserIdColumn, userName); int userId = GetUserId(db, userName);
if (userId == -1) if (userId == -1)
{ {
return false; // User not found return false; // User not found
@ -670,7 +699,7 @@ namespace WebMatrix.WebData
using (var db = ConnectToDatabase()) using (var db = ConnectToDatabase())
{ {
int userId = GetUserId(db, SafeUserTableName, SafeUserNameColumn, SafeUserIdColumn, username); int userId = GetUserId(db, username);
if (userId == -1) if (userId == -1)
{ {
return false; // User not found return false; // User not found
@ -746,7 +775,7 @@ namespace WebMatrix.WebData
{ {
using (var db = ConnectToDatabase()) using (var db = ConnectToDatabase())
{ {
int userId = GetUserId(db, SafeUserTableName, SafeUserNameColumn, SafeUserIdColumn, userName); int userId = GetUserId(db, userName);
if (userId == -1) if (userId == -1)
{ {
throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, WebDataResources.Security_NoUserFound, userName)); throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, WebDataResources.Security_NoUserFound, userName));
@ -761,7 +790,7 @@ namespace WebMatrix.WebData
{ {
using (var db = ConnectToDatabase()) using (var db = ConnectToDatabase())
{ {
int userId = GetUserId(db, SafeUserTableName, SafeUserNameColumn, SafeUserIdColumn, userName); int userId = GetUserId(db, userName);
if (userId == -1) if (userId == -1)
{ {
throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, WebDataResources.Security_NoUserFound, userName)); throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, WebDataResources.Security_NoUserFound, userName));
@ -781,7 +810,7 @@ namespace WebMatrix.WebData
{ {
using (var db = ConnectToDatabase()) using (var db = ConnectToDatabase())
{ {
int userId = GetUserId(db, SafeUserTableName, SafeUserNameColumn, SafeUserIdColumn, userName); int userId = GetUserId(db, userName);
if (userId == -1) if (userId == -1)
{ {
throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, WebDataResources.Security_NoUserFound, userName)); throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, WebDataResources.Security_NoUserFound, userName));
@ -801,7 +830,7 @@ namespace WebMatrix.WebData
{ {
using (var db = ConnectToDatabase()) using (var db = ConnectToDatabase())
{ {
int userId = GetUserId(db, SafeUserTableName, SafeUserNameColumn, SafeUserIdColumn, userName); int userId = GetUserId(db, userName);
if (userId == -1) if (userId == -1)
{ {
throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, WebDataResources.Security_NoUserFound, userName)); throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, WebDataResources.Security_NoUserFound, userName));
@ -852,7 +881,7 @@ namespace WebMatrix.WebData
// Ensures the user exists in the accounts table // Ensures the user exists in the accounts table
private int VerifyUserNameHasConfirmedAccount(IDatabase db, string username, bool throwException) private int VerifyUserNameHasConfirmedAccount(IDatabase db, string username, bool throwException)
{ {
int userId = GetUserId(db, SafeUserTableName, SafeUserNameColumn, SafeUserIdColumn, username); int userId = GetUserId(db, username);
if (userId == -1) if (userId == -1)
{ {
if (throwException) if (throwException)
@ -1004,7 +1033,7 @@ namespace WebMatrix.WebData
// GetUser will fail with an exception if the user table isn't set up properly // GetUser will fail with an exception if the user table isn't set up properly
try try
{ {
GetUserId(db, SafeUserTableName, SafeUserNameColumn, SafeUserIdColumn, "z"); GetUserId(db, "z");
} }
catch (Exception e) catch (Exception e)
{ {
@ -1255,7 +1284,7 @@ namespace WebMatrix.WebData
{ {
dynamic id = db.QueryValue(@"SELECT UserId FROM [" + MembershipTableName + "] WHERE UserId=@0", userId); dynamic id = db.QueryValue(@"SELECT UserId FROM [" + MembershipTableName + "] WHERE UserId=@0", userId);
return id != null; return id != null;
} }
} }
} }
} }

Просмотреть файл

@ -0,0 +1,32 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
namespace WebMatrix.WebData
{
/// <summary>
/// Configures the behavior of SimpleMembershipProvider for the casing of user name queries.
/// </summary>
public enum SimpleMembershipProviderCasingBehavior
{
/// <summary>
/// Uses the SQL Upper function to normalize the casing of user names for a case-insensitive comparion.
/// This is the default value.
/// </summary>
/// <remarks>
/// This option uses the SQL Upper function to perform case-normalization. This guarantees that the
/// the user name is searched case-insensitively, but can have a performance impact when a large number
/// of users exist.
/// </remarks>
NormalizeCasing,
/// <summary>
/// Relies on the database's configured collation to normalize casing for the comparison of user names. User
/// names are provided to the database exactly as entered by the user.
/// </summary>
/// <remarks>
/// This option relies on the configured collection of database table for user names to perform a correct comparison.
/// This is guaranteed to be correct for the chosen collation and performant. Only choose this option if the table storing
/// user names is configured with the desired collation.
/// </remarks>
RelyOnDatabaseCollation,
}
}

Просмотреть файл

@ -74,6 +74,8 @@ namespace WebMatrix.WebData
// REVIEW: we could get this from the primary key of UserTable in the future // REVIEW: we could get this from the primary key of UserTable in the future
public string UserIdColumn { get; set; } public string UserIdColumn { get; set; }
public SimpleMembershipProviderCasingBehavior CasingBehavior { get; set; }
internal DatabaseConnectionInfo ConnectionInfo { get; set; } internal DatabaseConnectionInfo ConnectionInfo { get; set; }
internal bool InitializeCalled { get; set; } internal bool InitializeCalled { get; set; }
@ -142,7 +144,7 @@ namespace WebMatrix.WebData
List<int> userIds = new List<int>(usernames.Length); List<int> userIds = new List<int>(usernames.Length);
foreach (string username in usernames) foreach (string username in usernames)
{ {
int id = SimpleMembershipProvider.GetUserId(db, SafeUserTableName, SafeUserNameColumn, SafeUserIdColumn, username); int id = SimpleMembershipProvider.GetUserId(db, SafeUserTableName, SafeUserNameColumn, SafeUserIdColumn, CasingBehavior, username);
if (id == -1) if (id == -1)
{ {
throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, WebDataResources.Security_NoUserFound, username)); throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, WebDataResources.Security_NoUserFound, username));
@ -307,7 +309,7 @@ namespace WebMatrix.WebData
} }
using (var db = ConnectToDatabase()) using (var db = ConnectToDatabase())
{ {
int userId = SimpleMembershipProvider.GetUserId(db, SafeUserTableName, SafeUserNameColumn, SafeUserIdColumn, username); int userId = SimpleMembershipProvider.GetUserId(db, SafeUserTableName, SafeUserNameColumn, SafeUserIdColumn, CasingBehavior, username);
if (userId == -1) if (userId == -1)
{ {
throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, WebDataResources.Security_NoUserFound, username)); throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, WebDataResources.Security_NoUserFound, username));

Просмотреть файл

@ -50,6 +50,7 @@
<DependentUpon>WebDataResources.resx</DependentUpon> <DependentUpon>WebDataResources.resx</DependentUpon>
</Compile> </Compile>
<Compile Include="SimpleMembershipProvider.cs" /> <Compile Include="SimpleMembershipProvider.cs" />
<Compile Include="SimpleMembershipProviderCasingBehavior.cs" />
<Compile Include="SimpleRoleProvider.cs" /> <Compile Include="SimpleRoleProvider.cs" />
<Compile Include="WebSecurity.cs" /> <Compile Include="WebSecurity.cs" />
</ItemGroup> </ItemGroup>

Просмотреть файл

@ -102,42 +102,99 @@ namespace WebMatrix.WebData
public static void InitializeDatabaseConnection(string connectionStringName, string userTableName, string userIdColumn, string userNameColumn, bool autoCreateTables) public static void InitializeDatabaseConnection(string connectionStringName, string userTableName, string userIdColumn, string userNameColumn, bool autoCreateTables)
{ {
DatabaseConnectionInfo connect = new DatabaseConnectionInfo(); InitializeDatabaseConnection(
connect.ConnectionStringName = connectionStringName; connectionStringName,
InitializeProviders(connect, userTableName, userIdColumn, userNameColumn, autoCreateTables); userTableName,
userIdColumn,
userNameColumn,
autoCreateTables,
SimpleMembershipProviderCasingBehavior.NormalizeCasing);
} }
public static void InitializeDatabaseConnection(string connectionString, string providerName, string userTableName, string userIdColumn, string userNameColumn, bool autoCreateTables) public static void InitializeDatabaseConnection(
string connectionStringName,
string userTableName,
string userIdColumn,
string userNameColumn,
bool autoCreateTables,
SimpleMembershipProviderCasingBehavior casingBehavior)
{
DatabaseConnectionInfo connect = new DatabaseConnectionInfo();
connect.ConnectionStringName = connectionStringName;
InitializeProviders(connect, userTableName, userIdColumn, userNameColumn, autoCreateTables, casingBehavior);
}
public static void InitializeDatabaseConnection(
string connectionString,
string providerName,
string userTableName,
string userIdColumn,
string userNameColumn,
bool autoCreateTables)
{
InitializeDatabaseConnection(
connectionString,
providerName,
userTableName,
userIdColumn,
userNameColumn,
autoCreateTables,
SimpleMembershipProviderCasingBehavior.NormalizeCasing);
}
public static void InitializeDatabaseConnection(
string connectionString,
string providerName,
string userTableName,
string userIdColumn,
string userNameColumn,
bool autoCreateTables,
SimpleMembershipProviderCasingBehavior casingBehavior)
{ {
DatabaseConnectionInfo connect = new DatabaseConnectionInfo(); DatabaseConnectionInfo connect = new DatabaseConnectionInfo();
connect.ConnectionString = connectionString; connect.ConnectionString = connectionString;
connect.ProviderName = providerName; connect.ProviderName = providerName;
InitializeProviders(connect, userTableName, userIdColumn, userNameColumn, autoCreateTables); InitializeProviders(connect, userTableName, userIdColumn, userNameColumn, autoCreateTables, casingBehavior);
} }
private static void InitializeProviders(DatabaseConnectionInfo connect, string userTableName, string userIdColumn, string userNameColumn, bool autoCreateTables) private static void InitializeProviders(
DatabaseConnectionInfo connect,
string userTableName,
string userIdColumn,
string userNameColumn,
bool autoCreateTables,
SimpleMembershipProviderCasingBehavior casingBehavior)
{ {
SimpleMembershipProvider simpleMembership = Membership.Provider as SimpleMembershipProvider; SimpleMembershipProvider simpleMembership = Membership.Provider as SimpleMembershipProvider;
if (simpleMembership != null) if (simpleMembership != null)
{ {
InitializeMembershipProvider(simpleMembership, connect, userTableName, userIdColumn, userNameColumn, autoCreateTables); InitializeMembershipProvider(simpleMembership, connect, userTableName, userIdColumn, userNameColumn, autoCreateTables, casingBehavior);
} }
SimpleRoleProvider simpleRoles = Roles.Provider as SimpleRoleProvider; SimpleRoleProvider simpleRoles = Roles.Provider as SimpleRoleProvider;
if (simpleRoles != null) if (simpleRoles != null)
{ {
InitializeRoleProvider(simpleRoles, connect, userTableName, userIdColumn, userNameColumn, autoCreateTables); InitializeRoleProvider(simpleRoles, connect, userTableName, userIdColumn, userNameColumn, autoCreateTables, casingBehavior);
} }
Initialized = true; Initialized = true;
} }
internal static void InitializeMembershipProvider(SimpleMembershipProvider simpleMembership, DatabaseConnectionInfo connect, string userTableName, string userIdColumn, string userNameColumn, bool createTables) internal static void InitializeMembershipProvider(
SimpleMembershipProvider simpleMembership,
DatabaseConnectionInfo connect,
string userTableName,
string userIdColumn,
string userNameColumn,
bool createTables,
SimpleMembershipProviderCasingBehavior casingBehavior)
{ {
if (simpleMembership.InitializeCalled) if (simpleMembership.InitializeCalled)
{ {
throw new InvalidOperationException(WebDataResources.Security_InitializeAlreadyCalled); throw new InvalidOperationException(WebDataResources.Security_InitializeAlreadyCalled);
} }
simpleMembership.CasingBehavior = casingBehavior;
simpleMembership.ConnectionInfo = connect; simpleMembership.ConnectionInfo = connect;
simpleMembership.UserIdColumn = userIdColumn; simpleMembership.UserIdColumn = userIdColumn;
simpleMembership.UserNameColumn = userNameColumn; simpleMembership.UserNameColumn = userNameColumn;
@ -154,16 +211,26 @@ namespace WebMatrix.WebData
simpleMembership.InitializeCalled = true; simpleMembership.InitializeCalled = true;
} }
internal static void InitializeRoleProvider(SimpleRoleProvider simpleRoles, DatabaseConnectionInfo connect, string userTableName, string userIdColumn, string userNameColumn, bool createTables) internal static void InitializeRoleProvider(
SimpleRoleProvider simpleRoles,
DatabaseConnectionInfo connect,
string userTableName,
string userIdColumn,
string userNameColumn,
bool createTables,
SimpleMembershipProviderCasingBehavior casingBehavior)
{ {
if (simpleRoles.InitializeCalled) if (simpleRoles.InitializeCalled)
{ {
throw new InvalidOperationException(WebDataResources.Security_InitializeAlreadyCalled); throw new InvalidOperationException(WebDataResources.Security_InitializeAlreadyCalled);
} }
simpleRoles.CasingBehavior = casingBehavior;
simpleRoles.ConnectionInfo = connect; simpleRoles.ConnectionInfo = connect;
simpleRoles.UserTableName = userTableName; simpleRoles.UserTableName = userTableName;
simpleRoles.UserIdColumn = userIdColumn; simpleRoles.UserIdColumn = userIdColumn;
simpleRoles.UserNameColumn = userNameColumn; simpleRoles.UserNameColumn = userNameColumn;
if (createTables) if (createTables)
{ {
simpleRoles.CreateTablesIfNeeded(); simpleRoles.CreateTablesIfNeeded();

Просмотреть файл

@ -170,6 +170,48 @@ namespace WebMatrix.WebData.Test
Assert.Equal("fGH_eKcjvW__P-5BOEW1AA2", result); Assert.Equal("fGH_eKcjvW__P-5BOEW1AA2", result);
} }
[Fact]
public void GetUserId_WithCaseNormalization()
{
// Arrange
var database = new Mock<MockDatabase>(MockBehavior.Strict);
var expectedQuery = @"SELECT userId FROM users WHERE (UPPER(userName) = UPPER(@0))";
database.Setup(d => d.QueryValue(expectedQuery, "zeke")).Returns(999);
// Act
var result = SimpleMembershipProvider.GetUserId(
database.Object,
"users",
"userName",
"userId",
SimpleMembershipProviderCasingBehavior.NormalizeCasing,
"zeke");
// Assert
Assert.Equal<int>(999, result);
}
[Fact]
public void GetUserId_WithoutCaseNormalization()
{
// Arrange
var database = new Mock<MockDatabase>(MockBehavior.Strict);
var expectedQuery = @"SELECT userId FROM users WHERE (userName = @0)";
database.Setup(d => d.QueryValue(expectedQuery, "zeke")).Returns(999);
// Act
var result = SimpleMembershipProvider.GetUserId(
database.Object,
"users",
"userName",
"userId",
SimpleMembershipProviderCasingBehavior.RelyOnDatabaseCollation,
"zeke");
// Assert
Assert.Equal<int>(999, result);
}
private static DynamicRecord GetRecord(int userId, string confirmationToken) private static DynamicRecord GetRecord(int userId, string confirmationToken)
{ {
var data = new Mock<IDataRecord>(MockBehavior.Strict); var data = new Mock<IDataRecord>(MockBehavior.Strict);