Ho un problema simile: ho diversi utenti con permessi diversi. Alcuni di loro possono fare tutto e alcuni di loro possono selezionare / aggiornare solo determinate colonne nelle tabelle. La soluzione che ho trovato è l'utilizzo del metodo OnModelCreating
.
Ora vorrei spiegare la logica generale. Ho un database con utenti autenticati dal login SQL. Questi utenti sono associati a uno dei ruoli di database personalizzati e questi ruoli, a loro volta, ricevono le autorizzazioni. Se si utilizza l'autenticazione di Windows, non c'è davvero alcuna differenza per il codice poiché l'idea sarebbe la stessa: si creerebbe gruppi di Windows, si mapperanno gli utenti a quei gruppi e, infine, si concedono le autorizzazioni ai gruppi.
Il nucleo di questa idea è il metodo Ignore
di EntityTypeConfiguration<T>
utilizzato nel metodo OnModelCreating
(che si sovrascrive nella classe di contesto personalizzata). Questo metodo ci consente di ignorare le colonne in entità che non verranno utilizzate dal contesto. Pertanto, dovresti impostare alcune condizioni su quali rami codificare con il filtro:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
// Get the entity to which we want to apply filter
var detailEntity = modelBuilder.Entity<Detail>();
// Check the condtion
if (someCondition)
{
// If condition is met, these two properties will be ignored by context.
// From now on, these properties will get their default values
// when selecting data from database:
// for numeric types - zero;
// for reference types - null.
detailEntity.Ignore(entity => entity.Price);
detailEntity.Ignore(entity => entity.Discount);
}
}
La condizione per il filtro è il nome del ruolo del database in SQL Server (se si utilizza il login SQL) o il gruppo Windows a cui appartiene l'utente (se si utilizza l'autenticazione di Windows).
1. Gruppi di Windows
Per i gruppi di Windows è facile: basta recuperare tutti i gruppi a cui appartiene l'utente e confrontarlo con quello di cui si ha bisogno. Ecco la classe helper che recupera i gruppi di Windows dell'utente e controlla se appartiene al gruppo passato come parametro:
static class User
{
internal static bool IsInGroup(string groupName)
{
return GetWindowsGroups().Any(g => g.ToLower() == groupName.ToLower());
}
internal static List<string> GetWindowsGroups()
{
List<string> groups = new List<string>();
WindowsIdentity user = WindowsIdentity.GetCurrent();
user.Groups.ToList().ForEach(ir =>
{
try
{
IdentityReference irt = ir.Translate(typeof(NTAccount));
groups.Add(irt.Value);
}
catch { /* just ignore */ }
});
return groups;
}
}
Ora possiamo usarlo in OnModelCreating
:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
var detailEntity = modelBuilder.Entity<Detail>();
if (User.IsInGroup("DB_OPERATOR"))
{
// Fully ignore properties in model
detailEntity.Ignore(entity => entity.Price);
detailEntity.Ignore(entity => entity.Discount);
}
}
Ora qualsiasi selezione dalla tabella Dettagli non selezionerà queste due proprietà (anche se, come ho detto prima, puoi usarle nella tua classe entità - saranno semplicemente inizializzate ai loro valori predefiniti, ma le modifiche ad esse non si propagheranno al database).
2. Login SQL
Con gli accessi SQL, le autorizzazioni dell'utente dipendono dal suo ruolo nel database, quindi devi ottenerlo prima di qualsiasi interazione con il contesto. Ecco dove le cose diventano un po 'complicate. Prima di andare oltre, per prima cosa userò la procedura dbo.GetUserRole
memorizzata che recupera solo un singolo valore - il ruolo a cui un utente appartiene:
CREATE PROCEDURE dbo.GetUserRole
AS
BEGIN
SET NOCOUNT ON;
SELECT dp.[name] --, us.[name]
FROM sys.sysusers AS us
RIGHT JOIN sys.database_role_members AS rm ON us.uid = rm.member_principal_id
JOIN sys.database_principals AS dp ON rm.role_principal_id = dp.principal_id
WHERE us.name = CURRENT_USER;
END;
In secondo luogo, aggiungerò la proprietà Role
statica che indica il ruolo del database dell'utente:
class TestContext : DbContext
{
internal static string Role { get; private set; }
}
Ora c'è un avvertimento che ci attende: Non puoi recuperare il ruolo all'interno del metodo OnModelCreating
.
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
Role = base.Database.SqlQuery<string>("dbo.GetUserRole", new object[] { }).First();
var detailEntity = modelBuilder.Entity<Detail>();
if (Role == "operator")
{
detailEntity.Ignore(entity => entity.Price);
detailEntity.Ignore(entity => entity.Discount);
}
}
Solo perché EF genererà un'eccezione:
The context cannot be used while the model is being created. This exception may be thrown if the context is used inside the OnModelCreating
method or if the same context instance is accessed by multiple threads concurrently. Note that instance members of DbContext
and related classes are not guaranteed to be thread safe.
Bene, se non puoi usare il contesto, possiamo usare DbConnection
del contesto direttamente dal costruttore:
class TestContext : DbContext
{
public DbSet<Detail> Details { get; set; }
internal static string Role { get; private set; }
public TestContext(string nameOrConnectionString) : base(nameOrConnectionString)
{
var conn = base.Database.Connection;
conn.Open();
using (var comm = conn.CreateCommand())
{
comm.CommandText = "dbo.GetUserRole";
comm.CommandType = CommandType.StoredProcedure;
Role = comm.ExecuteScalar() as string;
}
conn.Close();
}
}
Quindi, per ora abbiamo il seguente. Il OnModelCreating
viene chiamato in uno dei due casi:
1) Quando chiami il metodo DbContext.Database.Initialize
.
2) Quando si effettua una query sul database (in questo caso Database.Initialize
viene eseguita implicitamente).
Come nota a margine, documentazione dice:
This method is called only once when the first instance of a derived context is created.
Tuttavia, questo non è vero.
Quindi, la soluzione è usare la connessione del contesto e richiedere un ruolo. Questo:
1) deve essere eseguito prima di qualsiasi query o chiamata Database.Initialize
o
2) può essere fatto nel costruttore del contesto.
Ora, quando abbiamo un ruolo, possiamo usarlo nel nostro codice. Ho scelto il tipo String
per l'archiviazione di un ruolo, ma puoi anche utilizzare l'enumerazione per comodità:
enum Role
{
Admin,
Regular,
Operator
}
Non so se questo modello che ho scelto sia corretto, ma non ho trovato alcuna informazione su questo tema - e sono sorpreso, perché in realtà questo è il modo in cui i database sono progettati in mente - con la concessione di permessi diversi a diversi utenti. Quindi, spero che questo ti possa aiutare.
Buona fortuna!