Skip to content

Commit c3f4110

Browse files
committed
Merge pull request #61 from splunk/feature/authenticationerror_as_httperror
AuthenticationError subclasses HTTPError. Remove breaking change.
2 parents 1a8c7b9 + 344ab32 commit c3f4110

File tree

3 files changed

+65
-27
lines changed

3 files changed

+65
-27
lines changed

CHANGELOG.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
### New features and APIs
66

77
* An `AuthenticationError` exception has been added.
8+
This is a subclass of `HTTPError` so preexisting code that expects HTTP 401
9+
(Unauthorized) will still work.
810

911
* An `"autologin"` argument has been added to the `splunklib.client.connect` and
1012
`splunklib.binding.connect` functions. When set to true, Splunk automatically
@@ -57,9 +59,6 @@
5759

5860
### Breaking changes
5961

60-
* Authentication errors are now reported as `AuthenticationError` instead of as
61-
`HTTPError` with code 401.
62-
6362
* `Job` objects are no longer guaranteed to be ready for querying.
6463
Client code should call the `Job.is_ready` method to determine when it is safe
6564
to access properties on the job.

splunklib/binding.py

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import logging
3333
from datetime import datetime
3434
from functools import wraps
35+
from StringIO import StringIO
3536

3637
from contextlib import contextmanager
3738

@@ -63,16 +64,6 @@ def new_f(*args, **kwargs):
6364
return val
6465
return new_f
6566

66-
# Custom exceptions
67-
class AuthenticationError(Exception):
68-
"""Raised when a login request to Splunk fails.
69-
70-
If your username was unknown or you provided an incorrect password
71-
in a call to :meth:`Context.login` or :meth:`splunklib.client.Service.login`,
72-
this exception is raised.
73-
"""
74-
pass
75-
7667
# Singleton values to eschew None
7768
class _NoAuthenticationToken(object):
7869
"""The value stored in a :class:`Context` or :class:`splunklib.client.Service`
@@ -187,7 +178,7 @@ def _handle_auth_error(msg):
187178
yield
188179
except HTTPError as he:
189180
if he.status == 401:
190-
raise AuthenticationError(msg)
181+
raise AuthenticationError(msg, he)
191182
else:
192183
raise
193184

@@ -234,7 +225,11 @@ def wrapper(self, *args, **kwargs):
234225
# AuthenticationError if it fails.
235226
self.login()
236227
else:
237-
raise AuthenticationError("Request aborted: not logged in.")
228+
# Try the request anyway without authentication.
229+
# Most requests will fail. Some will succeed, such as
230+
# 'GET server/info'.
231+
with _handle_auth_error("Request aborted: not logged in."):
232+
return request_fun(self, *args, **kwargs)
238233
try:
239234
# Issue the request
240235
return request_fun(self, *args, **kwargs)
@@ -248,7 +243,7 @@ def wrapper(self, *args, **kwargs):
248243
with _handle_auth_error("Autologin succeeded, but there was an auth error on next request. Something's very wrong."):
249244
return request_fun()
250245
elif he.status == 401 and not self.autologin:
251-
raise AuthenticationError("Request failed: Session is not logged in.")
246+
raise AuthenticationError("Request failed: Session is not logged in.", he)
252247
else:
253248
raise
254249
return wrapper
@@ -426,12 +421,15 @@ def _auth_headers(self):
426421
427422
:returns: A list of 2-tuples containing key and value
428423
"""
429-
# Ensure the token is properly formatted
430-
if self.token.startswith('Splunk '):
431-
token = self.token
424+
if self.token is _NoAuthenticationToken:
425+
return []
432426
else:
433-
token = 'Splunk %s' % self.token
434-
return [("Authorization", token)]
427+
# Ensure the token is properly formatted
428+
if self.token.startswith('Splunk '):
429+
token = self.token
430+
else:
431+
token = 'Splunk %s' % self.token
432+
return [("Authorization", token)]
435433

436434
def connect(self):
437435
"""Returns an open connection (socket) to the Splunk instance.
@@ -757,7 +755,7 @@ def login(self):
757755
return self
758756
except HTTPError as he:
759757
if he.status == 401:
760-
raise AuthenticationError("Login failed.")
758+
raise AuthenticationError("Login failed.", he)
761759
else:
762760
raise
763761

@@ -874,18 +872,33 @@ def connect(**kwargs):
874872
# handler that wants to read multiple messages can do so.
875873
class HTTPError(Exception):
876874
"""This exception is raised for HTTP responses that return an error."""
877-
def __init__(self, response):
875+
def __init__(self, response, _message=None):
878876
status = response.status
879877
reason = response.reason
880878
body = response.body.read()
881879
detail = XML(body).findtext("./messages/msg")
882880
message = "HTTP %d %s%s" % (
883881
status, reason, "" if detail is None else " -- %s" % detail)
884-
Exception.__init__(self, message)
882+
Exception.__init__(self, _message or message)
885883
self.status = status
886884
self.reason = reason
887885
self.headers = response.headers
888886
self.body = body
887+
self._response = response
888+
889+
class AuthenticationError(HTTPError):
890+
"""Raised when a login request to Splunk fails.
891+
892+
If your username was unknown or you provided an incorrect password
893+
in a call to :meth:`Context.login` or :meth:`splunklib.client.Service.login`,
894+
this exception is raised.
895+
"""
896+
def __init__(self, message, cause):
897+
# Put the body back in the response so that HTTPError's constructor can
898+
# read it again.
899+
cause._response.body = StringIO(cause.body)
900+
901+
HTTPError.__init__(self, cause._response, message)
889902

890903
#
891904
# The HTTP interface used by the Splunk binding layer abstracts the underlying

tests/test_service.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,35 @@ def test_splunk_version(self):
107107
with self.fake_splunk_version(version):
108108
self.assertEqual(version, self.service.splunk_version)
109109

110-
def test_query_without_login(self):
111-
service = Service()
112-
self.assertRaises(AuthenticationError, lambda: service.splunk_version)
110+
def test_query_without_login_raises_auth_error(self):
111+
service = self._create_unauthenticated_service()
112+
self.assertRaises(AuthenticationError, lambda: service.indexes.list())
113+
114+
# This behavior is needed for backward compatibility for code
115+
# prior to the introduction of AuthenticationError
116+
def test_query_without_login_raises_http_401(self):
117+
service = self._create_unauthenticated_service()
118+
try:
119+
service.indexes.list()
120+
self.fail('Expected HTTP 401.')
121+
except HTTPError as he:
122+
if he.status == 401:
123+
# Good
124+
pass
125+
else:
126+
raise
127+
128+
def test_server_info_without_login(self):
129+
service = self._create_unauthenticated_service()
130+
# Should succeed without AuthenticationError
131+
service.info['version']
132+
133+
def _create_unauthenticated_service(self):
134+
return Service(**{
135+
'host': self.opts.get('host', 'localhost'),
136+
'port': self.opts.get('port', 8089),
137+
'scheme': self.opts.get('scheme', 'https')
138+
})
113139

114140
class TestSettings(testlib.SDKTestCase):
115141
def test_read_settings(self):

0 commit comments

Comments
 (0)