Permisos similares a los del sistema de archivos para miembros de tipo C ++

7

Resumen ( tl; dr )

Lea la pregunta completa, esto está muy simplificado:
¿Cómo se pueden aplicar las restricciones de estilo de permisos de archivos Unix a los flujos de datos / control entre tipos, permitiendo el acceso detallado a algunos miembros de la clase para algunos grupos de clases?

Información de fondo

Si piensa en los sistemas de archivos y permisos de Unix, existe una forma diversa de codificar los privilegios de acceso a los archivos de los usuarios (especialmente si también considera FACL ). Por ejemplo, si un directorio contiene 3 archivos, podrían pertenecer a varios usuarios y otros usuarios podrían tener permisos restringidos:

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

Idea principal

Como puede ver, según los grupos en los que se encuentre un usuario en particular, se permiten diferentes niveles de acceso. Por ejemplo, los miembros de crew pueden leer roster.txt , que pertenece a william , pero los invitados que presumiblemente no pertenecen a crew no pueden. Más importante aún, el grupo crew puede contener muchas personas.

Así que pensé que hay cierta similitud con los permisos de acceso dentro de lenguajes orientados a objetos como C ++ si piensas en los tipos (clases) como usuarios. Aunque una función solo se puede ejecutar, pero no leer, las marcas rwx representan descripciones significativas para los miembros de la clase. Un miembro de datos se puede leer ( r ) y escribir ( w ), quizás a través de los accesores, mientras que las funciones de miembro pueden ejecutarse ( x ) o no.

Sin embargo, en C ++ y otros lenguajes orientados a objetos (lo sé), esto es más o menos una cosa de todo o nada, si dejamos de lado la herencia por un segundo; Si la clase William hace público su miembro Txt roster; , todos lo verán. Si lo hace privado, nadie, excepto él, lo verá. Puede agregar uno o más amigos, friend JeanLuc; , pero luego verán a todos sus miembros privados (el equivalente a otorgar user:jean-luc:rwx a todos sus archivos, en la jerga de la FACL). Esto es completamente ortogonal a la herencia: JeanLuc y William no son parte de la misma jerarquía, no están relacionados.

Entonces, la idea principal sería permitir restricciones de acceso basadas en grupos, como una generalización de privado / público. Permitir el acceso entre clases más específico a las funciones y datos de los miembros.

Creo que este idioma podría ayudar a la facilidad de mantenimiento / legibilidad, ya que agrega facetas adicionales para restringir los permisos de interacción. Al igual que con los sistemas operativos, donde esto agrega una importante capa de seguridad al sistema, el mismo patrón familiar podría agregar seguridad a un proyecto C ++.

Pensamientos sobre la representación en C ++

Sin embargo, no puedo pensar en una buena manera de representar esto. Podría descomponer objetos William en varios objetos de subtipos: William_Crew , William_William , etc., representando los grupos respectivos. Esto parece ser horriblemente feo. Otra idea podría ser tipos dedicados con funciones de reenvío, que representan los grupos individuales, como este:

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;
};

Pero cada grupo tendría que adaptarse a una clase particular para ser utilizada, lo que parecería ser masivamente redundante, si el grupo es utilizado por varios usuarios / clases.

Pregunta

Los enfoques que proporcioné no son excelentes (para decirlo suavemente), y estoy seguro de que no funcionarán como se esperaba. No estoy seguro de si se trata de una idea novedosa / estúpida / conocida, pero me pregunto cómo podría implementar esto con las características proporcionadas por el lenguaje C ++ . ¿Hay argumentos objetivos por los que esto sería o no útil o no?

    
pregunta bitmask 04.05.2012 - 20:10

5 respuestas

2

Replanteando un poco el problema:

  • Tengo una instancia de MySpecialObject
  • una instancia de JLP quiere llamar a los métodos de MySpecialObject
  • una instancia de BEV quiere leer los datos públicos de MySpecialObject
  • la instancia de JLP no debería poder leer datos públicos y BEV no debería poder llamar a funciones miembro

Esto debería ser lo más seguro posible.

