====== 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: - Añadir configuración de roles y recursos en nuestros ficheros. - Usar ''role_allows'' en alguna de nuestras reglas. - 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"];