2009-12-07 4 views
6

J'ai une application Web basée sur Pylons qui se connecte via Sqlalchemy (v0.5) à une base de données Postgres. Pour la sécurité, plutôt que de suivre le modèle typique des applications web simples (comme dans tous les didacticiels), je n'utilise pas un utilisateur Postgres générique (par exemple "webapp"), mais je demande aux utilisateurs d'entrer leur propre ID utilisateur et mot de passe. , et je l'utilise pour établir la connexion. Cela signifie que nous bénéficions pleinement de la sécurité de Postgres. Pour compliquer encore les choses, il existe deux bases de données distinctes auxquelles se connecter. Bien qu'ils soient actuellement dans le même cluster Postgres, ils doivent pouvoir se déplacer vers des hôtes distincts à une date ultérieure.Avec sqlalchemy comment lier dynamiquement au moteur de base de données sur une base par requête

Nous utilisons le paquet declarative de sqlalchemy, bien que je ne puisse pas voir que cela a une incidence sur le sujet. La plupart des exemples de sqlalchemy présentent des approches triviales telles que la configuration des métadonnées une fois, au démarrage de l'application, avec un ID utilisateur et un mot de passe de base de données génériques, qui est utilisé via l'application Web. Cela est généralement fait avec Metadata.bind = create_engine(), parfois même au niveau du module dans les fichiers du modèle de base de données. Ma question est, comment pouvons-nous retarder l'établissement des connexions jusqu'à ce que l'utilisateur s'est connecté, puis (bien sûr) réutiliser ces connexions, ou les rétablir en utilisant les mêmes informations d'identification, pour chaque demande suivante.

Nous avons ce travail - nous pensons - mais je ne suis pas seulement sûr de la sécurité de celui-ci, je pense aussi qu'il semble incroyablement lourd pour la situation. Dans la méthode __call__ de BaseController, nous récupérons l'ID utilisateur et le mot de passe de la session Web, appelons une fois sqlalchemy create_engine() pour chaque base de données, puis appelons une routine qui appelle Session.bind_mapper() plusieurs fois, une fois pour chaque table peut être référencé sur chacune de ces connexions, même si une requête donnée ne référence généralement qu'une ou deux tables. Il ressemble à ceci: les appels

# in lib/base.py on the BaseController class 
def __call__(self, environ, start_response): 

    # note: web session contains {'username': XXX, 'password': YYY} 
    url1 = 'postgres://%(username)s:%(password)[email protected]/finance' % session 
    url2 = 'postgres://%(username)s:%(password)[email protected]/staff' % session 

    finance = create_engine(url1) 
    staff = create_engine(url2) 
    db_configure(staff, finance) # see below 
    ... etc 

# in another file 

Session = scoped_session(sessionmaker()) 

def db_configure(staff, finance): 
    s = Session() 

    from db.finance import Employee, Customer, Invoice 
    for c in [ 
     Employee, 
     Customer, 
     Invoice, 
     ]: 
     s.bind_mapper(c, finance) 

    from db.staff import Project, Hour 
    for c in [ 
     Project, 
     Hour, 
     ]: 
     s.bind_mapper(c, staff) 

    s.close() # prevents leaking connections between sessions? 

Ainsi, le create_engine() se produisent à chaque demande ... Je peux voir que d'être nécessaire, et le pool de connexion les met en cache sans doute et fait sensiblement les choses.

Mais appeler Session.bind_mapper() une fois pour chaque table , sur chaque demande ? On dirait qu'il doit y avoir un meilleur moyen. De toute évidence, comme un désir de sécurité forte sous-tend tout cela, nous ne voulons pas qu'une connexion établie pour un utilisateur de haute sécurité soit utilisée par inadvertance dans une requête ultérieure par un utilisateur de faible sécurité.

Répondre

3

La liaison d'objets globaux (mappeurs, métadonnées) à une connexion spécifique à l'utilisateur n'est pas une bonne solution. Ainsi que d'utiliser la session étendue. Je suggère de créer une nouvelle session pour chaque requête et de la configurer pour utiliser des connexions spécifiques à l'utilisateur. L'exemple suivant suppose que vous utilisez des objets de métadonnées distincts pour chaque base de données:

binds = {} 

finance_engine = create_engine(url1) 
binds.update(dict.fromkeys(finance_metadata.sorted_tables, finance_engine)) 
# The following line is required when mappings to joint tables are used (e.g. 
# in joint table inheritance) due to bug (or misfeature) in SQLAlchemy 0.5.4. 
# This issue might be fixed in newer versions. 
binds.update(dict.fromkeys([Employee, Customer, Invoice], finance_engine)) 

staff_engine = create_engine(url2) 
binds.update(dict.fromkeys(staff_metadata.sorted_tables, staff_engine)) 
# See comment above. 
binds.update(dict.fromkeys([Project, Hour], staff_engine)) 

session = sessionmaker(binds=binds)() 
+0

@Denis, nous avons des métadonnées distinctes, une pour chacune des deux bases de données. Étant donné que, sont les deux binds.Appels de mise à jour() requis pour chaque base de données, comme dans votre exemple, ou pourrions-nous nous débrouiller avec seulement ceux qui utilisent xxx_metadata.sorted_tables? J'imagine que j'espère trouver quelque chose qui lie le lien aux métadonnées, mais à la demande ... J'aurais dû le mentionner. –

+0

Les métadonnées sont globales. Une liaison globale n'est toujours pas très bonne, mais elle n'entraîne pas de problèmes lorsque le moteur est constant. L'utilisation d'un moteur global variable nécessitera des hacks horribles similaires à 'threading.local()' qui sont très mauvais et sujettes aux erreurs. L'utilisation d'une session qui survit à une demande peut potentiellement entraîner la fuite d'objets d'un utilisateur vers un autre. Alors que vous pouvez écrire du code pour le protéger, il est préférable d'aller de l'avant sans avoir un tel trou. –

+0

Je ne veux pas dire mettre des metadata.bind, bien sûr, parce que vous dites que c'est global. Je veux dire en utilisant le fait que l'objet de métadonnées connaît déjà toutes les tables associées. Pourquoi devons-nous lister explicitement [Employee, Customer, Invoice] etc, au lieu de simplement dire (pseudo-code) "sessionmaker (binds = {finance_metadata: finance_engine, staff_metadata: staff_ending})". J'aurais pensé qu'il y aurait un certain soutien pour utiliser ce niveau d'indirection, plutôt que d'avoir à spécifier chaque table individuellement. –

-1

Je regarderais le regroupement de connexion et verrais si vous ne pouvez pas trouver un moyen d'avoir un pool par utilisateur. Vous pouvez dispose() le pool lorsque la session de l'utilisateur a expiré

Questions connexes