From bb99ecfdb39a3937cb5a0cbb6f06944e924416a3 Mon Sep 17 00:00:00 2001 From: "David D. Riddle" Date: Thu, 28 Aug 2025 10:21:09 -0500 Subject: [PATCH 01/10] Add disconnect_mac_address function Closes #28 --- cassettes/test_404_disable_mac.yaml | 4 +- cassettes/test_404_enable_mac.yaml | 4 +- cassettes/test_connectivity.yaml | 2 +- cassettes/test_disable_mac.yaml | 10 +-- cassettes/test_enable_mac.yaml | 15 +++-- cassettes/test_get_info_for_mac_address.yaml | 17 ++--- requirements-test.txt | 66 ++++++++++++++++++++ src/clearpass/client.py | 53 +++++++++++++--- tests/conftest.py | 26 +++++--- tests/test_connector.py | 7 +++ 10 files changed, 161 insertions(+), 43 deletions(-) create mode 100644 requirements-test.txt diff --git a/cassettes/test_404_disable_mac.yaml b/cassettes/test_404_disable_mac.yaml index 15848a1..6132b8f 100644 --- a/cassettes/test_404_disable_mac.yaml +++ b/cassettes/test_404_disable_mac.yaml @@ -30,7 +30,7 @@ interactions: Content-Type: - application/json Date: - - Mon, 17 Nov 2025 22:02:31 GMT + - Mon, 26 Jan 2026 17:53:03 GMT Expires: - Thu, 19 Nov 1981 08:52:00 GMT Keep-Alive: @@ -82,7 +82,7 @@ interactions: Content-Type: - application/problem+json Date: - - Mon, 17 Nov 2025 22:02:31 GMT + - Mon, 26 Jan 2026 17:53:04 GMT Expires: - Thu, 19 Nov 1981 08:52:00 GMT Keep-Alive: diff --git a/cassettes/test_404_enable_mac.yaml b/cassettes/test_404_enable_mac.yaml index c545c36..09e52f2 100644 --- a/cassettes/test_404_enable_mac.yaml +++ b/cassettes/test_404_enable_mac.yaml @@ -30,7 +30,7 @@ interactions: Content-Type: - application/json Date: - - Mon, 17 Nov 2025 22:02:33 GMT + - Mon, 26 Jan 2026 17:52:28 GMT Expires: - Thu, 19 Nov 1981 08:52:00 GMT Keep-Alive: @@ -82,7 +82,7 @@ interactions: Content-Type: - application/problem+json Date: - - Mon, 17 Nov 2025 22:02:34 GMT + - Mon, 26 Jan 2026 17:52:29 GMT Expires: - Thu, 19 Nov 1981 08:52:00 GMT Keep-Alive: diff --git a/cassettes/test_connectivity.yaml b/cassettes/test_connectivity.yaml index bf2cf93..212a2cb 100644 --- a/cassettes/test_connectivity.yaml +++ b/cassettes/test_connectivity.yaml @@ -30,7 +30,7 @@ interactions: Content-Type: - application/json Date: - - Mon, 17 Nov 2025 22:02:28 GMT + - Mon, 26 Jan 2026 17:53:35 GMT Expires: - Thu, 19 Nov 1981 08:52:00 GMT Keep-Alive: diff --git a/cassettes/test_disable_mac.yaml b/cassettes/test_disable_mac.yaml index 0776e75..d7ca263 100644 --- a/cassettes/test_disable_mac.yaml +++ b/cassettes/test_disable_mac.yaml @@ -30,7 +30,7 @@ interactions: Content-Type: - application/json Date: - - Mon, 17 Nov 2025 22:02:29 GMT + - Mon, 26 Jan 2026 17:51:42 GMT Expires: - Thu, 19 Nov 1981 08:52:00 GMT Keep-Alive: @@ -71,7 +71,7 @@ interactions: response: body: string: '{"id":4524,"mac_address":"123123123123","status":"Known","randomized_mac":true,"attributes":{},"added_at":"Nov - 15, 2023 14:52:34 CST","updated_at":"Nov 17, 2025 15:02:29 CST","_links":{"self":{"href":"https:\/\/cleaned.example.edu\/api\/endpoint\/4524"}}}' + 15, 2023 14:52:34 CST","updated_at":"Jan 26, 2026 11:50:00 CST","_links":{"self":{"href":"https:\/\/notauri.edu\/api\/endpoint\/4524"}}}' headers: Cache-Control: - no-store, no-cache, must-revalidate @@ -82,7 +82,7 @@ interactions: Content-Type: - application/hal+json Date: - - Mon, 17 Nov 2025 22:02:30 GMT + - Mon, 26 Jan 2026 17:51:43 GMT Expires: - Thu, 19 Nov 1981 08:52:00 GMT Keep-Alive: @@ -129,7 +129,7 @@ interactions: body: string: '{"id":4524,"mac_address":"123123123123","status":"Disabled","randomized_mac":true,"attributes":{"Disabled By":"TESTING","Disabled Reason":"Still testing..."},"added_at":"Nov 15, 2023 - 14:52:34 CST","updated_at":"Nov 17, 2025 16:02:30 CST","_links":{"self":{"href":"https:\/\/cleaned.example.edu\/api\/endpoint\/4524"}}}' + 14:52:34 CST","updated_at":"Jan 26, 2026 11:51:43 CST","_links":{"self":{"href":"https:\/\/notauri.edu\/api\/endpoint\/4524"}}}' headers: Cache-Control: - no-store, no-cache, must-revalidate @@ -140,7 +140,7 @@ interactions: Content-Type: - application/hal+json Date: - - Mon, 17 Nov 2025 22:02:30 GMT + - Mon, 26 Jan 2026 17:51:43 GMT Expires: - Thu, 19 Nov 1981 08:52:00 GMT Keep-Alive: diff --git a/cassettes/test_enable_mac.yaml b/cassettes/test_enable_mac.yaml index a336e37..13ea955 100644 --- a/cassettes/test_enable_mac.yaml +++ b/cassettes/test_enable_mac.yaml @@ -30,7 +30,7 @@ interactions: Content-Type: - application/json Date: - - Mon, 17 Nov 2025 22:02:32 GMT + - Mon, 26 Jan 2026 17:49:59 GMT Expires: - Thu, 19 Nov 1981 08:52:00 GMT Keep-Alive: @@ -70,20 +70,19 @@ interactions: uri: https://cleaned.example.edu/api/endpoint/mac-address/123123123123 response: body: - string: '{"id":4524,"mac_address":"123123123123","status":"Disabled","randomized_mac":true,"attributes":{"Disabled - By":"TESTING","Disabled Reason":"Still testing..."},"added_at":"Nov 15, 2023 - 14:52:34 CST","updated_at":"Nov 17, 2025 16:02:30 CST","_links":{"self":{"href":"https:\/\/cleaned.example.edu\/api\/endpoint\/4524"}}}' + string: '{"id":4524,"mac_address":"123123123123","status":"Known","randomized_mac":true,"attributes":{},"added_at":"Nov + 15, 2023 14:52:34 CST","updated_at":"Jan 26, 2026 11:49:20 CST","_links":{"self":{"href":"https:\/\/notauri.edu\/api\/endpoint\/4524"}}}' headers: Cache-Control: - no-store, no-cache, must-revalidate Connection: - Keep-Alive Content-Length: - - '330' + - '267' Content-Type: - application/hal+json Date: - - Mon, 17 Nov 2025 22:02:32 GMT + - Mon, 26 Jan 2026 17:50:00 GMT Expires: - Thu, 19 Nov 1981 08:52:00 GMT Keep-Alive: @@ -128,7 +127,7 @@ interactions: response: body: string: '{"id":4524,"mac_address":"123123123123","status":"Known","randomized_mac":true,"attributes":{},"added_at":"Nov - 15, 2023 14:52:34 CST","updated_at":"Nov 17, 2025 16:02:33 CST","_links":{"self":{"href":"https:\/\/cleaned.example.edu\/api\/endpoint\/4524"}}}' + 15, 2023 14:52:34 CST","updated_at":"Jan 26, 2026 11:50:00 CST","_links":{"self":{"href":"https:\/\/notauri.edu\/api\/endpoint\/4524"}}}' headers: Cache-Control: - no-store, no-cache, must-revalidate @@ -139,7 +138,7 @@ interactions: Content-Type: - application/hal+json Date: - - Mon, 17 Nov 2025 22:02:33 GMT + - Mon, 26 Jan 2026 17:50:00 GMT Expires: - Thu, 19 Nov 1981 08:52:00 GMT Keep-Alive: diff --git a/cassettes/test_get_info_for_mac_address.yaml b/cassettes/test_get_info_for_mac_address.yaml index 9b9ee49..65b6798 100644 --- a/cassettes/test_get_info_for_mac_address.yaml +++ b/cassettes/test_get_info_for_mac_address.yaml @@ -30,7 +30,7 @@ interactions: Content-Type: - application/json Date: - - Mon, 17 Nov 2025 22:02:34 GMT + - Mon, 26 Jan 2026 17:55:57 GMT Expires: - Thu, 19 Nov 1981 08:52:00 GMT Keep-Alive: @@ -70,8 +70,9 @@ interactions: uri: https://cleaned.example.edu/api/endpoint?filter=%7B%22mac_address%22%3A%20%22123123123123%22%7D response: body: - string: '{"_links":{"self":{"href":"https:\/\/cleaned.example.edu\/api\/endpoint?calculate_count=false\u0026offset=0\u0026limit=25\u0026sort=%2Bid\u0026filter=%7B%22mac_address%22%3A+%22123123123123%22%7D"},"first":{"href":"https:\/\/cleaned.example.edu\/api\/endpoint?calculate_count=false\u0026offset=0\u0026limit=25\u0026sort=%2Bid\u0026filter=%7B%22mac_address%22%3A+%22123123123123%22%7D"}},"_embedded":{"items":[{"id":4524,"mac_address":"123123123123","status":"Known","randomized_mac":true,"attributes":{},"added_at":"Nov - 15, 2023 14:52:34 CST","updated_at":"Nov 17, 2025 16:02:33 CST","_links":{"self":{"href":"https:\/\/cleaned.example.edu\/api\/endpoint\/4524"}}}]}}' + string: '{"_links":{"self":{"href":"https:\/\/notauri.edu\/api\/endpoint?calculate_count=false\u0026offset=0\u0026limit=25\u0026sort=%2Bid\u0026filter=%7B%22mac_address%22%3A+%22123123123123%22%7D"},"first":{"href":"https:\/\/notauri.edu\/api\/endpoint?calculate_count=false\u0026offset=0\u0026limit=25\u0026sort=%2Bid\u0026filter=%7B%22mac_address%22%3A+%22123123123123%22%7D"}},"_embedded":{"items":[{"id":4524,"mac_address":"123123123123","status":"Disabled","randomized_mac":true,"attributes":{"Disabled + By":"TESTING","Disabled Reason":"Still testing..."},"added_at":"Nov 15, 2023 + 14:52:34 CST","updated_at":"Jan 26, 2026 11:51:43 CST","_links":{"self":{"href":"https:\/\/notauri.edu\/api\/endpoint\/4524"}}}]}}' headers: Cache-Control: - no-store, no-cache, must-revalidate @@ -82,7 +83,7 @@ interactions: Content-Type: - application/hal+json Date: - - Mon, 17 Nov 2025 22:02:35 GMT + - Mon, 26 Jan 2026 17:55:57 GMT Expires: - Thu, 19 Nov 1981 08:52:00 GMT Keep-Alive: @@ -134,7 +135,7 @@ interactions: Content-Type: - application/problem+json Date: - - Mon, 17 Nov 2025 22:02:35 GMT + - Mon, 26 Jan 2026 17:55:58 GMT Expires: - Thu, 19 Nov 1981 08:52:00 GMT Keep-Alive: @@ -186,7 +187,7 @@ interactions: Content-Type: - application/problem+json Date: - - Mon, 17 Nov 2025 22:02:36 GMT + - Mon, 26 Jan 2026 17:55:58 GMT Expires: - Thu, 19 Nov 1981 08:52:00 GMT Keep-Alive: @@ -238,7 +239,7 @@ interactions: Content-Type: - application/problem+json Date: - - Mon, 17 Nov 2025 22:02:36 GMT + - Mon, 26 Jan 2026 17:55:58 GMT Expires: - Thu, 19 Nov 1981 08:52:00 GMT Keep-Alive: @@ -290,7 +291,7 @@ interactions: Content-Type: - application/problem+json Date: - - Mon, 17 Nov 2025 22:02:37 GMT + - Mon, 26 Jan 2026 17:55:58 GMT Expires: - Thu, 19 Nov 1981 08:52:00 GMT Keep-Alive: diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 0000000..221c41b --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,66 @@ +argcomplete==3.6.3 +asttokens==3.0.1 +autopep8==2.3.2 +certifi==2026.1.4 +cfgv==3.5.0 +charset-normalizer==3.4.4 +coverage==7.13.2 +decorator==5.2.1 +detect-secrets==1.5.0 +distlib==0.4.0 +executing==2.2.1 +filelock==3.20.3 +flake8==7.3.0 +identify==2.6.16 +idna==3.11 +iniconfig==2.3.0 +ipdb==0.13.13 +ipython==9.9.0 +ipython_pygments_lexers==1.1.1 +jedi==0.19.2 +librt==0.7.8 +matplotlib-inline==0.2.1 +mccabe==0.7.0 +mypy==1.19.1 +mypy_extensions==1.1.0 +nodeenv==1.10.0 +packaging==26.0 +parso==0.8.5 +pathspec==1.0.3 +pexpect==4.9.0 +platformdirs==4.5.1 +pluggy==1.6.0 +pre_commit==4.5.1 +prompt_toolkit==3.0.52 +ptyprocess==0.7.0 +pure_eval==0.2.3 +pycodestyle==2.14.0 +pyflakes==3.4.0 +Pygments==2.19.2 +PyJWT==2.10.1 +pytest==9.0.2 +PyYAML==6.0.3 +requests==2.32.5 +setuptools==80.10.2 +setuptools-scm==9.2.2 +stack-data==0.6.3 +tomlkit==0.14.0 +traitlets==5.14.3 +types-PyMySQL==1.1.0.20251220 +types-python-dateutil==2.9.0.20260124 +types-pytz==2025.2.0.20251108 +types-PyYAML==6.0.12.20250915 +types-requests==2.31.0.6 +types-setuptools==80.10.0.20260124 +types-simplejson==3.20.0.20250822 +types-six==1.17.0.20251009 +types-urllib3==1.26.25.14 +typing_extensions==4.15.0 +urllib3==1.26.19 +vcr_cleaner @ git+https://github.com/techservicesillinois/vcrpy-cleaner.git@bab3df739be91c92fc2a073f09598c3b816e7f3c +vcrpy==8.1.1 +virtualenv==20.36.1 +wcwidth==0.4.0 +wrapt==2.0.1 +xmltodict==1.0.2 +yq==3.4.3 diff --git a/src/clearpass/client.py b/src/clearpass/client.py index 59dd2c3..c25c4fb 100644 --- a/src/clearpass/client.py +++ b/src/clearpass/client.py @@ -1,8 +1,14 @@ +import json import logging import re import requests -import json -import urllib.parse + +from urllib.parse import ( + SplitResult, + quote, + urlencode, + urlunsplit, +) from clearpass.exceptions import TokenError @@ -54,6 +60,7 @@ def hyphenate_mac(macstring): class APIConnection(): def __init__(self, username, password, endpoint, client_id, client_secret): self._baseurl = f"https://{endpoint}/" + self._endpoint = endpoint self._authurl = f"{self._baseurl}api/oauth" self._authpayload = { @@ -111,19 +118,36 @@ def postheaders(self): self._postheaders.update(self.getheaders) return self._postheaders - def _put_api(self, resource, payload): + def _put_api(self, resource, **kwargs): return requests.put( url=f"{self._baseurl}api/{resource}", headers=self.postheaders, - data=json.dumps(payload), verify=False, + **kwargs + ) + + def _post_api(self, resource, **kwargs): + return requests.post( + url=f"{self._baseurl}api/{resource}", + headers=self.postheaders, + verify=False, + **kwargs ) - def _get_api(self, resource, filter=None): + def _get_api(self, resource, **kwargs): url = f"{self._baseurl}api/{resource}" - if filter: - url += f"?filter={urllib.parse.quote(json.dumps(filter))}" + params = {} + for parameter, value in kwargs.items(): + if value is not None: + params[parameter] = json.dumps(value) + url = urlunsplit(SplitResult( + scheme="https", + netloc=f"{self._endpoint}", + path=f"api/{resource}", + query=urlencode(params, quote_via=quote), + fragment=None, + )) return requests.get( url=url, headers=self.getheaders, @@ -176,7 +200,7 @@ def set_mac_address( if attributes is not None: data["attributes"] = attributes - return self._put_api(f"endpoint/{mac_id}", data) + return self._put_api(f"endpoint/{mac_id}", json=data) def enable_mac_address(self, mac): mac_id = self.get_mac_id(mac) @@ -196,3 +220,16 @@ def disable_mac_address(self, mac, disabled_by, reason): if res.status_code == 404: raise ValueError(f"{mac} not found.") return res + + def disconnect_mac_address(self, mac): + res = self._post_api( + f"session-action/disconnect/mac/{mac}", + json={} + ) + if res.status_code == 404: + raise ValueError(f"{mac} not found.") + return res + + def block_mac_address(self, mac, disabled_by, reason): + self.disable_mac_address(mac, disabled_by, reason) + self.disconnect_mac_address(mac) diff --git a/tests/conftest.py b/tests/conftest.py index 892a92c..d413d4a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,9 +7,8 @@ import logging from clearpass.client import APIConnection -from vcr_cleaner import CleanYAMLSerializer -from vcr_cleaner.filters import if_uri_endswith -from vcr_cleaner.cleaners.uri import clean_domains +from vcr_cleaner import CleanYAMLSerializer, filters +from vcr_cleaner.cleaners import uri # Set up logger logger = logging.getLogger(__name__) @@ -19,6 +18,7 @@ CASSETTE_ENDPOINT = "cleaned.example.edu" CASSETTE_CLIENT_ID = "FAKEID" CASSETTE_CLIENT_SECRET = "NOTASECRET" # pragma: allowlist secret +URL = f"https://{CASSETTE_ENDPOINT}" CLEANER_SALT = 'salty' CLEANER_JWT_TOKEN = {'exp': datetime.datetime(2049, 6, 25)} @@ -26,10 +26,11 @@ # To record, `export VCR_RECORD=True` VCR_RECORD = "VCR_RECORD" in os.environ MAC_404 = 'deadbeef1234' # pragma: allowlist secret -TEST_DATA = {'mac': '123123123123', # pragma: allowlist secret - 'disabled_by': 'TESTING', - 'reason': 'Still testing...' - } +TEST_DATA = { + 'mac': '123123123123', # pragma: allowlist secret + 'disabled_by': 'TESTING', + 'reason': 'Still testing...' +} @pytest.fixture @@ -105,8 +106,15 @@ def cassette(request) -> vcr.cassette.Cassette: yaml_cleaner = CleanYAMLSerializer() my_vcr.register_serializer("cleanyaml", yaml_cleaner) - yaml_cleaner.register_cleaner(clean_domains('illinois.edu')) - yaml_cleaner.register_cleaner(if_uri_endswith("/api/oauth", clean_token)) + yaml_cleaner.register_cleaner(uri.clean_domains( + os.environ.get("CLEARPASS_ENDPOINT", CASSETTE_ENDPOINT), + CASSETTE_ENDPOINT, + ) + ) + yaml_cleaner.register_cleaner(filters.if_uri_contains( + f"{URL}/api/oauth", + clean_token), + ) yaml_cleaner.register_cleaner(clean_cookie) with my_vcr.use_cassette(f'{request.function.__name__}.yaml', diff --git a/tests/test_connector.py b/tests/test_connector.py index 215ff32..16ea052 100644 --- a/tests/test_connector.py +++ b/tests/test_connector.py @@ -1,3 +1,4 @@ +import os import pytest from conftest import TEST_DATA, MAC_404 @@ -41,3 +42,9 @@ def test_404_enable_mac(cassette, clearpass_client): def test_get_info_for_mac_address(cassette, clearpass_client): result = clearpass_client.get_info_for_mac_address(mac=TEST_DATA['mac']) assert result + + +def test_disconnect_mac_address(cassette, clearpass_client): + result = clearpass_client.disconnect_mac_address( + mac=os.environ["CLEARPASS_MAC"]) + assert result From 4aedf10d2c93b88ee68e2b077d176d63ee12ceb7 Mon Sep 17 00:00:00 2001 From: Zach Carrington Date: Mon, 26 Jan 2026 14:31:42 -0600 Subject: [PATCH 02/10] fixup --- tests/test_connector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_connector.py b/tests/test_connector.py index 16ea052..ee50fcd 100644 --- a/tests/test_connector.py +++ b/tests/test_connector.py @@ -46,5 +46,5 @@ def test_get_info_for_mac_address(cassette, clearpass_client): def test_disconnect_mac_address(cassette, clearpass_client): result = clearpass_client.disconnect_mac_address( - mac=os.environ["CLEARPASS_MAC"]) + mac=os.environ.get("CLEARPASS_MAC", TEST_DATA['mac'])) assert result From 8b2d5e69f77584026a2bec70deed7f94780dbcbe Mon Sep 17 00:00:00 2001 From: Zach Carrington Date: Mon, 26 Jan 2026 15:38:17 -0600 Subject: [PATCH 03/10] Fixup --- requirements-test.txt | 66 ------------------------------------------- 1 file changed, 66 deletions(-) delete mode 100644 requirements-test.txt diff --git a/requirements-test.txt b/requirements-test.txt deleted file mode 100644 index 221c41b..0000000 --- a/requirements-test.txt +++ /dev/null @@ -1,66 +0,0 @@ -argcomplete==3.6.3 -asttokens==3.0.1 -autopep8==2.3.2 -certifi==2026.1.4 -cfgv==3.5.0 -charset-normalizer==3.4.4 -coverage==7.13.2 -decorator==5.2.1 -detect-secrets==1.5.0 -distlib==0.4.0 -executing==2.2.1 -filelock==3.20.3 -flake8==7.3.0 -identify==2.6.16 -idna==3.11 -iniconfig==2.3.0 -ipdb==0.13.13 -ipython==9.9.0 -ipython_pygments_lexers==1.1.1 -jedi==0.19.2 -librt==0.7.8 -matplotlib-inline==0.2.1 -mccabe==0.7.0 -mypy==1.19.1 -mypy_extensions==1.1.0 -nodeenv==1.10.0 -packaging==26.0 -parso==0.8.5 -pathspec==1.0.3 -pexpect==4.9.0 -platformdirs==4.5.1 -pluggy==1.6.0 -pre_commit==4.5.1 -prompt_toolkit==3.0.52 -ptyprocess==0.7.0 -pure_eval==0.2.3 -pycodestyle==2.14.0 -pyflakes==3.4.0 -Pygments==2.19.2 -PyJWT==2.10.1 -pytest==9.0.2 -PyYAML==6.0.3 -requests==2.32.5 -setuptools==80.10.2 -setuptools-scm==9.2.2 -stack-data==0.6.3 -tomlkit==0.14.0 -traitlets==5.14.3 -types-PyMySQL==1.1.0.20251220 -types-python-dateutil==2.9.0.20260124 -types-pytz==2025.2.0.20251108 -types-PyYAML==6.0.12.20250915 -types-requests==2.31.0.6 -types-setuptools==80.10.0.20260124 -types-simplejson==3.20.0.20250822 -types-six==1.17.0.20251009 -types-urllib3==1.26.25.14 -typing_extensions==4.15.0 -urllib3==1.26.19 -vcr_cleaner @ git+https://github.com/techservicesillinois/vcrpy-cleaner.git@bab3df739be91c92fc2a073f09598c3b816e7f3c -vcrpy==8.1.1 -virtualenv==20.36.1 -wcwidth==0.4.0 -wrapt==2.0.1 -xmltodict==1.0.2 -yq==3.4.3 From f66de420676e7c53dab46faf58a5fde12a18b454 Mon Sep 17 00:00:00 2001 From: Zach Carrington Date: Mon, 2 Feb 2026 16:12:46 -0600 Subject: [PATCH 04/10] add async flag to disconnect URI --- src/clearpass/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clearpass/client.py b/src/clearpass/client.py index c25c4fb..532a6bd 100644 --- a/src/clearpass/client.py +++ b/src/clearpass/client.py @@ -223,7 +223,7 @@ def disable_mac_address(self, mac, disabled_by, reason): def disconnect_mac_address(self, mac): res = self._post_api( - f"session-action/disconnect/mac/{mac}", + f"session-action/disconnect/mac/{mac}?async=false", json={} ) if res.status_code == 404: From 0c1ea150d834fa73aed67ea55888fe5fe678fc24 Mon Sep 17 00:00:00 2001 From: Edward Date: Tue, 10 Mar 2026 15:10:06 -0500 Subject: [PATCH 05/10] Update disconnect test assertions. --- tests/test_connector.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_connector.py b/tests/test_connector.py index ee50fcd..ae31363 100644 --- a/tests/test_connector.py +++ b/tests/test_connector.py @@ -1,3 +1,4 @@ +import json import os import pytest from conftest import TEST_DATA, MAC_404 @@ -48,3 +49,7 @@ def test_disconnect_mac_address(cassette, clearpass_client): result = clearpass_client.disconnect_mac_address( mac=os.environ.get("CLEARPASS_MAC", TEST_DATA['mac'])) assert result + decoded_json = json.loads(result.text) + # breakpoint() + assert decoded_json['count_success'] == 1 + From 234d2608a9638c47ce65131068d5cbcfbde9051c Mon Sep 17 00:00:00 2001 From: Edward Date: Tue, 10 Mar 2026 15:26:53 -0500 Subject: [PATCH 06/10] Fixup --- cassettes/test_disconnect_mac_not_found.yaml | 111 +++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 cassettes/test_disconnect_mac_not_found.yaml diff --git a/cassettes/test_disconnect_mac_not_found.yaml b/cassettes/test_disconnect_mac_not_found.yaml new file mode 100644 index 0000000..b65d7a0 --- /dev/null +++ b/cassettes/test_disconnect_mac_not_found.yaml @@ -0,0 +1,111 @@ +interactions: +- request: + body: '{"grant_type": "password", "username": "JOE", "password": "NOTAPASSWORD", + "client_id": "FAKEID", "client_secret": "NOTASECRET"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '238' + Content-Type: + - application/json + User-Agent: + - python-requests/2.32.5 + method: POST + uri: https://cleaned.example.edu/api/oauth + response: + body: + string: '{"access_token": "NOTASECRET"}' + headers: + Cache-Control: + - no-store + Connection: + - Keep-Alive + Content-Length: + - '172' + Content-Type: + - application/json + Date: + - Tue, 10 Mar 2026 20:25:17 GMT + Expires: + - Thu, 19 Nov 1981 08:52:00 GMT + Keep-Alive: + - timeout=4, max=500 + Pragma: + - no-cache + Server: + - Apache + Set-Cookie: NO-COOKIE-FOR-YOU + Vary: + - X-Forwarded-For + X-Content-Type-Options: + - nosniff + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - 1;mode=block + - 1;mode=block + status: + code: 200 + message: OK +- request: + body: '{}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Authorization: + - Bearer FAKE_TOKEN + Connection: + - keep-alive + Content-Length: + - '2' + Content-Type: + - application/json + User-Agent: + - python-requests/2.32.5 + method: POST + uri: https://cleaned.example.edu/api/session-action/disconnect/mac/deadbeef1234?async=false + response: + body: + string: '{"action_id":"async_action_1773174318368","status":"failed","count":0,"count_success":0,"count_failed":0,"count_queue":0,"_links":{"self":{"href":"https:\/\/cleaned.example.edu\/api\/session-action\/disconnect%2Fmac%2Fdeadbeef1234"}}}' + headers: + Cache-Control: + - no-store, no-cache, must-revalidate + Connection: + - Keep-Alive + Content-Length: + - '251' + Content-Type: + - application/hal+json + Date: + - Tue, 10 Mar 2026 20:25:18 GMT + Expires: + - Thu, 19 Nov 1981 08:52:00 GMT + Keep-Alive: + - timeout=4, max=500 + Pragma: + - no-cache + Server: + - Apache + Set-Cookie: NO-COOKIE-FOR-YOU + Vary: + - X-Forwarded-For + X-Content-Type-Options: + - nosniff + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - 1;mode=block + - 1;mode=block + status: + code: 200 + message: OK +version: 1 From 5ea7da3948e137c6e5552cebd9426a3bc1d970b6 Mon Sep 17 00:00:00 2001 From: Edward Date: Tue, 10 Mar 2026 15:27:15 -0500 Subject: [PATCH 07/10] Add Mac cleaner --- tests/conftest.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index d413d4a..a8b5366 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -93,6 +93,11 @@ def remove_creds(request): request.body = json.dumps(data) return request +def clean_test_mac(request: dict, response: dict): + test_mac = os.environ.get('CLEARPASS_MAC', '') + request['uri'] = request['uri'].replace(test_mac, MAC_404) + response['body']['string'] = response['body']['string'].replace(test_mac, MAC_404) + @pytest.fixture def cassette(request) -> vcr.cassette.Cassette: @@ -116,6 +121,8 @@ def cassette(request) -> vcr.cassette.Cassette: clean_token), ) yaml_cleaner.register_cleaner(clean_cookie) + yaml_cleaner.register_cleaner(clean_test_mac) + with my_vcr.use_cassette(f'{request.function.__name__}.yaml', serializer='cleanyaml') as tape: From 608b7b106c70041a7c4edce68fd15c1e763ca462 Mon Sep 17 00:00:00 2001 From: Edward Date: Tue, 10 Mar 2026 15:41:22 -0500 Subject: [PATCH 08/10] Fixup - tests for disconnect mac --- cassettes/test_disconnect_mac_address.yaml | 111 +++++++++++++++++++ cassettes/test_disconnect_mac_not_found.yaml | 4 +- tests/conftest.py | 4 +- 3 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 cassettes/test_disconnect_mac_address.yaml diff --git a/cassettes/test_disconnect_mac_address.yaml b/cassettes/test_disconnect_mac_address.yaml new file mode 100644 index 0000000..47846a0 --- /dev/null +++ b/cassettes/test_disconnect_mac_address.yaml @@ -0,0 +1,111 @@ +interactions: +- request: + body: '{"grant_type": "password", "username": "JOE", "password": "NOTAPASSWORD", + "client_id": "FAKEID", "client_secret": "NOTASECRET"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '238' + Content-Type: + - application/json + User-Agent: + - python-requests/2.32.5 + method: POST + uri: https://cleaned.example.edu/api/oauth + response: + body: + string: '{"access_token": "NOTASECRET"}' + headers: + Cache-Control: + - no-store + Connection: + - Keep-Alive + Content-Length: + - '172' + Content-Type: + - application/json + Date: + - Tue, 10 Mar 2026 20:25:17 GMT + Expires: + - Thu, 19 Nov 1981 08:52:00 GMT + Keep-Alive: + - timeout=4, max=500 + Pragma: + - no-cache + Server: + - Apache + Set-Cookie: NO-COOKIE-FOR-YOU + Vary: + - X-Forwarded-For + X-Content-Type-Options: + - nosniff + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - 1;mode=block + - 1;mode=block + status: + code: 200 + message: OK +- request: + body: '{}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Authorization: + - Bearer FAKE_TOKEN + Connection: + - keep-alive + Content-Length: + - '2' + Content-Type: + - application/json + User-Agent: + - python-requests/2.32.5 + method: POST + uri: https://cleaned.example.edu/api/session-action/disconnect/mac/123123123123?async=false + response: + body: + string: '{"action_id":"async_action_1773174318368","status":"complete","count":1,"count_success":1,"count_failed":0,"count_queue":0,"_links":{"self":{"href":"https:\/\/cleaned.example.edu\/api\/session-action\/disconnect%2Fmac%2F123123123123"}}}' + headers: + Cache-Control: + - no-store, no-cache, must-revalidate + Connection: + - Keep-Alive + Content-Length: + - '251' + Content-Type: + - application/hal+json + Date: + - Tue, 10 Mar 2026 20:25:18 GMT + Expires: + - Thu, 19 Nov 1981 08:52:00 GMT + Keep-Alive: + - timeout=4, max=500 + Pragma: + - no-cache + Server: + - Apache + Set-Cookie: NO-COOKIE-FOR-YOU + Vary: + - X-Forwarded-For + X-Content-Type-Options: + - nosniff + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - 1;mode=block + - 1;mode=block + status: + code: 200 + message: OK +version: 1 diff --git a/cassettes/test_disconnect_mac_not_found.yaml b/cassettes/test_disconnect_mac_not_found.yaml index b65d7a0..5da5773 100644 --- a/cassettes/test_disconnect_mac_not_found.yaml +++ b/cassettes/test_disconnect_mac_not_found.yaml @@ -71,10 +71,10 @@ interactions: User-Agent: - python-requests/2.32.5 method: POST - uri: https://cleaned.example.edu/api/session-action/disconnect/mac/deadbeef1234?async=false + uri: https://cleaned.example.edu/api/session-action/disconnect/mac/123123123123?async=false response: body: - string: '{"action_id":"async_action_1773174318368","status":"failed","count":0,"count_success":0,"count_failed":0,"count_queue":0,"_links":{"self":{"href":"https:\/\/cleaned.example.edu\/api\/session-action\/disconnect%2Fmac%2Fdeadbeef1234"}}}' + string: '{"action_id":"async_action_1773174318368","status":"failed","count":0,"count_success":0,"count_failed":0,"count_queue":0,"_links":{"self":{"href":"https:\/\/cleaned.example.edu\/api\/session-action\/disconnect%2Fmac%2F123123123123"}}}' headers: Cache-Control: - no-store, no-cache, must-revalidate diff --git a/tests/conftest.py b/tests/conftest.py index a8b5366..7b3beac 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -95,8 +95,8 @@ def remove_creds(request): def clean_test_mac(request: dict, response: dict): test_mac = os.environ.get('CLEARPASS_MAC', '') - request['uri'] = request['uri'].replace(test_mac, MAC_404) - response['body']['string'] = response['body']['string'].replace(test_mac, MAC_404) + request['uri'] = request['uri'].replace(test_mac, TEST_DATA['mac']) + response['body']['string'] = response['body']['string'].replace(test_mac, TEST_DATA['mac']) @pytest.fixture From fc54f40f446d40302e51c166caeb86bce05a071d Mon Sep 17 00:00:00 2001 From: Edward Date: Tue, 10 Mar 2026 15:44:13 -0500 Subject: [PATCH 09/10] Add a note about disconnect status --- CONTRIBUTING.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..5f87181 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,12 @@ +## Translation Table for Disconnect JSON Response + +When calling `disconnect_mac_address`, `count`, `count_success` and `count_failed` may be returned. + +When `count` is `0`, no active session was found. This is a success, +since the `mac` is not connected. + +Otherwise, look for `count_success` or `count_failed`. + + count - 0 - no active session found (success) + count - 1 - count_success 1 = found and disconnected active session + count - 1 - count_failed 1 = found active session, but could not disconnect From 4b0268dc527426609064efd46fcdcb4b6b475627 Mon Sep 17 00:00:00 2001 From: Zach Carrington Date: Thu, 12 Mar 2026 11:12:54 -0500 Subject: [PATCH 10/10] fixup --- tests/conftest.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 7b3beac..eadfcda 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -93,10 +93,13 @@ def remove_creds(request): request.body = json.dumps(data) return request + def clean_test_mac(request: dict, response: dict): test_mac = os.environ.get('CLEARPASS_MAC', '') request['uri'] = request['uri'].replace(test_mac, TEST_DATA['mac']) - response['body']['string'] = response['body']['string'].replace(test_mac, TEST_DATA['mac']) + response['body']['string'] = ( + response['body']['string'].replace(test_mac, TEST_DATA['mac']) + ) @pytest.fixture @@ -123,7 +126,6 @@ def cassette(request) -> vcr.cassette.Cassette: yaml_cleaner.register_cleaner(clean_cookie) yaml_cleaner.register_cleaner(clean_test_mac) - with my_vcr.use_cassette(f'{request.function.__name__}.yaml', serializer='cleanyaml') as tape: yield tape