<part_1<part_2
Le prime basi di pylons mi sono piaciute parecchio.
Ho testato i vari semplici esempi disponibili sulle guide e sulla documentazione
reperibile on-line, poi mi sono imbattuto sul problema dell’autenticazione e
delle autorizzazioni.
I metodi sono parecchi, ma ho scelto di appoggiarmi per questo compito
a Repoze.what (come suggerito dalla guida ufficiale).
Tale guida è mutuata da questa e, vista la scarsa documentazioni disponibile
in Italiano ho pensato di riassumere un po’ quello che ho provato e testato.
Per i programmatori più esperti, forse non ci sono stati problemi, per me, aspirante tale, invece sì.
Anticipo subito che i problemi più fastidiosi li ho avuti codificando i models di
Sqlalchemy, avvalendomi del declarative_base.
Non ho avuto modo di vedere repoze funzionante appieno, a causa di un errore riguardante il mapper di SQLA.
‘EagerLoader’ object has no attribute ‘mapper’
Per questa ragione ho dovuto ricodificare tutte le classi della mia applicazione:
Giocatore, Squadra ecc.
Comunque…
I componenti utili alla causa sono:
-Pylons v1.0
-repoze.what v1.0.9
-repoze.what-pylons v1.0
-repoze.what-quickstart v1.0.8
perchè tutto funzioni, sono necessarie le seguenti dipendenze:
-repoze.who
-repoze.who-friendlyform
-repoze.what
-repoze.what-pylons
-repoze.what-quickstart
-repoze.what-plugins-sql
-repoze.who.plugins.sa
Dovrebbero comunque installarsi tutte automaticamente via easy_install
easy_install repoze.what-pylons
easy_install repoze.what-quickstart
Ora riprendendo a mano il mio progetto di esempio, creo il file:
..FantaManagerfantamanagermodelauth.py
dove definisco i modelli utilizzati da repoze.what
from sqlalchemy import Table, ForeignKey, Column
from sqlalchemy.types import Unicode, UnicodeText, Integer, Date, CHAR
from sqlalchemy import orm
from fantamanager.model.meta import metadata
import os
from hashlib import sha1
group_table = Table('group', metadata,
Column('id', Integer(), primary_key=True),
Column('name', Unicode(255), unique=True, nullable=False),
)
permission_table = Table('permission', metadata,
Column('id', Integer(), primary_key=True),
Column('name', Unicode(255), unique=True, nullable=False),
)
user_table = Table('user', metadata,
Column('id', Integer(), primary_key=True),
Column('username', Unicode(255), unique=True, nullable=False),
Column('email', Unicode(255), unique=True, nullable=False),
Column('password', Unicode(80), nullable=False),
Column('fullname', Unicode(255), nullable=False),
)
# many-to-many relationship between groups and permissions.
group_permission_table = Table('group_permission', metadata,
Column('group_id', Integer, ForeignKey('group.id')),
Column('permission_id', Integer, ForeignKey('permission.id')),
)
# many-to-many relationship between groups and users
user_group_table = Table('user_group', metadata,
Column('user_id', Integer, ForeignKey('user.id')),
Column('group_id', Integer, ForeignKey('group.id')),
)
class Group(object):
pass
class Permission(object):
pass
class User(object):
def _set_password(self, password):
"""Hash password on the fly."""
hashed_password = password
if isinstance(password, unicode):
password_8bit = password.encode('UTF-8')
else:
password_8bit = password
salt = sha1()
salt.update(os.urandom(60))
hash = sha1()
hash.update(password_8bit + salt.hexdigest())
hashed_password = salt.hexdigest() + hash.hexdigest()
# Make sure the hased password is an UTF-8 object at the end of the
# process because SQLAlchemy _wants_ a unicode object for Unicode
# fields
if not isinstance(hashed_password, unicode):
hashed_password = hashed_password.decode('UTF-8')
self.password = hashed_password
def _get_password(self):
"""Return the password hashed"""
return self.password
def validate_password(self, password):
"""
Check the password against existing credentials.
:param password: the password that was provided by the user to
try and authenticate. This is the clear text version that we will
need to match against the hashed one in the database.
:type password: unicode object.
:return: Whether the password is valid.
:rtype: bool
"""
hashed_pass = sha1()
hashed_pass.update(password + self.password[:40])
return self.password[40:] == hashed_pass.hexdigest()
# Map SQLAlchemy table definitions to python classes
orm.mapper(Group, group_table, properties={
'permissions':orm.relation(Permission, secondary=group_permission_table),
'users':orm.relation(User, secondary=user_group_table),
})
orm.mapper(Permission, permission_table, properties={
'groups':orm.relation(Group, secondary=group_permission_table),
})
orm.mapper(User, user_table, properties={
'groups':orm.relation(Group, secondary=user_group_table),
})
il file ..FantaManagerfantamanagermodelmeta.py è così composto:
"""SQLAlchemy Metadata and Session object"""
from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy import MetaData
__all__ = ['Base', 'Session']
Session = scoped_session(sessionmaker())
metadata = MetaData()
Ora importiamo Group, User, Permission nel file ..FantaManagerfantamanagermodel__init__.py
"""The application's model objects"""
import sqlalchemy as sa
from sqlalchemy import orm
from fantamanager.model import meta
from fantamanager.model.auth import User, Group, Permission
def init_model(engine):
"""Call me before using any of the tables or classes in the model"""
sm = orm.sessionmaker(autoflush=True, autocommit=False, bind=engine)
meta.engine = engine
meta.Session = orm.scoped_session(sm)
Una volta definite le class/table per le autorizzazioni/autenticazioni, dobbiamo settare
repoze.what in modo che le utilizzi. Per fare questo si utilizza la funzione
setup_sql_auth() all’interno della funzione add_auth(), nel file
..FantaManagerfantamanagerlibauth.py
from repoze.what.plugins.quickstart import setup_sql_auth
from fantamanager.model import meta
from fantamanager.model.auth import User, Group, Permission
def add_auth(app, config):
"""
Add authentication and authorization middleware to the ``app``.
We're going to define post-login and post-logout pages
to do some cool things.
"""
# we need to provide repoze.what with translations as described here:
# http://what.repoze.org/docs/plugins/quickstart/
return setup_sql_auth(app, User, Group, Permission, meta.Session,
login_url='/account/login',
post_login_url='/account/login',
post_logout_url='/',
login_handler='/account/login_handler',
logout_handler='/account/logout',
cookie_secret=config.get('cookie_secret'),
translations={
'user_name': 'username',
'group_name': 'name',
'permission_name': 'name',
})
Editiamo il file ..FantaManagerdevelopment.ini ed inseriamo il cookie_secret
[app:main]
use = egg:FantaManager
full_stack = true
static_files = true
sqlalchemy.url = sqlite:///%(here)s/FantaManager.sqlite
cache_dir = %(here)s/data
beaker.session.key = fantamanager
beaker.session.secret = somesecret
# set repoze cookie secret
cookie_secret = 'your-own-secret'
il paramtero cookie_secret, essendo config, un argomento passato alla precedente funzione add_auth(),
ci serve per generare i cookies.
Ora aggiungiamo il middleware (lo strato di cipolla Repoze).
Editiamo il file ..FantaManagerfantamanagerconfigmiddleware.py
"""Pylons middleware initialization"""
from beaker.middleware import SessionMiddleware
from paste.cascade import Cascade
from paste.registry import RegistryManager
from paste.urlparser import StaticURLParser
from paste.deploy.converters import asbool
from pylons.middleware import ErrorHandler, StatusCodeRedirect
from pylons.wsgiapp import PylonsApp
from routes.middleware import RoutesMiddleware
from fantamanager.config.environment import load_environment
from fantamanager.lib.auth import add_auth
# from repoze.who
from repoze.who.config import make_middleware_with_config as make_who_with_config
def make_app(global_conf, full_stack=True, static_files=True, **app_conf):
...
# Configure the Pylons environment
config = load_environment(global_conf, app_conf)
# The Pylons WSGI app
app = PylonsApp(config=config)
# Routing/Session/Cache Middleware
app = RoutesMiddleware(app, config['routes.map'], singleton=False)
app = SessionMiddleware(app, config)
# CUSTOM MIDDLEWARE HERE (filtered by error handling middlewares)
# added for repoze.what auth
app = add_auth(app, config)
if asbool(full_stack):
# Handle Python exceptions
app = ErrorHandler(app, global_conf, **config['pylons.errorware'])
# Display error documents for 401, 403, 404 status codes (and
# 500 when debug is disabled)
if asbool(config['debug']):
app = StatusCodeRedirect(app)
else:
app = StatusCodeRedirect(app, [400, 401, 403, 404, 500])
# Establish the Registry for this application
app = RegistryManager(app)
if asbool(static_files):
# Serve static files
static_app = StaticURLParser(config['pylons.paths']['static_files'])
app = Cascade([static_app, app])
app.config = config
return app
il nostro strato di cipolla è rappresentato dalla riga di codice
app = add_auth(app, config)
subito sotto
# CUSTOM MIDDLEWARE HERE
Aggiungiamo ora un Gruppo ‘admin’, un paio di utenti e i permessi di admin.
Inseriamo il codice necessario, all’interno del file
..FantaManagerfantamanagerwebsetup.py
"""Setup the FantaManager application"""
import logging
import pylons.test
from fantamanager import model
from fantamanager.model import meta
from fantamanager.config.environment import load_environment
from fantamanager.model.meta import Session
from fantamanager.model.auth import User, Group, Permission
from fantamanager.model.models import Giocatore, Squadra
log = logging.getLogger(__name__)
def setup_app(command, conf, vars):
"""Place any commands to setup fantamanager here"""
# Don't reload the app if it was loaded under the testing environment
if not pylons.test.pylonsapp:
load_environment(conf.global_conf, conf.local_conf)
meta.metadata.bind = meta.engine
# Create the tables if they don't already exist
log.info("Creating tables")
meta.metadata.drop_all(checkfirst=True, bind=Session.bind)
meta.metadata.create_all(bind=Session.bind)
# Now let's create users, group and permission
# ADMIN group
log.info("Adding initial users, groups and permissions...")
log.info("Adding 'Admin' Group...")
g = Group()
g.name = u'admin'
meta.Session.add(g)
meta.Session.commit()
log.info("+++ 'Admin' Group Done!")
# ADMIN permission
log.info("-> Adding 'Admin' permission...")
p = Permission()
p.name = u'admin'
p.groups.append(g)
meta.Session.add(p)
meta.Session.commit()
log.info("+++ 'Admin' permission assigned to 'Admin' group!")
# User ADMIN
log.info("-> Adding 'Admin' user...")
u = User()
u.username = u'admin'
u.fullname = u'admin'
u._set_password('admin')
u.email = u'[email protected]'
u.groups.append(g)
meta.Session.add(u)
meta.Session.commit()
log.info("+++ 'Admin' user created!")
log.info("+++ 'Admin' user assigned to 'Admin' group!")
# User TEST
log.info("-> Adding 'Test' user...")
u = User()
u.username = u'test'
u.fullname = u'test'
u._set_password('test')
u.email = u'[email protected]'
meta.Session.add(u)
meta.Session.commit()
log.info("+++ 'Test' user created!")
log.info("+++ Done!")
alternativamente utilizzare la shell di paster e creare gli oggetti interattivamente:
paster shell
Ora non resta che definire il controller e la action di Login:
paster controller account
Edito il controller appena creato (..FantaManagerfantamanagercontrollersaccount.py)
import logging
from pylons import request, response, session, tmpl_context as c, url
from pylons.controllers.util import abort, redirect
from fantamanager.lib.base import BaseController, render
from repoze.what.predicates import not_anonymous, has_permission
from repoze.what.plugins.pylonshq import ActionProtector
from pylons.controllers.util import redirect
log = logging.getLogger(__name__)
class AccountController(BaseController):
def login(self):
"""
This is where the login form should be rendered.
Without the login counter, we won't be able to tell if the user has
tried to log in with wrong credentials
"""
identity = request.environ.get('repoze.who.identity')
came_from = str(request.GET.get('came_from', '')) or
url(controller='account', action='welcome')
if identity:
redirect(url(came_from))
else:
c.came_from = came_from
c.login_counter = request.environ['repoze.who.logins'] + 1
return render('/derived/account/login.html')
@ActionProtector(not_anonymous())
def welcome(self):
"""
Greet the user if she logged in successfully or redirect back
to the login form otherwise(using ActionProtector decorator).
"""
identity = request.environ.get('repoze.who.identity')
return 'Welcome back %s' % identity['repoze.who.userid']
@ActionProtector(not_anonymous())
def test_user_access(self):
return 'You are inside user section'
@ActionProtector(has_permission('admin'))
def test_admin_access(self):
return 'You are inside admin section'
…e la relativa template ..FantaManagerfantamanagertemplatesderivedaccountlogin.html
% if c.login_counter > 1:
Incorrect Username or Password
% endif
<form action="${h.url(controller='account', action='login_handler'
,came_from=c.came_from, __logins=c.login_counter)}" method="POST">
<label for="login">Username:</label>
<input type="text" id="login" name="login" /><br />
<label for="password">Password:</label>
<input type="password" id="password" name="password" /><br />
<input type="submit" id="submit" value="Submit" />
</form>
Prima di lanciare il server, ricodifico le class/table della mia applicazione
(precedentemente create utilizzando il declarative_base).
Per renderla breve utilizzo solo la classe Giocatore e Squadra:
# -*- coding: utf-8 -*-#
## ..modelmodels.py
'''model for FantaManager application powered by Pylons'''
from fantamanager.model.meta import metadata
from sqlalchemy.types import Unicode
from sqlalchemy import create_engine, Column, Integer, String, Float
from sqlalchemy import ForeignKey, Table
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import backref, aliased
from sqlalchemy.orm import relation as relationship
from sqlalchemy.orm import sessionmaker
from sqlalchemy import orm, func, desc, asc, and_
#metadata = Base.metadata
giocatore_table = Table('giocatori', metadata,
Column('id', Integer(), primary_key=True),
Column('idgaz', Integer()),
Column('nome', Unicode(255)),
Column('squadra', Unicode(255)),
Column('valore', Integer()),
Column('ruolo', Unicode(255)),
)
squadra_table = Table('squadre', metadata,
Column('id', Integer(), primary_key=True),
Column('nome', Unicode(255)),
Column('budget', Integer()),
Column('cost', Integer()),
Column('pts', Float()),
)
# M2M relation
squadre_giocatori_table = Table('squadre_giocatori', metadata,
Column('squadre_id', Integer, ForeignKey('squadre.id')),
Column('giocatori_id', Integer, ForeignKey('giocatori.id')),
)
class Giocatore(object):
pass
class Squadra(object):
pass
# Map SQLAlchemy table definitions to python classes
orm.mapper(Squadra, squadra_table, properties={
'giocatori':orm.relation(Giocatore, secondary=squadre_giocatori_table),})
orm.mapper(Giocatore, giocatore_table, properties={
'squadre':orm.relation(Squadra, secondary=squadre_giocatori_table),})
Lanciamo il setup dell’applicazione:
paster setup-app development.ini
e avviamo il server:
paster serve --reload development.ini
se visito la pagina:
http://127.0.0.1:5000/account/test_user_access
e inserisco username e password (‘test’, ‘test’)
vedo renderizzato correttamente: You are inside user sectionse visito la pagina
http://127.0.0.1:5000/account/test_admin_access
ottengo il FORBIDDEN default di pylons poichè mi vedo ancora assegnato il cookie
relativo al test-user
ATTENZIONE! svuotare la cache di firefox prima di testare la pagina di Admin!
se ri-visito la pagina:
http://127.0.0.1:5000/account/test_admin_access
e inserisco username e password (‘admin’, ‘admin’)
vedo renderizzato correttamente: You are inside ADMIN section
Tutto ok!
Ora se vogliamo decorare la azioni del mio controller personale (la mia app),
basta appunto anteporre il decorator desiderato, prima della action che vogliamo
proteggere, es:
import logging
from sqlalchemy import distinct
from pylons import request, response, session, tmpl_context as c, url
from pylons.controllers.util import abort, redirect
from fantamanager.lib.base import Session, BaseController, render
from fantamanager.model.models import Giocatore
from repoze.what.predicates import not_anonymous, has_permission
from repoze.what.plugins.pylonshq import ActionProtector
import fantamanager.lib.helpers as h
log = logging.getLogger(__name__)
class SerieaController(BaseController):
def __before__(self):
self.ateam_q = Session.query(distinct(Giocatore.squadra))
@ActionProtector(not_anonymous())
def index(self):
return 'Test: >> Controller Serie_A >> Connection OK!'
@ActionProtector(has_permission('admin'))
def squadre(self):
c.squadre = [team[0] for team in self.ateam_q.all()]
return render('/seriea.mako')
se visito la pagina http://127.0.0.1:5000/seriea/squadre
devo fare il login: se utilizzo test mi becco il Forbidden
se svuoto la cache ed entro come admin, passo!
Questo è quanto.
Commenti recenti