Skip to content

Commit 003de93

Browse files
author
Shakeel Mohamed
committed
Merge branch 'feature/cookie-auth' into develop
2 parents 7951707 + c7a20c8 commit 003de93

File tree

7 files changed

+422
-29
lines changed

7 files changed

+422
-29
lines changed

CONTRIBUTING.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Contributing Guidelines
2+
3+
## How to contribute
4+
5+
If you would like to contribute to this project, go here for more information:
6+
7+
* [Splunk and open source][contributions]
8+
* [Individual contributions][indivcontrib]
9+
* [Company contributions][companycontrib]
10+
11+
## Issues & Bug Reports
12+
13+
If you're seeing some unexpected behavior with this project, please create an [issue on GitHub][issues] with the following information:
14+
15+
0. Version of this project you're using (ex: 1.3.1)
16+
0. Platform version (ex: Windows Server 2012)
17+
0. Framework version (ex: Python 2.7.8)
18+
0. Splunk version (ex: 6.2.2)
19+
0. Other relevant information (ex: local/remote environment, Splunk network configuration)
20+
21+
Alternatively, if you have a Splunk question please ask on [Splunk Answers][answers]
22+
23+
## Pull requests
24+
25+
We love to see pull requests!
26+
27+
To create a pull request:
28+
29+
0. Fill out the [Individual Contributor Agreement][indivcontrib].
30+
0. Fork [the repository][repo].
31+
0. Make changes to the **`develop`** branch, preferably with tests.
32+
0. Create a [pull request][pulls] against the **`develop`** branch.
33+
34+
## Contact us
35+
36+
You can reach Splunk support at _[email protected]_ if you have Splunk related questions.
37+
38+
You can reach the Developer Platform team at _[email protected]_.
39+
40+
[contributions]: http://dev.splunk.com/view/opensource/SP-CAAAEDM
41+
[indivcontrib]: http://dev.splunk.com/goto/individualcontributions
42+
[companycontrib]: http://dev.splunk.com/view/companycontributions/SP-CAAAEDR
43+
[answers]: http://answers.splunk.com/
44+
[repo]: https://github.com/splunk/splunk-sdk-python
45+
[issues]: https://github.com/splunk/splunk-sdk-python/issues
46+
[pulls]: https://github.com/splunk/splunk-sdk-python/pulls

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
# License for the specific language governing permissions and limitations
1515
# under the License.
1616

17-
from distutils.core import setup, Command
17+
from setuptools import setup, Command
1818
from contextlib import closing
1919
from fnmatch import fnmatch
2020

splunklib/binding.py

Lines changed: 107 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import urllib
3232
import io
3333
import sys
34+
import Cookie
3435

3536
from datetime import datetime
3637
from functools import wraps
@@ -67,6 +68,45 @@ def new_f(*args, **kwargs):
6768
return new_f
6869

6970

