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 <aplicacion.resources.Root object at 0x7f501c490e90>): ACLDenied permission 'detallar' via ACE '<default deny>' in ACL [('Allow', 'system.Everyone', 'listar'), ('Allow', 'system.Authenticated', 'detallar'), ('Allow', 'groups:admins', 'creacion'), ('Allow', 'groups:editors', 'modificar'), ('Allow', 'groups:admins', 'eliminar')] on context <aplicacion.resources.Root object at 0x7f501c490e90> 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">« 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">« 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