martes, 7 de junio de 2016

Un vistazo a la autenticación y autorización con Pyramid

Acostumbrado a que las Universidades nos hagan ver ambas cosas como una misma, siempre es un poco complicado entrar a este tema en cualquier framework, aunque después que has abierto los ojos todos es coser y cantar. Según la documentación de Pyramid al respecto, la forma más sencilla de empezar a configurar la seguridad en esta wea es la siguiente: Creamos el fichero ./ambiente/aplicacion/aplicacion/security.py con el siguiente contenido:
USERS = {'vtacius':'editor',
          'viewer':'viewer'}
GROUPS = {'vtacius':['group:editors','group:admins']}

def groupfinder(userid, request):
    if userid in USERS:
        return GROUPS.get(userid, [])
Lo que necesitamos en realidad es a groupfinder. Los diccionarios USERS y GROUPS son, por decirlo de una forma, nuestra forma de simular nuestra base de datos. De hecho, esta función no es del todo obligatoria. Sólo la usamos si queremos agregar un principals a nuestro usuario logueado. Sea cual sea la forma que usemos para esta función (Seguramente habrá una consulta a una base de datos), lo importante es que debe devolver una lista de principals (Que a esta altura se antoja entenderlos como "roles") en la forma mostrada:
['groups:editors','groups:publishers']

Luego creamos el ficheros ./ambiente/aplicacion/aplicacion/resources.py con el siguiente contenido:
from pyramid.security import Allow, Everyone, Authenticated

class Root(object):

        __acl__ = [
                (Allow, Everyone, 'listar'),
                (Allow, Authenticated, 'detallar'),
                (Allow, 'groups:admins', 'creacion'),
                (Allow, 'groups:editors', 'modificacion'),
                (Allow, 'groups:admins', 'borrado')
                ]

        def __init__(self, request):
                pass

Que es básicamente la ACL de la aplicación. La ACL en cuestión de compone de una lista de tuplas (Llamadas ACE). que tienen la siguiente forma:
({{Acción a tomar}}, {{Principals necesario}}, {{Nombre de ACE}})
Para unir todo esto, necesitamos modificar el ./ambiente/aplicacion/aplicacion/__init__.py de nuestra aplicación para configurar la autenticación y autorización:
# coding: utf-8
from pyramid.config import Configurator

# Empieza el trabajo con la autenticación de la wea esta
from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy
from pyramid.security import Authenticated

from .security import groupfinder

def main(global_config, **settings):
    """ This function returns a Pyramid WSGI application.
    """
    # Empieza el trabajo con la autenticacion: Creo las politicas, así nomás y sin gracia
    # Por cierto, 'c3cre3t0 debería cambiarse a algo más personal
    # como callback, aparece nuestro amigo groupfinder, pero esto podría obviarse
    authn_policy = AuthTktAuthenticationPolicy('c3cr3t0', hashalg='sha512', callback=groupfinder)
    authz_policy = ACLAuthorizationPolicy()

    # Y por acá agregamos a resources.py
    config = Configurator(settings=settings, root_factory='.resources.Root')

    # Empieza el trabajo con la autenticacion: Configuro las politicas en la aplicación
    config.set_authentication_policy(authn_policy)
    config.set_authorization_policy(authz_policy)

    # Donde sucede la magia del login
    config.add_route(name='login', pattern='/login', request_method='POST')

    # Cuando request_method se encuentra acá, es devuelto un error como 
    # "The resource could not be found." 
    # Así que es como mejor configurar desde acá a request_method
    config.add_route('ficheros_listado', '/ficheros', request_method='GET')
    config.add_route('ficheros_detalle', '/ficheros/{usuario}', request_method='GET')
    config.add_route('ficheros_creacion', '/ficheros', request_method='POST')
    config.add_route('ficheros_modificacion', '/ficheros/{usuario}', request_method='PUT')
    config.add_route('ficheros_borrado', '/ficheros/{usuario}', request_method='DELETE')
    config.scan()
    return config.make_wsgi_app()
