Herramientas de usuario

Herramientas del sitio


wiki2:oso

OSO

Básico

Es una librería para desarrollar la autorización a recursos de un sistema.

Usa un lenguaje propio denominado Polar, con este se definen una serie de reglas. Luego, desde Python se realizará la pregunta a Oso si tal usuario tiene un acceso concreto (que podría ser lectura, escritura…) a un recurso. Oso pasará esa pregunta por la serie de reglas y se obtendrá un booleano con la respuesta.

El código en Polar es interpretado en tiempo de ejecución.

Por defecto Polar rechaza el acceso a un recurso a no ser que la política diga lo contrario.

Conceptos

Un actor es el elemento que hace una acción sobre un recurso. Es la base del funcionamiento de oso con esta misma terminología.

Enforcement es la acción de decidir la autorización en un punto concreto.

Autorización basada en RBAC (roles, Role Based Access control) es aquella en la que se asigna uno o varios roles a un actor. Un rol es un grupo de permisos.

Autorización basada en ReBAC (relaciones, Relationship Based Access control) es aquella en la que el actor y el recurso comparten una relación (un comentario “creado” por un usuario, un usuario “es dueño” de un repositorio, un usuario “pertenece” a un equipo).

Autorización basada en ABAC (atributos, Attribute Based Access control) es aquella en la que el actor tendrá acceso al recurso dependiendo en el valor de alguno de sus atributos (ya sea del mismo actor o del recurso en sí). Por ejemplo un actor marcado como administrador puede acceder a todos los recursos.

Un atributo puede ser cualquier cosa, incluso un rol o una relación. Incluso un rol podría ser una forma de relación. Por eso RBAC y ReBAC se consideran subsets de ABAC. Pero pongamos por ejemplo un atributo público en un repositorio, cualquier usuario puede acceder a ese repositorio si lo tiene marcado como tal. Esto no es ni una relación ni un rol.

Oso en Python

Tras inicializar Oso en tu aplicación podrás llamar a las siguientes funciones de la instancia:

load_file para cargar el archivo .polar.

register_class para añadir el acceso a un nuevo tipo personalizado al código en Polar.

is_allowed(actor, action, resource) es el método para realizar el enforcing. El que responde si está o no autorizado a acceder de esa forma a ese recurso.

enable_roles checkea y valida la configuración de los roles. Debe ser llamado después de cargar los ficheros de reglas.

Flask

Inicializando FlaskOso en la aplicación de Flask permite realizar ciertas acciones sobre la librería tales como…

require_authorization para indicar que ninguna request podrá devolver una response si no se ha llamado a un código de autorización desde dentro, si ocurre saltará una excepción. Siempre se podrá llamar a skip_authorization para que no salte la excepción.

authorize(action, resource) es un hook que llama a is_allowed pero sin necesidad de indicar el actor ya que este lo toma de flask.g.current_user. Si falla devuelve un 403. Para indicar una alternativa de dónde tomar el actor se usa la función set_get_actor.

Para cambiar el comportamiento de un fallo de autenticación se usa set_unauthorized_action.

Los decoradores @authorize y @skip_authorization permiten realizar estas acciones antes de entrar al código.

perform_route_authorization permite que a @authorize se le pase como parámetro la request. El código sería algo parecido a lo siguiente:

from flask import request

@flask_oso.authorize(resource=request)
@app.route("/")
def route():
    return "authorized"

Y en Polar:

# Allow any actor to make a GET request to "/".
allow(_actor, "GET", _resource: Request{path: "/"});

SqlAlchemy

Polar

Usa una sintaxis declarativa, en vez de imperativa. Es decir, se le va indicando “qué aceptar” mediante reglas. Sigue un estilo lógico que sustituye el flujo (loops, condicionales…) por “facts” o reglas. En sí, el conjunto de estas reglas es conocido como base de conocimiento.