Una solución que veo involucra envoltorios :)

  • escribe el código para MySpecialObject como lo haría normalmente, ignorando este requisito adicional
  • usted escribe una envoltura para cada aspecto de su clase que desea restringir. Diga, MySpecialObject_Read y MySpecialObject_Execute .
  • estos envoltorios reenvían las solicitudes (llamadas de método, captadores / configuradores) a un shared_ptr de MySpecialObject subyacente
  • sus clases MySpecialObject , MySpecialObject_Read y MySpecialObject_Execute tienen (según sea necesario) un método de "conversión a ...". Este método devuelve un contenedor apropiado sobre el MySpecialObject subyacente

Esta solución proporciona un acceso seguro para el tipo con las limitaciones deseadas.

Cada clase de cliente puede elegir qué tipo de acceso necesita. (Y como usted escribe estas clases, usted sabe qué acceso necesita ). Si eso no es aceptable, podría agregar fábricas que solo creen envoltorios con ciertas limitaciones dependiendo de un "token". Ese token podría ser la información RTTI de la instancia de la clase que realiza la llamada, aunque esto podría ser objeto de abuso.

¿Resuelve esto el problema original?

EDITAR:

Recuerde que, dado que usted es el programador, puede crear una instancia de A siempre que lo desee. Agregando su clase a una lista de friend o lo que sea ...

Lo que proporciona esta solución es una interfaz más clara. Eliminar las conversiones explícitas probablemente lo hace más seguro pero menos flexible. Pero, como son explícitos, puede buscarlos fácilmente en el código y tratarlos como signos de que su arquitectura tiene un defecto en alguna parte.

Específicamente, puedes tener un código como este:

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);

Aquí hay una conversión explícita entre un envoltorio de ejecución y un envoltorio de escritura, pero puede decidir esto según sus requisitos específicos.

Pero, con poco esfuerzo (sabiendo qué conversiones son válidas), solo con mirar las ubicaciones de las llamadas puede ver eso (¡con confianza!):

  • el logger no cambiará el estado de su A
  • view no llamará a los métodos en su A (aparte de los configuradores)

Esto es cierto incluso si esas llamadas a un método en particular terminan llamando a cientos de otros métodos, más de lo que te gustaría examinar a mano.

Al costo de unos pocos envoltorios delgados, usted obtiene la capacidad de ver de un vistazo lo que hará una llamada a una función en particular con los parámetros que envíe. Esto puede ayudar mucho durante la depuración al ayudarlo a eliminar algunas sucursales de su investigación y, en general, ayudaría a las personas que intentan entender el programa.

Realmente no pude encontrar otras razones para usar esta idea de ACL, al menos en las que los costos no superan los beneficios. Sin embargo, parece más intuitivo que la solución de visitante mencionada en la otra respuesta.

    
respondido por el Andrei 04.05.2012 - 21:44
1

No comparto su valoración de los beneficios, y sospecho que esta es la razón por la que no se hace: haría que el sistema de objetos sea mucho más complejo, con muy pocos beneficios.

Pero en general, los permisos (r, w, x) se hacen explícitos mediante el uso de métodos: en lugar de tener un miembro de acceso público, se proporciona acceso explícito de lectura o escritura a través de captadores y definidores.

Por supuesto, esto no permite modelar usuarios o grupos (aparte de hacer que un objeto sea friend ). Otros lenguajes de programación permiten restringir el acceso a otras clases dentro del mismo paquete (Java, predeterminado) o dentro del mismo ensamblaje (C #, friend ). Una restricción similar se logra en C ++ al separar el proyecto en diferentes unidades de compilación y usar el firewall del compilador (también conocido como PIMPL) para restringir acceso de algunos aspectos de una clase a esa unidad de compilación.

Por lo tanto, do tiene diferentes modos de acceso, así como diferentes grupos de "usuarios". Simplemente no están disponibles en un modelo unificado con terminología similar a FACL, pero obtienes el mismo efecto.

    
respondido por el Konrad Rudolph 28.05.2012 - 12:45
0

Ok, creo que la solución obvia es implementar el patrón de visitantes:

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

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

Dado que alguien implementa Crew , "se une a Crew group".

EDIT Si quiere un caso más general, puede inventar algunos descriptores de seguridad, pero creo que es una complicación excesiva:

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
    }
};

O, si es absolutamente 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(); }
};
    
respondido por el Lol4t0 04.05.2012 - 21:00
0

Cualquier solución que creas, tendrá complicaciones sintácticas. Es todo acerca de saber el remitente. Algo como:

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