Y añadimos los permisos en nuestras vistas. Pues parece que sólo funcionan de esa manera. Así que el contenido de ./ambiente/aplicacion/aplicacion/views/actividades.py queda de la siguiente forma
# coding: utf-8
from pyramid.view import view_config
from ..fichero_app.ficheros import Usuarios
from pyramid import httpexceptions as exception

# Podrías usar los siguientes en lugar de los permisos personalizados que tienes en este momento, pero no
# La importación de hecho no es necesaria para nada
from pyramid.security import Everyone, Authenticated

@view_config(route_name='ficheros_listado', renderer='json', permission='listar')
def ficheros_listado(request):
    """Cuando request_method se configura acà, el mensaje es diferente porque 
    la operación alcanzada es diferente
        The resource could not be found.


     predicate mismatch for view get_listar_ficheros (request_method = GET,HEAD)
    Por tanto lo mejor es configurarlo allá en __init__
    """
    ficheros = Usuarios()
    listado = ficheros.listado()
    return {'respuesta': listado}

@view_config(route_name='ficheros_detalle', renderer='json', permission='detallar')
def ficheros_detalle(request):
    usuario = request.matchdict['usuario']
    ficheros = Usuarios()
    detalle = ficheros.detalle(usuario)
    return {'respuesta': detalle}

@view_config(route_name='ficheros_creacion', renderer='json', permission='creacion')
def ficheros_creacion(request):
    try:
        usuario = request.json_body['usuario']
        data = request.json_body['data']
    except Exception as e:
        return exception.HTTPBadRequest()
    ficheros = Usuarios()
    creacion = ficheros.creacion(usuario, data)
    return {'respuesta': creacion}

@view_config(route_name='ficheros_modificacion', renderer='json', permission='modificacion')
def ficheros_modificacion(request):
    usuario = request.matchdict['usuario']
    try:
        data = request.json_body['data']
    except Exception as e:
        return exception.HTTPBadRequest()
    ficheros = Usuarios()
    modificacion = ficheros.modificacion(usuario, data)
    return {'respuesta': modificacion}

@view_config(route_name='ficheros_borrado', renderer='json', permission='borrado')
def ficheros_borrado(request):
    usuario = request.matchdict['usuario']
    ficheros = Usuarios()
    borrado = ficheros.borrado(usuario)

    return {'respuesta': borrado}

Y solo faltaría agregar la pequeña vista que se encarga del login ./ambiente/aplicacion/aplicacion/views/login.py:
# coding: utf-8
from pyramid.view import view_config
from pyramid.security import remember
from pyramid.httpexceptions import HTTPFound

@view_config(route_name='login', request_method='POST')
def login(request):
        usuario = request.POST.get('username')
        cabeceras = remember(request, usuario)
        return HTTPFound(headers=cabeceras)
        # De la siguiente forma, no hay HTML devuelto, pero todo funciona bien
        #response = request.response
        #response.headerlist.extend(cabeceras)
        #return response
Así procedemos a probar nuestra aplicación desde consola:
$ curl -w '\n' -X GET http://localhost:6543/ficheros 
{"respuesta": ["alortiz", "kpenate", "opineda"]}
Atentos a este. -i agregará las cabeceras que recibimos de respuesta para que quede totalmente claro lo que pasa:
$ curl -i -w '\n' -X GET http://localhost:6543/ficheros/alortiz
HTTP/1.1 403 Forbidden
Content-Length: 1085
Content-Type: text/html; charset=UTF-8
Date: Tue, 07 Jun 2016 02:36:22 GMT
Server: waitress

<html>
 <head>
  <title>403 Forbidden</title>
 </head>
 <body>
  <h1>403 Forbidden</h1>
  Access was denied to this resource.<br/><br/>
debug_authorization of url http://localhost:6543/ficheros/alortiz (view name u'' against context &lt;aplicacion.resources.Root object at 0x7f501c490e90&gt;): ACLDenied permission 'detallar' via ACE '&lt;default deny&gt;' in ACL [('Allow', 'system.Everyone', 'listar'), ('Allow', 'system.Authenticated', 'detallar'), ('Allow', 'groups:admins', 'creacion'), ('Allow', 'groups:editors', 'modificar'), ('Allow', 'groups:admins', 'eliminar')] on context &lt;aplicacion.resources.Root object at 0x7f501c490e90&gt; for principals ['system.Everyone']


 <link rel="stylesheet" type="text/css" href="http://localhost:6543/_debug_toolbar/static/toolbar/toolbar_button.css">

