summaryrefslogtreecommitdiffstats
path: root/ipalib/session.py
diff options
context:
space:
mode:
Diffstat (limited to 'ipalib/session.py')
-rw-r--r--ipalib/session.py1098
1 files changed, 1098 insertions, 0 deletions
diff --git a/ipalib/session.py b/ipalib/session.py
new file mode 100644
index 000000000..a58643983
--- /dev/null
+++ b/ipalib/session.py
@@ -0,0 +1,1098 @@
+# Authors: John Dennis <jdennis@redhat.com>
+#
+# Copyright (C) 2011 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import memcache
+import Cookie
+import random
+import errors
+import os
+import re
+import time
+from text import _
+from ipapython.ipa_log_manager import *
+from ipalib.krb_utils import *
+
+__doc__ = '''
+Session Support for IPA
+John Dennis <jdennis@redhat.com>
+
+Goals
+=====
+
+Provide per-user session data caching which persists between
+requests. Desired features are:
+
+* Integrates cleanly with minimum impact on existing infrastructure.
+
+* Provides maximum security balanced against real-world performance
+ demands.
+
+* Sessions must be able to be revoked (flushed).
+
+* Should be flexible and easy to use for developers.
+
+* Should leverage existing technology and code to the maximum extent
+ possible to avoid re-invention, excessive implementation time and to
+ benefit from robustness in field proven components commonly shared
+ in the open source community.
+
+* Must support multiple independent processes which share session
+ data.
+
+* System must function correctly if session data is available or not.
+
+* Must be high performance.
+
+* Should not be tied to specific web servers or browsers. Should
+ integrate with our chosen WSGI model.
+
+Issues
+======
+
+Cookies
+-------
+
+Most session implementations are based on the use of cookies. Cookies
+have some inherent problems.
+
+* User has the option to disable cookies.
+
+* User stored cookie data is not secure. Can be mitigated by setting
+ flags indicating the cookie is only to be used with SSL secured HTTP
+ connections to specific web resources and setting the cookie to
+ expire at session termination. Most modern browsers enforce these.
+
+Where to store session data?
+----------------------------
+
+Session data may be stored on either on the client or on the
+server. Storing session data on the client addresses the problem of
+session data availability when requests are serviced by independent web
+servers because the session data travels with the request. However
+there are data size limitations. Storing session data on the client
+also exposes sensitive data but this can be mitigated by encrypting
+the session data such that only the server can decrypt it.
+
+The more conventional approach is to bind session data to a unique
+name, the session ID. The session ID is transmitted to the client and
+the session data is paired with the session ID on the server in a
+associative data store. The session data is retrieved by the server
+using the session ID when the receiving the request. This eliminates
+exposing sensitive session data on the client along with limitations
+on data size. It however introduces the issue of session data
+availability when requests are serviced by more than one server
+process.
+
+Multi-process session data availability
+---------------------------------------
+
+Apache (and other web servers) fork child processes to handle requests
+in parallel. Also web servers may be deployed in a farm where requests
+are load balanced in round robin fashion across different nodes. In
+both cases session data cannot be stored in the memory of a server
+process because it is not available to other processes, either sibling
+children of a master server process or server processes on distinct
+nodes.
+
+Typically this is addressed by storing session data in a SQL
+database. When a request is received by a server process containing a
+session ID in it's cookie data the session ID is used to perform a SQL
+query and the resulting data is then attached to the request as it
+proceeds through the request processing pipeline. This of course
+introduces coherency issues.
+
+For IPA the introduction of a SQL database dependency is undesired and
+should be avoided.
+
+Session data may also be shared by independent processes by storing
+the session data in files.
+
+An alternative solution which has gained considerable popularity
+recently is the use of a fast memory based caching server. Data is
+stored in a single process memory and may be queried and set via a
+light weight protocol using standard socket mechanisms, memcached is
+one example. A typical use is to optimize SQL queries by storing a SQL
+result in shared memory cache avoiding the more expensive SQL
+operation. But the memory cache has distinct advantages in non-SQL
+situations as well.
+
+Possible implementations for use by IPA
+=======================================
+
+Apache Sessions
+---------------
+
+Apache has 2.3 has implemented session support via these modules:
+
+ mod_session
+ Overarching session support based on cookies.
+
+ See: http://httpd.apache.org/docs/2.3/mod/mod_session.html
+
+ mod_session_cookie
+ Stores session data in the client.
+
+ See: http://httpd.apache.org/docs/2.3/mod/mod_session_cookie.html
+
+ mod_session_crypto
+ Encrypts session data for security. Encryption key is shared
+ configuration parameter visible to all Apache processes and is
+ stored in a configuration file.
+
+ See: http://httpd.apache.org/docs/2.3/mod/mod_session_crypto.html
+
+ mod_session_dbd
+ Stores session data in a SQL database permitting multiple
+ processes to access and share the same session data.
+
+ See: http://httpd.apache.org/docs/2.3/mod/mod_session_dbd.html
+
+Issues with Apache sessions
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Although Apache has implemented generic session support and Apache is
+our web server of preference it nonetheless introduces issues for IPA.
+
+ * Session support is only available in httpd >= 2.3 which at the
+ time of this writing is currently only available as a Beta release
+ from upstream. We currently only ship httpd 2.2, the same is true
+ for other distributions.
+
+ * We could package and ship the sessions modules as a temporary
+ package in httpd 2.2 environments. But this has the following
+ consequences:
+
+ - The code has to be backported. the module API has changed
+ slightly between httpd 2.2 and 2.3. The backporting is not
+ terribly difficult and a proof of concept has been
+ implemented.
+
+ - We would then be on the hook to package and maintain a special
+ case Apache package. This is maintenance burden as well as a
+ distribution packaging burden. Both of which would be best
+ avoided if possible.
+
+ * The design of the Apache session modules is such that they can
+ only be manipulated by other Apache modules. The ability of
+ consumers of the session data to control the session data is
+ simplistic, constrained and static during the period the request
+ is processed. Request handlers which are not native Apache modules
+ (e.g. IPA via WSGI) can only examine the session data
+ via request headers and reset it in response headers.
+
+ * Shared session data is available exclusively via SQL.
+
+However using the 2.3 Apache session modules would give us robust
+session support implemented in C based on standardized Apache
+interfaces which are widely used.
+
+Python Web Frameworks
+---------------------
+
+Virtually every Python web framework supports cookie based sessions,
+e.g. Django, Twisted, Zope, Turbogears etc. Early on in IPA we decided
+to avoid the use of these frameworks. Trying to pull in just one part
+of these frameworks just to get session support would be problematic
+because the code does not function outside it's framework.
+
+IPA implemented sessions
+------------------------
+
+Originally it was believed the path of least effort was to utilize
+existing session support, most likely what would be provided by
+Apache. However there are enough basic modular components available in
+native Python and other standard packages it should be possible to
+provide session support meeting the aforementioned goals with a modest
+implementation effort. Because we're leveraging existing components
+the implementation difficulties are subsumed by other components which
+have already been field proven and have community support. This is a
+smart strategy.
+
+Proposed Solution
+=================
+
+Our interface to the web server is via WSGI which invokes a callback
+per request passing us an environmental context for the request. For
+this discussion we'll name the the WSGI callback "application()", a
+conventional name in WSGI parlance.
+
+Shared session data will be handled by memcached. We will create one
+instance of memcached on each server node dedicated to IPA
+exclusively. Communication with memcached will be via a UNIX socket
+located in the file system under /var/run/ipa_memcached. It will be
+protected by file permissions and optionally SELinux policy.
+
+In application() we examine the request cookies and if there is an IPA
+session cookie with a session ID we retrieve the session data from our
+memcached instance.
+
+The session data will be a Python dict. IPA components will read or
+write their session information by using a pre-agreed upon name
+(e.g. key) in the dict. This is a very flexible system and consistent
+with how we pass data in most parts of IPA.
+
+If the session data is not available an empty session data dict will
+be created.
+
+How does this session data travel with the request in the IPA
+pipeline? In IPA we use the HTTP request/response to implement RPC. In
+application() we convert the request into a procedure call passing it
+arguments derived from the HTTP request. The passed parameters are
+specific to the RPC method being invoked. The context the RPC call is
+executing in is not passed as an RPC parameter.
+
+How would the contextual information such as session data be bound to
+the request and hence the RPC call?
+
+In IPA when a RPC invocation is being prepared from a request we
+recognize this will only ever be processed serially by one Python
+thread. A thread local dict called "context" is allocated for each
+thread. The context dict is cleared in between requests (e.g. RPC method
+invocations). The per-thread context dict is populated during the
+lifetime of the request and is used as a global data structure unique to
+the request that various IPA component can read from and write to with
+the assurance the data is unique to the current request and/or method
+call.
+
+The session data dict will be written into the context dict under the
+session key before the RPC method begins execution. Thus session data
+can be read and written by any IPA component by accessing
+``context.session``.
+
+When the RPC method finishes execution the session data bound to the
+request/method is retrieved from the context and written back to the
+memcached instance. The session ID is set in the response sent back to
+the client in the ``Set-Cookie`` header along with the flags
+controlling it's usage.
+
+Issues and details
+------------------
+
+IPA code cannot depend on session data being present, however it
+should always update session data with the hope it will be available
+in the future. Session data may not be available because:
+
+ * This is the first request from the user and no session data has
+ been created yet.
+
+ * The user may have cookies disabled.
+
+ * The session data may have been flushed. memcached operates with
+ a fixed memory allocation and will flush entries on a LRU basis,
+ like with any cache there is no guarantee of persistence.
+
+ Also we may have have deliberately expired or deleted session
+ data, see below.
+
+Cookie manipulation is done via the standard Python Cookie module.
+
+Session cookies will be set to only persist as long as the browser has
+the session open. They will be tagged so the the browser only returns
+the session ID on SSL secured HTTP requests. They will not be visible
+to Javascript in the browser.
+
+Session ID's will be created by using 48 bits of random data and
+converted to 12 hexadecimal digits. Newly generated session ID's will
+be checked for prior existence to handle the unlikely case the random
+number repeats.
+
+memcached will have significantly higher performance than a SQL or file
+based storage solution. Communication is effectively though a pipe
+(UNIX socket) using a very simple protocol and the data is held
+entirely in process memory. memcached also scales easily, it is easy
+to add more memcached processes and distribute the load across them.
+At this point in time we don't anticipate the need for this.
+
+A very nice feature of the Python memcached module is that when a data
+item is written to the cache it is done with standard Python pickling
+(pickling is a standard Python mechanism to marshal and unmarshal
+Python objects). We adopt the convention the object written to cache
+will be a dict to meet our internal data handling conventions. The
+pickling code will recursively handle nested objects in the dict. Thus
+we gain a lot of flexibility using standard Python data structures to
+store and retrieve our session data without having to author and debug
+code to marshal and unmarshal the data if some other storage mechanism
+had been used. This is a significant implementation win. Of course
+some common sense limitations need to observed when deciding on what
+is written to the session cache keeping in mind the data is shared
+between processes and it should not be excessively large (a
+configurable option)
+
+We can set an expiration on memcached entries. We may elect to do that
+to force session data to be refreshed periodically. For example we may
+wish the client to present fresh credentials on a periodic basis even
+if the cached credentials are otherwise within their validity period.
+
+We can explicitly delete session data if for some reason we believe it
+is stale, invalid or compromised.
+
+memcached also gives us certain facilities to prevent race conditions
+between different processes utilizing the cache. For example you can
+check of the entry has been modified since you last read it or use CAS
+(Check And Set) semantics. What has to be protected in terms of cache
+coherency will likely have to be determined as the session support is
+utilized and different data items are added to the cache. This is very
+much data and context specific. Fortunately memcached operations are
+atomic.
+
+Controlling the memcached process
+---------------------------------
+
+We need a mechanism to start the memcached process and secure it so
+that only IPA components can access it.
+
+Although memcached ships with both an initscript and systemd unit
+files those are for generic instances. We want a memcached instance
+dedicated exclusively to IPA usage. To accomplish this we would install
+a systemd unit file or an SysV initscript to control the IPA specific
+memcached service. ipactl would be extended to know about this
+additional service. systemd's cgroup facility would give us additional
+mechanisms to integrate the IPA memcached service within a larger IPA
+process group.
+
+Protecting the memcached data would be done via file permissions (and
+optionally SELinux policy) on the UNIX domain socket. Although recent
+implementations of memcached support authentication via SASL this
+introduces a performance and complexity burden not warranted when
+cached is dedicated to our exclusive use and access controlled by OS
+mechanisms.
+
+Conventionally daemons are protected by assigning a system uid and/or
+gid to the daemon. A daemon launched by root will drop it's privileges
+by assuming the effective uid:gid assigned to it. File system access
+is controlled by the OS via the effective identity and SELinux policy
+can be crafted based on the identity. Thus the memcached UNIX socket
+would be protected by having it owned by a specific system user and/or
+membership in a restricted system group (discounting for the moment
+SELinux).
+
+Unfortunately we currently do not have an IPA system uid whose
+identity our processes operate under nor do we have an IPA system
+group. IPA does manage a collection of related processes (daemons) and
+historically each has been assigned their own uid. When these
+unrelated processes communicate they mutually authenticate via other
+mechanisms. We do not have much of a history of using shared file
+system objects across identities. When file objects are created they
+are typically assigned the identity of daemon needing to access the
+object and are not accessed by other daemons, or they carry root
+identity.
+
+When our WSGI application runs in Apache it is run as a WSGI
+daemon. This means when Apache starts up it forks off WSGI processes
+for us and we are independent of other Apache processes. When WSGI is
+run in this mode there is the ability to set the uid:gid of the WSGI
+process hosting us, however we currently do not take advantage of this
+option. WSGI can be run in other modes as well, only in daemon mode
+can the uid:gid be independently set from the rest of Apache. All
+processes started by Apache can be set to a common uid:gid specified
+in the global Apache configuration, by default it's
+apache:apache. Thus when our IPA code executes it is running as
+apache:apache.
+
+To protect our memcached UNIX socket we can do one of two things:
+
+1. Assign it's uid:gid as apache:apache. This would limit access to
+ our cache only to processes running under httpd. It's somewhat
+ restricted but far from ideal. Any code running in the web server
+ could potentially access our cache. It's difficult to control what the
+ web server runs and admins may not understand the consequences of
+ configuring httpd to serve other things besides IPA.
+
+2. Create an IPA specific uid:gid, for example ipa:ipa. We then configure
+ our WSGI application to run as the ipa:ipa user and group. We also
+ configure our memcached instance to run as the ipa:ipa user and
+ group. In this configuration we are now fully protected, only our WSGI
+ code can read & write to our memcached UNIX socket.
+
+However there may be unforeseen issues by converting our code to run as
+something other than apache:apache. This would require some
+investigation and testing.
+
+IPA is dependent on other system daemons, specifically Directory
+Server (ds) and Certificate Server (cs). Currently we configure ds to
+run under the dirsrv:dirsrv user and group, an identity of our
+creation. We allow cs to default to it's pkiuser:pkiuser user and
+group. Should these other cooperating daemons also run under the
+common ipa:ipa user and group identities? At first blush there would
+seem to be an advantage to coalescing all process identities under a
+common IPA user and group identity. However these other processes do
+not depend on user and group permissions when working with external
+agents, processes, etc. Rather they are designed to be stand-alone
+network services which authenticate their clients via other
+mechanisms. They do depend on user and group permission to manage
+their own file system objects. If somehow the ipa user and/or group
+were compromised or malicious code somehow executed under the ipa
+identity there would be an advantage in having the cooperating
+processes cordoned off under their own identities providing one extra
+layer of protection. (Note, these cooperating daemons may not even be
+co-located on the same node in which case the issue is moot)
+
+The UNIX socket behavior (ldapi) with Directory Server is as follows:
+
+ * The socket ownership is: root:root
+
+ * The socket permissions are: 0666
+
+ * When connecting via ldapi you must authenticate as you would
+ normally with a TCP socket, except ...
+
+ * If autobind is enabled and the uid:gid is available via
+ SO_PEERCRED and the uid:gid can be found in the set of users known
+ to the Directory Server then that connection will be bound as that
+ user.
+
+ * Otherwise an anonymous bind will occur.
+
+memcached UNIX socket behavior is as follows:
+
+ * memcached can be invoked with a user argument, no group may be
+ specified. The effective uid is the uid of the user argument and
+ the effective gid is the primary group of the user, let's call
+ this euid:egid
+
+ * The socket ownership is: euid:egid
+
+ * The socket permissions are 0700 by default, but this can be
+ modified by the -a mask command line arg which sets the umask
+ (defaults to 0700).
+
+Overview of authentication in IPA
+=================================
+
+This describes how we currently authenticate and how we plan to
+improve authentication performance. First some definitions.
+
+There are 4 major players:
+
+ 1. client
+ 2. mod_auth_kerb (in Apache process)
+ 3. wsgi handler (in IPA wsgi python process)
+ 4. ds (directory server)
+
+There are several resources:
+
+ 1. /ipa/ui (unprotected, web UI static resources)
+ 2. /ipa/xml (protected, xmlrpc RPC used by command line clients)
+ 3. /ipa/json (protected, json RPC used by javascript in web UI)
+ 4. ds (protected, wsgi acts as proxy, our LDAP server)
+
+Current Model
+-------------
+
+This describes how things work in our current system for the web UI.
+
+ 1. Client requests /ipa/ui, this is unprotected, is static and
+ contains no sensitive information. Apache replies with html and
+ javascript. The javascript requests /ipa/json.
+
+ 2. Client sends post to /ipa/json.
+
+ 3. mod_auth_kerb is configured to protect /ipa/json, replies 401
+ authenticate negotiate.
+
+ 4. Client resends with credentials
+
+ 5. mod_auth_kerb validates credentials
+
+ a. if invalid replies 403 access denied (stops here)
+
+ b. if valid creates temporary ccache, adds KRB5CCNAME to request
+ headers
+
+ 6. Request passed to wsgi handler
+
+ a. validates request, KRB5CCNAME must be present, referrer, etc.
+
+ b. ccache saved and used to bind to ds
+
+ c. routes to specified RPC handler.
+
+ 7. wsgi handler replies to client
+
+Proposed new session based optimization
+---------------------------------------
+
+The round trip negotiate and credential validation in steps 3,4,5 is
+expensive. This can be avoided if we can cache the client
+credentials. With client sessions we can store the client credentials
+in the session bound to the client.
+
+A few notes about the session implementation.
+
+ * based on session cookies, cookies must be enabled
+
+ * session cookie is secure, only passed on secure connections, only
+ passed to our URL resource, never visible to client javascript
+ etc.
+
+ * session cookie has a session id which is used by wsgi handler to
+ retrieve client session data from shared multi-process cache.
+
+Changes to Apache's resource protection
+---------------------------------------
+
+ * /ipa/json is no longer protected by mod_auth_kerb. This is
+ necessary to avoid the negotiate expense in steps 3,4,5
+ above. Instead the /ipa/json resource will be protected in our wsgi
+ handler via the session cookie.
+
+ * A new protected URI is introduced, /ipa/login. This resource
+ does no serve any data, it is used exclusively for authentication.
+
+The new sequence is:
+
+ 1. Client requests /ipa/ui, this is unprotected. Apache replies with
+ html and javascript. The javascript requests /ipa/json.
+
+ 2. Client sends post to /ipa/json, which is unprotected.
+
+ 3. wsgi handler obtains session data from session cookie.
+
+ a. if ccache is present in session data and is valid
+
+ - request is further validated
+
+ - ccache is established for bind to ds
+
+ - request is routed to RPC handler
+
+ - wsgi handler eventually replies to client
+
+ b. if ccache is not present or not valid processing continues ...
+
+ 4. wsgi handler replies with 401 Unauthorized
+
+ 5. client sends request to /ipa/login to obtain session credentials
+
+ 6. mod_auth_kerb replies 401 negotiate on /ipa/login
+
+ 7. client sends credentials to /ipa/login
+
+ 8. mod_auth_kerb validates credentials
+
+ a. if valid
+
+ - mod_auth_kerb permits access to /ipa/login. wsgi handler is
+ invoked and does the following:
+
+ * establishes session for client
+
+ * retrieves the ccache from KRB5CCNAME and stores it
+
+ a. if invalid
+
+ - mod_auth_kerb sends 403 access denied (processing stops)
+
+ 9. client now posts the same data again to /ipa/json including
+ session cookie. Processing repeats starting at step 2 and since
+ the session data now contains a valid ccache step 3a executes, a
+ successful reply is sent to client.
+
+Command line client using xmlrpc
+--------------------------------
+
+The above describes the web UI utilizing the json RPC mechanism. The
+IPA command line tools utilize a xmlrpc RPC mechanism on the same
+HTTP server. Access to the xmlrpc is via the /ipa/xml URI. The json
+and xmlrpc API's are the same, they differ only on how their procedure
+calls are marshalled and unmarshalled.
+
+Under the new scheme /ipa/xml will continue to be Kerberos protected
+at all times. Apache's mod_auth_kerb will continue to require the
+client provides valid Kerberos credentials.
+
+When the WSGI handler routes to /ipa/xml the Kerberos credentials will
+be extracted from the KRB5CCNAME environment variable as provided by
+mod_auth_kerb. Everything else remains the same.
+
+'''
+
+#-------------------------------------------------------------------------------
+
+default_max_session_lifetime = 60*60 # number of seconds
+
+ISO8601_DATETIME_FMT = '%Y-%m-%dT%H:%M:%S' # FIXME jrd, this should be defined elsewhere
+def fmt_time(timestamp):
+ return time.strftime(ISO8601_DATETIME_FMT, time.localtime(timestamp))
+
+#-------------------------------------------------------------------------------
+
+class SessionManager(object):
+
+ '''
+ This class is used to manage a set of sessions. Each client
+ connecting to the server is assigned a session id wich is then
+ used to store data bound to the client's session in between server
+ requests.
+ '''
+
+ def __init__(self):
+ '''
+ :returns:
+ `SessionManager` object
+ '''
+
+ log_mgr.get_logger(self, True)
+ self.generated_session_ids = set()
+
+ def generate_session_id(self, n_bits=48):
+ '''
+ Return a random string to be used as a session id.
+
+ This implementation creates a string of hexadecimal digits.
+ There is no guarantee of uniqueness, it is the caller's
+ responsibility to validate the returned id is not currently in
+ use.
+
+ :parameters:
+ n_bits
+ number of bits of random data, will be rounded to next
+ highest multiple of 4
+ :returns:
+ string of random hexadecimal digits
+ '''
+ # round up to multiple of 4
+ n_bits = (n_bits + 3) & ~3
+ session_id = '%0*x' % (n_bits >> 2, random.getrandbits(n_bits))
+ return session_id
+
+ def new_session_id(self, max_retries=5):
+ '''
+ Returns a new *unique* session id. See `generate_session_id()`
+ for how the session id's are formulated.
+
+ The scope of the uniqueness of the id is limited to id's
+ generated by this instance of the `SessionManager`.
+
+ :parameters:
+ max_retries
+ Maximum number of attempts to produce a unique id.
+ :returns:
+ Unique session id as a string.
+ '''
+ n_retries = 0
+ while n_retries < max_retries:
+ session_id = self.generate_session_id()
+ if not session_id in self.generated_session_ids:
+ break
+ n_retries += 1
+ if n_retries >= max_retries:
+ self.error('could not allocate unique new session_id, %d retries exhausted', n_retries)
+ raise errors.ExecutionError(message=_('could not allocate unique new session_id'))
+ self.generated_session_ids.add(session_id)
+ return session_id
+
+
+class MemcacheSessionManager(SessionManager):
+ '''
+
+ This class is used to assign a session id to a HTTP server client
+ and then store client specific data associated with the session in
+ a memcached memory cache instance. Multiple processes may share
+ the memory cache permitting session data to be shared between
+ forked HTTP server children handling server requests.
+
+ The session id is guaranteed to be unique.
+
+ The session id is set into a session cookie returned to the client
+ and is secure (see `generate_cookie()`). Future requests from the
+ client will send the session id which is then used to retrieve the
+ session data (see `load_session_data()`)
+ '''
+
+ memcached_socket_path = '/var/run/ipa_memcached/ipa_memcached'
+ session_cookie_name = 'ipa_session'
+ mc_server_stat_name_re = re.compile(r'(.+)\s+\((\d+)\)')
+
+ def __init__(self):
+ '''
+ :returns:
+ `MemcacheSessionManager` object.
+ '''
+
+ super(MemcacheSessionManager, self).__init__()
+ self.servers = ['unix:%s' % self.memcached_socket_path]
+ self.mc = memcache.Client(self.servers, debug=0)
+
+ if not self.servers_running():
+ self.warning("session memcached servers not running")
+
+ def get_server_statistics(self):
+ '''
+ Return memcached server statistics.
+
+ Return value is a dict whose keys are server names and whose
+ value is a dict of key/value statistics as returned by the
+ memcached server.
+
+ :returns:
+ dict of server names, each value is dict of key/value server
+ statistics.
+
+ '''
+ result = {}
+ stats = self.mc.get_stats()
+ for server in stats:
+ match = self.mc_server_stat_name_re.search(server[0])
+ if match:
+ name = match.group(1)
+ result[name] = server[1]
+ else:
+ self.warning('unparseable memcached server name "%s"', server[0])
+ return result
+
+ def servers_running(self):
+ '''
+ Check if all configured memcached servers are running and can
+ be communicated with.
+
+ :returns:
+ True if at least one server is configured and all servers
+ can respond, False otherwise.
+
+ '''
+
+ if len(self.servers) == 0:
+ return False
+ stats = self.get_server_statistics()
+ return len(self.servers) == len(stats)
+
+ def new_session_id(self, max_retries=5):
+ '''
+ Returns a new *unique* session id. See `generate_session_id()`
+ for how the session id's are formulated.
+
+ The scope of the uniqueness of the id is limited to id's
+ generated by this instance of the `SessionManager` and session
+ id's currently stored in the memcache instance.
+
+ :parameters:
+ max_retries
+ Maximum number of attempts to produce a unique id.
+ :returns:
+ Unique session id as a string.
+ '''
+ n_retries = 0
+ while n_retries < max_retries:
+ session_id = super(MemcacheSessionManager, self).new_session_id(max_retries)
+ session_key = self.session_key(session_id)
+ session_data = self.mc.get(session_key)
+ if session_data is None:
+ break
+ n_retries += 1
+ if n_retries >= max_retries:
+ self.error('could not allocate unique new session_id, %d retries exhausted', n_retries)
+ raise errors.ExecutionError(message=_('could not allocate unique new session_id'))
+ return session_id
+
+ def new_session_data(self, session_id):
+ '''
+ Return a new session data dict. The session data will be
+ associated with it's session id. The dict will be
+ pre-populated with:
+
+ session_id
+ The session ID used to identify this session data.
+ session_start_timestamp
+ Timestamp when this session was created.
+ session_write_timestamp
+ Timestamp when the session was last written to cache.
+ session_expiration_timestamp
+ Timestamp when session expires. Defaults to zero which
+ implies no expiration. See `set_session_expiration_time()`.
+
+ :parameters:
+ session_id
+ The session id used to look up this session data.
+ :returns:
+ Session data dict populated with a session_id key.
+ '''
+
+ now = time.time()
+ return {'session_id' : session_id,
+ 'session_start_timestamp' : now,
+ 'session_write_timestamp' : now,
+ 'session_expiration_timestamp' : 0,
+ }
+
+ def session_key(self, session_id):
+ '''
+ Given a session id return a memcache key used to look up the
+ session data in the memcache.
+
+ :parameters:
+ session_id
+ The session id from which the memcache key will be derived.
+ :returns:
+ A key (string) used to look up the session data in the memcache.
+ '''
+ return 'ipa.session.%s' % (session_id)
+
+ def get_session_id_from_http_cookie(self, cookie_header):
+ '''
+ Parse an HTTP cookie header and search for our session
+ id. Return the session id if found, return None if not
+ found.
+
+ :parameters:
+ cookie_header
+ An HTTP cookie header. May be None, if None return None.
+ :returns:
+ Session id as string or None if not found.
+ '''
+ session_id = None
+ if cookie_header is not None:
+ cookie = Cookie.SimpleCookie()
+ cookie.load(cookie_header)
+ session_cookie = cookie.get(self.session_cookie_name)
+ if session_cookie is not None:
+ session_id = session_cookie.value
+ self.debug('found session cookie_id = %s', session_id)
+ return session_id
+
+
+ def load_session_data(self, cookie_header):
+ '''
+ Parse an HTTP cookie header looking for our session
+ information.
+
+ * If no session id is found then a new session id and new
+ session data dict will be generated, stored in the memcache
+ and returned. The new session data dict will contain the new
+ session id.
+
+ * If the session id is found in the cookie an attempt is made
+ to retrieve the session data from the memcache using the
+ session id.
+
+ - If existing session data is found in the memcache it is
+ returned.
+
+ - If no session data is found in the memcache then a new
+ session data dict will be generated, stored in the
+ memcache and returned. The new session data dict will
+ contain the session id found in the cookie header.
+
+ :parameters:
+ cookie_header
+ An HTTP cookie header. May be None.
+ :returns:
+ Session data dict containing at a minimum the session id it
+ is bound to.
+ '''
+
+ session_id = self.get_session_id_from_http_cookie(cookie_header)
+ if session_id is None:
+ session_id = self.new_session_id()
+ self.debug('no session id in request, generating empty session data with id=%s', session_id)
+ session_data = self.new_session_data(session_id)
+ self.store_session_data(session_data)
+ return session_data
+ else:
+ session_key = self.session_key(session_id)
+ session_data = self.mc.get(session_key)
+ if session_data is None:
+ self.debug('no session data in cache with id=%s, generating empty session data', session_id)
+ session_data = self.new_session_data(session_id)
+ self.store_session_data(session_data)
+ return session_data
+ else:
+ self.debug('found session data in cache with id=%s', session_id)
+ return session_data
+
+ def store_session_data(self, session_data):
+ '''
+ Store the supplied session_data dict in the memcached instance.
+
+ The session_expiration_timestamp is always passed to memcached
+ when the session data is written back to the memcache. This is
+ because otherwise the memcache expiration will default to zero
+ if it's not specified which implies no expiration. Thus a
+ failure to specify an exiration time when writing an item to
+ memcached will cause a previously set expiration time for the
+ item to be discarded and the item will no longer expire.
+
+ :parameters:
+ session_data
+ Session data dict, must contain session_id key.
+
+ :returns:
+ session_id
+ '''
+ session_id = session_data['session_id']
+ session_key = self.session_key(session_id)
+ now = time.time()
+ session_data['session_write_timestamp'] = now
+ session_expiration_timestamp = session_data['session_expiration_timestamp']
+
+ self.debug('store session: session_id=%s start_timestamp=%s write_timestamp=%s expiration_timestamp=%s',
+ session_id,
+ fmt_time(session_data['session_start_timestamp']),
+ fmt_time(session_data['session_write_timestamp']),
+ fmt_time(session_data['session_expiration_timestamp']))
+
+ self.mc.set(session_key, session_data, time=session_expiration_timestamp)
+ return session_id
+
+ def generate_cookie(self, url_path, session_id, add_header=False):
+ '''
+ Return a session cookie containing the session id. The cookie
+ will be contrainted to the url path, defined for use
+ with HTTP only, and only returned on secure connections (SSL).
+
+ :parameters:
+ url_path
+ The cookie will be returned in a request if it begins
+ with this url path.
+ session_id
+ The session id identified by the session cookie
+ add_header
+ If true format cookie string with Set-Cookie: header
+
+ :returns:
+ cookie string
+ '''
+ cookie = Cookie.SimpleCookie()
+ cookie[self.session_cookie_name] = session_id
+ cookie[self.session_cookie_name]['path'] = url_path
+ cookie[self.session_cookie_name]['httponly'] = True
+ cookie[self.session_cookie_name]['secure'] = True
+ if add_header:
+ result = cookie.output().strip()
+ else:
+ result = cookie.output(header='').strip()
+
+ return result
+
+ def set_session_expiration_time(self, session_data,
+ lifetime=default_max_session_lifetime,
+ max_age=None):
+ '''
+ memcached permits setting an expiration time on entries. The
+ expiration time may either be Unix time (number of seconds since
+ January 1, 1970, as a 32-bit value), or a number of seconds starting
+ from current time. In the latter case, this number of seconds may
+ not exceed 60*60*24*30 (number of seconds in 30 days); if the number
+ sent by a client is larger than that, the server will consider it to
+ be real Unix time value rather than an offset from current time.
+
+ We never use the duration value (< 30 days), we always use a
+ timestamp, this makes it easier to integrate with other time
+ constraints.
+
+ When a session is created it's start time is recorded in the
+ session data as the session_start_timestamp value. The
+ expiration timestamp is computed by adding the lifetime to the
+ session_start_timestamp. Then if the max_age is specified the
+ expiration is constrained to be not greater than the max_age.
+
+ The final computed expiration is then written into the
+ session_data as the session_expiration_timestamp value. The
+ session_expiration_timestamp is always passed to memcached
+ when the session data is written back to the memcache. This is
+ because otherwise the memcache expiration will default to zero
+ if it's not specified which implies no expiration. Thus a
+ failure to specify an exiration time when writing an item to
+ memcached will cause a previously set expiration time for the
+ item to be discarded and the item will no longer expire.
+
+
+ :parameters:
+ session_data
+ Session data dict, must contain session_id key.
+ lifetime
+ Number of seconds cache entry should live. This is a
+ duration value, not a timestamp. Zero implies no
+ expiration.
+
+ max_age
+ Unix time value when cache entry must expire by.
+
+ :returns:
+ expiration timestamp, zero implies no expiration
+ '''
+
+ if lifetime == 0 and max_age is None:
+ expiration = 0
+ session_data['session_expiration_timestamp'] = expiration
+ return expiration
+
+ session_start_timestamp = session_data['session_start_timestamp']
+ expiration = session_start_timestamp + lifetime
+
+ if max_age is not None:
+ expiration = min(expiration, max_age)
+
+ session_data['session_expiration_timestamp'] = expiration
+
+ return expiration
+
+ def delete_session_data(self, session_id):
+ '''
+ Given a session id removed the session data bound to the id from the memcache.
+
+ :parameters:
+ session_id
+ The ID of the session which should be removed from the cache.
+ :returns:
+ None
+ '''
+ session_key = self.session_key(session_id)
+
+ self.debug('delete session data from memcache, session_id=%s', session_id)
+ self.mc.delete(session_key)
+
+
+#-------------------------------------------------------------------------------
+krbccache_dir ='/var/run/ipa_memcached'
+krbccache_prefix = 'krbcc_'
+
+def get_krbccache_pathname():
+ return os.path.join(krbccache_dir, '%s%s' % (krbccache_prefix, os.getpid()))
+
+def read_krbccache_file(krbccache_pathname):
+ root_logger.debug('reading krbccache data from "%s"', krbccache_pathname)
+ src = open(krbccache_pathname)
+ ccache_data = src.read()
+ src.close()
+ return ccache_data
+
+def store_krbccache_file(ccache_data):
+ krbccache_pathname = get_krbccache_pathname()
+ root_logger.debug('storing krbccache data into "%s"', krbccache_pathname)
+ dst = open(krbccache_pathname, 'w')
+ dst.write(ccache_data)
+ dst.close()
+
+ return krbccache_pathname
+
+def delete_krbccache_file(krbccache_pathname=None):
+ if krbccache_pathname is None:
+ krbccache_pathname = get_krbccache_pathname()
+
+ try:
+ os.unlink(krbccache_pathname)
+ except Exception, e:
+ root_logger.error('unable to delete session krbccache file "%s", %s',
+ krbccache_pathname, e)
+
+
+#-------------------------------------------------------------------------------
+
+
+session_mgr = MemcacheSessionManager()