Autorizzazioni di tipo filesystem per i membri di tipo C ++

7

Abstract ( tl; dr )

Si prega di leggere l'intera domanda, questo è terribilmente semplificato:
In che modo è possibile applicare restrizioni di stile per i permessi dei file ai flussi di dati / controllo di tipo inter-type, consentendo un accesso preciso a alcuni membri della classe per alcuni gruppi di classi?

Informazioni di base

Se pensi ai file system e alle autorizzazioni unix, c'è un modo diverso di codificare i privilegi di accesso ai file degli utenti (specialmente se consideri anche FACL ). Ad esempio, se una directory contiene 3 file, potrebbero appartenere a più utenti e diversi altri utenti potrebbero disporre di autorizzazioni limitate:

-rwxr-xr--  jean-luc  staff    engage.sh
-rw-r-----  william   crew     roster.txt
-rw-------  beverly   beverly  patients.txt

Idea principale

Come puoi vedere, a seconda dei gruppi in cui si trova un particolare utente, sono consentiti diversi livelli di accesso. Ad esempio, crew membri possono leggere roster.txt , che appartiene a william , ma gli ospiti che presumibilmente non appartengono a crew non possono. Ancora più importante, il gruppo crew può contenere molte persone.

Quindi stavo pensando che ci sia una certa somiglianza con i permessi di accesso all'interno di linguaggi orientati agli oggetti come il C ++ se si pensa ai tipi (classi) come utenti. Sebbene una funzione possa essere eseguita, ma non letta, i flag rwx rappresentano descrizioni significative per i membri della classe. Un membro dati può essere letto ( r ) e scritto ( w ), forse tramite gli accessor, mentre le funzioni membro possono essere eseguite ( x ) o no.

Tuttavia, in C ++ e in altri linguaggi orientati agli oggetti (lo so), questo è più o meno un tutto o niente, se lasciamo fuori l'ereditarietà per un secondo; Se la classe William rende pubblico il suo membro Txt roster; , tutti lo vedranno. Se lo rende privato, nessuno tranne se stesso lo vedrà. Può aggiungere uno o più amici, friend JeanLuc; , ma poi vedranno tutti i suoi membri privati (l'equivalente di concedere user:jean-luc:rwx a tutti i suoi file, nel gergo FACL).
Questo è interamente ortogonale all'eredità - JeanLuc e William non fanno parte della stessa gerarchia, non sono correlati.

Quindi l'idea principale sarebbe quella di consentire restrizioni di accesso basate su gruppi, come una generalizzazione del privato / pubblico. Consentendo un accesso inter-class più fine alle funzioni membro e ai dati dei membri.

Credo che questo idioma possa aiutare la manutenibilità / leggibilità, poiché aggiunge ulteriori sfaccettature per limitare le autorizzazioni di interazione. Come con i sistemi operativi, dove questo aggiunge un importante livello di sicurezza al sistema, lo stesso modello familiare potrebbe aggiungere sicurezza a un progetto C ++.

Pensieri sulla rappresentazione in C ++

Tuttavia, sto perdendo il pensiero di un buon modo per rappresentarlo. Puoi decomporre William oggetti in diversi oggetti di sottotipi: William_Crew , William_William e così via, che rappresentano i rispettivi gruppi. Questo sembra essere orribilmente brutto. Un'altra idea potrebbe essere tipi dedicati con funzioni di inoltro, che rappresentano i singoli gruppi, come questo:

class Crew { // group class
  // in this group are:
  friend JeanLuc;
  friend Geordi;
  friend Beverly;
  // ...
  static Txt getRoster(William*);
};
class William {
  friend Crew; // Problem: Crew has full access (rwx)
  Txt roster;
};

Ma ogni gruppo dovrebbe essere adattato a una particolare classe con cui essere utilizzato, che sembrerebbe essere ridondante in maniera massiccia, se il gruppo viene utilizzato da più utenti / classi.

Domanda

