#c# #entity-framework-core #impersonation
Вопрос:
Я создаю приложение для подключения к базе данных, которое использует безопасность на уровне строк.
Я подключаюсь с помощью EF Core и в ходе исследований обнаружил, что до тех пор, пока пользователь, запускающий приложение на сервере, имеет разрешение выдавать себя за пользователя, он выполняет запросы от имени этого пользователя Execute as login = [user login]
.
У меня есть метод, который я вызываю после того, как моя фабрика контекста базы данных инициирует новый контекст.
public static void ChangeUser(String sUser, DbContext dbContext)
{
System.Data.Common.DbConnection dbCon = dbContext.Database.GetDbConnection();
if (dbCon.State != ConnectionState.Open)
dbCon.Open();
string userInDb;
using (var cmd = dbCon.CreateCommand())
{
cmd.CommandType = CommandType.Text;
cmd.Parameters.Add(new SqlParameter("user", sUser));
cmd.CommandText = "SELECT SUSER_NAME()";
userInDb = (string) cmd.ExecuteScalar();
}
if (userInDb.ToLower() == sUser.ToLower()) return;
using (var cmd = dbCon.CreateCommand())
{
cmd.CommandType = CommandType.Text;
cmd.Parameters.Add(new SqlParameter("user", sUser));
cmd.CommandText = "EXECUTE AS LOGIN = @user;";
cmd.ExecuteNonQuery();
}
}
Это работает для первых нескольких вызовов, но в конечном итоге я получаю ошибку: ERROR|Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost|Unhandled exception in circuit 'n1Y3qPEm60j0xxOFc6O83IrFu3jHD7EQI0_zPnO_2RQ'. Microsoft.Data.SqlClient.SqlException (0x80131904): Cannot continue the execution because the session is in the kill state. A severe error occurred on the current command. The results, if any, should be discarded.
и, просматривая журналы на SQL-сервере, я получаю дополнительную информацию с помощью журнала: The connection has been dropped because the principal that opened it subsequently assumed a new security context, and then tried to reset the connection under its impersonated security context. This scenario is not supported. See "Impersonation Overview" in Books Online.
Как бы я правильно выдал себя за пользователя, используя ядро EF, где это не приведет к «состоянию отключения» для подключения?
Я предположил, что это произошло из Execute as
-за того, что его вызывали дважды, поэтому я попытался добавить проверку, прежде чем пытаться выдать себя за пользователя.
Комментарии:
1. Вы не можете запретить олицетворение пользователя на SQL-сервере?
2. SQL server имеет ограничения на уровне строк. Я пытаюсь настроить его так, чтобы мой пользователь среднего уровня (пользователь, запускающий пул приложений) мог выдавать себя за аутентифицированного пользователя с помощью среднего уровня (веб-api интрасети).
Ответ №1:
WITH COOKIE INTO ...
Форма ВЫПОЛНИТЬ КАК можно использовать, чтобы избежать ошибки «Не удается продолжить выполнение, так как сеанс находится в состоянии завершения», и рекомендуется, чтобы пользователь не открыл дыру в системе безопасности, вернувшись самостоятельно.
Официальное ВЫПОЛНЕНИЕ В ВИДЕ документов
Продолжая, вот утилиты, которые я использую, чтобы сделать это простым и предсказуемым, и некоторые примеры использования.
ЗДЕСЬ МОГУТ БЫТЬ ДРАКОНЫ: будьте особенно осторожны и вдумчивы с кодом, который перемещает пользователя через границы безопасности. Будьте особенно внимательны к user
тому, откуда берется аргумент, который вы передаете в эти ExecuteAs...
методы расширения. Если пользователь или злоумышленник может манипулировать этой строкой, они могут злоупотреблять этими методами расширения ExecuteAs двумя способами:
- Очевидно, что они могут предоставить другого пользователя с повышенными правами, и тогда любой выполняемый SQL будет иметь права более сильного пользователя, чем вы, возможно, предполагали.
- Они могут вводить произвольный SQL, потому что эта форма
EXECUTE AS
может выполняться только в контексте adhoc. Имя пользователя не может быть параметризовано.
Последнее замечание, вы можете EXECUTE AS LOGIN ...
или EXECUTE AS USER ...
. Эти методы расширения делают последнее.
// Usage examples
async void Main()
{
// These two will work because the user from the connection string can SELECT anything.
Groups.ToList();
PaymentRecords.ToList();
//using (var executeAsScope = await (DbContext.Database.GetDbConnection() as SqlConnection).ExecuteAsUserAsync("morePrivilegedUser'; PRINT 'do something malicious'; REVERT; EXECUTE AS USER = 'lessPrivilegedUser"))
//using (var executeAsScope = await (DbContext.Database.GetDbConnection() as SqlConnection).ExecuteAsUserAsync("morePrivilegedUser"))
using (var executeAsScope = await (DbContext.Database.GetDbConnection() as SqlConnection).ExecuteAsUserAsync("lessPrivilegedUser"))
{
// This will succeed because "lessPrivilegedUser" can SELECT the Groups table.
Groups.ToList();
// This will throw an exception because "lessPrivilegedUser" does not have permission to SELECT the PaymentRecords table.
PaymentRecords.ToList();
// This will fail because @cookie does not exist.
Database.ExecuteSqlRaw("REVERT WITH COOKIE = @cookie;");
}
// This would succeed because we've exited the "lessPrivilegedUser" execute as scope and we are back to the user determined by the connection string.
PaymentRecords.ToList();
// Similarly you can work with transactions using the other extension method
using (var sqlTransaction = await DbContext.Database.GetDbConnection().BeginTransactionAsync() as SqlTransaction)
using (var dbContextTransaction = await DbContext.Database.UseTransactionAsync(sqlTransaction))
using (var executeAsScope = await sqlTransaction.ExecuteAsUserAsync("lessPrivilegedUser"))
{
// This will succeed because "lessPrivilegedUser" can SELECT the Groups table.
Groups.ToList();
// This will fail because "lessPrivilegedUser" does not have permission to SELECT the PaymentRecords table.
PaymentRecords.ToList();
// This will fail because @cookie does not exist.
Database.ExecuteSqlRaw("REVERT WITH COOKIE = @cookie;");
}
}
// Utilities
public static class ExecuteAsExtensions
{
public static async Task<IDisposable> ExecuteAsUserAsync(this SqlConnection connection, string user)
{
byte[] cookie;
using (var cmd = connection.CreateCommand())
{
cmd.CommandText = $@"DECLARE @cookie VARBINARY(100); EXECUTE AS USER = '{user}' WITH COOKIE INTO @cookie; SELECT @cookie; SET @cookie = 0;";
await connection.OpenAsync();
cookie = (byte[])await cmd.ExecuteScalarAsync();
}
return new ExecuteAsScope(cookie, connection, transaction: null);
}
public static async Task<IDisposable> ExecuteAsUserAsync(this SqlTransaction transaction, string user)
{
byte[] cookie;
using (var cmd = transaction.Connection.CreateCommand())
{
cmd.Transaction = transaction;
cmd.CommandText = $@"DECLARE @cookie VARBINARY(100); EXECUTE AS USER = '{user}' WITH COOKIE INTO @cookie; SELECT @cookie; SET @cookie = 0;";
cookie = (byte[])await cmd.ExecuteScalarAsync();
}
return new ExecuteAsScope(cookie, transaction.Connection, transaction);
}
}
public class ExecuteAsScope : IDisposable
{
private byte[] cookie;
private SqlConnection connection;
private SqlTransaction transaction;
public ExecuteAsScope(byte[] cookie, SqlConnection connection, SqlTransaction transaction)
{
this.cookie = cookie;
this.connection = connection;
this.transaction = transaction;
}
#region IDisposable Support
private bool disposedValue = false;
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
using (var cmd = connection.CreateCommand())
{
cmd.Transaction = transaction;
cmd.CommandText = $@"REVERT WITH COOKIE = @cookie";
cmd.Parameters.AddWithValue("@cookie", cookie);
cmd.ExecuteNonQuery();
}
transaction = null;
connection = null;
}
disposedValue = true;
}
}
public void Dispose()
{
Dispose(true);
}
#endregion
}