Se pueden añadir tantas funciones\reglas con el mismo nombre como se quiera, no se sobreescriben sino que se añaden. Es decir, una consulta será cierta si hace match con una o varias reglas.

La regla principal y que ha de existir es allow(actor, acción, recurso).

Simple Polar rules

Si no se añade un if entonces se acepta cualquier combinación. La siguiente regla acepta todo en todo:

allow(actor, action, resource);

Permitir explícitamente a Zora leer el documento 1:

allow("Zora", "read", "document-1");

Si el usuario es el dueño del gasto:

allow(user: User, "read", expense: Expense) if
    user.id = expense.user_id;

Si el usuario es contable o contable senior:

user_in_role(user: User, "accountant") if
    user.title = "Accountant";
    
user_in_role(user: User, "accountant") if
    user.title = "Senior Accountant";
    
allow(user: User, "read", _expense: Expense) if
    user_in_role(user, "accountant");

Sintaxis

Chequear si una variable (actor) matchea con un valor concreto:

allow(actor, "read", "document-1") if
    actor = "Abagail" or
    actor = "Carol" or
    actor = "Johann";

La comparación pasa cuando se usa el =. Donde un parámetro se asigna ese valor y luego se compara. Esta comparación\asignación se puede dividir usando dos operadores diferentes, uno para asignar := y otro para consultar ==.

Se puede hacer una query de los valores internos de un objeto. Por ejemplo, para permitir la lectura a un usuario administrador con id 0 al documento con id 1:

allow(User{id: 0, admin: true}, "read", Document{id: 1, owner: 0})

O acceder a sus propiedades:

# A user that is an administrator may read any document.
allow(user, "read", _document) if
    user.admin = true;

Para comprobar el tipado usamos matches:

# A user that is an administrator may read any document.
allow(user, "read", _document) if
    user matches User and
    user.admin = true;

# A user may read any document that they own.
allow(user, "read", document) if
    user matches User and
    document matches Document and
    user.id = document.owner;
    
# ----- O lo que es lo mismo ----------------------

# A user that is an administrator may read any document.
allow(user: User, "read", _document: Document) if
    user.admin = true;

# A user may read any document that they own.
allow(user: User, "read", document: Document) if
    user.id = document.owner;

Podemos incluso matchear con los atributos, algo al estilo de…

# A user that is an administrator may read any document.
allow(_user: User{admin: true}, "read", _document: Document);

# A user may read any document that they own.
allow(_user: User{id: user_id}, "read", _document: Document{owner: document_owner}) if
    user_id = document_owner;
    
# A user may read any document that they own.
allow(_user: User{id: user_id}, "read", _document: Document{owner: user_id});

Podemos también llamar a métodos:

allow(_actor, action: String, _resource) if
    action.endswith("::resource");

Piensa que podemos llamar también reglas con atributos que no tenemos en esta:

allow(user, action, resource) if
    resource_role_applies_to(resource, role_resource) and
    user_in_role(user, role, role_resource) and
    role_allow(role, action, resource);

Equivalencias en Python

  • None → nil
  • int → Integer
  • float → Float
  • bool → Boolean
  • list → List
  • dict → Dictionary
  • str → String

Queries en listas o iterables (con yield):

allow(actor, _action, _resource) if "payroll" in actor.get_groups();
allow(actor, _action, _resource) if actor.groups.index("HR") == 0;

Diccionarios:

# diccionario: user.roles = {"project1": "admin"}
allow(actor, _action, _resource) if actor.roles.project1 = "admin";

Podemos acceder a métodos estáticos si la clase ha sido registrada. Por ejemplo, para permitir el acceso total en entorno de desarrollo:

# En Python
class Env:
    @staticmethod
    def var(variable):
        return os.environ[variable]
        
# En Polar
allow(_actor, _action, _resource) if Env.var("ENV") = "development";

Cómo implementar...

RBAC