Gli approcci che ho fornito non sono eccezionali (per usare un eufemismo) e sono sicuro che non funzionerebbero come previsto. Non sono sicuro che si tratti di un'idea romanzesca / stupida / ben conosciuta, ma mi chiedo come potresti implementarlo con le funzionalità fornite dal linguaggio C ++ . Esistono argomenti oggettivi sul perché questo sarebbe o non sarebbe utile / utile?

    
posta bitmask 04.05.2012 - 20:10
fonte

5 risposte

2

Riformulare un po 'il problema:

  • Ho un'istanza di MySpecialObject
  • un'istanza di JLP vuole chiamare i metodi di MySpecialObject
  • un'istanza di BEV vuole leggere i dati pubblici di MySpecialObject
  • l'istanza di JLP non dovrebbe essere in grado di leggere i dati pubblici e BEV non dovrebbe essere in grado di chiamare funzioni membro

Questo dovrebbe essere il più sicuro possibile.

Una soluzione che vedo riguarda i wrapper:)

  • scrivi il codice per MySpecialObject come faresti normalmente, ignorando questo requisito extra
  • scrivi un wrapper per ogni aspetto della tua classe che desideri limitare. Dì, MySpecialObject_Read e MySpecialObject_Execute .
  • questi wrapper inoltrano le richieste (chiamate di metodo, getter / setter) a un shared_ptr di MySpecialObject sottostante
  • le tue MySpecialObject , MySpecialObject_Read e MySpecialObject_Execute classi hanno ciascuna (se necessario) un metodo "converti in ...". Questo metodo restituisce un wrapper appropriato sul% co_de sottostante

Questa soluzione fornisce un accesso sicuro al tipo con le limitazioni desiderate.

