--- a/goa/gaesource.py Fri Sep 24 18:20:57 2010 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,331 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""Adapter for google appengine source.
-
-"""
-__docformat__ = "restructuredtext en"
-
-from cubicweb import AuthenticationError, UnknownEid
-from cubicweb.server.sources import AbstractSource, ConnectionWrapper
-from cubicweb.server.pool import SingleOperation
-from cubicweb.server.utils import crypt_password
-from cubicweb.goa.dbinit import set_user_groups
-from cubicweb.goa.rqlinterpreter import RQLInterpreter
-
-from google.appengine.api.datastore import Key, Entity, Put, Delete
-from google.appengine.api import datastore_errors, users
-
-def _init_groups(guser, euser):
- # set default groups
- if guser is None:
- groups = ['guests']
- else:
- groups = ['users']
- if users.is_current_user_admin():
- groups.append('managers')
- set_user_groups(euser, groups)
-
-def _clear_related_cache(session, gaesubject, rtype, gaeobject):
- subject, object = str(gaesubject.key()), str(gaeobject.key())
- for eid, role in ((subject, 'subject'), (object, 'object')):
- # clear related cache if necessary
- try:
- entity = session.entity_cache(eid)
- except KeyError:
- pass
- else:
- entity.cw_clear_relation_cache(rtype, role)
- if gaesubject.kind() == 'CWUser':
- for asession in session.repo._sessions.itervalues():
- if asession.user.eid == subject:
- asession.user.cw_clear_relation_cache(rtype, 'subject')
- if gaeobject.kind() == 'CWUser':
- for asession in session.repo._sessions.itervalues():
- if asession.user.eid == object:
- asession.user.cw_clear_relation_cache(rtype, 'object')
-
-def _mark_modified(session, gaeentity):
- modified = session.transaction_data.setdefault('modifiedentities', {})
- modified[str(gaeentity.key())] = gaeentity
- DatastorePutOp(session)
-
-def _rinfo(session, subject, rtype, object):
- gaesubj = session.datastore_get(subject)
- gaeobj = session.datastore_get(object)
- rschema = session.vreg.schema.rschema(rtype)
- cards = rschema.rproperty(gaesubj.kind(), gaeobj.kind(), 'cardinality')
- return gaesubj, gaeobj, cards
-
-def _radd(session, gaeentity, targetkey, relation, card):
- if card in '?1':
- gaeentity[relation] = targetkey
- else:
- try:
- related = gaeentity[relation]
- except KeyError:
- related = []
- else:
- if related is None:
- related = []
- related.append(targetkey)
- gaeentity[relation] = related
- _mark_modified(session, gaeentity)
-
-def _rdel(session, gaeentity, targetkey, relation, card):
- if card in '?1':
- gaeentity[relation] = None
- else:
- related = gaeentity[relation]
- if related is not None:
- related = [key for key in related if not key == targetkey]
- gaeentity[relation] = related or None
- _mark_modified(session, gaeentity)
-
-
-class DatastorePutOp(SingleOperation):
- """delayed put of entities to have less datastore write api calls
-
- * save all modified entities at precommit (should be the first operation
- processed, hence the 0 returned by insert_index())
-
- * in case others precommit operations modify some entities, resave modified
- entities at commit. This suppose that no db changes will occurs during
- commit event but it should be the case.
- """
- def insert_index(self):
- return 0
-
- def _put_entities(self):
- pending = self.session.transaction_data.get('pendingeids', ())
- modified = self.session.transaction_data.get('modifiedentities', {})
- for eid, gaeentity in modified.iteritems():
- assert not eid in pending
- Put(gaeentity)
- modified.clear()
-
- def commit_event(self):
- self._put_entities()
-
- def precommit_event(self):
- self._put_entities()
-
-
-class GAESource(AbstractSource):
- """adapter for a system source on top of google appengine datastore"""
-
- passwd_rql = "Any P WHERE X is CWUser, X login %(login)s, X upassword P"
- auth_rql = "Any X WHERE X is CWUser, X login %(login)s, X upassword %(pwd)s"
- _sols = ({'X': 'CWUser', 'P': 'Password'},)
-
- options = ()
-
- def __init__(self, repo, appschema, source_config, *args, **kwargs):
- AbstractSource.__init__(self, repo, appschema, source_config,
- *args, **kwargs)
- if repo.config['use-google-auth']:
- self.info('using google authentication service')
- self.authenticate = self.authenticate_gauth
- else:
- self.authenticate = self.authenticate_local
-
- def reset_caches(self):
- """method called during test to reset potential source caches"""
- pass
-
- def init_creating(self):
- pass
-
- def init(self):
- # XXX unregister unsupported hooks
- from cubicweb.server.hooks import sync_owner_after_add_composite_relation
- self.repo.hm.unregister_hook(sync_owner_after_add_composite_relation,
- 'after_add_relation', '')
-
- def get_connection(self):
- return ConnectionWrapper()
-
- # ISource interface #######################################################
-
- def compile_rql(self, rql):
- rqlst = self.repo.vreg.parse(rql)
- rqlst.restricted_vars = ()
- rqlst.children[0].solutions = self._sols
- return rqlst
-
- def set_schema(self, schema):
- """set the instance'schema"""
- self.interpreter = RQLInterpreter(schema)
- self.schema = schema
- if 'CWUser' in schema and not self.repo.config['use-google-auth']:
- # rql syntax trees used to authenticate users
- self._passwd_rqlst = self.compile_rql(self.passwd_rql)
- self._auth_rqlst = self.compile_rql(self.auth_rql)
-
- def support_entity(self, etype, write=False):
- """return true if the given entity's type is handled by this adapter
- if write is true, return true only if it's a RW support
- """
- return True
-
- def support_relation(self, rtype, write=False):
- """return true if the given relation's type is handled by this adapter
- if write is true, return true only if it's a RW support
- """
- return True
-
- def authenticate_gauth(self, session, login, password):
- guser = users.get_current_user()
- # allowing or not anonymous connection should be done in the app.yaml
- # file, suppose it's authorized if we are there
- if guser is None:
- login = u'anonymous'
- else:
- login = unicode(guser.nickname())
- # XXX http://code.google.com/appengine/docs/users/userobjects.html
- # use a reference property to automatically work with email address
- # changes after the propagation feature is implemented
- key = Key.from_path('CWUser', 'key_' + login, parent=None)
- try:
- euser = session.datastore_get(key)
- # XXX fix user. Required until we find a better way to fix broken records
- if not euser.get('s_in_group'):
- _init_groups(guser, euser)
- Put(euser)
- return str(key)
- except datastore_errors.EntityNotFoundError:
- # create a record for this user
- euser = Entity('CWUser', name='key_' + login)
- euser['s_login'] = login
- _init_groups(guser, euser)
- Put(euser)
- return str(euser.key())
-
- def authenticate_local(self, session, login, password):
- """return CWUser eid for the given login/password if this account is
- defined in this source, else raise `AuthenticationError`
-
- two queries are needed since passwords are stored crypted, so we have
- to fetch the salt first
- """
- args = {'login': login, 'pwd' : password}
- if password is not None:
- rset = self.syntax_tree_search(session, self._passwd_rqlst, args)
- try:
- pwd = rset[0][0]
- except IndexError:
- raise AuthenticationError('bad login')
- # passwords are stored using the bytea type, so we get a StringIO
- if pwd is not None:
- args['pwd'] = crypt_password(password, pwd[:2])
- # get eid from login and (crypted) password
- rset = self.syntax_tree_search(session, self._auth_rqlst, args)
- try:
- return rset[0][0]
- except IndexError:
- raise AuthenticationError('bad password')
-
- def syntax_tree_search(self, session, union, args=None, cachekey=None,
- varmap=None):
- """return result from this source for a rql query (actually from a rql
- syntax tree and a solution dictionary mapping each used variable to a
- possible type). If cachekey is given, the query necessary to fetch the
- results (but not the results themselves) may be cached using this key.
- """
- results, description = self.interpreter.interpret(union, args,
- session.datastore_get)
- return results # XXX description
-
- def flying_insert(self, table, session, union, args=None, varmap=None):
- raise NotImplementedError
-
- def add_entity(self, session, entity):
- """add a new entity to the source"""
- # do not delay add_entity as other modifications, new created entity
- # needs an eid
- entity.put()
-
- def update_entity(self, session, entity):
- """replace an entity in the source"""
- gaeentity = entity.to_gae_model()
- _mark_modified(session, entity.to_gae_model())
- if gaeentity.kind() == 'CWUser':
- for asession in self.repo._sessions.itervalues():
- if asession.user.eid == entity.eid:
- asession.user.update(dict(gaeentity))
-
- def delete_entity(self, session, entity):
- """delete an entity from the source"""
- # do not delay delete_entity as other modifications to ensure
- # consistency
- eid = entity.eid
- key = Key(eid)
- Delete(key)
- session.clear_datastore_cache(key)
- session.drop_entity_cache(eid)
- session.transaction_data.get('modifiedentities', {}).pop(eid, None)
-
- def add_relation(self, session, subject, rtype, object):
- """add a relation to the source"""
- gaesubj, gaeobj, cards = _rinfo(session, subject, rtype, object)
- _radd(session, gaesubj, gaeobj.key(), 's_' + rtype, cards[0])
- _radd(session, gaeobj, gaesubj.key(), 'o_' + rtype, cards[1])
- _clear_related_cache(session, gaesubj, rtype, gaeobj)
-
- def delete_relation(self, session, subject, rtype, object):
- """delete a relation from the source"""
- gaesubj, gaeobj, cards = _rinfo(session, subject, rtype, object)
- pending = session.transaction_data.setdefault('pendingeids', set())
- if not subject in pending:
- _rdel(session, gaesubj, gaeobj.key(), 's_' + rtype, cards[0])
- if not object in pending:
- _rdel(session, gaeobj, gaesubj.key(), 'o_' + rtype, cards[1])
- _clear_related_cache(session, gaesubj, rtype, gaeobj)
-
- # system source interface #################################################
-
- def eid_type_source(self, session, eid):
- """return a tuple (type, source, extid) for the entity with id <eid>"""
- try:
- key = Key(eid)
- except datastore_errors.BadKeyError:
- raise UnknownEid(eid)
- return key.kind(), 'system', None
-
- def create_eid(self, session):
- return None # let the datastore generating key
-
- def add_info(self, session, entity, source, extid=None):
- """add type and source info for an eid into the system table"""
- pass
-
- def delete_info(self, session, eid, etype, uri, extid):
- """delete system information on deletion of an entity by transfering
- record from the entities table to the deleted_entities table
- """
- pass
-
- def fti_unindex_entity(self, session, eid):
- """remove text content for entity with the given eid from the full text
- index
- """
- pass
-
- def fti_index_entity(self, session, entity):
- """add text content of a created/modified entity to the full text index
- """
- pass