Skip to content

Commit 6fd5183

Browse files
authored
🚀 release (#111)
2 parents 84824cf + 5b13798 commit 6fd5183

File tree

6 files changed

+73
-0
lines changed

6 files changed

+73
-0
lines changed

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ at [https://netboxlabs.com/blog/introducing-diode-streamlining-data-ingestion-in
1717
| >= 3.7.2 | 0.1.0 |
1818
| >= 4.1.0 | 0.4.0 |
1919
| >= 4.2.3 | 1.0.0 |
20+
| >= 4.2.3 | 1.1.0 |
21+
| >= 4.2.3 | 1.2.0 |
2022

2123
## Installation
2224

‎netbox-plugin.yaml‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
version: 0.1
22
package_name: netboxlabs-diode-netbox-plugin
33
compatibility:
4+
- release: 1.2.0
5+
netbox_min: 4.2.3
6+
netbox_max: 4.2.9
7+
- release: 1.1.0
8+
netbox_min: 4.2.3
9+
netbox_max: 4.2.9
10+
- release: 1.0.1
11+
netbox_min: 4.2.3
12+
netbox_max: 4.2.9
413
- release: 1.0.0
514
netbox_min: 4.2.3
615
netbox_max: 4.2.3

‎netbox_diode_plugin/__init__.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ class NetBoxDiodePluginConfig(PluginConfig):
3131
"secrets_path": "/run/secrets/",
3232
"netbox_to_diode_client_secret_name": "netbox_to_diode",
3333
"diode_max_auth_retries": 3,
34+
35+
# List of audiences to require for the diode-to-netbox token.
36+
# If empty, no audience is required.
37+
"required_token_audience": [],
3438
}
3539

3640

‎netbox_diode_plugin/api/authentication.py‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from netbox_diode_plugin.plugin_config import (
1515
get_diode_auth_introspect_url,
1616
get_diode_user,
17+
get_required_token_audience,
1718
)
1819

1920
logger = logging.getLogger("netbox.diode_data")
@@ -65,6 +66,16 @@ def _introspect_token(self, token: str):
6566
return None
6667

6768
if data.get("active"):
69+
# if the plugin is configured to require specific token audience(s),
70+
# reject the token if any are missing.
71+
required_audience = get_required_token_audience()
72+
if required_audience:
73+
token_audience = set(data.get("aud", []))
74+
missing_audience = set(required_audience) - token_audience
75+
if missing_audience:
76+
logger.error(f"Token audience(s) {missing_audience} not found in {token_audience}")
77+
return None
78+
6879
diode_user = SimpleNamespace(
6980
user=get_diode_user(),
7081
token_scopes=data.get("scope", "").split(),

‎netbox_diode_plugin/plugin_config.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,7 @@ def get_diode_user():
8989
diode_user = User.objects.create(username=diode_username, is_active=True)
9090

9191
return diode_user
92+
93+
def get_required_token_audience():
94+
"""Returns the require token audience."""
95+
return get_plugin_config("netbox_diode_plugin", "required_token_audience")

‎netbox_diode_plugin/tests/test_authentication.py‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,19 @@ def setUp(self):
4949
)
5050
self.introspect_url_patcher.start()
5151

52+
self.required_audience_patcher = mock.patch(
53+
'netbox_diode_plugin.api.authentication.get_required_token_audience',
54+
return_value=[]
55+
)
56+
self.required_audience_mock = self.required_audience_patcher.start()
57+
5258
def tearDown(self):
5359
"""Clean up after tests."""
5460
self.cache_patcher.stop()
5561
self.cache_set_patcher.stop()
5662
self.requests_patcher.stop()
5763
self.introspect_url_patcher.stop()
64+
self.required_audience_patcher.stop()
5865

5966
def test_authenticate_no_auth_header(self):
6067
"""Test authentication with no Authorization header."""
@@ -103,6 +110,42 @@ def test_authenticate_token_with_required_scope(self):
103110
self.assertEqual(user, self.diode_user.user)
104111
self.cache_set_mock.assert_called_once()
105112

113+
def test_authenticate_token_with_required_audience(self):
114+
"""Test authentication with token having required audience."""
115+
self.cache_get_mock.return_value = None
116+
self.requests_mock.return_value.json.return_value = {
117+
'active': True,
118+
'scope': 'netbox:read netbox:write',
119+
'exp': 1000,
120+
'iat': 500
121+
}
122+
123+
request = self.factory.get('/', HTTP_AUTHORIZATION=f'Bearer {self.token_with_scope}')
124+
125+
self.cache_get_mock.return_value = None
126+
self.required_audience_mock.return_value = ['netbox']
127+
try:
128+
# should fail if the token does not have the required audience
129+
with self.assertRaises(AuthenticationFailed):
130+
self.auth.authenticate(request)
131+
self.required_audience_mock.assert_called_once()
132+
self.cache_set_mock.assert_not_called()
133+
134+
# should succeed if the token has the required audience
135+
self.requests_mock.return_value.json.return_value = {
136+
'active': True,
137+
'aud': ['netbox', 'api', 'other'],
138+
'scope': 'netbox:read netbox:write',
139+
'exp': 1000,
140+
'iat': 500
141+
}
142+
143+
user, _ = self.auth.authenticate(request)
144+
self.assertEqual(user, self.diode_user.user)
145+
self.cache_set_mock.assert_called_once()
146+
finally:
147+
self.required_audience_patcher.return_value = []
148+
106149
def test_authenticate_token_introspection_failure(self):
107150
"""Test authentication when token introspection fails."""
108151
self.cache_get_mock.return_value = None

0 commit comments

Comments
 (0)