Architettura pulita, CQRS e autenticazione?

1

Sto lavorando per implementare la mia prima architettura pulita e l'applicazione CQRS, mi sono imbattuto in un po 'di logica che mi ha lasciato un po' perplesso. Sto lavorando per integrare l'autorizzazione e l'autenticazione nella mia applicazione, ma sto avendo qualche problema nel cercare di capire come progettare una parte della mia applicazione che gestirà la convalida dei comandi. Il mio sistema è multitenant e attualmente condivide un database tra tutti i miei clienti, e ho bisogno di implementare una sorta di sistema che mi consenta di eseguire tutte le mie verifiche in un modo semplice e non troppo stretto.

Nel mio sistema, ho implementazioni per Users e Clients . Gli utenti possono appartenere a qualsiasi numero di client. Questi utenti hanno anche diversi livelli di permessi a loro assegnati (ma questo è gestito dai miei sistemi di ruoli e per lo più irrilevante qui). My WebAPI è progettato in modo tale da limitare l'autorizzazione in base a un numero fisso di Roles e Claims . Questi rimangono in gran parte statici, quindi non ho bisogno di molta flessibilità qui.

La mia principale fonte di mal di testa è determinare le autorizzazioni di interazione "Utente-Cliente". Vale a dire, voglio determinare se un utente ha accesso al client per il quale si è tentato di aggiornare i record. Un approccio sarebbe quello di aggiungere UserId a tutti i miei comandi e verificare singolarmente le autorizzazioni durante ogni comando e query. Questo sembra noioso e soggetto a problemi.

