Skip to content

Commit 349dd27

Browse files
committedOct 11, 2022
Add public write support
1 parent 0268539 commit 349dd27

11 files changed

+157
-13
lines changed
 

‎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

+37-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,41 @@ 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(f"/taxii2/{api_root_id}/status/{job_id}/",
318+
headers={"Accept": "application/taxii+json;version=2.1"},
319+
)
320+
assert response.status_code == expected_status_code
289321

290322

291323
def test_job_cleanup(app, db_jobs):

‎tests/taxii2/utils.py

+16-1
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@
117117
"Read only description",
118118
None,
119119
False,
120+
False,
120121
),
121122
Collection(
122123
str(uuid4()),
@@ -125,6 +126,7 @@
125126
"Write only description",
126127
None,
127128
False,
129+
False,
128130
),
129131
Collection(
130132
str(uuid4()),
@@ -133,6 +135,7 @@
133135
"Read/Write description",
134136
None,
135137
False,
138+
False,
136139
),
137140
Collection(
138141
str(uuid4()),
@@ -141,15 +144,17 @@
141144
"No permissions description",
142145
None,
143146
False,
147+
False,
144148
),
145-
Collection(str(uuid4()), API_ROOTS[0].id, "4No description", "", None, False),
149+
Collection(str(uuid4()), API_ROOTS[0].id, "4No description", "", None, False, False),
146150
Collection(
147151
str(uuid4()),
148152
API_ROOTS[0].id,
149153
"5With alias",
150154
"With alias description",
151155
"this-is-an-alias",
152156
False,
157+
False,
153158
),
154159
Collection(
155160
str(uuid4()),
@@ -158,6 +163,16 @@
158163
"public description",
159164
"",
160165
True,
166+
False,
167+
),
168+
Collection(
169+
str(uuid4()),
170+
API_ROOTS[0].id,
171+
"7Publicwrite",
172+
"public write description",
173+
None,
174+
False,
175+
True,
161176
),
162177
)
163178
STIX_OBJECTS = (

‎tests/test_cli.py

+22
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,7 @@ def test_add_api_root(
352352
"description": None,
353353
"alias": None,
354354
"is_public": False,
355+
"is_public_write": False,
355356
}, # expected_call
356357
id="rootid, title only",
357358
),
@@ -374,6 +375,7 @@ def test_add_api_root(
374375
"description": "my description",
375376
"alias": None,
376377
"is_public": False,
378+
"is_public_write": False,
377379
}, # expected_call
378380
id="rootid, title, description",
379381
),
@@ -398,6 +400,7 @@ def test_add_api_root(
398400
"description": "my description",
399401
"alias": "my-alias",
400402
"is_public": False,
403+
"is_public_write": False,
401404
}, # expected_call
402405
id="rootid, title, description, alias",
403406
),
@@ -413,9 +416,26 @@ def test_add_api_root(
413416
"description": None,
414417
"alias": None,
415418
"is_public": True,
419+
"is_public_write": False,
416420
}, # expected_call
417421
id="rootid, titlei, public",
418422
),
423+
pytest.param(
424+
["-r", API_ROOTS[0].id, "-t", "my new collection", "--public-write"], # argv
425+
False, # raises
426+
None, # message
427+
"", # stdout
428+
"", # stderr
429+
{
430+
"api_root_id": API_ROOTS[0].id,
431+
"title": "my new collection",
432+
"description": None,
433+
"alias": None,
434+
"is_public": False,
435+
"is_public_write": True,
436+
}, # expected_call
437+
id="rootid, titlei, publicwrite",
438+
),
419439
pytest.param(
420440
["-r", "fake-uuid", "-t", "my new collection"], # argv
421441
SystemExit, # raises
@@ -430,6 +450,7 @@ def test_add_api_root(
430450
"[-d DESCRIPTION]",
431451
"[-a ALIAS]",
432452
"[--public]",
453+
"[--public-write]",
433454
": error: argument -r/--rootid: invalid choice: 'fake-uuid'",
434455
"(choose from WRAPPED_ROOTIDS)",
435456
]
@@ -451,6 +472,7 @@ def test_add_api_root(
451472
"[-d DESCRIPTION]",
452473
"[-a ALIAS]",
453474
"[--public]",
475+
"[--public-write]",
454476
": error: the following arguments are required: -r/--rootid, -t/--title",
455477
]
456478
),

0 commit comments

Comments
 (0)
Please sign in to comment.