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 |
|