|
30 | 30 | import socket |
31 | 31 | import ssl |
32 | 32 | import urllib |
| 33 | +import functools |
33 | 34 |
|
34 | 35 | from contextlib import contextmanager |
35 | 36 |
|
@@ -157,6 +158,68 @@ def _handle_auth_error(msg): |
157 | 158 | else: |
158 | 159 | raise |
159 | 160 |
|
| 161 | +def _authentication(request_fun): |
| 162 | + """Decorator to handle autologin and authentication errors. |
| 163 | +
|
| 164 | + *request_fun* is a function taking no arguments that needs to |
| 165 | + be run with this ``Context`` logged into Splunk. |
| 166 | +
|
| 167 | + ``_authentication``'s behavior depends on whether the |
| 168 | + ``autologin`` field of ``Context`` is set to ``True`` or |
| 169 | + ``False``. If it's ``False``, then ``_authentication`` |
| 170 | + aborts if the ``Context`` is not logged in, and raises an |
| 171 | + ``AuthenticationError`` if an ``HTTPError`` of status 401 is |
| 172 | + raised in *request_fun*. If it's ``True``, then |
| 173 | + ``_authentication`` will try at all sensible places to |
| 174 | + log in before issuing the request. |
| 175 | +
|
| 176 | + If ``autologin`` is ``False``, ``_authentication`` makes |
| 177 | + one roundtrip to the server if the ``Context`` is logged in, |
| 178 | + or zero if it is not. If ``autologin`` is ``True``, it's less |
| 179 | + deterministic, and may make at most three roundtrips (though |
| 180 | + that would be a truly pathological case). |
| 181 | +
|
| 182 | + :param request_fun: A function of no arguments encapsulating |
| 183 | + the request to make to the server. |
| 184 | +
|
| 185 | + **Example**:: |
| 186 | +
|
| 187 | + import splunklib.binding as binding |
| 188 | + c = binding.connect(..., autologin=True) |
| 189 | + c.logout() |
| 190 | + def f(): |
| 191 | + c.get("/services") |
| 192 | + return 42 |
| 193 | + print _authentication(f) |
| 194 | + """ |
| 195 | + @functools.wraps(request_fun) |
| 196 | + def wrapper(self, *args, **kwargs): |
| 197 | + if self.token is None: |
| 198 | + # Not yet logged in. |
| 199 | + if self.autologin and self.username and self.password: |
| 200 | + # This will throw an uncaught |
| 201 | + # AuthenticationError if it fails. |
| 202 | + self.login() |
| 203 | + else: |
| 204 | + raise AuthenticationError("Request aborted: not logged in.") |
| 205 | + try: |
| 206 | + # Issue the request |
| 207 | + return request_fun(self, *args, **kwargs) |
| 208 | + except HTTPError as he: |
| 209 | + if he.status == 401 and self.autologin: |
| 210 | + # Authentication failed. Try logging in, and then |
| 211 | + # rerunning the request. If either step fails, throw |
| 212 | + # an AuthenticationError and give up. |
| 213 | + with _handle_auth_error("Autologin failed."): |
| 214 | + self.login() |
| 215 | + with _handle_auth_error("Autologin succeeded, but there was an auth error on next request. Something's very wrong."): |
| 216 | + return request_fun() |
| 217 | + elif he.status == 401 and not self.autologin: |
| 218 | + raise AuthenticationError("Request failed: Session is not logged in.") |
| 219 | + else: |
| 220 | + raise |
| 221 | + return wrapper |
| 222 | + |
160 | 223 | def _authority(scheme=DEFAULT_SCHEME, host=DEFAULT_HOST, port=DEFAULT_PORT): |
161 | 224 | """Construct a URL authority from the given *scheme*, *host*, and *port*. |
162 | 225 |
|
@@ -353,65 +416,7 @@ def connect(self): |
353 | 416 | sock.connect((self.host, self.port)) |
354 | 417 | return sock |
355 | 418 |
|
356 | | - def _authentication(self, request_fun): |
357 | | - """Wrapper to handle autologin and authentication errors. |
358 | | -
|
359 | | - *request_fun* is a function taking no arguments that needs to |
360 | | - be run with this ``Context`` logged into Splunk. |
361 | | -
|
362 | | - ``_authentication``'s behavior depends on whether the |
363 | | - ``autologin`` field of ``Context`` is set to ``True`` or |
364 | | - ``False``. If it's ``False``, then ``_authentication`` |
365 | | - aborts if the ``Context`` is not logged in, and raises an |
366 | | - ``AuthenticationError`` if an ``HTTPError`` of status 401 is |
367 | | - raised in *request_fun*. If it's ``True``, then |
368 | | - ``_authentication`` will try at all sensible places to |
369 | | - log in before issuing the request. |
370 | | -
|
371 | | - If ``autologin`` is ``False``, ``_authentication`` makes |
372 | | - one roundtrip to the server if the ``Context`` is logged in, |
373 | | - or zero if it is not. If ``autologin`` is ``True``, it's less |
374 | | - deterministic, and may make at most three roundtrips (though |
375 | | - that would be a truly pathological case). |
376 | | -
|
377 | | - :param request_fun: A function of no arguments encapsulating |
378 | | - the request to make to the server. |
379 | | -
|
380 | | - **Example**:: |
381 | | -
|
382 | | - import splunklib.binding as binding |
383 | | - c = binding.connect(..., autologin=True) |
384 | | - c.logout() |
385 | | - def f(): |
386 | | - c.get("/services") |
387 | | - return 42 |
388 | | - print _authentication(f) |
389 | | - """ |
390 | | - if self.token is None: |
391 | | - # Not yet logged in. |
392 | | - if self.autologin: |
393 | | - # This will throw an uncaught |
394 | | - # AuthenticationError if it fails. |
395 | | - self.login() |
396 | | - else: |
397 | | - raise AuthenticationError("Request aborted: not logged in.") |
398 | | - try: |
399 | | - # Issue the request |
400 | | - return request_fun() |
401 | | - except HTTPError as he: |
402 | | - if he.status == 401 and self.autologin: |
403 | | - # Authentication failed. Try logging in, and then |
404 | | - # rerunning the request. If either step fails, throw |
405 | | - # an AuthenticationError and give up. |
406 | | - with _handle_auth_error("Autologin failed."): |
407 | | - self.login() |
408 | | - with _handle_auth_error("Autologin succeeded, but there was an auth error on next request. Something's very wrong."): |
409 | | - return request_fun() |
410 | | - elif he.status == 401 and not self.autologin: |
411 | | - raise AuthenticationError("Request failed: Session is not logged in.") |
412 | | - else: |
413 | | - raise |
414 | | - |
| 419 | + @_authentication |
415 | 420 | def delete(self, path_segment, owner=None, app=None, sharing=None, **query): |
416 | 421 | """DELETE at *path_segment* with the given namespace and query. |
417 | 422 |
|
@@ -454,12 +459,11 @@ def delete(self, path_segment, owner=None, app=None, sharing=None, **query): |
454 | 459 | c.logout() |
455 | 460 | c.delete('apps/local') # raises AuthenticationError |
456 | 461 | """ |
457 | | - def f(): |
458 | | - path = self.authority + self._abspath(path_segment, owner=owner, |
459 | | - app=app, sharing=sharing) |
460 | | - return self.http.delete(path, self._auth_headers, **query) |
461 | | - return self._authentication(f) |
| 462 | + path = self.authority + self._abspath(path_segment, owner=owner, |
| 463 | + app=app, sharing=sharing) |
| 464 | + return self.http.delete(path, self._auth_headers, **query) |
462 | 465 |
|
| 466 | + @_authentication |
463 | 467 | def get(self, path_segment, owner=None, app=None, sharing=None, **query): |
464 | 468 | """GET from *path_segment* with the given namespace and query. |
465 | 469 |
|
@@ -502,12 +506,11 @@ def get(self, path_segment, owner=None, app=None, sharing=None, **query): |
502 | 506 | c.logout() |
503 | 507 | c.get('apps/local') # raises AuthenticationError |
504 | 508 | """ |
505 | | - def f(): |
506 | | - path = self.authority + self._abspath(path_segment, owner=owner, |
507 | | - app=app, sharing=sharing) |
508 | | - return self.http.get(path, self._auth_headers, **query) |
509 | | - return self._authentication(f) |
| 509 | + path = self.authority + self._abspath(path_segment, owner=owner, |
| 510 | + app=app, sharing=sharing) |
| 511 | + return self.http.get(path, self._auth_headers, **query) |
510 | 512 |
|
| 513 | + @_authentication |
511 | 514 | def post(self, path_segment, owner=None, app=None, sharing=None, **query): |
512 | 515 | """POST to *path_segment* with the given namespace and query. |
513 | 516 |
|
@@ -553,12 +556,11 @@ def post(self, path_segment, owner=None, app=None, sharing=None, **query): |
553 | 556 | c.post('saved/searches', name='boris', |
554 | 557 | search='search * earliest=-1m | head 1') |
555 | 558 | """ |
556 | | - def f(): |
557 | | - path = self.authority + self._abspath(path_segment, owner=owner, |
558 | | - app=app, sharing=sharing) |
559 | | - return self.http.post(path, self._auth_headers, **query) |
560 | | - return self._authentication(f) |
| 559 | + path = self.authority + self._abspath(path_segment, owner=owner, |
| 560 | + app=app, sharing=sharing) |
| 561 | + return self.http.post(path, self._auth_headers, **query) |
561 | 562 |
|
| 563 | + @_authentication |
562 | 564 | def request(self, path_segment, method="GET", headers=[], body="", |
563 | 565 | owner=None, app=None, sharing=None): |
564 | 566 | """Issue an arbitrary HTTP request to *path_segment*. |
@@ -605,24 +607,24 @@ def request(self, path_segment, method="GET", headers=[], body="", |
605 | 607 | c.logout() |
606 | 608 | c.get('apps/local') # raises AuthenticationError |
607 | 609 | """ |
608 | | - def f(): |
609 | | - path = self.authority \ |
610 | | - + self._abspath(path_segment, owner=owner, |
611 | | - app=app, sharing=sharing) |
612 | | - # all_headers can't be named headers, due to a error in |
613 | | - # Python's implementation of closures. In particular: |
614 | | - # def f(x): |
615 | | - # def g(): |
616 | | - # x = x + "a" |
617 | | - # return x |
618 | | - # return g() |
619 | | - # throws UnboundLocalError, claiming that x is not bound. |
620 | | - all_headers = headers + self._auth_headers |
621 | | - return self.http.request(path, |
622 | | - {'method': method, |
623 | | - 'headers': all_headers, |
624 | | - 'body': body}) |
625 | | - return self._authentication(f) |
| 610 | + path = self.authority \ |
| 611 | + + self._abspath(path_segment, owner=owner, |
| 612 | + app=app, sharing=sharing) |
| 613 | + # all_headers can't be named headers, due to how |
| 614 | + # Python implements closures. In particular: |
| 615 | + # def f(x): |
| 616 | + # def g(): |
| 617 | + # x = x + "a" |
| 618 | + # return x |
| 619 | + # return g() |
| 620 | + # throws UnboundLocalError, since x must be either a member of |
| 621 | + # f's local namespace or g's, and cannot switch between them |
| 622 | + # during the run of the function. |
| 623 | + all_headers = headers + self._auth_headers |
| 624 | + return self.http.request(path, |
| 625 | + {'method': method, |
| 626 | + 'headers': all_headers, |
| 627 | + 'body': body}) |
626 | 628 |
|
627 | 629 | def login(self): |
628 | 630 | """Log into the Splunk instance referred to by this ``Context``. |
|
0 commit comments