Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
# Version 1.6.4
## Added features and functionality
+ Added: Optional `session` keyword argument (a `requests.Session` instance) accepted by `OAuth2`, `APIHarnessV2`,
the legacy `APIHarness`, and every Service Class, allowing callers to reuse a single HTTP connection across
login, every API call, token renewal, and logout. FalconPy never closes a session provided this way; the
caller retains ownership of its lifecycle. Behavior is unchanged when this keyword is omitted.
- `_util/_functions.py`
- `_api_request/_request.py`
- `_api_request/_request_connection.py`
- `_auth_object/_interface_config.py`
- `_auth_object/_falcon_interface.py`
- `_auth_object/_uber_interface.py`
- `oauth2.py`
- `_util/_service.py`
- `_util/_uber.py`
- `_service_class/_base_service_class.py`
- `api_complete/_legacy.py`
> Unit testing expanded to complete code coverage.
- `tests/test_session_support.py`
- `tests/test_session_connection_reuse.py`

+ Added: Added [PEP 561](https://peps.python.org/pep-0561/) type stub (`.pyi`) files for every service collection, along with a `py.typed` marker, so type checkers and IDEs can surface method signatures, keyword arguments, and return types. Deprecated and decommissioned methods are annotated with `@deprecated` so editors flag them at call sites.
- `py.typed`
- `*.pyi` (one stub per service collection)
Expand Down
27 changes: 27 additions & 0 deletions samples/authentication/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ The examples in this folder focus on authentication to CrowdStrike's APIs.
- [AES File Crypt](#aes-file-crypt) - Encrypt arbitrary files with AES/CBC
- [AWS Parameter Store](#aws-parameter-store) - CrowdStrike API authentication leveraging AWS Parameter Store for credential storage
- [Token Authentication](#token-authentication) - Token Authentication is the original solution for authenticating to a Service Class, and is still fully supported. This example demonstrates how to use Token Authentication to interact with multiple Service Classes.
- [Session Reuse](#session-reuse) - Reuse a single `requests.Session` for connection pooling across login, every API call, token renewal, and logout.

## Azure Key Vault Authentication
This application demonstrates storing CrowdStrike API credentials within the
Expand Down Expand Up @@ -577,3 +578,29 @@ This sample does not implement command line assistance.

### Example source code
Source code for this example can be found [here](token_authentication_example.py).

---
## Session Reuse
Every FalconPy client accepts an optional `session` keyword argument: an existing
`requests.Session` to reuse for connection pooling across login, every API call,
token renewal, and logout. This avoids repeating the TCP/TLS handshake for each
request. FalconPy never closes a session provided this way; the caller retains
full ownership of its lifecycle.

### Running the program
In order to run this demonstration, you will need access to CrowdStrike API keys with the following scopes:
| Service Collection | Scope |
| :---- | :---- |
| Hosts | __READ__ |

Credentials are provided via the `FALCON_CLIENT_ID` and `FALCON_CLIENT_SECRET` environment variables.

### Execution syntax
This application does not accept command line arguments.

```shell
python3 session_reuse.py
```

### Example source code
Source code for this example can be found [here](session_reuse.py).
56 changes: 56 additions & 0 deletions samples/authentication/session_reuse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""CrowdStrike FalconPy Persistent Session (Connection Reuse) Example.

_______ __ _______ __ __ __
| _ .----.-----.--.--.--.--| | _ | |_.----|__| |--.-----.
|. 1___| _| _ | | | | _ | 1___| _| _| | <| -__|
|. |___|__| |_____|________|_____|____ |____|__| |__|__|__|_____|
|: 1 | |: 1 |
|::.. . | CROWDSTRIKE FALCON |::.. . | FalconPy
`-------' `-------'

Every FalconPy client accepts an optional `session` keyword: an existing
`requests.Session` to reuse for connection pooling across login, every API
call, token renewal, and logout. This avoids repeating the TCP/TLS handshake
for each request, which matters most for workloads issuing many sequential
calls.

FalconPy never closes a session provided this way. The caller retains full
ownership of its lifecycle, most naturally by using it as a context manager
as demonstrated below.

If you share one session across multiple threads, you are responsible for
your own synchronization; requests.Session is not guaranteed safe for
concurrent use without care.

This sample requires API credentials with READ access to the Hosts service
collection, provided via the FALCON_CLIENT_ID and FALCON_CLIENT_SECRET
environment variables.
"""
import os
import requests
from falconpy import OAuth2, Hosts


def main():
"""Demonstrate session reuse across authentication and multiple API calls."""
client_id = os.getenv("FALCON_CLIENT_ID")
client_secret = os.getenv("FALCON_CLIENT_SECRET")

# The session is created (and closed) entirely by the caller.
with requests.Session() as session:
# Login and every request made by this auth_object reuse `session`.
auth = OAuth2(client_id=client_id, client_secret=client_secret, session=session)

# Service Classes constructed from a shared auth_object inherit its session.
hosts = Hosts(auth_object=auth)

for _ in range(3):
response = hosts.query_devices_by_filter(limit=1)
print(f"Status: {response['status_code']}")

auth.logout()
# The session is closed here, by the caller's `with` block, not by FalconPy.


if __name__ == "__main__":
main()
9 changes: 8 additions & 1 deletion src/falconpy/_api_request/_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"""
from typing import Union, Dict, Optional, List, Any
from logging import Logger
import requests
from ._request_behavior import RequestBehavior
from ._request_connection import RequestConnection
from ._request_meta import RequestMeta
Expand Down Expand Up @@ -69,7 +70,8 @@ def __init__(self,
self._connection = RequestConnection(user_agent=initializer.get("user_agent", None),
proxy=initializer.get("proxy", {}),
timeout=initializer.get("timeout", None),
verify=initializer.get("verify", True)
verify=initializer.get("verify", True),
session=initializer.get("session", None)
)
# Behavioral flags that alter the behavior of request processing
self._behavior = RequestBehavior(expand_result=initializer.get("expand_result", False),
Expand Down Expand Up @@ -265,6 +267,11 @@ def proxy(self) -> Optional[Dict[str, str]]:
"""Return the proxy dictionary."""
return self.connection.proxy

@property
def session(self) -> Optional[requests.Session]:
"""Return the requests.Session to use for this request, if one was provided."""
return self.connection.session

@property
def timeout(self) -> Optional[Union[int, tuple]]:
"""Return the timeout from the connection object.."""
Expand Down
2 changes: 2 additions & 0 deletions src/falconpy/_api_request/_request_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"""
from dataclasses import dataclass
from typing import Optional, Dict, Union
import requests


@dataclass
Expand All @@ -51,3 +52,4 @@ class RequestConnection:
verify: bool = True
timeout: Optional[Union[int, tuple]] = None
proxy: Optional[Dict[str, str]] = None
session: Optional[requests.Session] = None
22 changes: 17 additions & 5 deletions src/falconpy/_auth_object/_falcon_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
from contextvars import copy_context
from logging import Logger, getLogger
from typing import Dict, Optional, Union
import requests
from ._base_falcon_auth import BaseFalconAuth
from ._bearer_token import BearerToken
from .._log import LogFacility
Expand Down Expand Up @@ -89,7 +90,8 @@ def __init__(self, # noqa: C901
debug_record_count: Optional[int] = None,
sanitize_log: Optional[bool] = None,
pythonic: Optional[bool] = False,
environment: Optional[Dict[str, str]] = None
environment: Optional[Dict[str, str]] = None,
session: Optional[requests.Session] = None
) -> "FalconInterface":
"""Construct an instance of the FalconInterface class."""
# Set the pythonic behavior mode.
Expand All @@ -102,7 +104,8 @@ def __init__(self, # noqa: C901
proxy=proxy,
timeout=timeout,
user_agent=user_agent,
ssl_verify=ssl_verify
ssl_verify=ssl_verify,
session=session
) # \ o /
# ____ _ _ ___ _ _ ____ _ _ ___ _ ____ ____ ___ _ ____ _ _ |
# |__| | | | |__| |___ |\ | | | | |__| | | | | |\ | / \
Expand Down Expand Up @@ -307,7 +310,7 @@ def _login_handler(self, stateful: bool = True) -> dict:
returned = perform_request(method="POST", endpoint=target_url, data=data_payload,
headers={}, verify=self.ssl_verify, proxy=self.proxy,
timeout=self.timeout, user_agent=self.user_agent,
log_util=self.log, authenticating=True,
session=self.session, log_util=self.log, authenticating=True,
sanitize=self.sanitize_log
)
_returned_headers = returned["headers"]
Expand Down Expand Up @@ -363,8 +366,8 @@ def _logout_handler(self, token_value: str = None, stateful: bool = True, client
returned = perform_request(method="POST", endpoint=target_url, data=data_payload,
headers=header_payload, verify=self.ssl_verify,
proxy=self.proxy, timeout=self.timeout,
user_agent=self.user_agent, log_util=self.log,
sanitize=self.sanitize_log
user_agent=self.user_agent, session=self.session,
log_util=self.log, sanitize=self.sanitize_log
)
if stateful:
self.bearer_token: BearerToken = BearerToken()
Expand Down Expand Up @@ -429,6 +432,15 @@ def proxy(self) -> Dict[str, str]:
def proxy(self, value: Dict[str, str]):
self.config.proxy = value

@property
def session(self) -> Optional[requests.Session]:
"""Return the requests.Session in use, if one was provided."""
return self.config.session

@session.setter
def session(self, value: Optional[requests.Session]):
self.config.session = value

@property
def user_agent(self) -> str:
"""Return the current user agent setting."""
Expand Down
15 changes: 14 additions & 1 deletion src/falconpy/_auth_object/_interface_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
For more information, please refer to <https://unlicense.org>
"""
from typing import Dict, Union, Optional
import requests


class InterfaceConfiguration:
Expand All @@ -50,13 +51,15 @@ def __init__(self,
proxy: Optional[Dict[str, str]] = None,
timeout: Optional[Union[int, tuple]] = None,
user_agent: Optional[str] = None,
ssl_verify: Optional[bool] = True
ssl_verify: Optional[bool] = True,
session: Optional[requests.Session] = None
):
"""Construct an instance of the InterfaceConfiguration class."""
self._base_url: Optional[str] = base_url
self._proxy: Optional[Dict[str, str]] = proxy
self._timeout: Optional[Union[int, tuple]] = timeout
self._user_agent: Optional[str] = user_agent
self._session: Optional[requests.Session] = session

self._ssl_verify: bool = True
if isinstance(ssl_verify, bool):
Expand Down Expand Up @@ -116,3 +119,13 @@ def ssl_verify(self) -> bool:
def ssl_verify(self, value: bool):
"""Change the SSL verification setting."""
self._ssl_verify = value

@property
def session(self) -> Optional[requests.Session]:
"""Return the requests.Session in use, if one was provided."""
return self._session

@session.setter
def session(self, value: Optional[requests.Session]):
"""Update or replace the requests.Session reference. Never closes the outgoing session."""
self._session = value
13 changes: 11 additions & 2 deletions src/falconpy/_auth_object/_uber_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"""
from traceback import extract_tb
from typing import Dict, List, Optional, Union
import requests
from ._falcon_interface import FalconInterface
from .._constant import MAX_DEBUG_RECORDS
from .._endpoint import api_endpoints
Expand Down Expand Up @@ -79,7 +80,8 @@ def __init__(self,
debug_record_count: Optional[int] = MAX_DEBUG_RECORDS,
sanitize_log: Optional[bool] = None,
pythonic: Optional[bool] = None,
environment: Optional[Dict[str, str]] = None
environment: Optional[Dict[str, str]] = None,
session: Optional[requests.Session] = None
):
"""Construct an instance of the UberInterface class.

Expand Down Expand Up @@ -112,6 +114,12 @@ def __init__(self,
Max: 5000
sanitize_log: Enable / Disable log sanitization of client IDs, secrets and tokens.
Boolean. Defaults to enabled.
session: Existing requests.Session to reuse for connection pooling across login, every
API call, token renewal and logout. FalconPy never closes a session provided
this way; the caller retains ownership of its lifecycle (for example, by using
it as a context manager). A single Session is not guaranteed safe for
concurrent use across threads without external synchronization. When omitted
(default), behavior is unchanged and a new connection is used for each request.
This method only accepts keywords to specify arguments.
"""
super().__init__(base_url=confirm_base_url(base_url),
Expand All @@ -129,7 +137,8 @@ def __init__(self,
debug_record_count=debug_record_count,
sanitize_log=sanitize_log,
pythonic=pythonic,
environment=environment
environment=environment,
session=session
)

# Complete list of available API operations.
Expand Down
11 changes: 11 additions & 0 deletions src/falconpy/_service_class/_base_service_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from abc import ABC, abstractmethod
from logging import Logger, getLogger
from typing import Dict, Type, Union, Optional
import requests
from .._constant import MAX_DEBUG_RECORDS
from .._auth_object import FalconInterface, UberInterface
from .._error import FunctionalityNotImplemented
Expand Down Expand Up @@ -242,6 +243,16 @@ def user_agent(self) -> int:
def user_agent(self, _):
raise FunctionalityNotImplemented

@property
def session(self) -> Optional[requests.Session]:
"""Provide the requests.Session from the auth_object.

Not independently overridable per Service Class instance: session identity must
stay in lock-step with the shared auth_object so authentication and API calls
always use the same session.
"""
return self.auth_object.session

# Mutable
@property
def debug_record_count(self) -> int:
Expand Down
7 changes: 7 additions & 0 deletions src/falconpy/_service_class/_service_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,13 @@ def __init__(self: "ServiceClass",
Amount of time (in seconds) between now and the token expiration before
a refresh of the token is performed. Default: 120, Max: 1200
Values over 1200 will be reset to the maximum.
session : requests.Session
Existing HTTP session to reuse for connection pooling. Forwarded to the
auth_object when one is constructed automatically from credentials; ignored
if an explicit auth_object is supplied (that object's own session is used
instead, and is not independently overridable per Service Class instance).
FalconPy never closes a session provided this way; the caller retains
ownership of its lifecycle. When omitted (default), behavior is unchanged.

Arguments
----
Expand Down
20 changes: 15 additions & 5 deletions src/falconpy/_util/_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,11 @@ def service_request(caller: ServiceClass = None, **kwargs) -> Union[Dict[str, Un
except AttributeError:
user_agent = None

try:
session: Optional[requests.Session] = caller.session
except AttributeError:
session = None

try:
log_utility: Optional[Logger] = caller.log
except AttributeError:
Expand All @@ -256,6 +261,7 @@ def service_request(caller: ServiceClass = None, **kwargs) -> Union[Dict[str, Un
return perform_request(proxy=proxy,
timeout=timeout,
user_agent=user_agent,
session=session,
log_util=log_utility,
debug_record_count=debug_count,
sanitize=do_sanitize,
Expand Down Expand Up @@ -378,6 +384,9 @@ def perform_request(endpoint: str = "", # noqa: C901
debug_record_count: int - Maximum number of records to log in debug logs
authenticating: bool - This request is driving a token request
stream: bool - Enabling streaming download.
session: requests.Session - Existing HTTP session to reuse for connection pooling.
FalconPy never closes a session provided this way; the caller retains ownership.
- Example: requests.Session()
"""
# Shortcut for now
pythonic = kwargs.get("pythonic", False)
Expand Down Expand Up @@ -420,11 +429,12 @@ def perform_request(endpoint: str = "", # noqa: C901
allow_redirects = True
# Log our payloads if debugging is enabled
log_api_payloads(api, headers)
response = requests.request(api.method.upper(), endpoint, params=api.param_payload,
headers=headers, json=api.body_payload, data=api.data_payload,
files=api.files, verify=api.verify, allow_redirects=allow_redirects,
proxies=api.proxy, timeout=api.timeout, stream=api.stream
)
requester = api.session.request if api.session is not None else requests.request
response = requester(api.method.upper(), endpoint, params=api.param_payload,
headers=headers, json=api.body_payload, data=api.data_payload,
files=api.files, verify=api.verify, allow_redirects=allow_redirects,
proxies=api.proxy, timeout=api.timeout, stream=api.stream
)

api.debug_headers = response.headers

Expand Down
Loading
Loading