B requiere saber que A es la persona que llama, lo que implica transformar todos los métodos de B:: en plantillas y agregar un parámetro adicional. Lo único que se me ocurre es usar algún tipo de "testigo" que lleva al remitente y algún tipo de objeto RAII para guardar el "remitente" durante un tiempo:

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 clase sender sería algo como:

// 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.
}

Consideraciones:

  • Creo que la única forma de hacerlo de forma segura es que want_permissions se hereda de forma privada para evitar que otros objetos utilicen sus permisos, y todos los métodos want_permissions deben ser protected para evitar que otros objetos creen objetos de la clase want_permissions sin herencia. Por las mismas razones, want_permissions debe verificar si sender hereda de want_permissions o no antes de entregar una clave.

  • El key debe estar diseñado (¿cómo identificar al remitente, simplemente guardando un puntero? ¿por qué no?)

  • Si quieres que sea compatible con multithreading, las cosas se ponen más difíciles, ya que podría haber diferentes objetos autorizados a la vez. Las claves deben guardarse en un contenedor y los métodos de verificación deben buscarse en ese contenedor. El problema es que, una vez más, no sabe cuál de ellos es el remitente en un momento específico si guarda más de uno a la vez. Tal vez con thread_local variables y el pImpl idiom puedes hacer algo. O simplemente restringir un mismo target no se puede usar desde diferentes hilos. O simplemente usar mutexes para bloquear hasta que se destruya una instancia block_perm , para permitir que el siguiente objeto continúe.

  • Dado que no se piensa que ambas clases base relacionadas con los permisos se usen como puntero a las clases base o algo por el estilo, el destructor no necesita ser virtual , ¿no hay otros miembros virtual ? , por lo tanto, se evita toda sobrecarga de polimorfismo (no vtables para estas clases).

  • No todos los métodos requieren permisos. Solo agregue has_read_permission() , has_write_permission() o has_execution_permission para los métodos que desea proteger.

Los permisos de lectura / escritura / ejecución son solo etiquetas para el transporte de un tipo de contenedor:

template<class... allowed>
struct read_permissions;

template<class... allowed>
struct write_permissions;

Requiere permisos solo toma dos tipos, y con una especialización extraemos el paquete de parámetros:

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;
} 

Supongamos que key es solo el puntero del remitente, y get_key es un método mágico (para ser diseñado también) para obtener la clave actual.

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; }

Eso es todo lo que puedo hacer ahora por ti.

    
respondido por el Peregring-lk 19.09.2016 - 13:08
0

Usted dice que no quiere algo irrompible, solo algo que si un buen desarrollador simplemente sigue las reglas establecidas. Tengo un enfoque más simple de lo que se ha propuesto hasta ahora:

Ejecución por separado o lectura / escritura

Bueno, una manera fácil de hacer esto es dividir la clase entre datos y lógica. Similar a POJO y Service for Javaist. Los llamaré William y WilliamExecutor.

Lectura / escritura separada

Solo use la palabra clave const o la tecla const de boost para solo lectura. No es que esto no maneje solo la escritura de objetos, pero ¿es realmente necesario?

Así que simplemente tendrías:

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;

}

Esta es la versión más simple, ¿y ahora si quiero acceso de lectura y exec?

En este momento, es imposible ya que WilliamExecutor no podrá tomar una referencia constante a William , esto se debe a que actualmente WilliamExecutor no tiene estado, por lo que podemos usarlo como un singleton. Sin embargo, si eliminamos esa posibilidad, podemos hacerlo así:

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));

Ahora, si desea un objeto de solo escritura (sin lectura), podría tener una tercera clase sin estado WilliamWriter que se escribe así:

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

Y páselo a la clase que solo necesita escribir en la clase William .

Tengo la misma opinión que @KonradRudolph sobre este asunto, sin embargo, quería proporcionar una solución más simple que involucre menos número de clases, menos complejidad de uso que pueda ajustarse a sus necesidades.

Tal vez sea posible adaptar esto con algunos genéricos, pero se lo dejo a las personas que dominan c ++ mejor que yo.

Nota: el método de traslado a otra clase puede considerarse como romper la POO. Si bien puede ser cierto, no creo que puedas tener algo utilizable, es decir, factible Y que la gente quiera usar, si no lo haces.

    
respondido por el Walfrat 19.09.2016 - 14:06

Lea otras preguntas en las etiquetas