Se necesita que se den tres cosas para activar los roles:

  1. Añadir configuración de roles y recursos en nuestros ficheros.
  2. Usar role_allows en alguna de nuestras reglas.
  3. Asignar roles a usuarios.

Los roles están limitados a los recursos y son un conjunto de acciones permitidas. Para definirlos usamos la regla resource:

resource(_type: Org, "org", actions, roles) if
    actions = ["read", "create_repo"] and
    roles = {
        member: {
            permissions: ["read"],
        },
        owner: {
            permissions: ["read", "create_repo"],
        }
    };
    
# que es lo mismo que....

resource( _type: Org, "org", ["read", "create_repo"],
    {
        member: {
            permissions: ["read"],
        },
        owner: {
            permissions: ["read", "create_repo"],
        }
    }
);

Para usar un modelo de datos propio se ha de indicar una regla que devuelva si un actor tiene un rol concreto para un recurso concreto. Esta podría ser algo así:

actor_has_role_for_resource(actor, role_name, resource) if
    role in actor.get_roles() and
    role_name = role.name and
    resource = role.resource;

Básico

El siguiente fichero Polar indica que cualquier actor con el rol guest puede leer una página, pero olo actores con el rol admin pueden escribirla.

allow(actor, action, resource) if
    role_allows(actor, action, resource);

actor_has_role_for_resource(actor, role_name, resource) if
    role in actor.get_roles() and
    role_name = role.name and
    resource = role.resource;

resource(_type: Page, "page", actions, roles) if
    actions = ["read", "write"] and
    roles = {
        guest: {
            permissions: ["read"]
        },
        admin: {
            permissions: ["write"],
            implies: ["guest"]
        }
    };

Existe la regla principal allow. En este caso la decisión la toma otra regla propia de Polar denominada role_allows que implementa la lógica para el RBAC.

La ruta para el RBAC es una combinación de otras funciones actor_has_role_for_resource y resource.

Para hacer uso de las funciones y propiedades es necesario registrar las clases con register_class.

La regla actor_has_role_for_resource(actor, role_name, resource) ha de devolver si un objeto rol está asociado a un actor. Esta regla es necesaria ya que es la que se buscará por las reglas internas de Oso.

❓En este caso o siempre (?) los roles tienen el formato {name: “the-role-name”, resource: TheResourceObject}.

La regla recurso decide sobre un recurso específico en el sistema. Por ejemplo una página, una ruta o un registro. En el ejemplo Page puede tener dos acciones read y write. Repartidas en guest y admin. Cuando en un rol, a parte de los permissions, se le añade implies este “hereda” del rol indicado.

Los roles pueden implicar otros roles de otros recursos, por ejemplo repo:reader y repo:admin de la siguiente regla:

resource(_type: Org, "org", actions, roles) if
    actions = ["read", "create_repos", "list_repos",
               "create_role_assignments", "list_role_assignments", "update_role_assignments", "delete_role_assignments"] and
    roles = {
        member: {
            permissions: ["read", "list_repos", "list_role_assignments"],
            implies: ["repo:reader"]
        },
        owner: {
            permissions: ["create_repos", "create_role_assignments", "update_role_assignments", "delete_role_assignments"],
            implies: ["member", "repo:admin"]
        }
    };

Alternativas

# Defining roles for users
user_in_role(_user: User{username: "steve"}, "admin");
user_in_role(_user: User{username: "leina"}, "admin");

# Assigning groups of users to the same role
user_in_role(user: User, "admin") if
    user.username in ["steve", "leina", "alex", "sam"];
    
# Get role assignment from user
user_in_role(user: User, role) if
    role = user.role;
    
# Allow the admin role to take any action on any resource
role_allow("admin", _action, _resource);

# Allow the member role to read and write to any blog post
role_allow("member", action: String, _resource: BlogPost) if
    action in ["read", "write"];
wiki2/oso.txt · Última modificación: 2021/08/09 13:29 (editor externo)