Skip to content

Commit 278505a

Browse files
feat(mtls): implement of mutual TLS communication for fetching server info (#372)
ADDON-70857 Added fetching of the client certificates for mTLS communication for `ServerInfo` class.
1 parent 40099a9 commit 278505a

File tree

7 files changed

+149
-16
lines changed

7 files changed

+149
-16
lines changed

.github/workflows/build-test-release.yml

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -119,23 +119,23 @@ jobs:
119119
export SPLUNK_HOME=/opt/splunk
120120
wget -qO /tmp/splunk.tgz "${SPLUNK_BUILD_URL}"
121121
sudo tar -C /opt -zxf /tmp/splunk.tgz
122-
sudo cp -r tests/integration/data/solnlib_demo $SPLUNK_HOME/etc/apps
123-
sudo cp -r solnlib $SPLUNK_HOME/etc/apps/solnlib_demo/bin/
124-
sudo mkdir -p $SPLUNK_HOME/etc/apps/Splunk_TA_test/default/
125-
sudo chown -R "$USER":"$USER" /opt/splunk
122+
sudo chown -R "$USER":"$USER" $SPLUNK_HOME
123+
cp -r tests/integration/data/solnlib_demo $SPLUNK_HOME/etc/apps
124+
cp -r solnlib $SPLUNK_HOME/etc/apps/solnlib_demo/bin/
125+
mkdir -p $SPLUNK_HOME/etc/apps/Splunk_TA_test/default/
126126
ls $SPLUNK_HOME/etc/apps/solnlib_demo/bin/
127-
echo -e "[user_info]\nUSERNAME=Admin\nPASSWORD=Chang3d"'!' | sudo tee -a /opt/splunk/etc/system/local/user-seed.conf
128-
echo 'OPTIMISTIC_ABOUT_FILE_LOCKING=1' | sudo tee -a /opt/splunk/etc/splunk-launch.conf
129-
sudo /opt/splunk/bin/splunk start --accept-license
130-
sudo /opt/splunk/bin/splunk cmd python -m pip install solnlib
131-
sudo /opt/splunk/bin/splunk set servername custom-servername -auth admin:Chang3d!
132-
sudo /opt/splunk/bin/splunk restart
127+
echo -e "[user_info]\nUSERNAME=Admin\nPASSWORD=Chang3d"'!' | tee -a $SPLUNK_HOME/etc/system/local/user-seed.conf
128+
echo 'OPTIMISTIC_ABOUT_FILE_LOCKING=1' | tee -a $SPLUNK_HOME/etc/splunk-launch.conf
129+
$SPLUNK_HOME/bin/splunk start --accept-license
130+
$SPLUNK_HOME/bin/splunk cmd python -m pip install solnlib
131+
$SPLUNK_HOME/bin/splunk set servername custom-servername -auth admin:Chang3d!
132+
$SPLUNK_HOME/bin/splunk restart
133133
until curl -k -s -u admin:Chang3d! https://localhost:8089/services/server/info\?output_mode\=json | jq '.entry[0].content.kvStoreStatus' | grep -o "ready" ; do echo -n "Waiting for KVStore to become ready-" && sleep 5 ; done
134134
timeout-minutes: 5
135135
- name: Run tests
136136
run: |
137137
poetry install
138-
SPLUNK_HOME=/opt/splunk/ poetry run pytest --junitxml=test-results/results.xml -v tests/integration
138+
SPLUNK_HOME=/opt/splunk SPLUNK_DB=$SPLUNK_HOME/var/lib/splunk poetry run pytest --junitxml=test-results/results.xml -v tests/integration
139139
- uses: actions/upload-artifact@v4
140140
with:
141141
name: test-splunk-${{ matrix.splunk.version }}

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
# IDE related files
22
*.idea
33
*.DS_Store*
4-
.venv/
4+
# ignore all virtual environments
5+
.venv*
56

67
# Compiled files
78
__pycache__
89
*.pyc
910
*.pyo
1011

1112
.coverage
13+
*.log
14+
events.pickle

solnlib/server_info.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@
1616

1717
"""This module contains Splunk server info related functionalities."""
1818

19+
import os
1920
import json
2021
from typing import Any, Dict, Optional
2122

23+
from splunk.rest import getWebCertFile, getWebKeyFile
2224
from splunklib import binding
23-
2425
from solnlib import splunk_rest_client as rest_client
2526
from solnlib import utils
27+
from solnlib.splunkenv import get_splunkd_access_info
2628

2729
__all__ = ["ServerInfo", "ServerInfoException"]
2830

@@ -56,6 +58,28 @@ def __init__(
5658
port: The port number, default is None.
5759
context: Other configurations for Splunk rest client.
5860
"""
61+
is_localhost = False
62+
if not all([scheme, host, port]) and os.environ.get("SPLUNK_HOME"):
63+
scheme, host, port = get_splunkd_access_info()
64+
is_localhost = (
65+
host == "localhost" or host == "127.0.0.1" or host in ("::1", "[::1]")
66+
)
67+
68+
if getWebCertFile() and getWebKeyFile():
69+
context["cert_file"] = getWebCertFile()
70+
context["key_file"] = getWebKeyFile()
71+
72+
if all([is_localhost, context.get("verify") is None]):
73+
# NOTE: this is specifically for mTLS communication
74+
# ONLY if scheme, host, port aren't provided AND user hasn't provided server certificate
75+
# we set verify to off (similar to 'rest.simpleRequest' implementation)
76+
context["verify"] = False
77+
78+
elif getWebCertFile() is not None:
79+
context["cert_file"] = getWebCertFile()
80+
if all([is_localhost, context.get("verify") is None]):
81+
context["verify"] = False
82+
5983
self._rest_client = rest_client.SplunkRestClient(
6084
session_key, "-", scheme=scheme, host=host, port=port, **context
6185
)

solnlib/splunk_rest_client.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,13 @@ def _request_handler(context):
8888
verify = context.get("verify", False)
8989

9090
if context.get("key_file") and context.get("cert_file"):
91-
# cert = ('/path/client.cert', '/path/client.key')
92-
cert = context["key_file"], context["cert_file"]
91+
# cert: if tuple, ('cert', 'key') pair as per requests library
92+
cert = context["cert_file"], context["key_file"]
9393
elif context.get("cert_file"):
9494
cert = context["cert_file"]
95+
elif context.get("cert"):
96+
# as the solnlib uses requests, we need to have a check for 'cert' key as well
97+
cert = context["cert"]
9598
else:
9699
cert = None
97100

tests/integration/conftest.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import os
2+
import sys
3+
4+
# path manipulation get the 'splunk' library for the imports while running on GH Actions
5+
sys.path.append(
6+
os.path.sep.join([os.environ["SPLUNK_HOME"], "lib", "python3.7", "site-packages"])
7+
)
8+
# TODO: 'python3.7' needs to be updated as and when Splunk has new folder for Python.

tests/unit/conftest.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import sys
2+
from unittest.mock import MagicMock
3+
4+
# mock modules of 'splunk' library added 'splunk_rest_client'
5+
sys.modules["splunk"] = MagicMock()
6+
sys.modules["splunk.rest"] = MagicMock()

tests/unit/test_server_info.py

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515
#
16-
import common
1716
import pytest
17+
import common
18+
from unittest.mock import patch, MagicMock
1819
from splunklib import binding
1920

2021
from solnlib import server_info
@@ -72,3 +73,91 @@ def _mock_get(self, path_segment, owner=None, app=None, sharing=None, **query):
7273

7374
si = server_info.ServerInfo(common.SESSION_KEY)
7475
assert si.is_captain_ready()
76+
77+
@patch("solnlib.server_info.os.environ", autospec=True, return_value="$SPLUNK_HOME")
78+
@patch(
79+
"solnlib.server_info.get_splunkd_access_info",
80+
autospec=True,
81+
return_value=("https", "127.0.0.1", "8089"),
82+
)
83+
@patch("solnlib.server_info.rest_client", autospec=True)
84+
@patch("solnlib.server_info.getWebCertFile", return_value=None)
85+
@patch("solnlib.server_info.getWebKeyFile", return_value=None)
86+
def test_server_info_object_with_no_certs(
87+
self, mock_web_key, mock_web_cert, mock_rest_client, mock_splunkd, mock_os_env
88+
):
89+
mock_rest_client.SplunkRestClient = MagicMock()
90+
91+
server_info.ServerInfo(common.SESSION_KEY)
92+
93+
for call_arg in mock_rest_client.SplunkRestClient.call_args_list:
94+
_, kwargs = call_arg
95+
assert kwargs.get("cert_file") is None
96+
assert kwargs.get("key_file") is None
97+
assert kwargs.get("verify") is None
98+
99+
@patch("solnlib.server_info.os.environ", autospec=True, return_value="$SPLUNK_HOME")
100+
@patch(
101+
"solnlib.server_info.get_splunkd_access_info",
102+
autospec=True,
103+
return_value=("https", "127.0.0.1", "8089"),
104+
)
105+
@patch("solnlib.server_info.rest_client", autospec=True)
106+
@patch("solnlib.server_info.getWebCertFile", return_value="/path/cert/pem")
107+
@patch("solnlib.server_info.getWebKeyFile", return_value="/path/key/pem")
108+
def test_server_info_object_with_both_certs(
109+
self, mock_web_key, mock_web_cert, mock_rest_client, mock_splunkd, mock_os_env
110+
):
111+
mock_rest_client.SplunkRestClient = MagicMock()
112+
113+
server_info.ServerInfo(common.SESSION_KEY)
114+
115+
for call_arg in mock_rest_client.SplunkRestClient.call_args_list:
116+
_, kwargs = call_arg
117+
assert kwargs.get("cert_file") == "/path/cert/pem"
118+
assert kwargs.get("key_file") == "/path/key/pem"
119+
assert kwargs.get("verify") is False
120+
121+
@patch("solnlib.server_info.os.environ", autospec=True, return_value="$SPLUNK_HOME")
122+
@patch(
123+
"solnlib.server_info.get_splunkd_access_info",
124+
autospec=True,
125+
return_value=("https", "127.0.0.1", "8089"),
126+
)
127+
@patch("solnlib.server_info.rest_client", autospec=True)
128+
@patch("solnlib.server_info.getWebCertFile", return_value="/path/cert/pem")
129+
@patch("solnlib.server_info.getWebKeyFile", return_value=None)
130+
def test_server_info_object_with_cert_file(
131+
self, mock_web_key, mock_web_cert, mock_rest_client, mock_splunkd, mock_os_env
132+
):
133+
mock_rest_client.SplunkRestClient = MagicMock()
134+
135+
server_info.ServerInfo(common.SESSION_KEY)
136+
137+
for call_arg in mock_rest_client.SplunkRestClient.call_args_list:
138+
_, kwargs = call_arg
139+
assert kwargs.get("cert_file") == "/path/cert/pem"
140+
assert kwargs.get("key_file") is None
141+
assert kwargs.get("verify") is False
142+
143+
@patch("solnlib.server_info.os.environ", autospec=True, return_value="$SPLUNK_HOME")
144+
@patch(
145+
"solnlib.server_info.get_splunkd_access_info",
146+
autospec=True,
147+
return_value=("https", "127.0.0.1", "8089"),
148+
)
149+
@patch("solnlib.server_info.rest_client", autospec=True)
150+
@patch("solnlib.server_info.getWebCertFile", return_value=None)
151+
@patch("solnlib.server_info.getWebKeyFile", return_value="/path/key/pem")
152+
def test_server_info_object_with_key_file(
153+
self, mock_web_key, mock_web_cert, mock_rest_client, mock_splunkd, mock_os_env
154+
):
155+
mock_rest_client.SplunkRestClient = MagicMock()
156+
157+
server_info.ServerInfo(common.SESSION_KEY)
158+
159+
for call_arg in mock_rest_client.SplunkRestClient.call_args_list:
160+
_, kwargs = call_arg
161+
assert kwargs.get("cert_file") is None
162+
assert kwargs.get("key_file") is None
163+
assert kwargs.get("verify") is None

0 commit comments

Comments
 (0)