1 .. -*- coding: utf-8 -*- |
|
2 |
|
3 Sessions |
|
4 ======== |
|
5 |
|
6 Sessions are objects linked to an authenticated user. The `Session.new_cnx` |
|
7 method returns a new Connection linked to that session. |
|
8 |
|
9 Connections |
|
10 =========== |
|
11 |
|
12 Connections provide the `.execute` method to query the data sources, along with |
|
13 `.commit` and `.rollback` methods for transaction management. |
|
14 |
|
15 Kinds of connections |
|
16 -------------------- |
|
17 |
|
18 There are two kinds of connections. |
|
19 |
|
20 * `normal connections` are the most common: they are related to users and |
|
21 carry security checks coming with user credentials |
|
22 |
|
23 * `internal connections` have all the powers; they are also used in only a |
|
24 few situations where you don't already have an adequate session at |
|
25 hand, like: user authentication, data synchronisation in |
|
26 multi-source contexts |
|
27 |
|
28 Normal connections are typically named `_cw` in most appobjects or |
|
29 sometimes just `session`. |
|
30 |
|
31 Internal connections are available from the `Repository` object and are |
|
32 to be used like this: |
|
33 |
|
34 .. sourcecode:: python |
|
35 |
|
36 with self.repo.internal_cnx() as cnx: |
|
37 do_stuff_with(cnx) |
|
38 cnx.commit() |
|
39 |
|
40 Connections should always be used as context managers, to avoid leaks. |
|
41 |
|
42 |
|
43 Python/RQL API |
|
44 ~~~~~~~~~~~~~~ |
|
45 |
|
46 The Python API developped to interface with RQL is inspired from the standard db-api, |
|
47 but since `execute` returns its results directly, there is no `cursor` concept. |
|
48 |
|
49 .. sourcecode:: python |
|
50 |
|
51 execute(rqlstring, args=None, build_descr=True) |
|
52 |
|
53 :rqlstring: the RQL query to execute (unicode) |
|
54 :args: if the query contains substitutions, a dictionary containing the values to use |
|
55 |
|
56 The `Connection` object owns the methods `commit` and `rollback`. You |
|
57 *should never need to use them* during the development of the web |
|
58 interface based on the *CubicWeb* framework as it determines the end |
|
59 of the transaction depending on the query execution success. They are |
|
60 however useful in other contexts such as tests or custom controllers. |
|
61 |
|
62 .. note:: |
|
63 |
|
64 If a query generates an error related to security (:exc:`Unauthorized`) or to |
|
65 integrity (:exc:`ValidationError`), the transaction can still continue but you |
|
66 won't be able to commit it, a rollback will be necessary to start a new |
|
67 transaction. |
|
68 |
|
69 Also, a rollback is automatically done if an error occurs during commit. |
|
70 |
|
71 .. note:: |
|
72 |
|
73 A :exc:`ValidationError` has a `entity` attribute. In CubicWeb, |
|
74 this atttribute is set to the entity's eid (not a reference to the |
|
75 entity itself). |
|
76 |
|
77 Executing RQL queries from a view or a hook |
|
78 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
79 |
|
80 When you're within code of the web interface, the Connection is handled by the |
|
81 request object. You should not have to access it directly, but use the |
|
82 `execute` method directly available on the request, eg: |
|
83 |
|
84 .. sourcecode:: python |
|
85 |
|
86 rset = self._cw.execute(rqlstring, kwargs) |
|
87 |
|
88 Similarly, on the server side (eg in hooks), there is no request object (since |
|
89 you're directly inside the data-server), so you'll have to use the execute method |
|
90 of the Connection object. |
|
91 |
|
92 Proper usage of `.execute` |
|
93 ~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
94 |
|
95 Let's say you want to get T which is in configuration C, this translates to: |
|
96 |
|
97 .. sourcecode:: python |
|
98 |
|
99 self._cw.execute('Any T WHERE T in_conf C, C eid %s' % entity.eid) |
|
100 |
|
101 But it must be written in a syntax that will benefit from the use |
|
102 of a cache on the RQL server side: |
|
103 |
|
104 .. sourcecode:: python |
|
105 |
|
106 self._cw.execute('Any T WHERE T in_conf C, C eid %(x)s', {'x': entity.eid}) |
|
107 |
|
108 The syntax tree is built once for the "generic" RQL and can be re-used |
|
109 with a number of different eids. The rql IN operator is an exception |
|
110 to this rule. |
|
111 |
|
112 .. sourcecode:: python |
|
113 |
|
114 self._cw.execute('Any T WHERE T in_conf C, C name IN (%s)' |
|
115 % ','.join(['foo', 'bar'])) |
|
116 |
|
117 Alternatively, some of the common data related to an entity can be |
|
118 obtained from the `entity.related()` method (which is used under the |
|
119 hood by the ORM when you use attribute access notation on an entity to |
|
120 get a relation. The initial request would then be translated to: |
|
121 |
|
122 .. sourcecode:: python |
|
123 |
|
124 entity.related('in_conf', 'object') |
|
125 |
|
126 Additionally this benefits from the fetch_attrs policy (see :ref:`FetchAttrs`) |
|
127 optionally defined on the class element, which says which attributes must be |
|
128 also loaded when the entity is loaded through the ORM. |
|
129 |
|
130 .. _resultset: |
|
131 |
|
132 The `ResultSet` API |
|
133 ~~~~~~~~~~~~~~~~~~~ |
|
134 |
|
135 ResultSet instances are a very commonly manipulated object. They have |
|
136 a rich API as seen below, but we would like to highlight a bunch of |
|
137 methods that are quite useful in day-to-day practice: |
|
138 |
|
139 * `__str__()` (applied by `print`) gives a very useful overview of both |
|
140 the underlying RQL expression and the data inside; unavoidable for |
|
141 debugging purposes |
|
142 |
|
143 * `printable_rql()` returns a well formed RQL expression as a |
|
144 string; it is very useful to build views |
|
145 |
|
146 * `entities()` returns a generator on all entities of the result set |
|
147 |
|
148 * `get_entity(row, col)` gets the entity at row, col coordinates; one |
|
149 of the most used result set methods |
|
150 |
|
151 .. autoclass:: cubicweb.rset.ResultSet |
|
152 :members: |
|
153 |
|
154 |
|
155 Authentication and management of sessions |
|
156 ----------------------------------------- |
|
157 |
|
158 The authentication process is a ballet involving a few dancers: |
|
159 |
|
160 * through its `get_session` method the top-level application object (the |
|
161 `CubicWebPublisher`) will open a session whenever a web request |
|
162 comes in; it asks the `session manager` to open a session (giving |
|
163 the web request object as context) using `open_session` |
|
164 |
|
165 * the session manager asks its authentication manager (which is a |
|
166 `component`) to authenticate the request (using `authenticate`) |
|
167 |
|
168 * the authentication manager asks, in order, to its authentication |
|
169 information retrievers, a login and an opaque object containing |
|
170 other credentials elements (calling `authentication_information`), |
|
171 giving the request object each time |
|
172 |
|
173 * the default retriever (named `LoginPasswordRetriever`) |
|
174 will in turn defer login and password fetching to the request |
|
175 object (which, depending on the authentication mode (`cookie` |
|
176 or `http`), will do the appropriate things and return a login |
|
177 and a password) |
|
178 |
|
179 * the authentication manager, on success, asks the `Repository` |
|
180 object to connect with the found credentials (using `connect`) |
|
181 |
|
182 * the repository object asks authentication to all of its |
|
183 sources which support the `CWUser` entity with the given |
|
184 credentials; when successful it can build the cwuser entity, |
|
185 from which a regular `Session` object is made; it returns the |
|
186 session id |
|
187 |
|
188 * the source in turn will delegate work to an authentifier |
|
189 class that defines the ultimate `authenticate` method (for |
|
190 instance the native source will query the database against |
|
191 the provided credentials) |
|
192 |
|
193 * the authentication manager, on success, will call back _all_ |
|
194 retrievers with `authenticated` and return its authentication |
|
195 data (on failure, it will try the anonymous login or, if the |
|
196 configuration forbids it, raise an `AuthenticationError`) |
|
197 |
|
198 Writing authentication plugins |
|
199 ------------------------------ |
|
200 |
|
201 Sometimes CubicWeb's out-of-the-box authentication schemes (cookie and |
|
202 http) are not sufficient. Nowadays there is a plethora of such schemes |
|
203 and the framework cannot provide them all, but as the sequence above |
|
204 shows, it is extensible. |
|
205 |
|
206 Two levels have to be considered when writing an authentication |
|
207 plugin: the web client and the repository. |
|
208 |
|
209 We invented a scenario where it makes sense to have a new plugin in |
|
210 each side: some middleware will do pre-authentication and under the |
|
211 right circumstances add a new HTTP `x-foo-user` header to the query |
|
212 before it reaches the CubicWeb instance. For a concrete example of |
|
213 this, see the `trustedauth`_ cube. |
|
214 |
|
215 .. _`trustedauth`: http://www.cubicweb.org/project/cubicweb-trustedauth |
|
216 |
|
217 Repository authentication plugins |
|
218 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
219 |
|
220 On the repository side, it is possible to register a source |
|
221 authentifier using the following kind of code: |
|
222 |
|
223 .. sourcecode:: python |
|
224 |
|
225 from cubicweb.server.sources import native |
|
226 |
|
227 class FooAuthentifier(native.LoginPasswordAuthentifier): |
|
228 """ a source authentifier plugin |
|
229 if 'foo' in authentication information, no need to check |
|
230 password |
|
231 """ |
|
232 auth_rql = 'Any X WHERE X is CWUser, X login %(login)s' |
|
233 |
|
234 def authenticate(self, session, login, **kwargs): |
|
235 """return CWUser eid for the given login |
|
236 if this account is defined in this source, |
|
237 else raise `AuthenticationError` |
|
238 """ |
|
239 session.debug('authentication by %s', self.__class__.__name__) |
|
240 if 'foo' not in kwargs: |
|
241 return super(FooAuthentifier, self).authenticate(session, login, **kwargs) |
|
242 try: |
|
243 rset = session.execute(self.auth_rql, {'login': login}) |
|
244 return rset[0][0] |
|
245 except Exception, exc: |
|
246 session.debug('authentication failure (%s)', exc) |
|
247 raise AuthenticationError('foo user is unknown to us') |
|
248 |
|
249 Since repository authentifiers are not appobjects, we have to register |
|
250 them through a `server_startup` hook. |
|
251 |
|
252 .. sourcecode:: python |
|
253 |
|
254 class ServerStartupHook(hook.Hook): |
|
255 """ register the foo authenticator """ |
|
256 __regid__ = 'fooauthenticatorregisterer' |
|
257 events = ('server_startup',) |
|
258 |
|
259 def __call__(self): |
|
260 self.debug('registering foo authentifier') |
|
261 self.repo.system_source.add_authentifier(FooAuthentifier()) |
|
262 |
|
263 Web authentication plugins |
|
264 ~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
265 |
|
266 .. sourcecode:: python |
|
267 |
|
268 class XFooUserRetriever(authentication.LoginPasswordRetriever): |
|
269 """ authenticate by the x-foo-user http header |
|
270 or just do normal login/password authentication |
|
271 """ |
|
272 __regid__ = 'x-foo-user' |
|
273 order = 0 |
|
274 |
|
275 def authentication_information(self, req): |
|
276 """retrieve authentication information from the given request, raise |
|
277 NoAuthInfo if expected information is not found |
|
278 """ |
|
279 self.debug('web authenticator building auth info') |
|
280 try: |
|
281 login = req.get_header('x-foo-user') |
|
282 if login: |
|
283 return login, {'foo': True} |
|
284 else: |
|
285 return super(XFooUserRetriever, self).authentication_information(self, req) |
|
286 except Exception, exc: |
|
287 self.debug('web authenticator failed (%s)', exc) |
|
288 raise authentication.NoAuthInfo() |
|
289 |
|
290 def authenticated(self, retriever, req, cnx, login, authinfo): |
|
291 """callback when return authentication information have opened a |
|
292 repository connection successfully. Take care req has no session |
|
293 attached yet, hence req.execute isn't available. |
|
294 |
|
295 Here we set a flag on the request to indicate that the user is |
|
296 foo-authenticated. Can be used by a selector |
|
297 """ |
|
298 self.debug('web authenticator running post authentication callback') |
|
299 cnx.foo_user = authinfo.get('foo') |
|
300 |
|
301 In the `authenticated` method we add (in an admitedly slightly hackish |
|
302 way) an attribute to the connection object. This, in turn, can be used |
|
303 to build a selector dispatching on the fact that the user was |
|
304 preauthenticated or not. |
|
305 |
|
306 .. sourcecode:: python |
|
307 |
|
308 @objectify_selector |
|
309 def foo_authenticated(cls, req, rset=None, **kwargs): |
|
310 if hasattr(req.cnx, 'foo_user') and req.foo_user: |
|
311 return 1 |
|
312 return 0 |
|
313 |
|
314 Full Session and Connection API |
|
315 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
316 |
|
317 .. autoclass:: cubicweb.server.session.Session |
|
318 .. autoclass:: cubicweb.server.session.Connection |
|