Come gestire la sicurezza di accesso a livello di campo quando si utilizza Entity Framework

1

Scenario: un grande sistema esistente (~ 300 tabelle, 500 stored procs, 200 visualizzazioni e una base di codice di diverse linee da 100k) con la maggior parte delle cose a livello di sicurezza nelle stored procedure deve essere refactored (per motivi di manutenibilità e solo disponibilità di competenze più probabilmente passerà al livello C #, e speriamo in prestazioni, poiché saremo in grado di controllare meglio ciò che viene tirato quando è meglio).

Entity Framework è qualcosa su cui stiamo seriamente pensando di rendere le cose più facilmente estendibili (ad esempio ereditando lo schema di backend da una classe base senza dover rintracciare un enorme join ogni volta).

Domanda: come gestisci la sicurezza con Entity Framework? Gli esempi che ho visto dove solo come ottenere il modello / modello di dati per gestire la sicurezza del servizio (i token per possono questo tipo di accesso di accesso? Tipi di cose). Come puoi dire che un utente normale può vedere questi 3 campi in una classe, ma un amministratore può vedere questi 10? Questi campi potrebbero essere logicamente altre tabelle di classi (ad esempio, ordini di clienti particolari). Che ne dici di cose come "questo post è letto"? Devi solo aggiungere un elenco di persone "haveRead" alla classe o esiste un modo più intelligente per ottenere EF per restituire versioni diverse dello stesso oggetto a seconda di chi sei? C'è un modo per convincere EF a fare questo per te senza bisogno di molta logica nei proc memorizzati? In caso contrario, come gestisci le prestazioni (ad esempio, una persona può vedere un singolo oggetto e fai clic sul modello per un elenco di oggetti, quindi applica il filtro più in alto in C #, il che significa che potresti ricevere migliaia di elementi ma passare solo 1 al client ). Riuscirai a caricare i singoli campi in modo pigro, in modo che se solo gli utenti deboli eseguono richieste, tutti i campi di amministrazione non vengono estratti dal database?

    
posta Mike 22.02.2014 - 15:51
fonte

1 risposta

2

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!

    
risposta data 03.12.2017 - 13:21
fonte

Leggi altre domande sui tag