Skip to content

Commit aea56b7

Browse files
authored
Merge pull request #246 from eclecticiq/public-write
Public write support
2 parents 0268539 + c4923e4 commit aea56b7

13 files changed

+163
-14
lines changed

CHANGES.rst

+4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
Changelog
22
=========
33

4+
0.9.3 (2022-10-11)
5+
------------------
6+
* Add public write support.
7+
48
0.9.2 (2022-08-26)
59
------------------
610
* Improve readability and navigation of docs (`#238 <https://github.com/eclecticiq/OpenTAXII/pull/238>`_ thanks `@zed-eiq <https://github.com/zed-eiq>`_ for the improvement).

opentaxii/_version.py

+1-1
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.9.2'
6+
__version__ = '0.9.3'

opentaxii/cli/persistence.py

+4
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,9 @@ def add_collection():
137137
parser.add_argument(
138138
"--public", action="store_true", help="allow public read access"
139139
)
140+
parser.add_argument(
141+
"--public-write", action="store_true", help="allow public write access"
142+
)
140143
parser.set_defaults(public=False)
141144

142145
args = parser.parse_args()
@@ -147,6 +150,7 @@ def add_collection():
147150
description=args.description,
148151
alias=args.alias,
149152
is_public=args.public,
153+
is_public_write=args.public_write,
150154
)
151155

152156

opentaxii/persistence/sqldb/api.py

+6
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,7 @@ def get_collections(self, api_root_id: str) -> List[entities.Collection]:
647647
description=obj.description,
648648
alias=obj.alias,
649649
is_public=obj.is_public,
650+
is_public_write=obj.is_public_write,
650651
)
651652
for obj in query.all()
652653
]
@@ -678,6 +679,7 @@ def get_collection(
678679
description=obj.description,
679680
alias=obj.alias,
680681
is_public=obj.is_public,
682+
is_public_write=obj.is_public_write,
681683
)
682684

683685
def add_collection(
@@ -687,6 +689,7 @@ def add_collection(
687689
description: Optional[str] = None,
688690
alias: Optional[str] = None,
689691
is_public: bool = False,
692+
is_public_write: bool = False,
690693
) -> entities.Collection:
691694
"""
692695
Add a new collection.
@@ -696,6 +699,7 @@ def add_collection(
696699
:param str description: [Optional] Description of the new collection
697700
:param str alias: [Optional] Alias of the new collection
698701
:param bool is_public: [Optional] Whether collection should be publicly readable
702+
:param bool is_public_write: [Optional] Whether collection should be publicly writable
699703
700704
:return: The added Collection entity.
701705
"""
@@ -705,6 +709,7 @@ def add_collection(
705709
description=description,
706710
alias=alias,
707711
is_public=is_public,
712+
is_public_write=is_public_write,
708713
)
709714
self.db.session.add(collection)
710715
self.db.session.commit()
@@ -716,6 +721,7 @@ def add_collection(
716721
description=collection.description,
717722
alias=collection.alias,
718723
is_public=collection.is_public,
724+
is_public_write=collection.is_public_write,
719725
)
720726

721727
def _objects_query(self, collection_id: str, ordered: bool) -> Query:

opentaxii/persistence/sqldb/taxii2models.py

+1
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ class Collection(Base):
126126
description = sqlalchemy.Column(sqlalchemy.Text)
127127
alias = sqlalchemy.Column(sqlalchemy.String(100), nullable=True)
128128
is_public = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False)
129+
is_public_write = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False)
129130

130131
api_root = relationship("ApiRoot", back_populates="collections")
131132
objects = relationship("STIXObject", back_populates="collection")

opentaxii/server.py

+16-2
Original file line numberDiff line numberDiff line change
@@ -509,8 +509,19 @@ def api_root_handler(self, api_root_id):
509509
response["description"] = api_root.description
510510
return make_taxii2_response(response)
511511