Un altro approccio che ho considerato era quello di definire un'interfaccia o una classe base che aggiungesse semplicemente gli ID utente e utente a qualsiasi oggetto in cui fossero richiesti, ma questo ha avuto l'effetto negativo di esporre tali implementazioni alle mie WebAPI (tramite swagger e UserId / ClientId è una parte dell'oggetto Request / Command).

Un ultimo approccio sarebbe quello di rendere i miei comandi sottostanti ancora implementare quelle interfacce, ma il mio controller contiene una logica minima per mappare una richiesta API in uno dei miei oggetti comando. Di nuovo, questo sarebbe noioso e inizierebbe a far filtrare la mia logica nei miei controllori.

Nel complesso, è come se avessi bisogno di qualche struttura aggiuntiva nella mia applicazione in cui posso reindirizzare qualsiasi oggetto contenente un ClientId in modo da poter mantenere la Authentication e la Authorization logica dalla mia app principale. Tuttavia, sono per lo più perplesso e sto cercando modi in cui posso semplificare la mia applicazione riducendo al minimo il sovraccarico di aggiunta di comandi e query dipendenti dal client.

Se è pertinente, alcuni degli strumenti e delle tecnologie principali che sto sfruttando sono:

  • .Net Core 2.1
  • Mediatr
  • Entity Framework

E la mia applicazione principale implementa i comandi in modo simile a:

public class CreateProductCommand : IRequest
{
    public int ClientId { get;set; }
    public int ParentProductId { get; set; }
    public string ProductName { get; set; }
    // other creation specific props here
}

public class CreateProductCommandHandler : IHandler<CreateProductCommand>
{
    public async Task<Unit> Handle(CreateProductCommand command)
    {
        // check parent permissions, make sure parent product
        // belongs to client who is entered. 
        // -----
        // rest of logic to save

        // -- Ideally the User-Client check would happen before
        // -- the command is ever sent to the handler, so that
        // -- only client-specific logic and permissions are checked.
        // -- As long as the user can edit the specific client, anything
        // -- that happens to the client is determined by standard business 
        // -- and domain logic.
    } 
}
    
posta JD Davis 28.11.2018 - 05:16
fonte

2 risposte

1

Mi sembra che il tuo problema sia:

Ho una discussione che agisce per conto di un utente. Non voglio inquinare la mia bella logica aziendale pulita con i dettagli dell'autorizzazione. Ma il mio archivio dati necessita di dettagli di autorizzazione per rafforzare il controllo dei dati.

Dalla tua descrizione sembra abbastanza importante che l'utente A non abbia / non abbia accesso al client B. Sembra molto simile a Business Concern . Quindi la prima risposta è abbastanza ovvia: La logica di business dovrebbe preoccuparsi di quale utente sta facendo ciò che . Passare direttamente i dettagli dell'utente attraverso la logica di business appropriata è il modo più semplice per gestirli. Questo dà molti aspetti positivi: è chiaro, è pulito, è facilmente testato e l'API non è accoppiato al Data Store.

Se necessario, ci sarà un punto nella tua Business Logic in cui puoi tradurre parlando di un User , per parlare specificamente di Authorised to do X . Inoltre, non vi è alcun motivo per cui queste informazioni sugli utenti non possano essere rese più anemiche o arricchite in parti diverse della tua business logic.

Pragmaticamente, se il thread serve solo un utente alla volta, utilizzare l'archiviazione locale di Thread e inserire un riferimento all'utente in tale posizione. Più tardi nel tuo archivio dati (alias ovunque tu ne abbia bisogno) accedi a tale riferimento. Questa è una soluzione terribile. Applica l'accoppiamento diretto tra API e Data Store (la posizione del destinatario), Business Concern non è nella Business Logic e hai un argomento indiretto non intuitivo, che potrebbe non essere impostato o pulito in modo appropriato per le chiamate future sul thread. In breve, ciò che riattivi dalla tedio, ti ripagherà più e più volte in bug e cambi resistenza.

Aggiunta risposta extra

Soluzioni possibili

  1. Attributo con una programmazione orientata all'aspetto.
  2. una classe astratta / interfaccia con una programmazione orientata all'aspetto.
  3. Una classe generica che avvolge un'interfaccia lambda.
  4. un Meta Program che costruisce una classe Abstract per la derivazione.
  5. un Meta Program che costruisce una classe su misura che avvolge un'interfaccia lambda.

Le soluzioni 1 e 2 utilizzano l'orientamento delle lingue (attributi C #) per rilevare le funzioni di "autorizzazione richieste" e intercettare le chiamate verso di loro. Genererà un'eccezione su non autorizzato, ma consentirà la chiamata se autorizzato.

class command
{
    public command(User user);

    public User user {get; }

    [AuthorisedFor("xyz")]
    public void action(object a);

    [AuthorisedFor("xyz")]
    public void action(User user, object a);
}

La soluzione 1 richiede di decorare direttamente le funzioni di autorizzazione richieste e fornire proprietà / argomenti per le informazioni dell'utente.

La soluzione 2 consente di specificare in anticipo l'autorizzazione richiesta per le funzioni standard e fornisce proprietà / argomenti per le informazioni dell'utente.

Soluzione 3 è un decoratore e cattura attraverso il suo costruttore la conoscenza dell'autorità richiesta, le funzioni di permesso e rifiuto. L'unico problema è che l'interfaccia Comando ti obbligherà a usare% argomenti diObject, un Tipo generico e un limite sugli argomenti, o richiederà copie per ogni interfaccia di comando downstream.

class command<T>
{
    public command(User user, Authority[] required, Action<T> permit, Action<T> deny);

    public void action(T arguments);
}
class command2
{
    public command(Authority[] required, SomeInterface permit, SomeInterface2 deny);

    public void action();
}

Le soluzioni 4 e 5 non sono facilmente ottenibili in C #. Dovresti assolutamente scrivere codice che JIT sia una nuova classe base e derivazioni, o classi di decoratori per le varie interfacce. Li includo solo per completezza.

Le opzioni flessibili di runtime sono 3 o 5. Le altre soluzioni 1, 2 o 4 sono più ragionevoli per il tempo di compilazione.

Passaggio di utenti e autorizzazioni

Ortogonale a quale soluzione scegli per la gestione permetti / nega, quelle soluzioni avranno bisogno di accedere all'utente / autorizzazioni. Questi potrebbero essere archiviati in varie forme di archiviazione:

  • globale
  • thread locale
  • proprietà dell'oggetto
  • argomento della funzione

Scegliere Global o thread locale ti darà l'utente implicito che ti passa, ma complicherà i test e sarà fonte di bug non intuitivi.

Il prelievo di una proprietà Object o di un argomento di funzione richiede il passaggio delle informazioni utente / autorizzazione tramite i comandi. Rende esplicita la richiesta di informazioni, semplifica i test e riduce la capacità di bug non intuitivi. Richiederà più digitazione.

La mia preferenza è il passaggio esplicito dell'utente / delle autorizzazioni. Tuttavia, potrebbe esserci una ragione che rende l'opzione implicita Global / Thread locale la scelta migliore.

    
risposta data 28.11.2018 - 07:38
fonte
1

Crea IPermissionValidator da iniettare dal costruttore del comando usando dependency injection. Quando devi controllare le autorizzazioni per CreateProduct chiami _permissionValidator.ValidateCreateProduct(command.ParentProductId); . Se l'utente non dispone dell'autorizzazione, genera un'eccezione PermissionValidationException che l'API Web prenderà e restituirà il codice di stato HTTP corretto. Se devi controllare se l'utente ha il permesso e non lanciare, aggiungi _permissionValidator.TryValidateCreateProduct(command.ParentProductId); che restituisce bool true se è valido. L'implementazione di IPermissionValidator può utilizzare l'iniezione di dipendenza per ottenere l'utente corrente e convalidare di conseguenza.

Questo rende anche i tuoi comandi facilmente testabili, perché puoi implementare il proprio IPermissionValidator per i test. Dovresti testare tutti i casi di convalida nei tuoi comandi.

    
risposta data 03.12.2018 - 12:47
fonte

Leggi altre domande sui tag