This patch is a prerequisite for patch 801 which will follow. It was
developed to enable to use ipalib RPC client in Web UI tests. Plus it
will enable to significantly speed up Web UI tests suite (if preparation
of data is transformed to use this method).
Partly related https://fedorahosted.org/freeipa/ticket/4772 and
https://fedorahosted.org/freeipa/ticket/4307
Leverage session support to enable forms-based authenticate in rpc client.
In order to do that session support in KerbTransport was moved to new
SessionTransport. RPCClient.create_connection is then modified to
force forms-based auth if new optional options - user and password are
specified. For this case SessionTransport is used and user is
authenticated by calling
'https://ipa.server/ipa/session/login_password'. Session cookie is
stored and used in subsequent calls.
This feature is usable for use cases where one wants to call the API
without being on ipa client. Non-being on ipa client also means that
IPA's NSS database and configuration is not available. Therefore one
has to define "~/.ipa/default.conf" in a similar way as ipa client
does and prepare a NSS database with IPA CA cert.
Usage:
api.Backend.rpcclient.connect(
nss_dir=my_nss_dir_path,
user=user,
password=password
)
It's possible to switch users with:
api.Backend.rpcclient.disconnect()
api.Backend.rpcclient.connect(
nss_dir=my_nss_dir_path,
user=other_user,
password=other_password
)
Or check connection with:
api.Backend.rpcclient.isconnected()
Example: download a CA cert and add it to a new temporary NSS database:
from urllib2 import urlparse
from ipaplatform.paths import paths
from ipapython import certdb, ipautil
from ipapython.ipautil import run
from ipalib import x509
# create new NSSDatabase
tmp_db = certdb.NSSDatabase()
pwd_file = ipautil.write_tmp_file(ipautil.ipa_generate_password())
tmp_db.create_db(pwd_file.name)
# download and add cert
url = urlparse.urlunparse(('http', ipautil.format_netloc(ipa_server),
'/ipa/config/ca.crt', '', '', ''))
stdout, stderr, rc = run([paths.BIN_WGET, "-O", "-", url])
certs = x509.load_certificate_list(stdout, tmp_db.secdir)
ca_certs = [cert.der_data for cert in certs]
for i, cert in enumerate(ca_certs):
tmp_db.add_cert(cert, 'CA certificate %d' % (i + 1), 'C,,')
my_nss_dir_path = tmp_db.secdir
--
Petr Vobornik
From 38ade84e5e6601171ad080fe8c427c78f1d946b8 Mon Sep 17 00:00:00 2001
From: Petr Vobornik <pvobo...@redhat.com>
Date: Wed, 10 Dec 2014 18:57:51 +0100
Subject: [PATCH] rpc-client: add forms based auth support
Leverage session support to enable forms-based authenticate in rpc client.
In order to do that session support in KerbTransport was moved to new
SessionTransport. RPCClient.create_connection is then modified to
force forms-based auth if new optional options - user and password are
specified. For this case SessionTransport is used and user is
authenticated by calling
'https://ipa.server/ipa/session/login_password'. Session cookie is
stored and used in subsequent calls.
This feature is usable for use cases where one wants to call the API
without being on ipa client. Non-being on ipa client also means that
IPA's NSS database and configuration is not available. Therefore one
has to define "~/.ipa/default.conf" in a similar way as ipa client
does and prepare a NSS database with IPA CA cert.
Usage:
api.Backend.rpcclient.connect(
nss_dir=my_nss_dir_path,
user=user,
password=password
)
It's possible to switch users with:
api.Backend.rpcclient.disconnect()
api.Backend.rpcclient.connect(
nss_dir=my_nss_dir_path,
user=other_user,
password=other_password
)
Or check connection with:
api.Backend.rpcclient.isconnected()
Example: download a CA cert and add it to a new temporary NSS database:
from urllib2 import urlparse
from ipaplatform.paths import paths
from ipapython import certdb, ipautil
from ipapython.ipautil import run
from ipalib import x509
# create new NSSDatabase
tmp_db = certdb.NSSDatabase()
pwd_file = ipautil.write_tmp_file(ipautil.ipa_generate_password())
tmp_db.create_db(pwd_file.name)
# download and add cert
url = urlparse.urlunparse(('http', ipautil.format_netloc(ipa_server),
'/ipa/config/ca.crt', '', '', ''))
stdout, stderr, rc = run([paths.BIN_WGET, "-O", "-", url])
certs = x509.load_certificate_list(stdout, tmp_db.secdir)
ca_certs = [cert.der_data for cert in certs]
for i, cert in enumerate(ca_certs):
tmp_db.add_cert(cert, 'CA certificate %d' % (i + 1), 'C,,')
my_nss_dir_path = tmp_db.secdir
---
ipalib/rpc.py | 313 ++++++++++++++++++++++++++++++++++++++--------------------
1 file changed, 204 insertions(+), 109 deletions(-)
diff --git a/ipalib/rpc.py b/ipalib/rpc.py
index 05ef3143324b0f6d260678ad354464511f907eac..7367054e31893b3cb545765223798a6c8fa3db32 100644
--- a/ipalib/rpc.py
+++ b/ipalib/rpc.py
@@ -38,9 +38,11 @@ import os
import locale
import base64
import urllib
+import urllib2
import json
import socket
-from urllib2 import urlparse
+from urllib2 import urlparse, HTTPError
+import cookielib
from xmlrpclib import (Binary, Fault, DateTime, dumps, loads, ServerProxy,
Transport, ProtocolError, MININT, MAXINT)
@@ -52,7 +54,8 @@ from nss.error import NSPRError
from ipalib.backend import Connectible
from ipalib.constants import LDAP_GENERALIZED_TIME_FORMAT
from ipalib.errors import (public_errors, UnknownError, NetworkError,
- KerberosError, XMLRPCMarshallError, JSONError, ConversionError)
+ KerberosError, XMLRPCMarshallError, JSONError, ConversionError,
+ InvalidSessionPassword)
from ipalib import errors, capabilities
from ipalib.request import context, Connection
from ipalib.util import get_current_principal
@@ -502,7 +505,76 @@ class SSLTransport(LanguageAwareTransport):
return self._connection[1]
-class KerbTransport(SSLTransport):
+class SessionTransport(SSLTransport):
+
+ def get_host_info(self, host):
+ """
+ Adds session cookie
+ """
+ (host, extra_headers, x509) = LanguageAwareTransport.get_host_info(self, host)
+
+ if not isinstance(extra_headers, list):
+ extra_headers = []
+
+ session_cookie = getattr(context, 'session_cookie', None)
+ root_logger.debug("SessionTransport, using session: " + str(session_cookie))
+ if session_cookie:
+ extra_headers.append(('Cookie', session_cookie))
+ return (host, extra_headers, x509)
+
+ def store_session_cookie(self, cookie_header):
+ '''
+ Given the contents of a Set-Cookie header scan the header and
+ extract each cookie contained within until the session cookie
+ is located. Examine the session cookie if the domain and path
+ are specified, if not update the cookie with those values from
+ the request URL. Then write the session cookie into the key
+ store for the principal. If the cookie header is None or the
+ session cookie is not present in the header no action is
+ taken.
+
+ Context Dependencies:
+
+ The per thread context is expected to contain:
+ principal
+ The current pricipal the HTTP request was issued for.
+ request_url
+ The URL of the HTTP request.
+
+ '''
+
+ if cookie_header is None:
+ return
+
+ principal = getattr(context, 'principal', None)
+ request_url = getattr(context, 'request_url', None)
+ root_logger.debug("received Set-Cookie '%s'", cookie_header)
+
+ # Search for the session cookie
+ try:
+ session_cookie = Cookie.get_named_cookie_from_string(cookie_header,
+ COOKIE_NAME, request_url)
+ except Exception, e:
+ root_logger.error("unable to parse cookie header '%s': %s", cookie_header, e)
+ return
+
+ if session_cookie is None:
+ return
+
+ cookie_string = str(session_cookie)
+ root_logger.debug("storing cookie '%s' for principal %s", cookie_string, principal)
+ try:
+ update_persistent_client_session_data(principal, cookie_string)
+ except Exception, e:
+ # Not fatal, we just can't use the session cookie we were sent.
+ pass
+
+ def parse_response(self, response):
+ self.store_session_cookie(response.getheader('Set-Cookie'))
+ return SSLTransport.parse_response(self, response)
+
+
+class KerbTransport(SessionTransport):
"""
Handles Kerberos Negotiation authentication to an XML-RPC server.
"""
@@ -530,14 +602,10 @@ class KerbTransport(SSLTransport):
Two things can happen here. If we have a session we will add
a cookie for that. If not we will set an Authorization header.
"""
- (host, extra_headers, x509) = SSLTransport.get_host_info(self, host)
-
- if not isinstance(extra_headers, list):
- extra_headers = []
+ (host, extra_headers, x509) = SessionTransport.get_host_info(self, host)
session_cookie = getattr(context, 'session_cookie', None)
if session_cookie:
- extra_headers.append(('Cookie', session_cookie))
return (host, extra_headers, x509)
# Set the remote host principal
@@ -566,61 +634,10 @@ class KerbTransport(SSLTransport):
def single_request(self, host, handler, request_body, verbose=0):
try:
- return SSLTransport.single_request(self, host, handler, request_body, verbose)
+ return SessionTransport.single_request(self, host, handler, request_body, verbose)
finally:
self.close()
- def store_session_cookie(self, cookie_header):
- '''
- Given the contents of a Set-Cookie header scan the header and
- extract each cookie contained within until the session cookie
- is located. Examine the session cookie if the domain and path
- are specified, if not update the cookie with those values from
- the request URL. Then write the session cookie into the key
- store for the principal. If the cookie header is None or the
- session cookie is not present in the header no action is
- taken.
-
- Context Dependencies:
-
- The per thread context is expected to contain:
- principal
- The current pricipal the HTTP request was issued for.
- request_url
- The URL of the HTTP request.
-
- '''
-
- if cookie_header is None:
- return
-
- principal = getattr(context, 'principal', None)
- request_url = getattr(context, 'request_url', None)
- root_logger.debug("received Set-Cookie '%s'", cookie_header)
-
- # Search for the session cookie
- try:
- session_cookie = Cookie.get_named_cookie_from_string(cookie_header,
- COOKIE_NAME, request_url)
- except Exception, e:
- root_logger.error("unable to parse cookie header '%s': %s", cookie_header, e)
- return
-
- if session_cookie is None:
- return
-
- cookie_string = str(session_cookie)
- root_logger.debug("storing cookie '%s' for principal %s", cookie_string, principal)
- try:
- update_persistent_client_session_data(principal, cookie_string)
- except Exception, e:
- # Not fatal, we just can't use the session cookie we were sent.
- pass
-
- def parse_response(self, response):
- self.store_session_cookie(response.getheader('Set-Cookie'))
- return SSLTransport.parse_response(self, response)
-
class DelegatedKerbTransport(KerbTransport):
"""
@@ -758,24 +775,46 @@ class RPCClient(Connectible):
setattr(context, 'session_cookie', session_cookie.http_cookie())
# Form the session URL by substituting the session path into the original URL
- scheme, netloc, path, params, query, fragment = urlparse.urlparse(original_url)
+ return self.get_session_url(original_url)
+
+ def get_session_url(self, url):
+ scheme, netloc, path, params, query, fragment = urlparse.urlparse(url)
path = self.session_path
session_url = urlparse.urlunparse((scheme, netloc, path, params, query, fragment))
-
return session_url
+ def destroy_session(self, principal):
+ if hasattr(context, 'session_cookie'):
+ delattr(context, 'session_cookie')
+ try:
+ delete_persistent_client_session_data(principal)
+ except Exception, e:
+ # This shouldn't happen if we have a session but it isn't fatal.
+ pass
+
def create_connection(self, ccache=None, verbose=0, fallback=True,
- delegate=False, nss_dir=None):
- try:
- rpc_uri = self.env[self.env_rpc_uri_key]
+ delegate=False, nss_dir=None, user=None, password=None):
+ """
+ Create connection
+ """
+
+ setattr(context, 'user', user)
+ setattr(context, 'password', password)
+ use_krb = not user or not password
+
+ rpc_uri = self.env[self.env_rpc_uri_key]
+ if use_krb:
principal = get_current_principal()
- setattr(context, 'principal', principal)
+ else:
+ principal = user
+ setattr(context, 'principal', principal)
+ try:
# We have a session cookie, try using the session URI to see if it
# is still valid
if not delegate:
rpc_uri = self.apply_session_cookie(rpc_uri)
except ValueError:
- # No session key, do full Kerberos auth
+ # No session key, do full auth
pass
# This might be dangerous. Use at your own risk!
if nss_dir:
@@ -786,7 +825,9 @@ class RPCClient(Connectible):
kw = dict(allow_none=True, encoding='UTF-8')
kw['verbose'] = verbose
if url.startswith('https://'):
- if delegate:
+ if not use_krb:
+ transport_class = SessionTransport
+ elif delegate:
transport_class = DelegatedKerbTransport
else:
transport_class = KerbTransport
@@ -794,46 +835,41 @@ class RPCClient(Connectible):
transport_class = LanguageAwareTransport
kw['transport'] = transport_class(protocol=self.protocol)
self.log.info('trying %s' % url)
- setattr(context, 'request_url', url)
- serverproxy = self.server_proxy_class(url, **kw)
- if len(urls) == 1:
- # if we have only 1 server and then let the
- # main requester handle any errors. This also means it
- # must handle a 401 but we save a ping.
- return serverproxy
try:
- command = getattr(serverproxy, 'ping')
- try:
- response = command([], {})
- except Fault, e:
- e = decode_fault(e)
- if e.faultCode in errors_by_code:
- error = errors_by_code[e.faultCode]
- raise error(message=e.faultString)
- else:
- raise UnknownError(
- code=e.faultCode,
- error=e.faultString,
- server=url,
- )
- # We don't care about the response, just that we got one
+ if use_krb:
+ setattr(context, 'request_url', url)
+ serverproxy = self.server_proxy_class(url, **kw)
+ if len(urls) == 1:
+ # if we have only 1 server and then let the
+ # main requester handle any errors. This also means it
+ # must handle a 401 but we save a ping.
+ return serverproxy
+ self.krb_auth(serverproxy, url)
+ else:
+ self.forms_auth(url, principal, user, password)
+ # further forms-based communication requires session url
+ if self.session_path not in url:
+ url = self.get_session_url(url)
+ setattr(context, 'request_url', url)
+ serverproxy = self.server_proxy_class(url, **kw)
break
- except KerberosError, krberr:
- # kerberos error on one server is likely on all
- raise errors.KerberosError(major=str(krberr), minor='')
+
except ProtocolError, e:
if hasattr(context, 'session_cookie') and e.errcode == 401:
- # Unauthorized. Remove the session and try again.
- delattr(context, 'session_cookie')
- try:
- delete_persistent_client_session_data(principal)
- except Exception, e:
- # This shouldn't happen if we have a session but it isn't fatal.
- pass
- return self.create_connection(ccache, verbose, fallback, delegate)
+ self.destroy_session(principal)
+ return self.create_connection(
+ ccache, verbose, fallback, delegate, nss_dir,
+ user, password)
if not fallback:
raise
serverproxy = None
+ except HTTPError, e:
+ if e.code == 401:
+ self.destroy_session(principal)
+ if not fallback:
+ raise
+ self.log.info('Connection to %s failed with %s', url, e)
+ serverproxy = None
except Exception, e:
if not fallback:
raise
@@ -846,6 +882,63 @@ class RPCClient(Connectible):
error=', '.join(urls))
return serverproxy
+ def krb_auth(self, serverproxy, url):
+ try:
+ command = getattr(serverproxy, 'ping')
+ try:
+ response = command([], {})
+ except Fault, e:
+ e = decode_fault(e)
+ if e.faultCode in errors_by_code:
+ error = errors_by_code[e.faultCode]
+ raise error(message=e.faultString)
+ else:
+ raise UnknownError(
+ code=e.faultCode,
+ error=e.faultString,
+ server=url,
+ )
+ except KerberosError, krberr:
+ # kerberos error on one server is likely on all
+ raise errors.KerberosError(major=str(krberr), minor='')
+
+ def forms_auth(self, rpc_url, principal, user, password):
+ """
+ Try forms-based authentication.
+ """
+ (scheme, netloc, path, params, query, fragment) = urlparse.urlparse(rpc_url)
+ login_url = urlparse.urlunparse(
+ (scheme, netloc, 'ipa/session/login_password', '', '', ''))
+ self.log.debug('Forms-based authentication for: %s' % rpc_url)
+ self.log.debug('User: %s' % user)
+
+ headers = {
+ 'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
+ 'Referer': 'https://%s/ipa/xml' % str(netloc),
+ }
+ data = {
+ 'user': user,
+ 'password': password,
+ }
+ data = urllib.urlencode(data)
+ handlers = [urllib2.HTTPSHandler()]
+ opener = urllib2.build_opener(*handlers)
+ req = urllib2.Request(login_url, data, headers)
+ response = urllib2.urlopen(req)
+ if response.getcode() == 200:
+ self.log.debug("Forms based auth successfull")
+ session_cookie = Cookie.get_named_cookie_from_string(
+ response.info().getheader('Set-Cookie'),
+ COOKIE_NAME,
+ rpc_url
+ )
+ cookie_string = str(session_cookie)
+ update_persistent_client_session_data(principal, cookie_string)
+ ipa_cookie = session_cookie.http_cookie()
+ setattr(context, 'session_cookie', ipa_cookie)
+ self.log.debug("forms_auth session:" + ipa_cookie)
+
+
def destroy_connection(self):
if sys.version_info >= (2, 7):
conn = getattr(context, self.id, None)
@@ -902,19 +995,21 @@ class RPCClient(Connectible):
session_cookie = getattr(context, 'session_cookie', None)
if session_cookie and e.errcode == 401:
# Unauthorized. Remove the session and try again.
- delattr(context, 'session_cookie')
- try:
- principal = getattr(context, 'principal', None)
- delete_persistent_client_session_data(principal)
- except Exception, e:
- # This shouldn't happen if we have a session but it isn't fatal.
- pass
+ self.destroy_session(getattr(context, 'principal', None))
# Create a new serverproxy with the non-session URI. If there
# is an existing connection we need to save the NSS dbdir so
# we can skip an unnecessary NSS_Initialize() and avoid
# NSS_Shutdown issues.
- serverproxy = self.create_connection(os.environ.get('KRB5CCNAME'), self.env.verbose, self.env.fallback, self.env.delegate)
+ serverproxy = self.create_connection(
+ os.environ.get('KRB5CCNAME'),
+ self.env.verbose,
+ self.env.fallback,
+ self.env.delegate,
+ getattr(context, 'nss_dir', None),
+ getattr(context, 'user', None),
+ getattr(context, 'password', None)
+ )
dbdir = None
current_conn = getattr(context, self.id, None)
--
2.1.0
_______________________________________________
Freeipa-devel mailing list
Freeipa-devel@redhat.com
https://www.redhat.com/mailman/listinfo/freeipa-devel