goa/gaesource.py
branchstable
changeset 6340 470d8e828fda
parent 6339 bdc3dc94d744
child 6341 ad5e08981153
equal deleted inserted replaced
6339:bdc3dc94d744 6340:470d8e828fda
     1 # copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     3 #
       
     4 # This file is part of CubicWeb.
       
     5 #
       
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
       
     7 # terms of the GNU Lesser General Public License as published by the Free
       
     8 # Software Foundation, either version 2.1 of the License, or (at your option)
       
     9 # any later version.
       
    10 #
       
    11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT
       
    12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
       
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
       
    14 # details.
       
    15 #
       
    16 # You should have received a copy of the GNU Lesser General Public License along
       
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
       
    18 """Adapter for google appengine source.
       
    19 
       
    20 """
       
    21 __docformat__ = "restructuredtext en"
       
    22 
       
    23 from cubicweb import AuthenticationError, UnknownEid
       
    24 from cubicweb.server.sources import AbstractSource, ConnectionWrapper
       
    25 from cubicweb.server.pool import SingleOperation
       
    26 from cubicweb.server.utils import crypt_password
       
    27 from cubicweb.goa.dbinit import set_user_groups
       
    28 from cubicweb.goa.rqlinterpreter import RQLInterpreter
       
    29 
       
    30 from google.appengine.api.datastore import Key, Entity, Put, Delete
       
    31 from google.appengine.api import datastore_errors, users
       
    32 
       
    33 def _init_groups(guser, euser):
       
    34     # set default groups
       
    35     if guser is None:
       
    36         groups = ['guests']
       
    37     else:
       
    38         groups = ['users']
       
    39         if users.is_current_user_admin():
       
    40             groups.append('managers')
       
    41     set_user_groups(euser, groups)
       
    42 
       
    43 def _clear_related_cache(session, gaesubject, rtype, gaeobject):
       
    44     subject, object = str(gaesubject.key()), str(gaeobject.key())
       
    45     for eid, role in ((subject, 'subject'), (object, 'object')):
       
    46         # clear related cache if necessary
       
    47         try:
       
    48             entity = session.entity_cache(eid)
       
    49         except KeyError:
       
    50             pass
       
    51         else:
       
    52             entity.cw_clear_relation_cache(rtype, role)
       
    53     if gaesubject.kind() == 'CWUser':
       
    54         for asession in session.repo._sessions.itervalues():
       
    55             if asession.user.eid == subject:
       
    56                 asession.user.cw_clear_relation_cache(rtype, 'subject')
       
    57     if gaeobject.kind() == 'CWUser':
       
    58         for asession in session.repo._sessions.itervalues():
       
    59             if asession.user.eid == object:
       
    60                 asession.user.cw_clear_relation_cache(rtype, 'object')
       
    61 
       
    62 def _mark_modified(session, gaeentity):
       
    63     modified = session.transaction_data.setdefault('modifiedentities', {})
       
    64     modified[str(gaeentity.key())] = gaeentity
       
    65     DatastorePutOp(session)
       
    66 
       
    67 def _rinfo(session, subject, rtype, object):
       
    68     gaesubj = session.datastore_get(subject)
       
    69     gaeobj = session.datastore_get(object)
       
    70     rschema = session.vreg.schema.rschema(rtype)
       
    71     cards = rschema.rproperty(gaesubj.kind(), gaeobj.kind(), 'cardinality')
       
    72     return gaesubj, gaeobj, cards
       
    73 
       
    74 def _radd(session, gaeentity, targetkey, relation, card):
       
    75     if card in '?1':
       
    76         gaeentity[relation] = targetkey
       
    77     else:
       
    78         try:
       
    79             related = gaeentity[relation]
       
    80         except KeyError:
       
    81             related = []
       
    82         else:
       
    83             if related is None:
       
    84                 related = []
       
    85         related.append(targetkey)
       
    86         gaeentity[relation] = related
       
    87     _mark_modified(session, gaeentity)
       
    88 
       
    89 def _rdel(session, gaeentity, targetkey, relation, card):
       
    90     if card in '?1':
       
    91         gaeentity[relation] = None
       
    92     else:
       
    93         related = gaeentity[relation]
       
    94         if related is not None:
       
    95             related = [key for key in related if not key == targetkey]
       
    96             gaeentity[relation] = related or None
       
    97     _mark_modified(session, gaeentity)
       
    98 
       
    99 
       
   100 class DatastorePutOp(SingleOperation):
       
   101     """delayed put of entities to have less datastore write api calls
       
   102 
       
   103     * save all modified entities at precommit (should be the first operation
       
   104       processed, hence the 0 returned by insert_index())
       
   105 
       
   106     * in case others precommit operations modify some entities, resave modified
       
   107       entities at commit. This suppose that no db changes will occurs during
       
   108       commit event but it should be the case.
       
   109     """
       
   110     def insert_index(self):
       
   111         return 0
       
   112 
       
   113     def _put_entities(self):
       
   114         pending = self.session.transaction_data.get('pendingeids', ())
       
   115         modified = self.session.transaction_data.get('modifiedentities', {})
       
   116         for eid, gaeentity in modified.iteritems():
       
   117             assert not eid in pending
       
   118             Put(gaeentity)
       
   119         modified.clear()
       
   120 
       
   121     def commit_event(self):
       
   122         self._put_entities()
       
   123 
       
   124     def precommit_event(self):
       
   125         self._put_entities()
       
   126 
       
   127 
       
   128 class GAESource(AbstractSource):
       
   129     """adapter for a system source on top of google appengine datastore"""
       
   130 
       
   131     passwd_rql = "Any P WHERE X is CWUser, X login %(login)s, X upassword P"
       
   132     auth_rql = "Any X WHERE X is CWUser, X login %(login)s, X upassword %(pwd)s"
       
   133     _sols = ({'X': 'CWUser', 'P': 'Password'},)
       
   134 
       
   135     options = ()
       
   136 
       
   137     def __init__(self, repo, appschema, source_config, *args, **kwargs):
       
   138         AbstractSource.__init__(self, repo, appschema, source_config,
       
   139                                 *args, **kwargs)
       
   140         if repo.config['use-google-auth']:
       
   141             self.info('using google authentication service')
       
   142             self.authenticate = self.authenticate_gauth
       
   143         else:
       
   144             self.authenticate = self.authenticate_local
       
   145 
       
   146     def reset_caches(self):
       
   147         """method called during test to reset potential source caches"""
       
   148         pass
       
   149 
       
   150     def init_creating(self):
       
   151         pass
       
   152 
       
   153     def init(self):
       
   154         # XXX unregister unsupported hooks
       
   155         from cubicweb.server.hooks import sync_owner_after_add_composite_relation
       
   156         self.repo.hm.unregister_hook(sync_owner_after_add_composite_relation,
       
   157                                      'after_add_relation', '')
       
   158 
       
   159     def get_connection(self):
       
   160         return ConnectionWrapper()
       
   161 
       
   162     # ISource interface #######################################################
       
   163 
       
   164     def compile_rql(self, rql):
       
   165         rqlst = self.repo.vreg.parse(rql)
       
   166         rqlst.restricted_vars = ()
       
   167         rqlst.children[0].solutions = self._sols
       
   168         return rqlst
       
   169 
       
   170     def set_schema(self, schema):
       
   171         """set the instance'schema"""
       
   172         self.interpreter = RQLInterpreter(schema)
       
   173         self.schema = schema
       
   174         if 'CWUser' in schema and not self.repo.config['use-google-auth']:
       
   175             # rql syntax trees used to authenticate users
       
   176             self._passwd_rqlst = self.compile_rql(self.passwd_rql)
       
   177             self._auth_rqlst = self.compile_rql(self.auth_rql)
       
   178 
       
   179     def support_entity(self, etype, write=False):
       
   180         """return true if the given entity's type is handled by this adapter
       
   181         if write is true, return true only if it's a RW support
       
   182         """
       
   183         return True
       
   184 
       
   185     def support_relation(self, rtype, write=False):
       
   186         """return true if the given relation's type is handled by this adapter
       
   187         if write is true, return true only if it's a RW support
       
   188         """
       
   189         return True
       
   190 
       
   191     def authenticate_gauth(self, session, login, password):
       
   192         guser = users.get_current_user()
       
   193         # allowing or not anonymous connection should be done in the app.yaml
       
   194         # file, suppose it's authorized if we are there
       
   195         if guser is None:
       
   196             login = u'anonymous'
       
   197         else:
       
   198             login = unicode(guser.nickname())
       
   199         # XXX http://code.google.com/appengine/docs/users/userobjects.html
       
   200         # use a reference property to automatically work with email address
       
   201         # changes after the propagation feature is implemented
       
   202         key = Key.from_path('CWUser', 'key_' + login, parent=None)
       
   203         try:
       
   204             euser = session.datastore_get(key)
       
   205             # XXX fix user. Required until we find a better way to fix broken records
       
   206             if not euser.get('s_in_group'):
       
   207                 _init_groups(guser, euser)
       
   208                 Put(euser)
       
   209             return str(key)
       
   210         except datastore_errors.EntityNotFoundError:
       
   211             # create a record for this user
       
   212             euser = Entity('CWUser', name='key_' + login)
       
   213             euser['s_login'] = login
       
   214             _init_groups(guser, euser)
       
   215             Put(euser)
       
   216             return str(euser.key())
       
   217 
       
   218     def authenticate_local(self, session, login, password):
       
   219         """return CWUser eid for the given login/password if this account is
       
   220         defined in this source, else raise `AuthenticationError`
       
   221 
       
   222         two queries are needed since passwords are stored crypted, so we have
       
   223         to fetch the salt first
       
   224         """
       
   225         args = {'login': login, 'pwd' : password}
       
   226         if password is not None:
       
   227             rset = self.syntax_tree_search(session, self._passwd_rqlst, args)
       
   228             try:
       
   229                 pwd = rset[0][0]
       
   230             except IndexError:
       
   231                 raise AuthenticationError('bad login')
       
   232             # passwords are stored using the bytea type, so we get a StringIO
       
   233             if pwd is not None:
       
   234                 args['pwd'] = crypt_password(password, pwd[:2])
       
   235         # get eid from login and (crypted) password
       
   236         rset = self.syntax_tree_search(session, self._auth_rqlst, args)
       
   237         try:
       
   238             return rset[0][0]
       
   239         except IndexError:
       
   240             raise AuthenticationError('bad password')
       
   241 
       
   242     def syntax_tree_search(self, session, union, args=None, cachekey=None,
       
   243                            varmap=None):
       
   244         """return result from this source for a rql query (actually from a rql
       
   245         syntax tree and a solution dictionary mapping each used variable to a
       
   246         possible type). If cachekey is given, the query necessary to fetch the
       
   247         results (but not the results themselves) may be cached using this key.
       
   248         """
       
   249         results, description = self.interpreter.interpret(union, args,
       
   250                                                           session.datastore_get)
       
   251         return results # XXX description
       
   252 
       
   253     def flying_insert(self, table, session, union, args=None, varmap=None):
       
   254         raise NotImplementedError
       
   255 
       
   256     def add_entity(self, session, entity):
       
   257         """add a new entity to the source"""
       
   258         # do not delay add_entity as other modifications, new created entity
       
   259         # needs an eid
       
   260         entity.put()
       
   261 
       
   262     def update_entity(self, session, entity):
       
   263         """replace an entity in the source"""
       
   264         gaeentity = entity.to_gae_model()
       
   265         _mark_modified(session, entity.to_gae_model())
       
   266         if gaeentity.kind() == 'CWUser':
       
   267             for asession in self.repo._sessions.itervalues():
       
   268                 if asession.user.eid == entity.eid:
       
   269                     asession.user.update(dict(gaeentity))
       
   270 
       
   271     def delete_entity(self, session, entity):
       
   272         """delete an entity from the source"""
       
   273         # do not delay delete_entity as other modifications to ensure
       
   274         # consistency
       
   275         eid = entity.eid
       
   276         key = Key(eid)
       
   277         Delete(key)
       
   278         session.clear_datastore_cache(key)
       
   279         session.drop_entity_cache(eid)
       
   280         session.transaction_data.get('modifiedentities', {}).pop(eid, None)
       
   281 
       
   282     def add_relation(self, session, subject, rtype, object):
       
   283         """add a relation to the source"""
       
   284         gaesubj, gaeobj, cards = _rinfo(session, subject, rtype, object)
       
   285         _radd(session, gaesubj, gaeobj.key(), 's_' + rtype, cards[0])
       
   286         _radd(session, gaeobj, gaesubj.key(), 'o_' + rtype, cards[1])
       
   287         _clear_related_cache(session, gaesubj, rtype, gaeobj)
       
   288 
       
   289     def delete_relation(self, session, subject, rtype, object):
       
   290         """delete a relation from the source"""
       
   291         gaesubj, gaeobj, cards = _rinfo(session, subject, rtype, object)
       
   292         pending = session.transaction_data.setdefault('pendingeids', set())
       
   293         if not subject in pending:
       
   294             _rdel(session, gaesubj, gaeobj.key(), 's_' + rtype, cards[0])
       
   295         if not object in pending:
       
   296             _rdel(session, gaeobj, gaesubj.key(), 'o_' + rtype, cards[1])
       
   297         _clear_related_cache(session, gaesubj, rtype, gaeobj)
       
   298 
       
   299     # system source interface #################################################
       
   300 
       
   301     def eid_type_source(self, session, eid):
       
   302         """return a tuple (type, source, extid) for the entity with id <eid>"""
       
   303         try:
       
   304             key = Key(eid)
       
   305         except datastore_errors.BadKeyError:
       
   306             raise UnknownEid(eid)
       
   307         return key.kind(), 'system', None
       
   308 
       
   309     def create_eid(self, session):
       
   310         return None # let the datastore generating key
       
   311 
       
   312     def add_info(self, session, entity, source, extid=None):
       
   313         """add type and source info for an eid into the system table"""
       
   314         pass
       
   315 
       
   316     def delete_info(self, session, eid, etype, uri, extid):
       
   317         """delete system information on deletion of an entity by transfering
       
   318         record from the entities table to the deleted_entities table
       
   319         """
       
   320         pass
       
   321 
       
   322     def fti_unindex_entity(self, session, eid):
       
   323         """remove text content for entity with the given eid from the full text
       
   324         index
       
   325         """
       
   326         pass
       
   327 
       
   328     def fti_index_entity(self, session, entity):
       
   329         """add text content of a created/modified entity to the full text index
       
   330         """
       
   331         pass