doc/book/devrepo/repo/sessions.rst
changeset 10491 c67bcee93248
parent 10333 569324f890d7
child 10496 e95b559a06a2
equal deleted inserted replaced
10490:76ab3c71aff2 10491:c67bcee93248
       
     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