512-
@register_handler(r"^/taxii2/(?P<api_root_id>[^/]+)/status/(?P<job_id>[^/]+)/$")
512+
@register_handler(
513+
r"^/taxii2/(?P<api_root_id>[^/]+)/status/(?P<job_id>[^/]+)/$",
514+
handles_own_auth=True,
515+
)
513516
def job_handler(self, api_root_id, job_id):
517+
try:
518+
api_root = self.persistence.get_api_root(api_root_id=api_root_id)
519+
except DoesNotExistError:
520+
if context.account is None:
521+
raise Unauthorized()
522+
raise NotFound()
523+
if context.account is None and not api_root.is_public:
524+
raise Unauthorized()
514525
try:
515526
job = self.persistence.get_job_and_details(
516527
api_root_id=api_root_id, job_id=job_id
@@ -564,7 +575,10 @@ def collection_handler(self, api_root_id, collection_id_or_alias):
564575
if context.account is None:
565576
raise Unauthorized()
566577
raise NotFound()
567-
if context.account is None and not collection.can_read(context.account):
578+
if context.account is None and not (
579+
collection.can_read(context.account)
580+
or collection.can_write(context.account)
581+
):
568582
raise Unauthorized()
569583
response = {
570584
"id": collection.id,

opentaxii/taxii2/entities.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class Collection(Entity):
3939
:param str description: human readable plain text description for this collection
4040
:param str alias: human readable collection name that can be used on systems to alias a collection id
4141
:param bool is_public: whether this is a publicly readable collection
42+
:param bool is_public_write: whether this is a publicly writable collection
4243
"""
4344

4445
def __init__(
@@ -49,6 +50,7 @@ def __init__(
4950
description: str,
5051
alias: str,
5152
is_public: bool,
53+
is_public_write: bool,
5254
):
5355
"""Initialize Collection."""
5456
self.id = id
@@ -57,6 +59,7 @@ def __init__(
5759
self.description = description
5860
self.alias = alias
5961
self.is_public = is_public
62+
self.is_public_write = is_public_write
6063

6164
def can_read(self, account: Optional[Account]):
6265
"""Determine if `account` is allowed to read from this collection."""
@@ -69,8 +72,11 @@ def can_read(self, account: Optional[Account]):
6972

7073
def can_write(self, account: Optional[Account]):
7174
"""Determine if `account` is allowed to write to this collection."""
72-
return account and (
73-
account.is_admin or "write" in set(account.permissions.get(self.id, []))
75+
return self.is_public_write or (
76+
account
77+
and (
78+
account.is_admin or "write" in set(account.permissions.get(self.id, []))
79+
)
7480
)
7581

7682

tests/taxii2/test_taxii2_collection.py

+16-2
Original file line numberDiff line numberDiff line change
@@ -205,18 +205,26 @@ def test_collection(
205205

206206

207207
@pytest.mark.parametrize("is_public", [True, False])
208+
@pytest.mark.parametrize("is_public_write", [True, False])
208209
@pytest.mark.parametrize("method", ["get", "post", "delete"])
209210
def test_collection_unauthenticated(
210211
client,
211212
method,
212213
is_public,
214+
is_public_write,
213215
):
214216
if is_public:
215217
collection_id = COLLECTIONS[6].id
216218
if method == "get":
217219
expected_status_code = 200
218220
else:
219221
expected_status_code = 405
222+
elif is_public_write:
223+
collection_id = COLLECTIONS[7].id
224+
if method == "get":
225+
expected_status_code = 200
226+
else:
227+
expected_status_code = 405
220228
else:
221229
collection_id = COLLECTIONS[0].id
222230
if method == "get":
@@ -241,14 +249,15 @@ def test_collection_unauthenticated(
241249

242250

243251
@pytest.mark.parametrize(
244-
["api_root_id", "title", "description", "alias", "is_public"],
252+
["api_root_id", "title", "description", "alias", "is_public", "is_public_write"],
245253
[
246254
pytest.param(
247255
API_ROOTS[0].id, # api_root_id
248256
"my new collection", # title
249257
None, # description
250258
None, # alias
251259
False, # is_public
260+
False, # is_public_write
252261
id="api_root_id, title",
253262
),
254263
pytest.param(
@@ -257,6 +266,7 @@ def test_collection_unauthenticated(
257266
"my description", # description
258267
None, # alias
259268
True, # is_public
269+
False, # is_public_write
260270
id="api_root_id, title, description",
261271
),
262272
pytest.param(
@@ -265,26 +275,29 @@ def test_collection_unauthenticated(
265275
"my description", # description
266276
"my-alias", # alias
267277
False, # is_public
278+
True, # is_public_write
268279
id="api_root_id, title, description, alias",
269280
),
270281
],
271282
)
272283
def test_add_collection(
273-
app, api_root_id, title, description, alias, is_public, db_api_roots, db_collections
284+
app, api_root_id, title, description, alias, is_public, is_public_write, db_api_roots, db_collections
274285
):
275286
collection = app.taxii_server.servers.taxii2.persistence.api.add_collection(
276287
api_root_id=api_root_id,
277288
title=title,
278289
description=description,
279290
alias=alias,
280291
is_public=is_public,
292+
is_public_write=is_public_write,
281293
)
282294
assert collection.id is not None
283295
assert str(collection.api_root_id) == api_root_id
284296
assert collection.title == title
285297
assert collection.description == description
286298
assert collection.alias == alias
287299
assert collection.is_public == is_public
300+
assert collection.is_public_write == is_public_write
288301
db_collection = (
289302
app.taxii_server.servers.taxii2.persistence.api.db.session.query(
290303
taxii2models.Collection
@@ -297,3 +310,4 @@ def test_add_collection(
297310
assert db_collection.description == description
298311
assert db_collection.alias == alias
299312
assert db_collection.is_public == is_public
313+
assert db_collection.is_public_write == is_public_write

tests/taxii2/test_taxii2_collections.py

+8
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,14 @@
8787
"can_write": False,
8888
"media_types": ["application/stix+json;version=2.1"],
8989
},
90+
{
91+
"id": COLLECTIONS[7].id,
92+
"title": "7Publicwrite",
93+
"description": "public write description",
94+
"can_read": False,
95+
"can_write": True,
96+
"media_types": ["application/stix+json;version=2.1"],
97+
},
9098
]
9199
},
92100
id="good, first",

tests/taxii2/test_taxii2_objects.py

+23-1
Original file line numberDiff line numberDiff line change
@@ -1239,11 +1239,13 @@ def test_objects(
12391239

12401240

12411241
@pytest.mark.parametrize("is_public", [True, False])
1242+
@pytest.mark.parametrize("is_public_write", [True, False])
12421243
@pytest.mark.parametrize("method", ["get", "post", "delete"])
12431244
def test_objects_unauthenticated(
12441245
client,
12451246
method,
12461247
is_public,
1248+
is_public_write,
12471249
):
12481250
if is_public:
12491251
collection_id = COLLECTIONS[6].id
@@ -1253,6 +1255,14 @@ def test_objects_unauthenticated(
12531255
expected_status_code = 401
12541256
else:
12551257
expected_status_code = 405
1258+
elif is_public_write:
1259+
collection_id = COLLECTIONS[7].id
1260+
if method == "get":
1261+
expected_status_code = 401
1262+
elif method == "post":
1263+
expected_status_code = 202
1264+
else:
1265+
expected_status_code = 405
12561266
else:
12571267
collection_id = COLLECTIONS[0].id
12581268
if method == "get":
@@ -1269,7 +1279,11 @@ def test_objects_unauthenticated(
12691279
client.application.taxii_server.servers.taxii2.persistence.api,
12701280
"get_collection",
12711281
side_effect=GET_COLLECTION_MOCK,
1272-
):
1282+
), patch.object(
1283+
client.application.taxii_server.servers.taxii2.persistence.api,
1284+
"add_objects",
1285+
side_effect=ADD_OBJECTS_MOCK,
1286+
) as add_objects_mock:
12731287
kwargs = {
12741288
"headers": {
12751289
"Accept": "application/taxii+json;version=2.1",
@@ -1302,3 +1316,11 @@ def test_objects_unauthenticated(
13021316
**kwargs,
13031317
)
13041318
assert response.status_code == expected_status_code
1319+
if method == "post" and expected_status_code == 202:
1320+
add_objects_mock.assert_called_once_with(
1321+
api_root_id=API_ROOTS[0].id,
1322+
collection_id=COLLECTIONS[7].id,
1323+
objects=kwargs["json"]["objects"],
1324+
)
1325+
else:
1326+
add_objects_mock.assert_not_called()

tests/taxii2/test_taxii2_status.py

+38-5
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
import pytest
66
from opentaxii.persistence.sqldb import taxii2models
77
from opentaxii.taxii2.utils import taxii2_datetimeformat
8-
from tests.taxii2.utils import (API_ROOTS, GET_JOB_AND_DETAILS_MOCK, JOBS,
9-
config_noop, server_mapping_noop,
8+
from tests.taxii2.utils import (API_ROOTS, GET_API_ROOT_MOCK,
9+
GET_JOB_AND_DETAILS_MOCK, JOBS, config_noop,
10+
server_mapping_noop,
1011
server_mapping_remove_fields)
1112

1213

@@ -251,6 +252,10 @@ def test_status(
251252
authenticated_client.application.taxii_server.servers.taxii2.persistence.api,
252253
"get_api_roots",
253254
return_value=API_ROOTS,
255+
), patch.object(
256+
authenticated_client.application.taxii_server.servers.taxii2.persistence.api,
257+
"get_api_root",
258+
side_effect=GET_API_ROOT_MOCK,
254259
), patch.object(
255260
authenticated_client.application.taxii_server.servers.taxii2.persistence.api,
256261
"get_job_and_details",
@@ -278,14 +283,42 @@ def test_status(
278283
assert content == expected_content
279284

280285

286+
@pytest.mark.parametrize("is_public", [True, False])
281287
@pytest.mark.parametrize("method", ["get", "post", "delete"])
282288
def test_status_unauthenticated(
283289
client,
284290
method,
291+
is_public,
285292
):
286-
func = getattr(client, method)
287-
response = func(f"/taxii2/{API_ROOTS[0].id}/status/{JOBS[0].id}/")
288-
assert response.status_code == 401
293+
if is_public:
294+
api_root_id = API_ROOTS[1].id
295+
job_id = JOBS[2].id
296+
if method == "get":
297+
expected_status_code = 200
298+
else:
299+
expected_status_code = 405
300+
else:
301+
api_root_id = API_ROOTS[0].id
302+
job_id = JOBS[0].id
303+
if method == "get":
304+
expected_status_code = 401
305+
else:
306+
expected_status_code = 405
307+
with patch.object(
308+
client.application.taxii_server.servers.taxii2.persistence.api,
309+
"get_api_root",
310+
side_effect=GET_API_ROOT_MOCK,
311+
), patch.object(
312+
client.application.taxii_server.servers.taxii2.persistence.api,
313+
"get_job_and_details",
314+
side_effect=GET_JOB_AND_DETAILS_MOCK,
315+
):
316+
func = getattr(client, method)
317+
response = func(
318+
f"/taxii2/{api_root_id}/status/{job_id}/",
319+
headers={"Accept": "application/taxii+json;version=2.1"},
320+
)
321+
assert response.status_code == expected_status_code
289322

290323

291324
def test_job_cleanup(app, db_jobs):

0 commit comments

Comments
 (0)