Ogni classe cliente può scegliere il tipo di accesso di cui ha bisogno. (E poiché tu scrivi queste classi, tu conosci l'accesso che hai necessario). Se ciò non è accettabile, è possibile aggiungere fabbriche che creano solo wrapper con determinate limitazioni a seconda di un "token". Questo token potrebbe essere l'informazione RTTI dell'istanza della classe chiamante, anche se questo potrebbe essere abusato.

Questo risolve il problema originale?

EDIT:

Ricorda che poiché tu sei il programmatore, puoi istanziare un MySpecialObject ogni volta che vuoi. Aggiungendo la tua classe a un elenco A o qualsiasi altra cosa ...

Ciò che questa soluzione fornisce è un'interfaccia più chiara. Rimuovere le conversioni esplicite probabilmente lo rende più sicuro ma meno flessibile. Ma poiché sono espliciti, puoi cercarli facilmente nel codice e trattarli come segni in cui la tua architettura ha un difetto da qualche parte.

In particolare, puoi avere un codice come questo:

shared_ptr<A> * a = new A(parameters);

A_read aRead(a);
A_execute aExec(a);
A_write aWrite(aExec);

logger->Log(aRead);
view->SetUserData(aWrite);
controller->Execute(aExec);

Qui c'è una conversione esplicita tra un wrapper execute e un wrapper di scrittura, ma puoi decidere in base ai tuoi requisiti specifici.

Ma, con un piccolo sforzo (sapendo quali conversioni sono valide), solo osservando le posizioni delle chiamate puoi vederlo (con sicurezza!):

  • il friend non cambierà lo stato del tuo logger
  • il A non chiamerà i metodi sul tuo view (diverso dai setter)

Questo è vero anche se quelle particolari chiamate di metodo finiscono per chiamare centinaia di altri metodi, più di quanto vorresti esaminare a mano.

Al costo di pochi involucri sottili si ha la possibilità di vedere a colpo d'occhio che cosa farà una particolare funzione con i parametri inviati. Ciò può essere di grande aiuto durante il debug, aiutandoti ad eliminare alcuni rami dalle tue indagini e, in generale, aiuterebbe le persone a cercare di capire il programma.

Non ho potuto trovare altre ragioni per utilizzare questa idea ACL, almeno quelle in cui i costi non superano i benefici. Tuttavia, sembra più intuitivo rispetto alla soluzione per i visitatori menzionata nell'altra risposta.

    
risposta data 04.05.2012 - 21:44
fonte
1

Non condivido la tua valutazione dei benefici, e sospetto che questo sia il motivo per cui non è fatto: renderebbe il sistema degli oggetti molto più complesso, con pochissimi benefici.

Ma in generale, i permessi (r, w, x) sono resi espliciti attraverso l'uso di metodi: piuttosto che avere un membro accessibile pubblicamente, fornisci accesso esplicito in lettura o scrittura tramite getter e setter.

Ovviamente, questo non consente la modellazione di utenti o gruppi (oltre a creare un oggetto a friend ). Altri linguaggi di programmazione consentono di limitare l'accesso ad altre classi all'interno dello stesso pacchetto (Java, predefinito) o all'interno dello stesso assembly (C #, friend ). Una restrizione simile si ottiene in C ++ separando il progetto in diverse unità di compilazione e usando il firewall del compilatore (noto anche come PIMPL) per limitare accesso di alcuni aspetti di una classe a quell'unità di compilazione.

Quindi in un certo modo fai hai differenti modalità di accesso, così come diversi gruppi di "utenti". Non sono disponibili in un modello unificato con termini simili a FACL, ma ottieni lo stesso effetto.

    
risposta data 28.05.2012 - 12:45
fonte
0

Ok, penso che la soluzione ovvia sia quella di implementare il modello dei visitatori:

struct Crew 
{
    virtual void setRoster(Txt roster) = 0;
    virtual ~Crew(){}
};

class William 
{
public:
    void getRoster(Crew& crew) { crew.setRoster(roster); }
private:
    Txt roster;
}

Poiché qualcuno implementa Crew , "unisce Crew gruppo".

Modifica se vuoi un caso più generale, puoi inventare alcuni descrittori di sicurezza, ma penso che sia una complicazione:

class  CrewDescriptorRead
{
protected:
    CrewDescriptorRead(){}
    friend class User;
};

class CrewDescriptorWrite
{
protected:
    CrewDescriptorWrite(){}
    friend class User;
};

class CrewDescriptorFullAccess: public CrewDescriptorRead, public CrewDescriptorWrite
{
public:
    CrewDescriptorFullAccess(const CrewDescriptorRead&, const CrewDescriptorWrite&){}
};



class William 
{
public:
    Txt getRoster(const CrewDescriptorRead& ) const {return roster;}
private:
    Txt roster;
};


struct User
{
    void f()
    {
        William w;
        w.getRoster(CrewDescriptorFullAccess(CrewDescriptorRead(), CrewDescriptorWrite()));
    }
};

struct UnAutorized
{
    void f()
    {
        William w;
        w.getRoster(CrewDescriptorFullAccess(CrewDescriptorRead(), CrewDescriptorWrite()));
        //OOps, I'm not friend
    }
};

Oppure, se assolutamente paranoico ,

class  CrewDescriptorRead
{
private:
    CrewDescriptorRead(){}
    friend class User;
    friend class CrewDescriptorFullAccess;
};

class CrewDescriptorWrite
{
private:
    CrewDescriptorWrite(){}
    friend class User;
    friend class CrewDescriptorFullAccess;
};

class CrewDescriptorFullAccess
{
public:
    CrewDescriptorFullAccess(const CrewDescriptorRead&, const CrewDescriptorWrite&){}
    operator CrewDescriptorRead() { return CrewDescriptorRead(); }
    operator CrewDescriptorWrite() { return CrewDescriptorWrite(); }
};
    
risposta data 04.05.2012 - 21:00
fonte
0

Qualsiasi soluzione tu pensi, avrà complicazioni sintattiche. È tutto sulla conoscenza del mittente. Qualcosa come:

void A::f() { b.method(); }

B richiede di sapere che A è il chiamante, il che implica trasformare tutti i tuoi metodi B:: in modelli e aggiungere un parametro aggiuntivo. L'unico pensiero che posso pensare è usare una sorta di "testimone" che porta il mittente, e qualche tipo di oggetto RAII per salvare il "mittente" durante un po ':

class target : public requires_permissions<read_permissions<A, B>,
                                           write_permissions<B>,
                                           execute_permissions<>>
{
public:
    // Must call has_read_permissions() as first line (for example)
    int f() const; 

    // Must call has_write_permissions() (for example).
    void g();
};

La classe sender sarebbe qualcosa di simile a:

// Curiour recursive pattern, to know things about 'sender'.
// See later.
class sender : private want_permissions<sender>
{
public:
   void caller();
};

void sender::caller()
{
    // give_me... is a 'want_permissions' method.
    scoped_perms sc(give_me_an_access_key_for(my_target)); 

    my_target.f();

    // The give_me method requires my_target inherits from
    // 'requires_permissions', checked at compile time.
    // The key given to 'sc' is also sent to 'my_target', in order
    // 'requires_permissions' knows which object is asking for
    // using the class, like when calling 'f'. my_target's guards 
    // (read/write_permissions()) will take care of the rest (using
    //  the key to know the sender), thowing an exception in case
    // of permission mismatch.

    // 'scoped_perms' must allow permissions only for an object
    // at once, to make checking faster and to avoid unintended
    // actions, but must be also recursive: what if this->f() calls
    // this->g() and both requieres permissions? A recursive
    // permission checker!!

    // Of course 'scoped_perms' frees the key when is destructed.
}

Considerazioni:

  • Penso che l'unico modo per farlo in sicurezza sia want_permissions è ereditato privatamente per evitare che altri oggetti utilizzino le tue autorizzazioni e tutti i metodi want_permissions devono essere protected per evitare che altri oggetti creino oggetti di classe want_permissions senza inheritage. Per gli stessi motivi, want_permissions deve controllare se sender eredita da want_permissions o meno prima di consegnare una chiave.

  • È necessario progettare key (come identificare il mittente, salvando solo un puntatore? perché no?)

  • Se vuoi supportare il multithreading, le cose si complicano, perché potrebbero esserci diversi oggetti autorizzati contemporaneamente. Le chiavi devono essere salvate in un contenitore e controllare che i metodi debbano cercare su quel contenitore. Il problema è, ancora una volta, che non sai quale di loro è il mittente in un momento specifico se ne salvi più di uno alla volta. Forse con thread_local variabili e pImpl idiom puoi fare qualcosa. O semplicemente non è possibile utilizzare lo stesso target da diversi thread. O semplicemente usando i mutex per bloccare fino a quando un'istanza di block_perm viene distrutta, per consentire al prossimo oggetto di andare avanti.

  • Poiché entrambe le classi base relative ai permessi non sono pensate per essere usate come puntatore alle classi base o qualcosa del genere, il distruttore non deve essere virtual , non ci sono altri membri virtual , quindi, tutto il sovraccarico del polimorfismo viene evitato (nessun vtable per queste classi).

  • Non tutti i metodi richiedono autorizzazioni. Basta aggiungere has_read_permission() , has_write_permission() o has_execution_permission per i metodi che desideri proteggere.

Le autorizzazioni di lettura / scrittura / esecuzione sono solo tag per il trasporto di un contenitore di tipi:

template<class... allowed>
struct read_permissions;

template<class... allowed>
struct write_permissions;

Richiede autorizzazioni richiede solo due tipi e con una specializzazione estraiamo il pacchetto parametri:

template<class readers_type, class writers_type>
struct requires_permissions;

template<class... readers, class... writers>
struct requires_permissions<read_permissions<readers...>,
                            write_permissions<writers...> >
{
protected:
   void has_read_permissions() const;
   void has_write_permissions() const;

   // other must-be-well-designed stuff

private:
   template<class T*>
   T const* get_key() const;
} 

Supponi che key sia solo il puntatore del mittente e get_key sia un metodo magico (anch'esso progettato) per ottenere la chiave corrente.

template<class... readers, class... writers>
void requires_permissions<read_permissions<readers...>,
                          write_permission<writers...> >::
has_read_permissions() const
{
   auto* key_ptr = get_key();
   // magic, I said that. It's even possible that it can't be done
   // (with type erasure sure you can).
   // Of course, when creating perm blocks, there should be
   // a way of passing the sender type to this class, in order to
   // get_key() is instantiated in compiler time and 
   // 'has_read_permissios' can be a compiler time checker as well,
   // but it requieres a peacefully time to think.

   if (!key_ptr) throw something();

   if (!is_in_pack<decltype(auto), readers...>()) throw something();
}

// no matter where is this function implemented,
// if external or internal:

// They are not specializations each other (partial specializations
// are not allowed for functions). They are two different templates,
// one with at least two template parameters, and other with only one.
template<class guilty, class type, class... suspected>
constexpr bool is_in_pack()
{
    return std::is_same<guilty, type>::value or
      is_in_pack<guilty, suspected...>();
}

template<class guilty> constexpr bool is_in_pack()
{ return false; }

Questo è tutto ciò che posso fare ora per te.

    
risposta data 19.09.2016 - 13:08
fonte
0

Dici che non vuoi qualcosa di indistruttibile, solo qualcosa che se un buon sviluppatore segue semplicemente le regole stabilite. Ho un approccio più semplice a quello che è stato proposto fino ad ora:

Separa l'esecuzione o la lettura / scrittura

Un modo semplice per farlo è quello di avere una divisione di classe tra dati e logica. Simile a POJO e Service for Javaist. Li chiamerò William e WilliamExecutor.

Separa lettura / scrittura

Basta usare la parola chiave const o il const da boost per sola lettura. Non che questo non gestisca la scrittura solo dell'oggetto, ma è davvero necessario?

Quindi avresti solo:

class William{
    private: int a;
}
class WilliamExecutor{
    public: void t(William william){}
}

class A{
    // read only
    const Williaw& value;
}

class B{
    //read/write
    William &value;
}
//full
class C{
    William &value;
    WilliamExecutor &executor;

}

Questa è la versione più semplice, ora cosa succede se voglio accesso in lettura ed exec?

Al momento, impossibile come WilliamExecutor non sarà in grado di prendere un riferimento const a William , questo perché attualmente WilliamExecutor è senza stato e quindi possiamo usarlo come un singleton. Tuttavia, se rimuoviamo questa possibilità, possiamo farlo in questo modo:

class WilliamExecutor{
     private: William &value;
     public : void test(){}
}

class D{
   private: 
        const William &value;
        WilliamExecutor &exec;
}

//be carefull however, it will be the responsability of the developer who instantiate D to properly have the exec pointing to the same instance of William

new D(williamRef, new WilliamExecutor(williamRef));

Ora se vuoi un oggetto di sola scrittura (senza leggere) potresti avere solo una terza classe stateless WilliamWriter che è appena scritta in questo modo:

public class WilliamWriter{
    William &value;
    public void setFoo(String newValue){
        value.setFoo(newValue);
    }
}

E passa alla classe che ha solo bisogno di scrivere nella classe William .

Ho la stessa opinione di @KonradRudolph su questo argomento, tuttavia volevo fornire una soluzione più semplice che includesse un minor numero di classi, meno complessità di utilizzo che potesse soddisfare le tue esigenze.

Forse è possibile adattare questo con un po 'di generici, ma lo lascio alle persone che padroneggiano c ++ meglio di me.

Nota: il metodo di spostamento su un'altra classe può essere considerato come la rottura di OOP. Anche se potrebbe essere vero, non penso che tu possa essere in grado di usare qualcosa di utile, cioè fattibile E che la gente vuole usare, se non lo fai.

    
risposta data 19.09.2016 - 14:06
fonte

Leggi altre domande sui tag