<div id="pDebug">
    <div  id="pDebugToolbarHandle">
        <a title="Show Toolbar" id="pShowToolBarButton"
           href="http://localhost:6543/_debug_toolbar/313339393832303438363539353336" target="pDebugToolbar">&#171; FIXME: Debug Toolbar</a>
    </div>
</div>
</body>
</html>
HTTP/1.1 403 Forbidden. Que ha funcionado. Así que ahora probamos la autenticación mediante curl desde la siguiente forma:
$ curl -i -w "\n" -X POST http://localhost:6543/login -d "username=vtacius" 
HTTP/1.1 302 Found
Content-Length: 556
Content-Type: text/html; charset=UTF-8
Date: Tue, 07 Jun 2016 02:43:25 GMT
Location: http://localhost:6543/login
Server: waitress
Set-Cookie: auth_tkt=95c36928020725f40edd54266285117cfddd4d05b0cc42f969b9fee13a3ff52f4e5f98491c426f6c6bc5242d1ace926e264865a8cc1a30f127d1b1a6ec3de457575634cddnRhY2l1cw%3D%3D!userid_type:b64unicode; Path=/
Set-Cookie: auth_tkt=95c36928020725f40edd54266285117cfddd4d05b0cc42f969b9fee13a3ff52f4e5f98491c426f6c6bc5242d1ace926e264865a8cc1a30f127d1b1a6ec3de457575634cddnRhY2l1cw%3D%3D!userid_type:b64unicode; Domain=localhost; Path=/
Set-Cookie: auth_tkt=95c36928020725f40edd54266285117cfddd4d05b0cc42f969b9fee13a3ff52f4e5f98491c426f6c6bc5242d1ace926e264865a8cc1a30f127d1b1a6ec3de457575634cddnRhY2l1cw%3D%3D!userid_type:b64unicode; Domain=.localhost; Path=/

<html>
 <head>
  <title>302 Found</title>
 </head>
 <body>
  <h1>302 Found</h1>
  The resource was found at ; you should be redirected automatically.


 <link rel="stylesheet" type="text/css" href="http://localhost:6543/_debug_toolbar/static/toolbar/toolbar_button.css">

<div id="pDebug">
    <div  id="pDebugToolbarHandle">
        <a title="Show Toolbar" id="pShowToolBarButton"
           href="http://localhost:6543/_debug_toolbar/313339393832303434383337353834" target="pDebugToolbar">&#171; FIXME: Debug Toolbar</a>
    </div>
</div>
</body>
</html>
(Para correr este última petición podría ser necesario reiniciar nuestro servidor web de prueba)
Y otra vez nos hemos ahorrado escribir un formulario a la carrera con la opción -d de curl, con la que enviamos los datos que la aplicación requiere.

Lo que necesitamos de ahora en adelante es usar la opción -H para configurar a curl que use la cookie auth_tkt que nos envío como respuesta la aplicación en cada petición que necesite autenticación. Cuidado con usar comillas dobles para limitar el contenido de la cookie, desde consola existe el inconveniente de:
$ curl -w '\n' -X GET http://localhost:6543/ficheros/alortiz -H 'Cookie: auth_tkt=95c36928020725f40edd54266285117cfddd4d05b0cc42f969b9fee13a3ff52f4e5f98491c426f6c6bc5242d1ace926e264865a8cc1a30f127d1b1a6ec3de457575634cddnRhY2l1cw%3D%3D!userid_type:b64unicode'
{"respuesta": {"palabras": ["ambiente", "publico"], "nombre": "Alexander", "apellido": "Ort\u00edz"}}

Fuentes:
How to create high scalable web-backends for ios developers 0.0.0 documentation

No hay comentarios:

Publicar un comentario en la entrada

Otros apuntes interesantes

Otros apuntes interesantes