Skip to content

Commit e848ee3

Browse files
authored
Merge pull request #229 from eclecticiq/public-discovery
Public discovery
2 parents 80cd524 + d500dfa commit e848ee3

12 files changed

Lines changed: 131 additions & 24 deletions

File tree

CHANGES.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
Changelog
22
=========
33

4+
0.6.0 (2022-05-25
5+
------------------
6+
* Add `public_discovery` option to taxii2 config
7+
* Add support for publicly readable taxii 2 api roots
8+
49
0.5.0 (2022-05-24)
510
------------------
611
* Add support for publicly readable taxii 2 collections

opentaxii/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
This module defines the package version for use in __init__.py and setup.py.
44
"""
55

6-
__version__ = '0.5.0'
6+
__version__ = '0.6.0'

opentaxii/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ class ServerConfig(dict):
6262
"description",
6363
"max_content_length",
6464
"title",
65+
"public_discovery",
6566
)
6667
ALL_VALID_OPTIONS = VALID_BASE_OPTIONS + VALID_TAXII_OPTIONS + VALID_TAXII1_OPTIONS
6768

opentaxii/persistence/sqldb/api.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,7 @@ def get_api_roots(self) -> List[entities.ApiRoot]:
520520
default=obj.default,
521521
title=obj.title,
522522
description=obj.description,
523+
is_public=obj.is_public,
523524
)
524525
for obj in query.all()
525526
]
@@ -536,6 +537,7 @@ def get_api_root(self, api_root_id: str) -> Optional[entities.ApiRoot]:
536537
default=api_root.default,
537538
title=api_root.title,
538539
description=api_root.description,
540+
is_public=api_root.is_public,
539541
)
540542
else:
541543
return None
@@ -545,18 +547,20 @@ def add_api_root(
545547
title: str,
546548
description: Optional[str] = None,
547549
default: Optional[bool] = False,
550+
is_public: bool = False,
548551
) -> entities.ApiRoot:
549552
"""
550553
Add a new api root.
551554
552555
:param str title: Title of the new api root
553556
:param str description: [Optional] Description of the new api root
554557
:param bool default: [Optional, False] If the new api should be the default
558+
:param bool is_public: whether this is a publicly readable API root
555559
556560
:return: The added ApiRoot entity.
557561
"""
558562
api_root = taxii2models.ApiRoot(
559-
title=title, description=description, default=False
563+
title=title, description=description, default=default, is_public=is_public
560564
)
561565
self.db.session.add(api_root)
562566
self.db.session.commit()
@@ -567,6 +571,7 @@ def add_api_root(
567571
default=api_root.default,
568572
title=api_root.title,
569573
description=api_root.description,
574+
is_public=is_public,
570575
)
571576

572577
def get_job_and_details(

opentaxii/persistence/sqldb/taxii2models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class ApiRoot(Base):
2121
default = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False)
2222
title = sqlalchemy.Column(sqlalchemy.String(100), nullable=False, index=True)
2323
description = sqlalchemy.Column(sqlalchemy.Text)
24+
is_public = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False)
2425

2526
collections = relationship("Collection", back_populates="api_root")
2627

opentaxii/server.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -468,8 +468,10 @@ def handle_request(self, endpoint: Callable[[], Response]):
468468
self.check_headers(endpoint)
469469
return endpoint()
470470

471-
@register_handler(r"^/taxii2/$")
471+
@register_handler(r"^/taxii2/$", handles_own_auth=True)
472472
def discovery_handler(self):
473+
if context.account is None and not self.config["public_discovery"]:
474+
raise Unauthorized()
473475
response = {
474476
"title": self.config["title"],
475477
}
@@ -482,12 +484,16 @@ def discovery_handler(self):
482484
response["api_roots"] = [f"/{api_root.id}/" for api_root in api_roots]
483485
return make_taxii2_response(response)
484486

485-
@register_handler(r"^/(?P<api_root_id>[^/]+)/$")
487+
@register_handler(r"^/(?P<api_root_id>[^/]+)/$", handles_own_auth=True)
486488
def api_root_handler(self, api_root_id):
487489
try:
488490
api_root = self.persistence.get_api_root(api_root_id=api_root_id)
489491
except DoesNotExistError:
492+
if context.account is None:
493+
raise Unauthorized()
490494
raise NotFound()
495+
if context.account is None and not api_root.is_public:
496+
raise Unauthorized()
491497
response = {
492498
"title": api_root.title,
493499
"versions": ["application/taxii+json;version=2.1"],
@@ -525,12 +531,16 @@ def job_handler(self, api_root_id, job_id):
525531
}
526532
return make_taxii2_response(response)
527533

528-
@register_handler(r"^/(?P<api_root_id>[^/]+)/collections/$")
534+
@register_handler(r"^/(?P<api_root_id>[^/]+)/collections/$", handles_own_auth=True)
529535
def collections_handler(self, api_root_id):
530536
try:
531-
self.persistence.get_api_root(api_root_id=api_root_id)
537+
api_root = self.persistence.get_api_root(api_root_id=api_root_id)
532538
except DoesNotExistError:
539+
if context.account is None:
540+
raise Unauthorized()
533541
raise NotFound()
542+
if context.account is None and not api_root.is_public:
543+
raise Unauthorized()
534544
collections = self.persistence.get_collections(api_root_id=api_root_id)
535545
response = {}
536546
if collections:

opentaxii/taxii2/entities.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,18 @@ class ApiRoot(Entity):
1515
:param bool default: indicator of default api root, should only be True once
1616
:param str title: human readable plain text name used to identify this API Root
1717
:param str description: human readable plain text description for this API Root
18+
:param bool is_public: whether this is a publicly readable API root
1819
"""
1920

20-
def __init__(self, id: str, default: bool, title: str, description: str):
21+
def __init__(
22+
self, id: str, default: bool, title: str, description: str, is_public: bool
23+
):
2124
"""Initialize ApiRoot."""
2225
self.id = id
2326
self.default = default
2427
self.title = title
2528
self.description = description
29+
self.is_public = is_public
2630

2731

2832
class Collection(Entity):

tests/taxii2/test_taxii2_api_root.py

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -198,58 +198,85 @@ def test_api_root(
198198
assert content == expected_content
199199

200200

201+
@pytest.mark.parametrize("is_public", [True, False])
201202
@pytest.mark.parametrize("method", ["get", "post", "delete"])
202203
def test_api_root_unauthenticated(
203204
client,
204205
method,
206+
is_public,
205207
):
206-
func = getattr(client, method)
207-
response = func(f"/{API_ROOTS[0].id}/")
208-
assert response.status_code == 401
208+
if is_public:
209+
api_root_id = API_ROOTS[1].id
210+
if method == "get":
211+
expected_status_code = 200
212+
else:
213+
expected_status_code = 405
214+
else:
215+
api_root_id = API_ROOTS[0].id
216+
if method == "get":
217+
expected_status_code = 401
218+
else:
219+
expected_status_code = 405
220+
with patch.object(
221+
client.application.taxii_server.servers.taxii2.persistence.api,
222+
"get_api_root",
223+
side_effect=GET_API_ROOT_MOCK,
224+
):
225+
func = getattr(client, method)
226+
response = func(
227+
f"/{api_root_id}/",
228+
headers={"Accept": "application/taxii+json;version=2.1"},
229+
)
230+
assert response.status_code == expected_status_code
209231

210232

211233
@pytest.mark.parametrize(
212-
["title", "description", "default", "db_api_roots"],
234+
["title", "description", "default", "is_public", "db_api_roots"],
213235
[
214236
pytest.param(
215237
"my new api root", # title
216238
None, # description
217239
False, # default
240+
False, # is_public
218241
[], # db_api_roots
219242
id="title only",
220243
),
221244
pytest.param(
222245
"my new api root", # title
223246
"my description", # description
224247
False, # default
248+
True, # is_public
225249
[], # db_api_roots
226250
id="title, description",
227251
),
228252
pytest.param(
229253
"my new api root", # title
230254
None, # description
231255
True, # default
256+
False, # is_public
232257
[], # db_api_roots
233258
id="title, default",
234259
),
235260
pytest.param(
236261
"my new api root", # title
237262
"my description", # description
238263
True, # default
264+
True, # is_public
239265
API_ROOTS_WITH_DEFAULT, # db_api_roots
240266
id="title, description, default, existing",
241267
),
242268
],
243269
indirect=["db_api_roots"],
244270
)
245-
def test_add_api_root(app, title, description, default, db_api_roots):
271+
def test_add_api_root(app, title, description, default, is_public, db_api_roots):
246272
api_root = app.taxii_server.servers.taxii2.persistence.api.add_api_root(
247-
title, description, default
273+
title, description, default, is_public
248274
)
249275
assert api_root.id is not None
250276
assert api_root.title == title
251277
assert api_root.description == description
252278
assert api_root.default == default
279+
assert api_root.is_public == is_public
253280
db_api_root = (
254281
app.taxii_server.servers.taxii2.persistence.api.db.session.query(
255282
taxii2models.ApiRoot
@@ -260,6 +287,7 @@ def test_add_api_root(app, title, description, default, db_api_roots):
260287
assert db_api_root.title == title
261288
assert db_api_root.description == description
262289
assert db_api_root.default == default
290+
assert db_api_root.is_public == is_public
263291
if default:
264292
assert (
265293
app.taxii_server.servers.taxii2.persistence.api.db.session.query(

tests/taxii2/test_taxii2_collections.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -228,11 +228,37 @@ def test_collections(
228228
assert content == expected_content
229229

230230

231+
@pytest.mark.parametrize("is_public", [True, False])
231232
@pytest.mark.parametrize("method", ["get", "post", "delete"])
232233
def test_collections_unauthenticated(
233234
client,
234235
method,
236+
is_public,
235237
):
236-
func = getattr(client, method)
237-
response = func(f"/{API_ROOTS[0].id}/collections/")
238-
assert response.status_code == 401
238+
if is_public:
239+
api_root_id = API_ROOTS[1].id
240+
if method == "get":
241+
expected_status_code = 200
242+
else:
243+
expected_status_code = 405
244+
else:
245+
api_root_id = API_ROOTS[0].id
246+
if method == "get":
247+
expected_status_code = 401
248+
else:
249+
expected_status_code = 405
250+
with patch.object(
251+
client.application.taxii_server.servers.taxii2.persistence.api,
252+
"get_api_root",
253+
side_effect=GET_API_ROOT_MOCK,
254+
), patch.object(
255+
client.application.taxii_server.servers.taxii2.persistence.api,
256+
"get_collections",
257+
side_effect=GET_COLLECTIONS_MOCK,
258+
):
259+
func = getattr(client, method)
260+
response = func(
261+
f"/{api_root_id}/collections/",
262+
headers={"Accept": "application/taxii+json;version=2.1"},
263+
)
264+
assert response.status_code == expected_status_code

tests/taxii2/test_taxii2_discovery.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,11 +149,35 @@ def test_discovery(
149149
assert json.loads(response.data) == expected_content
150150

151151

152+
@pytest.mark.parametrize("public_discovery", [True, False])
152153
@pytest.mark.parametrize("method", ["get", "post", "delete"])
153154
def test_discovery_unauthenticated(
154155
client,
155156
method,
157+
public_discovery,
156158
):
157-
func = getattr(client, method)
158-
response = func("/taxii2/")
159-
assert response.status_code == 401
159+
if public_discovery:
160+
if method == "get":
161+
expected_status_code = 200
162+
else:
163+
expected_status_code = 405
164+
else:
165+
if method == "get":
166+
expected_status_code = 401
167+
else:
168+
expected_status_code = 405
169+
with patch.object(
170+
client.application.taxii_server.servers.taxii2,
171+
"config",
172+
{
173+
**client.application.taxii_server.servers.taxii2.config,
174+
"title": "Some TAXII Server",
175+
"public_discovery": public_discovery,
176+
},
177+
):
178+
func = getattr(client, method)
179+
response = func(
180+
"/taxii2/",
181+
headers={"Accept": "application/taxii+json;version=2.1"},
182+
)
183+
assert response.status_code == expected_status_code

0 commit comments

Comments
 (0)