diff options
author | Simo Sorce <simo@redhat.com> | 2016-08-19 09:23:55 -0400 |
---|---|---|
committer | Jan Cholasta <jcholast@redhat.com> | 2017-02-15 07:13:37 +0100 |
commit | c894ebefc5c4c4c7ea340d6ddc4cd3c081917e4a (patch) | |
tree | 8511e93ca9e8e1df6c504b8f18d2fec733686d26 | |
parent | 11ef2cacbf2ebb67f80a0cf4a3e7b39da700188b (diff) | |
download | freeipa-c894ebefc5c4c4c7ea340d6ddc4cd3c081917e4a.tar.gz freeipa-c894ebefc5c4c4c7ea340d6ddc4cd3c081917e4a.tar.xz freeipa-c894ebefc5c4c4c7ea340d6ddc4cd3c081917e4a.zip |
Change session handling
Stop using memcache, use mod_auth_gssapi filesystem based ccaches.
Remove custom session handling, use mod_auth_gssapi and mod_session to
establish and keep a session cookie.
Add loopback to mod_auth_gssapi to do form absed auth and pass back a
valid session cookie.
And now that we do not remove ccaches files to move them to the
memcache, we can avoid the risk of pollutting the filesystem by keeping
a common ccache file for all instances of the same user.
https://fedorahosted.org/freeipa/ticket/5959
Signed-off-by: Simo Sorce <simo@redhat.com>
Reviewed-By: Jan Cholasta <jcholast@redhat.com>
-rw-r--r-- | freeipa.spec.in | 10 | ||||
-rw-r--r-- | init/Makefile.am | 6 | ||||
-rw-r--r-- | init/ipa_memcached.in | 5 | ||||
-rw-r--r-- | init/systemd/Makefile.am | 2 | ||||
-rw-r--r-- | init/systemd/ipa_memcached.service.in | 12 | ||||
-rw-r--r-- | init/tmpfilesd/ipa.conf.in | 1 | ||||
-rw-r--r-- | install/conf/ipa.conf | 22 | ||||
-rw-r--r-- | install/share/Makefile.am | 4 | ||||
-rw-r--r-- | install/share/gssapi.login | 0 | ||||
-rw-r--r-- | install/share/memcache-remove.uldif | 1 | ||||
-rw-r--r-- | ipalib/krb_utils.py | 2 | ||||
-rw-r--r-- | ipaplatform/base/paths.py | 3 | ||||
-rw-r--r-- | ipapython/cookie.py | 2 | ||||
-rw-r--r-- | ipaserver/install/installutils.py | 2 | ||||
-rw-r--r-- | ipaserver/install/ipa_backup.py | 4 | ||||
-rw-r--r-- | ipaserver/install/memcacheinstance.py | 24 | ||||
-rw-r--r-- | ipaserver/install/server/install.py | 7 | ||||
-rw-r--r-- | ipaserver/install/server/replicainstall.py | 5 | ||||
-rw-r--r-- | ipaserver/install/server/upgrade.py | 18 | ||||
-rw-r--r-- | ipaserver/install/service.py | 1 | ||||
-rw-r--r-- | ipaserver/plugins/session.py | 15 | ||||
-rw-r--r-- | ipaserver/rpcserver.py | 334 | ||||
-rw-r--r-- | ipaserver/session.py | 1262 | ||||
-rwxr-xr-x | ipaserver/setup.py | 1 | ||||
-rw-r--r-- | pylint_plugins.py | 3 |
25 files changed, 173 insertions, 1573 deletions
diff --git a/freeipa.spec.in b/freeipa.spec.in index 63f0e73ef..b65588a57 100644 --- a/freeipa.spec.in +++ b/freeipa.spec.in @@ -128,7 +128,6 @@ BuildRequires: pylint >= 1.0 # workaround for https://bugzilla.redhat.com/show_bug.cgi?id=1096506 BuildRequires: python2-polib BuildRequires: python-libipa_hbac -BuildRequires: python-memcached BuildRequires: python-lxml # 5.0.0: QRCode.print_ascii BuildRequires: python-qrcode-core >= 5.0.0 @@ -230,13 +229,12 @@ Requires: cyrus-sasl-gssapi%{?_isa} Requires: ntp Requires: httpd >= 2.4.6-31 Requires: mod_wsgi -Requires: mod_auth_gssapi >= 1.4.0 +Requires: mod_auth_gssapi >= 1.5.0 Requires: mod_nss >= 1.0.8-26 +Requires: mod_session Requires: python-ldap >= 2.4.15 Requires: python-gssapi >= 1.2.0 Requires: acl -Requires: memcached -Requires: python-memcached Requires: systemd-units >= 38 Requires(pre): shadow-utils Requires(pre): systemd-units @@ -1188,18 +1186,15 @@ fi %license COPYING %ghost %verify(not owner group) %dir %{_sharedstatedir}/kdcproxy %dir %attr(0755,root,root) %{_sysconfdir}/ipa/kdcproxy -%config(noreplace) %{_sysconfdir}/sysconfig/ipa_memcached %config(noreplace) %{_sysconfdir}/sysconfig/ipa-dnskeysyncd %config(noreplace) %{_sysconfdir}/sysconfig/ipa-ods-exporter %config(noreplace) %{_sysconfdir}/ipa/kdcproxy/kdcproxy.conf -%dir %attr(0700,apache,apache) %{_localstatedir}/run/ipa_memcached/ %dir %attr(0700,root,root) %{_localstatedir}/run/ipa/ %dir %attr(0700,apache,apache) %{_localstatedir}/run/httpd/ipa/ %dir %attr(0700,apache,apache) %{_localstatedir}/run/httpd/ipa/clientcaches/ %dir %attr(0700,apache,apache) %{_localstatedir}/run/httpd/ipa/krbcache/ # NOTE: systemd specific section %{_tmpfilesdir}/ipa.conf -%attr(644,root,root) %{_unitdir}/ipa_memcached.service %attr(644,root,root) %{_unitdir}/ipa-custodia.service %ghost %attr(644,root,root) %{etc_systemd_dir}/httpd.d/ipa.conf # END @@ -1289,6 +1284,7 @@ fi %dir %attr(0700,root,root) %{_sysconfdir}/ipa/custodia %dir %{_usr}/share/ipa/schema.d %attr(0644,root,root) %{_usr}/share/ipa/schema.d/README +%attr(0644,root,root) %{_usr}/share/ipa/gssapi.login %files server-dns %defattr(-,root,root,-) diff --git a/init/Makefile.am b/init/Makefile.am index 47f844ac1..8f4d1d0a8 100644 --- a/init/Makefile.am +++ b/init/Makefile.am @@ -8,13 +8,7 @@ dist_sysconfenv_DATA = \ ipa-dnskeysyncd \ ipa-ods-exporter -nodist_sysconfenv_DATA = \ - ipa_memcached - CLEANFILES = $(nodist_sysconfenv_DATA) -dist_noinst_DATA = \ - ipa_memcached.in - %: %.in Makefile sed -e 's|@localstatedir[@]|$(localstatedir)|g' '$(srcdir)/$@.in' >$@ diff --git a/init/ipa_memcached.in b/init/ipa_memcached.in deleted file mode 100644 index 56701383d..000000000 --- a/init/ipa_memcached.in +++ /dev/null @@ -1,5 +0,0 @@ -SOCKET_PATH=@localstatedir@/run/ipa_memcached/ipa_memcached -USER=apache -MAXCONN=1024 -CACHESIZE=64 -OPTIONS= diff --git a/init/systemd/Makefile.am b/init/systemd/Makefile.am index e978fe974..325e85748 100644 --- a/init/systemd/Makefile.am +++ b/init/systemd/Makefile.am @@ -4,12 +4,10 @@ AUTOMAKE_OPTIONS = 1.7 dist_noinst_DATA = \ ipa-custodia.service.in \ - ipa_memcached.service.in \ ipa.service.in systemdsystemunit_DATA = \ ipa-custodia.service \ - ipa_memcached.service \ ipa.service CLEANFILES = $(systemdsystemunit_DATA) diff --git a/init/systemd/ipa_memcached.service.in b/init/systemd/ipa_memcached.service.in deleted file mode 100644 index 0e163203a..000000000 --- a/init/systemd/ipa_memcached.service.in +++ /dev/null @@ -1,12 +0,0 @@ -[Unit] -Description=IPA memcached daemon, increases IPA server performance -After=network.target - -[Service] -Type=forking -EnvironmentFile=@sysconfenvdir@/ipa_memcached -PIDFile=@localstatedir@/run/ipa_memcached/ipa_memcached.pid -ExecStart=@bindir@/memcached -d -s $SOCKET_PATH -u $USER -m $CACHESIZE -c $MAXCONN -P @localstatedir@/run/ipa_memcached/ipa_memcached.pid $OPTIONS - -[Install] -WantedBy=multi-user.target diff --git a/init/tmpfilesd/ipa.conf.in b/init/tmpfilesd/ipa.conf.in index 4954000a3..5ac96f20f 100644 --- a/init/tmpfilesd/ipa.conf.in +++ b/init/tmpfilesd/ipa.conf.in @@ -1,4 +1,3 @@ -d @localstatedir@/run/ipa_memcached 0700 apache apache d @localstatedir@/run/ipa 0700 root root d @localstatedir@/run/httpd/ipa 0700 apache apache d @localstatedir@/run/httpd/ipa/clientcaches 0700 apache apache diff --git a/install/conf/ipa.conf b/install/conf/ipa.conf index 3e7435903..6ae416353 100644 --- a/install/conf/ipa.conf +++ b/install/conf/ipa.conf @@ -63,10 +63,15 @@ WSGIScriptReloading Off <Location "/ipa"> AuthType GSSAPI AuthName "Kerberos Login" + GssapiUseSessions On + Session On + SessionCookieName ipa_session path=/ipa;httponly;secure; + SessionHeader IPASESSION + GssapiSessionKey file:/etc/httpd/alias/ipasession.key + GssapiCredStore keytab:/etc/httpd/conf/ipa.keytab GssapiCredStore client_keytab:/etc/httpd/conf/ipa.keytab GssapiDelegCcacheDir /var/run/httpd/ipa/clientcaches - GssapiDelegCcacheUnique On GssapiUseS4U2Proxy on GssapiAllowedMech krb5 Require valid-user @@ -77,19 +82,10 @@ WSGIScriptReloading Off Header always append Content-Security-Policy "frame-ancestors 'none'" </Location> -# Turn off Apache authentication for sessions -<Location "/ipa/session/json"> - Satisfy Any - Order Deny,Allow - Allow from all -</Location> - -<Location "/ipa/session/xml"> - Satisfy Any - Order Deny,Allow - Allow from all -</Location> +# Target for login with internal connections +Alias /ipa/session/cookie "/usr/share/ipa/gssapi.login" +# Turn off Apache authentication for password/token based login pages <Location "/ipa/session/login_password"> Satisfy Any Order Deny,Allow diff --git a/install/share/Makefile.am b/install/share/Makefile.am index 715912d8b..6f35a329e 100644 --- a/install/share/Makefile.am +++ b/install/share/Makefile.am @@ -86,7 +86,9 @@ dist_app_DATA = \ vault.ldif \ kdcproxy-enable.uldif \ kdcproxy-disable.uldif \ - ipa-httpd.conf.template + ipa-httpd.conf.template \ + gssapi.login \ + $(NULL) kdcproxyconfdir = $(IPA_SYSCONF_DIR)/kdcproxy dist_kdcproxyconf_DATA = \ diff --git a/install/share/gssapi.login b/install/share/gssapi.login new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/install/share/gssapi.login diff --git a/install/share/memcache-remove.uldif b/install/share/memcache-remove.uldif new file mode 100644 index 000000000..e6ca1a617 --- /dev/null +++ b/install/share/memcache-remove.uldif @@ -0,0 +1 @@ +deleteentry: cn=MEMCACHE,cn=$FQDN,cn=masters,cn=ipa,cn=etc,$SUFFIX diff --git a/ipalib/krb_utils.py b/ipalib/krb_utils.py index d005a87b6..47d24c984 100644 --- a/ipalib/krb_utils.py +++ b/ipalib/krb_utils.py @@ -208,3 +208,5 @@ def get_credentials_if_valid(name=None, ccache_name=None): return None except gssapi.exceptions.ExpiredCredentialsError: return None + except gssapi.exceptions.GSSError: + return None diff --git a/ipaplatform/base/paths.py b/ipaplatform/base/paths.py index 423478917..44108e52a 100644 --- a/ipaplatform/base/paths.py +++ b/ipaplatform/base/paths.py @@ -328,8 +328,7 @@ class BasePathNamespace(object): KRB5CC_HTTPD = "/var/run/httpd/ipa/krbcache/krb5ccache" IPA_RENEWAL_LOCK = "/var/run/ipa/renewal.lock" SVC_LIST_FILE = "/var/run/ipa/services.list" - IPA_MEMCACHED_DIR = "/var/run/ipa_memcached" - VAR_RUN_IPA_MEMCACHED = "/var/run/ipa_memcached/ipa_memcached" + IPA_HTTPD_DIR = "/var/run/httpd" KRB5CC_SAMBA = "/var/run/samba/krb5cc_samba" SLAPD_INSTANCE_SOCKET_TEMPLATE = "/var/run/slapd-%s.socket" ALL_SLAPD_INSTANCE_SOCKETS = "/var/run/slapd-*.socket" diff --git a/ipapython/cookie.py b/ipapython/cookie.py index 8abd9617b..57523a402 100644 --- a/ipapython/cookie.py +++ b/ipapython/cookie.py @@ -112,7 +112,7 @@ class Cookie(object): cookie = Cookie('session', session_id, domain=my_domain, path=mypath, - httpOnly=True, secure=True, expires=expiration) + httponly=True, secure=True, expires=expiration) headers.append(('Set-Cookie', str(cookie))) diff --git a/ipaserver/install/installutils.py b/ipaserver/install/installutils.py index ab2596c8c..8602f59ca 100644 --- a/ipaserver/install/installutils.py +++ b/ipaserver/install/installutils.py @@ -64,7 +64,7 @@ if six.PY3: # Used to determine install status IPA_MODULES = [ 'httpd', 'kadmin', 'dirsrv', 'pki-tomcatd', 'install', 'krb5kdc', 'ntpd', - 'named', 'ipa_memcached'] + 'named'] class BadHostError(Exception): diff --git a/ipaserver/install/ipa_backup.py b/ipaserver/install/ipa_backup.py index 536914638..4ba61e529 100644 --- a/ipaserver/install/ipa_backup.py +++ b/ipaserver/install/ipa_backup.py @@ -36,11 +36,13 @@ from ipapython import admintool from ipapython.dn import DN from ipaserver.install.replication import wait_for_task from ipaserver.install import installutils -from ipaserver.session import ISO8601_DATETIME_FMT from ipapython import ipaldap from ipaplatform.constants import constants from ipaplatform.tasks import tasks + +ISO8601_DATETIME_FMT = '%Y-%m-%dT%H:%M:%S' + """ A test gpg can be generated like this: diff --git a/ipaserver/install/memcacheinstance.py b/ipaserver/install/memcacheinstance.py deleted file mode 100644 index 547ac2ba4..000000000 --- a/ipaserver/install/memcacheinstance.py +++ /dev/null @@ -1,24 +0,0 @@ -# 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/>. -# - -from ipaserver.install import service - -class MemcacheInstance(service.SimpleServiceInstance): - def __init__(self): - service.SimpleServiceInstance.__init__(self, "ipa_memcached") diff --git a/ipaserver/install/server/install.py b/ipaserver/install/server/install.py index 8178d4e29..8628572a7 100644 --- a/ipaserver/install/server/install.py +++ b/ipaserver/install/server/install.py @@ -32,7 +32,7 @@ from ipalib.util import ( import ipaclient.install.ntpconf from ipaserver.install import ( bindinstance, ca, cainstance, certs, dns, dsinstance, - httpinstance, installutils, kra, krbinstance, memcacheinstance, + httpinstance, installutils, kra, krbinstance, ntpinstance, otpdinstance, custodiainstance, replication, service, sysupgrade) from ipaserver.install.installutils import ( @@ -804,10 +804,6 @@ def install(installer): # generated ds.add_cert_to_service() - memcache = memcacheinstance.MemcacheInstance() - memcache.create_instance('MEMCACHE', host_name, - ipautil.realm_to_suffix(realm_name)) - otpd = otpdinstance.OtpdInstance() otpd.create_instance('OTPD', host_name, ipautil.realm_to_suffix(realm_name)) @@ -1052,7 +1048,6 @@ def uninstall(installer): if _server_trust_ad_installed: adtrustinstance.ADTRUSTInstance(fstore).uninstall() custodiainstance.CustodiaInstance().uninstall() - memcacheinstance.MemcacheInstance().uninstall() otpdinstance.OtpdInstance().uninstall() tasks.restore_hostname(fstore, sstore) fstore.restore_all_files() diff --git a/ipaserver/install/server/replicainstall.py b/ipaserver/install/server/replicainstall.py index fcb979c15..649184cbe 100644 --- a/ipaserver/install/server/replicainstall.py +++ b/ipaserver/install/server/replicainstall.py @@ -37,7 +37,7 @@ from ipalib.util import ( from ipaclient.install.client import configure_krb5_conf, purge_host_keytab from ipaserver.install import ( bindinstance, ca, certs, dns, dsinstance, httpinstance, - installutils, kra, krbinstance, memcacheinstance, + installutils, kra, krbinstance, ntpinstance, otpdinstance, custodiainstance, service) from ipaserver.install.installutils import ( create_replica_config, ReplicaConfig, load_pkcs12, is_ipa_configured) @@ -163,9 +163,6 @@ def install_http(config, auto_redirect, ca_is_configured, ca_file, pkcs12_info = make_pkcs12_info(config.dir, "httpcert.p12", "http_pin.txt") - memcache = memcacheinstance.MemcacheInstance() - memcache.create_instance('MEMCACHE', config.host_name, - ipautil.realm_to_suffix(config.realm_name)) http = httpinstance.HTTPInstance() http.create_instance( diff --git a/ipaserver/install/server/upgrade.py b/ipaserver/install/server/upgrade.py index 0e034efac..2bdf6eede 100644 --- a/ipaserver/install/server/upgrade.py +++ b/ipaserver/install/server/upgrade.py @@ -34,7 +34,6 @@ from ipaplatform.paths import paths from ipaserver.install import installutils from ipaserver.install import dsinstance from ipaserver.install import httpinstance -from ipaserver.install import memcacheinstance from ipaserver.install import ntpinstance from ipaserver.install import bindinstance from ipaserver.install import service @@ -74,6 +73,21 @@ def uninstall_ipa_kpasswd(): if enabled is not None and not enabled: ipa_kpasswd.remove() + +def uninstall_ipa_memcached(): + """ + We can't use the full service uninstaller because that will attempt + to stop and disable the service which by now doesn't exist. We just + want to clean up sysrestore.state to remove all references to + ipa_kpasswd. + """ + ipa_memcached = service.SimpleServiceInstance('ipa_memcached') + + enabled = not ipa_memcached.restore_state("enabled") + + if enabled is not None and not enabled: + ipa_memcached.remove() + def backup_file(filename, ext): """Make a backup of filename using ext as the extension. Do not overwrite previous backups.""" @@ -1570,6 +1584,7 @@ def upgrade_configuration(): update_dbmodules(api.env.realm) uninstall_ipa_kpasswd() + uninstall_ipa_memcached() removed_sysconfig_file = paths.SYSCONFIG_HTTPD if fstore.has_file(removed_sysconfig_file): @@ -1620,7 +1635,6 @@ def upgrade_configuration(): uninstall_dogtag_9(ds, http) simple_service_list = ( - (memcacheinstance.MemcacheInstance(), 'MEMCACHE'), (otpdinstance.OtpdInstance(), 'OTPD'), ) diff --git a/ipaserver/install/service.py b/ipaserver/install/service.py index fbe3f23e5..b80044f4b 100644 --- a/ipaserver/install/service.py +++ b/ipaserver/install/service.py @@ -46,7 +46,6 @@ SERVICE_LIST = { 'KDC': ('krb5kdc', 10), 'KPASSWD': ('kadmin', 20), 'DNS': ('named', 30), - 'MEMCACHE': ('ipa_memcached', 39), 'HTTP': ('httpd', 40), 'KEYS': ('ipa-custodia', 41), 'NTP': ('ntpd', 45), diff --git a/ipaserver/plugins/session.py b/ipaserver/plugins/session.py index 0efb53c88..c700ab9ba 100644 --- a/ipaserver/plugins/session.py +++ b/ipaserver/plugins/session.py @@ -5,7 +5,7 @@ from ipalib import Command from ipalib.request import context from ipalib.plugable import Registry -from ipaserver.session import get_session_mgr +from ipaserver.session import logout register = Registry() @@ -18,15 +18,10 @@ class session_logout(Command): NO_CLI = True def execute(self, *args, **options): - session_data = getattr(context, 'session_data', None) - if session_data is None: - self.debug('session logout command: no session_data found') - else: - session_id = session_data.get('session_id') - self.debug('session logout command: session_id=%s', session_id) + ccache_name = getattr(context, 'ccache_name', None) + if ccache_name is None: + self.debug('session logout command: no ccache_name found') - # Notifiy registered listeners - session_mgr = get_session_mgr() - session_mgr.auth_mgr.logout(session_data) + logout(ccache_name) return dict(result=None) diff --git a/ipaserver/rpcserver.py b/ipaserver/rpcserver.py index 45550fb1f..2b1e42bf6 100644 --- a/ipaserver/rpcserver.py +++ b/ipaserver/rpcserver.py @@ -25,11 +25,10 @@ Also see the `ipalib.rpc` module. from xml.sax.saxutils import escape import os -import datetime import json import traceback import gssapi -import time +import requests import ldap.controls from pyasn1.type import univ, namedtype @@ -51,22 +50,24 @@ from ipalib.errors import (PublicError, InternalError, JSONError, from ipalib.request import context, destroy_context from ipalib.rpc import (xml_dumps, xml_loads, json_encode_binary, json_decode_binary) -from ipalib.util import parse_time_duration, normalize_name +from ipalib.util import normalize_name from ipapython.dn import DN from ipaserver.plugins.ldap2 import ldap2 from ipaserver.session import ( - get_session_mgr, AuthManager, get_ipa_ccache_name, - load_ccache_data, bind_ipa_ccache, release_ipa_ccache, fmt_time, - default_max_session_duration, krbccache_dir, krbccache_prefix) + get_ipa_ccache_name, + krbccache_dir, krbccache_prefix) from ipalib.backend import Backend from ipalib.krb_utils import ( - krb_ticket_expiration_threshold, krb5_format_principal_name, - krb5_format_service_principal_name, get_credentials, get_credentials_if_valid) + krb5_format_principal_name, + krb5_format_service_principal_name, get_credentials_if_valid) from ipapython import ipautil from ipaplatform.paths import paths from ipapython.version import VERSION from ipalib.text import _ +from base64 import b64decode, b64encode +from requests.auth import AuthBase + if six.PY3: unicode = str @@ -303,6 +304,7 @@ class WSGIExecutioner(Executioner): Base class for execution backends with a WSGI application interface. """ + headers = None content_type = None key = '' @@ -424,22 +426,17 @@ class WSGIExecutioner(Executioner): try: status = HTTP_STATUS_SUCCESS response = self.wsgi_execute(environ) - headers = [('Content-Type', self.content_type + '; charset=utf-8')] + if self.headers: + headers = self.headers + else: + headers = [('Content-Type', + self.content_type + '; charset=utf-8')] except Exception: self.exception('WSGI %s.__call__():', self.name) status = HTTP_STATUS_SERVER_ERROR response = status.encode('utf-8') headers = [('Content-Type', 'text/plain; charset=utf-8')] - session_data = getattr(context, 'session_data', None) - if session_data is not None: - # Send session cookie back and store session data - # FIXME: the URL path should be retreived from somewhere (but where?), not hardcoded - session_mgr = get_session_mgr() - session_cookie = session_mgr.generate_cookie('/ipa', session_data['session_id'], - session_data['session_expiration_timestamp']) - headers.append(('Set-Cookie', session_cookie)) - start_response(status, headers) return [response] @@ -521,36 +518,73 @@ class jsonserver(WSGIExecutioner, HTTP_Status): options = dict((str(k), v) for (k, v) in options.items()) return (method, args, options, _id) -class AuthManagerKerb(AuthManager): - ''' - Instances of the AuthManger class are used to handle - authentication events delivered by the SessionManager. This class - specifcally handles the management of Kerbeos credentials which - may be stored in the session. - ''' - def __init__(self, name): - super(AuthManagerKerb, self).__init__(name) +class NegotiateAuth(AuthBase): + """Negotiate Augh using python GSSAPI""" + def __init__(self, target_host, ccache_name=None): + self.context = None + self.target_host = target_host + self.ccache_name = ccache_name + + def __call__(self, request): + self.initial_step(request) + request.register_hook('response', self.handle_response) + return request + + def deregister(self, response): + response.request.deregister_hook('response', self.handle_response) + + def _get_negotiate_token(self, response): + token = None + if response is not None: + h = response.headers.get('www-authenticate', '') + if h.startswith('Negotiate'): + val = h[h.find('Negotiate') + len('Negotiate'):].strip() + if len(val) > 0: + token = b64decode(val) + return token + + def _set_authz_header(self, request, token): + request.headers['Authorization'] = 'Negotiate ' + b64encode(token) + + def initial_step(self, request, response=None): + if self.context is None: + store = {'ccache': self.ccache_name} + creds = gssapi.Credentials(usage='initiate', store=store) + name = gssapi.Name('HTTP@{0}'.format(self.target_host), + name_type=gssapi.NameType.hostbased_service) + self.context = gssapi.SecurityContext(creds=creds, name=name, + usage='initiate') + + in_token = self._get_negotiate_token(response) + out_token = self.context.step(in_token) + self._set_authz_header(request, out_token) + + def handle_response(self, response, **kwargs): + status = response.status_code + if status >= 400 and status != 401: + return response + + in_token = self._get_negotiate_token(response) + if in_token is not None: + out_token = self.context.step(in_token) + if self.context.complete: + return response + elif not out_token: + return response + + self._set_authz_header(response.request, out_token) + # use response so we can make another request + _ = response.content # pylint: disable=unused-variable + response.raw.release_conn() + newresp = response.connection.send(response.request, **kwargs) + newresp.history.append(response) + return self.handle_response(newresp, **kwargs) - def logout(self, session_data): - ''' - The current user has requested to be logged out. To accomplish - this we remove the user's kerberos credentials from their - session. This does not destroy the session, it just prevents - it from being used for fast authentication. Because the - credentials are no longer in the session cache any future - attempt will require the acquisition of credentials using one - of the login mechanisms. - ''' - - if 'ccache_data' in session_data: - self.debug('AuthManager.logout.%s: deleting ccache_data', self.name) - del session_data['ccache_data'] - else: - self.error('AuthManager.logout.%s: session_data does not contain ccache_data', self.name) + return response -class KerberosSession(object): +class KerberosSession(HTTP_Status): ''' Functionally shared by all RPC handlers using both sessions and Kerberos. This class must be implemented as a mixin class rather @@ -558,101 +592,44 @@ class KerberosSession(object): needing this do not share a common base class. ''' - def kerb_session_on_finalize(self): - ''' - Initialize values from the Env configuration. - - Why do it this way and not simply reference - api.env.session_auth_duration? Because that config item cannot - be used directly, it must be parsed and converted to an - integer. It would be inefficient to reparse it on every - request. So we parse it once and store the result in the class - instance. - ''' - # Set the session expiration time - try: - seconds = parse_time_duration(self.api.env.session_auth_duration) - self.session_auth_duration = int(seconds) - self.debug("session_auth_duration: %s", datetime.timedelta(seconds=self.session_auth_duration)) - except Exception as e: - self.session_auth_duration = default_max_session_duration - self.error('unable to parse session_auth_duration, defaulting to %d: %s', - self.session_auth_duration, e) - - def update_session_expiration(self, session_data, krb_endtime): - ''' - Each time a session is created or accessed we need to update - it's expiration time. The expiration time is set inside the - session_data. - - :parameters: - session_data - The session data whose expiration is being updatded. - krb_endtime - The UNIX timestamp for when the Kerberos credentials expire. - :returns: - None - ''' - - # Account for clock skew and/or give us some time leeway - krb_expiration = krb_endtime - krb_ticket_expiration_threshold - - # Set the session expiration time - session_mgr = get_session_mgr() - session_mgr.set_session_expiration_time(session_data, - duration=self.session_auth_duration, - max_age=krb_expiration, - duration_type=self.api.env.session_duration_type) - def finalize_kerberos_acquisition(self, who, ccache_name, environ, start_response, headers=None): if headers is None: headers = [] - # Retrieve the session data (or newly create) - session_mgr = get_session_mgr() - session_data = session_mgr.load_session_data(environ.get('HTTP_COOKIE')) - session_id = session_data['session_id'] - - self.debug('finalize_kerberos_acquisition: %s ccache_name="%s" session_id="%s"', - who, ccache_name, session_id) - - # Copy the ccache file contents into the session data - session_data['ccache_data'] = load_ccache_data(ccache_name) - - # Set when the session will expire - creds = get_credentials(ccache_name=ccache_name) - endtime = creds.lifetime + time.time() - self.update_session_expiration(session_data, endtime) - - # Store the session data now that it's been updated with the ccache - session_mgr.store_session_data(session_data) - - # The request is finished with the ccache, destroy it. - release_ipa_ccache(ccache_name) + # Connect back to ourselves to get mod_auth_gssapi to + # generate a cookie for us. + try: + target = self.api.env.host + r = requests.get('http://{0}/ipa/session/cookie'.format(target), + auth=NegotiateAuth(target, ccache_name)) + session_cookie = r.cookies.get("ipa_session") + if not session_cookie: + raise ValueError('No session cookie found') + except Exception as e: + return self.unauthorized(environ, start_response, + str(e), + 'Authentication failed') - # Return success and set session cookie - session_cookie = session_mgr.generate_cookie('/ipa', session_id, - session_data['session_expiration_timestamp']) - headers.append(('Set-Cookie', session_cookie)) + headers.append(('IPASESSION', session_cookie)) start_response(HTTP_STATUS_SUCCESS, headers) return [''] -class KerberosWSGIExecutioner(WSGIExecutioner, HTTP_Status, KerberosSession): +class KerberosWSGIExecutioner(WSGIExecutioner, KerberosSession): """Base class for xmlserver and jsonserver_kerb """ def _on_finalize(self): super(KerberosWSGIExecutioner, self)._on_finalize() - self.kerb_session_on_finalize() def __call__(self, environ, start_response): self.debug('KerberosWSGIExecutioner.__call__:') user_ccache=environ.get('KRB5CCNAME') - headers = [('Content-Type', '%s; charset=utf-8' % self.content_type)] + self.headers = [('Content-Type', + '%s; charset=utf-8' % self.content_type)] if user_ccache is None: @@ -664,18 +641,19 @@ class KerberosWSGIExecutioner(WSGIExecutioner, HTTP_Status, KerberosSession): 'KRB5CCNAME not defined in HTTP request environment') return self.marshal(None, CCacheError()) + + logout_cookie = getattr(context, 'logout_cookie', None) + if logout_cookie: + self.headers.append(('IPASESSION', logout_cookie)) + try: self.create_context(ccache=user_ccache) response = super(KerberosWSGIExecutioner, self).__call__( environ, start_response) - session_data = getattr(context, 'session_data', None) - if (session_data is None and self.env.context != 'lite'): - self.finalize_kerberos_acquisition( - 'xmlserver', user_ccache, environ, start_response, headers) except PublicError as e: status = HTTP_STATUS_SUCCESS response = status.encode('utf-8') - start_response(status, headers) + start_response(status, self.headers) return self.marshal(None, e) finally: destroy_context() @@ -773,14 +751,9 @@ class jsonserver_session(jsonserver, KerberosSession): def __init__(self, api): super(jsonserver_session, self).__init__(api) - name = '{0}_{1}'.format(self.__class__.__name__, id(self)) - auth_mgr = AuthManagerKerb(name) - session_mgr = get_session_mgr() - session_mgr.auth_mgr.register(auth_mgr.name, auth_mgr) def _on_finalize(self): super(jsonserver_session, self)._on_finalize() - self.kerb_session_on_finalize() def need_login(self, start_response): status = '401 Unauthorized' @@ -798,68 +771,32 @@ class jsonserver_session(jsonserver, KerberosSession): self.debug('WSGI jsonserver_session.__call__:') - # Load the session data - session_mgr = get_session_mgr() - session_data = session_mgr.load_session_data(environ.get('HTTP_COOKIE')) - session_id = session_data['session_id'] - - self.debug('jsonserver_session.__call__: session_id=%s start_timestamp=%s access_timestamp=%s expiration_timestamp=%s', - session_id, - fmt_time(session_data['session_start_timestamp']), - fmt_time(session_data['session_access_timestamp']), - fmt_time(session_data['session_expiration_timestamp'])) - - ccache_data = session_data.get('ccache_data') + ccache_name = environ.get('KRB5CCNAME') # Redirect to login if no Kerberos credentials - if ccache_data is None: + if ccache_name is None: self.debug('no ccache, need login') return self.need_login(start_response) - ipa_ccache_name = bind_ipa_ccache(ccache_data) - # Redirect to login if Kerberos credentials are expired - creds = get_credentials_if_valid(ccache_name=ipa_ccache_name) + creds = get_credentials_if_valid(ccache_name=ccache_name) if not creds: self.debug('ccache expired, deleting session, need login') # The request is finished with the ccache, destroy it. - release_ipa_ccache(ipa_ccache_name) return self.need_login(start_response) - # Update the session expiration based on the Kerberos expiration - endtime = creds.lifetime + time.time() - self.update_session_expiration(session_data, endtime) - - # Store the session data in the per-thread context - setattr(context, 'session_data', session_data) + # Store the ccache name in the per-thread context + setattr(context, 'ccache_name', ccache_name) # This may fail if a ticket from wrong realm was handled via browser try: - self.create_context(ccache=ipa_ccache_name) + self.create_context(ccache=ccache_name) except ACIError as e: return self.unauthorized(environ, start_response, str(e), 'denied') try: response = super(jsonserver_session, self).__call__(environ, start_response) finally: - # Kerberos may have updated the ccache data during the - # execution of the command therefore we need refresh our - # copy of it in the session data so the next command sees - # the same state of the ccache. - # - # However we must be careful not to restore the ccache - # data in the session data if it was explicitly deleted - # during the execution of the command. For example the - # logout command removes the ccache data from the session - # data to invalidate the session credentials. - - if 'ccache_data' in session_data: - session_data['ccache_data'] = load_ccache_data(ipa_ccache_name) - - # The request is finished with the ccache, destroy it. - release_ipa_ccache(ipa_ccache_name) - # Store the session data. - session_mgr.store_session_data(session_data) destroy_context() return response @@ -873,13 +810,12 @@ class jsonserver_kerb(jsonserver, KerberosWSGIExecutioner): key = '/json' -class KerberosLogin(Backend, KerberosSession, HTTP_Status): +class KerberosLogin(Backend, KerberosSession): key = None def _on_finalize(self): super(KerberosLogin, self)._on_finalize() self.api.Backend.wsgi_dispatch.mount(self, self.key) - self.kerb_session_on_finalize() def __call__(self, environ, start_response): self.debug('WSGI KerberosLogin.__call__:') @@ -901,7 +837,7 @@ class login_x509(KerberosLogin): key = '/session/login_x509' -class login_password(Backend, KerberosSession, HTTP_Status): +class login_password(Backend, KerberosSession): content_type = 'text/plain' key = '/session/login_password' @@ -909,7 +845,6 @@ class login_password(Backend, KerberosSession, HTTP_Status): def _on_finalize(self): super(login_password, self)._on_finalize() self.api.Backend.wsgi_dispatch.mount(self, self.key) - self.kerb_session_on_finalize() def __call__(self, environ, start_response): self.debug('WSGI login_password.__call__:') @@ -1243,14 +1178,9 @@ class xmlserver_session(xmlserver, KerberosSession): def __init__(self, api): super(xmlserver_session, self).__init__(api) - name = '{0}_{1}'.format(self.__class__.__name__, id(self)) - auth_mgr = AuthManagerKerb(name) - session_mgr = get_session_mgr() - session_mgr.auth_mgr.register(auth_mgr.name, auth_mgr) def _on_finalize(self): super(xmlserver_session, self)._on_finalize() - self.kerb_session_on_finalize() def need_login(self, start_response): status = '401 Unauthorized' @@ -1268,64 +1198,26 @@ class xmlserver_session(xmlserver, KerberosSession): self.debug('WSGI xmlserver_session.__call__:') - # Load the session data - session_mgr = get_session_mgr() - session_data = session_mgr.load_session_data(environ.get('HTTP_COOKIE')) - session_id = session_data['session_id'] - - self.debug('xmlserver_session.__call__: session_id=%s start_timestamp=%s access_timestamp=%s expiration_timestamp=%s', - session_id, - fmt_time(session_data['session_start_timestamp']), - fmt_time(session_data['session_access_timestamp']), - fmt_time(session_data['session_expiration_timestamp'])) - - ccache_data = session_data.get('ccache_data') + ccache_name = environ.get('KRB5CCNAME') # Redirect to /ipa/xml if no Kerberos credentials - if ccache_data is None: + if ccache_name is None: self.debug('xmlserver_session.__call_: no ccache, need TGT') return self.need_login(start_response) - ipa_ccache_name = bind_ipa_ccache(ccache_data) - # Redirect to /ipa/xml if Kerberos credentials are expired - creds = get_credentials_if_valid(ccache_name=ipa_ccache_name) + creds = get_credentials_if_valid(ccache_name=ccache_name) if not creds: self.debug('xmlserver_session.__call_: ccache expired, deleting session, need login') # The request is finished with the ccache, destroy it. - release_ipa_ccache(ipa_ccache_name) return self.need_login(start_response) - # Update the session expiration based on the Kerberos expiration - endtime = creds.lifetime + time.time() - self.update_session_expiration(session_data, endtime) - # Store the session data in the per-thread context - setattr(context, 'session_data', session_data) - - environ['KRB5CCNAME'] = ipa_ccache_name + setattr(context, 'ccache_name', ccache_name) try: response = super(xmlserver_session, self).__call__(environ, start_response) finally: - # Kerberos may have updated the ccache data during the - # execution of the command therefore we need refresh our - # copy of it in the session data so the next command sees - # the same state of the ccache. - # - # However we must be careful not to restore the ccache - # data in the session data if it was explicitly deleted - # during the execution of the command. For example the - # logout command removes the ccache data from the session - # data to invalidate the session credentials. - - if 'ccache_data' in session_data: - session_data['ccache_data'] = load_ccache_data(ipa_ccache_name) - - # The request is finished with the ccache, destroy it. - release_ipa_ccache(ipa_ccache_name) - # Store the session data. - session_mgr.store_session_data(session_data) destroy_context() return response diff --git a/ipaserver/session.py b/ipaserver/session.py index 0f3a9ad58..cdf8995bc 100644 --- a/ipaserver/session.py +++ b/ipaserver/session.py @@ -16,1204 +16,19 @@ # 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 random import os -import re -import time -import io -# pylint: disable=import-error -from six.moves.urllib.parse import urlparse -# pylint: enable=import-error - -from ipalib import errors -from ipalib.text import _ -from ipapython.ipa_log_manager import root_logger, log_mgr -from ipalib import api -from ipaplatform.paths import paths +from ipalib.request import context from ipalib.krb_utils import ( krb5_parse_ccache, - krb5_unparse_ccache) -from ipapython.cookie import Cookie - -__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 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 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_gssapi (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_gssapi is configured to protect /ipa/json, replies 401 - authenticate negotiate. - - 4. Client resends with credentials - - 5. mod_auth_gssapi 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_gssapi. 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_gssapi replies 401 negotiate on /ipa/login - - 7. client sends credentials to /ipa/login - - 8. mod_auth_gssapi validates credentials - - a. if valid - - - mod_auth_gssapi 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_gssapi 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_gssapi 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_gssapi. Everything else remains the same. - -''' - -#------------------------------------------------------------------------------- - -default_max_session_duration = 60*60 # number of seconds - -ISO8601_DATETIME_FMT = '%Y-%m-%dT%H:%M:%S' # FIXME, this should be defined elsewhere -def fmt_time(timestamp): - return time.strftime(ISO8601_DATETIME_FMT, time.localtime(timestamp)) - -#------------------------------------------------------------------------------- - -class AuthManager(object): - ''' - This class is an abstract base class and is meant to be subclassed - to provide actual functionality. The purpose is to encapsulate all - the callbacks one might need to manage authenticaion. Different - authentication mechanisms will instantiate a subclass of this and - register it with the SessionAuthManger. When an authentication - event occurs the matching method will be called for each - registered class. This allows the SessionAuthManager to notify - interested parties. - ''' - - def __init__(self, name): - log_mgr.get_logger(self, True) - self.name = name - - - def logout(self, session_data): - ''' - Called when a user requests to be logged out of their session. - - :parameters: - session_data - The current session data - :returns: - None - ''' - self.debug('AuthManager.logout.%s:', self.name) - -class SessionAuthManager(object): - - def __init__(self): - log_mgr.get_logger(self, True) - self.auth_managers = {} - - def register(self, name, auth_mgr): - self.debug('SessionAuthManager.register: name=%s', name) - - existing_mgr = self.auth_managers.get(name) - if existing_mgr is not None: - raise KeyError('cannot register auth manager named "%s" one already exists, name="%s" object=%s', - name, existing_mgr.name, repr(existing_mgr)) - - if not isinstance(auth_mgr, AuthManager): - raise TypeError('auth_mgr must be an instance of AuthManager, not %s', - auth_mgr.__class__.__name__) - - self.auth_managers[name] = auth_mgr - - - def unregister(self, name): - self.debug('SessionAuthManager.unregister: name=%s', name) - - if name not in self.auth_managers: - raise KeyError('cannot unregister auth manager named "%s", does not exist', - name) - del self.auth_managers[name] - - - def logout(self, session_data): - self.debug('SessionAuthManager.logout:') - - for auth_mgr in self.auth_managers.values(): - try: - auth_mgr.logout(session_data) - except Exception as e: - self.error('%s auth_mgr logout failed: %s', auth_mgr.name, e) - -#------------------------------------------------------------------------------- - -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() - self.auth_mgr = SessionAuthManager() - - def generate_session_id(self, n_bits=128): - ''' - 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 = paths.VAR_RUN_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].decode()) - 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_data = self.get_session_data(session_id) - 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_access_timestamp - Timestamp when the session was last accessed. - 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_access_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_data(self, session_id): - ''' - Given a session id retrieve the session data associated with it. - If no session data exists for the session id return None. - - :parameters: - session_id - The session id whose session data is desired. - :returns: - Session data if found, None otherwise. - ''' - session_key = self.session_key(session_id) - session_data = self.mc.get(session_key) - - if session_data is not None: - # update the access timestamp - now = time.time() - session_data['session_access_timestamp'] = now - - return session_data - - 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. - ''' - - if cookie_header is None: - return None - - session_id = None - - try: - session_cookie = Cookie.get_named_cookie_from_string(cookie_header, self.session_cookie_name) - except Exception: - session_cookie = None - if session_cookie: - session_id = session_cookie.value - - if session_id is None: - self.debug('no session cookie found') - else: - 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_data = self.get_session_data(session_id) - 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) - - # update the access timestamp - now = time.time() - session_data['session_access_timestamp'] = now - - session_expiration_timestamp = session_data['session_expiration_timestamp'] - - self.debug('store session: session_id=%s start_timestamp=%s access_timestamp=%s expiration_timestamp=%s', - session_id, - fmt_time(session_data['session_start_timestamp']), - fmt_time(session_data['session_access_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, expiration=None, 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 - ''' - - if not expiration: # Catch zero unix timestamps - expiration = None - - cookie = Cookie(self.session_cookie_name, session_id, - domain=urlparse(api.env.xmlrpc_uri).netloc, - path=url_path, httponly=True, secure=True, - expires=expiration) - if add_header: - result = 'Set-Cookie: %s' % cookie - else: - result = str(cookie) - - return result - - def set_session_expiration_time(self, session_data, - duration=default_max_session_duration, - max_age=None, duration_type='inactivity_timeout'): - ''' - 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. - - There are two ways the expiration timestamp can be computed: - - from_start - A session has a fixed duration beginning with the start of - the session. The session expires when the duration - interval has elapsed relative to the start of the session. - inactivity_timeout - A session times out after a period of inactivity. The - expiration time is advanced by the value of the duration - interval everytime the session is updated. - - After the expiration is computed it may be capped at a maximum - value due to other constraints (e.g. authentication credential - expiration). If the optional max_age parameter is specified - then 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. - duration - 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 duration == 0 and max_age is None: - # No expiration - expiration = 0 - session_data['session_expiration_timestamp'] = expiration - return expiration - - if duration_type == 'inactivity_timeout': - now = time.time() - session_data['session_access_timestamp'] = now - expiration = now + duration - elif duration_type == 'from_start': - session_start_timestamp = session_data['session_start_timestamp'] - expiration = session_start_timestamp + duration - else: - # Don't throw an exception, it's critical the session be - # given some expiration, instead log the error and execute - # a default action of expiring the session 5 minutes after - # it was initiated (similar to from_start but with - # hardcoded duration) - default = 60*5 - self.warning('unknown session duration_type (%s), defaulting to %s seconds from session start', - duration_type, default) - session_start_timestamp = session_data['session_start_timestamp'] - expiration = session_start_timestamp + default - - # Cap the expiration if max_age is specified - if max_age is not None: - expiration = min(expiration, max_age) - - session_data['session_expiration_timestamp'] = expiration - - self.debug('set_session_expiration_time: duration_type=%s duration=%s max_age=%s expiration=%s (%s)', - duration_type, duration, max_age, expiration, fmt_time(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) + krb5_unparse_ccache +) +from ipaplatform.paths import paths -#------------------------------------------------------------------------------- -krbccache_dir =paths.IPA_MEMCACHED_DIR +krbccache_dir = paths.IPA_HTTPD_DIR krbccache_prefix = 'krbcc_' -def _get_krbccache_pathname(): - return os.path.join(krbccache_dir, '%s%s' % (krbccache_prefix, os.getpid())) def get_ipa_ccache_name(scheme='FILE'): if scheme == 'FILE': @@ -1225,62 +40,11 @@ def get_ipa_ccache_name(scheme='FILE'): return ccache_name -def load_ccache_data(ccache_name): - scheme, name = krb5_parse_ccache(ccache_name) - if scheme == 'FILE': - root_logger.debug('reading ccache data from file "%s"', name) - with io.open(name, "rb") as src: - ccache_data = src.read() - return ccache_data - else: - raise ValueError('ccache scheme "%s" unsupported (%s)', scheme, ccache_name) - -def bind_ipa_ccache(ccache_data, scheme='FILE'): - if scheme == 'FILE': - name = _get_krbccache_pathname() - root_logger.debug('storing ccache data into file "%s"', name) - with io.open(name, 'wb') as dst: - dst.write(ccache_data) - else: - raise ValueError('ccache scheme "%s" unsupported', scheme) - - ccache_name = krb5_unparse_ccache(scheme, name) - os.environ['KRB5CCNAME'] = ccache_name - return ccache_name - -def release_ipa_ccache(ccache_name): - ''' - Stop using the current request's ccache. - * Remove KRB5CCNAME from the enviroment - * Remove the ccache file from the file system - - Note, we do not demand any of these elements exist, but if they - do we'll remove them. - ''' - - if 'KRB5CCNAME' in os.environ: - if ccache_name != os.environ['KRB5CCNAME']: - root_logger.error('release_ipa_ccache: ccache_name (%s) != KRB5CCNAME environment variable (%s)', - ccache_name, os.environ['KRB5CCNAME']) - del os.environ['KRB5CCNAME'] - else: - root_logger.debug('release_ipa_ccache: KRB5CCNAME environment variable not set') - - scheme, name = krb5_parse_ccache(ccache_name) - if scheme == 'FILE': - if os.path.exists(name): - try: - os.unlink(name) - except Exception as e: - root_logger.error('unable to delete session ccache file "%s", %s', name, e) - else: - raise ValueError('ccache scheme "%s" unsupported (%s)', scheme, ccache_name) - -_session_mgr = None - - -def get_session_mgr(): - global _session_mgr - if _session_mgr is None: - _session_mgr = MemcacheSessionManager() - return _session_mgr +def logout(ccache_name=None): + if ccache_name is None: + ccache_name = getattr(context, 'ccache_name', None) + if ccache_name is not None: + scheme, name = krb5_parse_ccache(ccache_name) + if scheme == 'FILE': + os.unlink(name) + setattr(context, 'logout_cookie', '') diff --git a/ipaserver/setup.py b/ipaserver/setup.py index 1f1b424d2..95ba5ebfc 100755 --- a/ipaserver/setup.py +++ b/ipaserver/setup.py @@ -58,7 +58,6 @@ if __name__ == '__main__': "netaddr", "pyasn1", "pyldap", - "python-memcached", "python-nss", "six", # not available on PyPI diff --git a/pylint_plugins.py b/pylint_plugins.py index fc2ce9bb3..45adf7132 100644 --- a/pylint_plugins.py +++ b/pylint_plugins.py @@ -209,9 +209,6 @@ ipa_class_members = { 'ipaserver.rpcserver.KerberosSession': [ fake_api, ] + LOGGING_ATTRS, - 'ipaserver.session.AuthManager': LOGGING_ATTRS, - 'ipaserver.session.SessionAuthManager': LOGGING_ATTRS, - 'ipaserver.session.SessionManager': LOGGING_ATTRS, 'ipatests.test_integration.base.IntegrationTest': [ 'domain', {'master': [ |