71+
def _parse_cookies(cookie_str, dictionary):
72+
"""Tries to parse any key-value pairs of cookies in a string,
73+
then updates the the dictionary with any key-value pairs found.
74+
75+
**Example**::
76+
dictionary = {}
77+
_parse_cookies('my=value', dictionary)
78+
# Now the following is True
79+
dictionary['my'] == 'value'
80+
81+
:param cookie_str: A string containing "key=value" pairs from an HTTP "Set-Cookie" header.
82+
:type cookie_str: ``str``
83+
:param dictionary: A dictionary to update with any found key-value pairs.
84+
:type dictionary: ``dict``
85+
"""
86+
parsed_cookie = Cookie.SimpleCookie(cookie_str)
87+
for cookie in parsed_cookie.values():
88+
dictionary[cookie.key] = cookie.coded_value
89+
90+
91+
def _make_cookie_header(cookies):
92+
"""
93+
Takes a list of 2-tuples of key-value pairs of
94+
cookies, and returns a valid HTTP ``Cookie``
95+
header.
96+
97+
**Example**::
98+
99+
header = _make_cookie_header([("key", "value"), ("key_2", "value_2")])
100+
# Now the following is True
101+
header == "key=value; key_2=value_2"
102+
103+
:param cookies: A list of 2-tuples of cookie key-value pairs.
104+
:type cookies: ``list`` of 2-tuples
105+
:return: ``str` An HTTP header cookie string.
106+
:rtype: ``str``
107+
"""
108+
return "; ".join("%s=%s" % (key, value) for key, value in cookies)
109+
70110
# Singleton values to eschew None
71111
class _NoAuthenticationToken(object):
72112
"""The value stored in a :class:`Context` or :class:`splunklib.client.Service`
@@ -224,7 +264,8 @@ def f():
224264
"""
225265
@wraps(request_fun)
226266
def wrapper(self, *args, **kwargs):
227-
if self.token is _NoAuthenticationToken:
267+
if self.token is _NoAuthenticationToken and \
268+
not self.has_cookies():
228269
# Not yet logged in.
229270
if self.autologin and self.username and self.password:
230271
# This will throw an uncaught
@@ -390,6 +431,10 @@ class Context(object):
390431
:param app: The app context of the namespace (optional, the default is "None").
391432
:type app: ``string``
392433
:param token: A session token. When provided, you don't need to call :meth:`login`.
434+
:type token: ``string``
435+
:param cookie: A session cookie. When provided, you don't need to call :meth:`login`.
436+
This parameter is only supported for Splunk 6.2+.
437+
:type cookie: ``string``
393438
:param username: The Splunk account username, which is used to
394439
authenticate the Splunk instance.
395440
:type username: ``string``
@@ -405,8 +450,10 @@ class Context(object):
405450
c.login()
406451
# Or equivalently
407452
c = binding.connect(username="boris", password="natasha")
408-
# Of if you already have a session token
453+
# Or if you already have a session token
409454
c = binding.Context(token="atg232342aa34324a")
455+
# Or if you already have a valid cookie
456+
c = binding.Context(cookie="splunkd_8089=...")
410457
"""
411458
def __init__(self, handler=None, **kwargs):
412459
self.http = HttpLib(handler)
@@ -422,18 +469,41 @@ def __init__(self, handler=None, **kwargs):
422469
self.password = kwargs.get("password", "")
423470
self.autologin = kwargs.get("autologin", False)
424471

472+
# Store any cookies in the self.http._cookies dict
473+
if kwargs.has_key("cookie") and kwargs['cookie'] not in [None, _NoAuthenticationToken]:
474+
_parse_cookies(kwargs["cookie"], self.http._cookies)
475+
476+
def get_cookies(self):
477+
"""Gets the dictionary of cookies from the ``HttpLib`` member of this instance.
478+
479+
:return: Dictionary of cookies stored on the ``self.http``.
480+
:rtype: ``dict``
481+
"""
482+
return self.http._cookies
483+
484+
def has_cookies(self):
485+
"""Returns true if the ``HttpLib` member of this instance has at least
486+
one cookie stored.
487+
488+
:return: ``True`` if there is at least one cookie, else ``False``
489+
:rtype: ``bool``
490+
"""
491+
return len(self.get_cookies()) > 0
492+
425493
# Shared per-context request headers
426494
@property
427495
def _auth_headers(self):
428496
"""Headers required to authenticate a request.
429497
430-
Assumes your ``Context`` already has a authentication token,
431-
either provided explicitly or obtained by logging into the
432-
Splunk instance.
498+
Assumes your ``Context`` already has a authentication token or
499+
cookie, either provided explicitly or obtained by logging
500+
into the Splunk instance.
433501
434502
:returns: A list of 2-tuples containing key and value
435503
"""
436-
if self.token is _NoAuthenticationToken:
504+
if self.has_cookies():
505+
return [("Cookie", _make_cookie_header(self.get_cookies().items()))]
506+
elif self.token is _NoAuthenticationToken:
437507
return []
438508
else:
439509
# Ensure the token is properly formatted
@@ -749,17 +819,29 @@ def login(self):
749819
c = binding.Context(...).login()
750820
# Then issue requests...
751821
"""
822+
823+
if self.has_cookies() and \
824+
(not self.username and not self.password):
825+
# If we were passed session cookie(s), but no username or
826+
# password, then login is a nop, since we're automatically
827+
# logged in.
828+
return
829+
752830
if self.token is not _NoAuthenticationToken and \
753831
(not self.username and not self.password):
754832
# If we were passed a session token, but no username or
755833
# password, then login is a nop, since we're automatically
756834
# logged in.
757835
return
836+
837+
# Only try to get a token and updated cookie if username & password are specified
758838
try:
759839
response = self.http.post(
760840
self.authority + self._abspath("/services/auth/login"),
761841
username=self.username,
762-
password=self.password)
842+
password=self.password,
843+
cookie="1") # In Splunk 6.2+, passing "cookie=1" will return the "set-cookie" header
844+
763845
body = response.body.read()
764846
session = XML(body).findtext("./sessionKey")
765847
self.token = "Splunk %s" % session
@@ -771,8 +853,9 @@ def login(self):
771853
raise
772854

773855
def logout(self):
774-
"""Forgets the current session token."""
856+
"""Forgets the current session token, and cookies."""
775857
self.token = _NoAuthenticationToken
858+
self.http._cookies = {}
776859
return self
777860

778861
def _abspath(self, path_segment,
@@ -859,6 +942,9 @@ def connect(**kwargs):
859942
:param token: The current session token (optional). Session tokens can be
860943
shared across multiple service instances.
861944
:type token: ``string``
945+
:param cookie: A session cookie. When provided, you don't need to call :meth:`login`.
946+
This parameter is only supported for Splunk 6.2+.
947+
:type cookie: ``string``
862948
:param username: The Splunk account username, which is used to
863949
authenticate the Splunk instance.
864950
:type username: ``string``
@@ -1003,6 +1089,7 @@ class HttpLib(object):
10031089
"""
10041090
def __init__(self, custom_handler=None):
10051091
self.handler = handler() if custom_handler is None else custom_handler
1092+
self._cookies = {}
10061093

10071094
def delete(self, url, headers=None, **kwargs):
10081095
"""Sends a DELETE request to a URL.
@@ -1112,6 +1199,18 @@ def request(self, url, message, **kwargs):
11121199
response = record(response)
11131200
if 400 <= response.status:
11141201
raise HTTPError(response)
1202+
1203+
# Update the cookie with any HTTP request
1204+
# Initially, assume list of 2-tuples
1205+
key_value_tuples = response.headers
1206+
# If response.headers is a dict, get the key-value pairs as 2-tuples
1207+
# this is the case when using urllib2
1208+
if isinstance(response.headers, dict):
1209+
key_value_tuples = response.headers.items()
1210+
for key, value in key_value_tuples:
1211+
if key.lower() == "set-cookie":
1212+
_parse_cookies(value, self._cookies)
1213+
11151214
return response
11161215

11171216

splunklib/client.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
import socket
6868
import contextlib
6969

70-
from binding import Context, HTTPError, AuthenticationError, namespace, UrlEncoded, _encode
70+
from binding import Context, HTTPError, AuthenticationError, namespace, UrlEncoded, _encode, _make_cookie_header
7171
from data import record
7272
import data
7373

@@ -273,7 +273,6 @@ def _parse_atom_metadata(content):
273273

274274
return record({'access': access, 'fields': fields})
275275

276-
277276
# kwargs: scheme, host, port, app, owner, username, password
278277
def connect(**kwargs):
279278
"""This function connects and logs in to a Splunk instance.
@@ -296,6 +295,9 @@ def connect(**kwargs):
296295
:param `token`: The current session token (optional). Session tokens can be
297296
shared across multiple service instances.
298297
:type token: ``string``
298+
:param cookie: A session cookie. When provided, you don't need to call :meth:`login`.
299+
This parameter is only supported for Splunk 6.2+.
300+
:type cookie: ``string``
299301
:param autologin: When ``True``, automatically tries to log in again if the
300302
session terminates.
301303
:type autologin: ``boolean``
@@ -313,7 +315,9 @@ def connect(**kwargs):
313315
a = s.apps["my_app"]
314316
...
315317
"""
316-
return Service(**kwargs).login()
318+
s = Service(**kwargs)
319+
s.login()
320+
return s
317321

318322

319323
# In preparation for adding Storm support, we added an
@@ -356,6 +360,9 @@ class Service(_BaseService):
356360
:param `token`: The current session token (optional). Session tokens can be
357361
shared across multiple service instances.
358362
:type token: ``string``
363+
:param cookie: A session cookie. When provided, you don't need to call :meth:`login`.
364+
This parameter is only supported for Splunk 6.2+.
365+
:type cookie: ``string``
359366
:param `username`: The Splunk account username, which is used to
360367
authenticate the Splunk instance.
361368
:type username: ``string``
@@ -373,6 +380,8 @@ class Service(_BaseService):
373380
s = client.connect(username="boris", password="natasha")
374381
# Or if you already have a session token
375382
s = client.Service(token="atg232342aa34324a")
383+
# Or if you already have a valid cookie
384+
s = client.Service(cookie="splunkd_8089=...")
376385
"""
377386
def __init__(self, **kwargs):
378387
super(Service, self).__init__(**kwargs)
@@ -1911,16 +1920,23 @@ def attach(self, host=None, source=None, sourcetype=None):
19111920
if sourcetype is not None: args['sourcetype'] = sourcetype
19121921
path = UrlEncoded(PATH_RECEIVERS_STREAM + "?" + urllib.urlencode(args), skip_encode=True)
19131922

1923+
cookie_or_auth_header = "Authorization: %s\r\n" % self.service.token
1924+
1925+
# If we have cookie(s), use them instead of "Authorization: ..."
1926+
if self.service.has_cookies():
1927+
cookie_or_auth_header = "Cookie: %s\r\n" % _make_cookie_header(self.service.get_cookies().items())
1928+
19141929
# Since we need to stream to the index connection, we have to keep
19151930
# the connection open and use the Splunk extension headers to note
19161931
# the input mode
19171932
sock = self.service.connect()
19181933
headers = ["POST %s HTTP/1.1\r\n" % self.service._abspath(path),
19191934
"Host: %s:%s\r\n" % (self.service.host, int(self.service.port)),
19201935
"Accept-Encoding: identity\r\n",
1921-
"Authorization: %s\r\n" % self.service.token,
1936+
cookie_or_auth_header,
19221937
"X-Splunk-Input-Mode: Streaming\r\n",
19231938
"\r\n"]
1939+
19241940
for h in headers:
19251941
sock.write(h)
19261942
return sock

0 commit comments

Comments
 (0)