Skip to content

Commit

Permalink
Merge pull request #246 from eclecticiq/public-write
Browse files Browse the repository at this point in the history
Public write support
  • Loading branch information
erwin-eiq committed Oct 11, 2022
2 parents 0268539 + c4923e4 commit aea56b7
Show file tree
Hide file tree
Showing 13 changed files with 163 additions and 14 deletions.
4 changes: 4 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
Changelog
=========

0.9.3 (2022-10-11)
------------------
* Add public write support.

0.9.2 (2022-08-26)
------------------
* 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).
Expand Down
2 changes: 1 addition & 1 deletion opentaxii/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
This module defines the package version for use in __init__.py and setup.py.
"""

__version__ = '0.9.2'
__version__ = '0.9.3'
4 changes: 4 additions & 0 deletions opentaxii/cli/persistence.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@ def add_collection():
parser.add_argument(
"--public", action="store_true", help="allow public read access"
)
parser.add_argument(
"--public-write", action="store_true", help="allow public write access"
)
parser.set_defaults(public=False)

args = parser.parse_args()
Expand All @@ -147,6 +150,7 @@ def add_collection():
description=args.description,
alias=args.alias,
is_public=args.public,
is_public_write=args.public_write,
)


Expand Down
6 changes: 6 additions & 0 deletions opentaxii/persistence/sqldb/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,7 @@ def get_collections(self, api_root_id: str) -> List[entities.Collection]:
description=obj.description,
alias=obj.alias,
is_public=obj.is_public,
is_public_write=obj.is_public_write,
)
for obj in query.all()
]
Expand Down Expand Up @@ -678,6 +679,7 @@ def get_collection(
description=obj.description,
alias=obj.alias,
is_public=obj.is_public,
is_public_write=obj.is_public_write,
)

def add_collection(
Expand All @@ -687,6 +689,7 @@ def add_collection(
description: Optional[str] = None,
alias: Optional[str] = None,
is_public: bool = False,
is_public_write: bool = False,
) -> entities.Collection:
"""
Add a new collection.
Expand All @@ -696,6 +699,7 @@ def add_collection(
:param str description: [Optional] Description of the new collection
:param str alias: [Optional] Alias of the new collection
:param bool is_public: [Optional] Whether collection should be publicly readable
:param bool is_public_write: [Optional] Whether collection should be publicly writable
:return: The added Collection entity.
"""
Expand All @@ -705,6 +709,7 @@ def add_collection(
description=description,
alias=alias,
is_public=is_public,
is_public_write=is_public_write,
)
self.db.session.add(collection)
self.db.session.commit()
Expand All @@ -716,6 +721,7 @@ def add_collection(
description=collection.description,
alias=collection.alias,
is_public=collection.is_public,
is_public_write=collection.is_public_write,
)

def _objects_query(self, collection_id: str, ordered: bool) -> Query:
Expand Down
1 change: 1 addition & 0 deletions opentaxii/persistence/sqldb/taxii2models.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ class Collection(Base):
description = sqlalchemy.Column(sqlalchemy.Text)
alias = sqlalchemy.Column(sqlalchemy.String(100), nullable=True)
is_public = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False)
is_public_write = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False)

api_root = relationship("ApiRoot", back_populates="collections")
objects = relationship("STIXObject", back_populates="collection")
Expand Down
18 changes: 16 additions & 2 deletions opentaxii/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -509,8 +509,19 @@ def api_root_handler(self, api_root_id):
response["description"] = api_root.description
return make_taxii2_response(response)

@register_handler(r"^/taxii2/(?P<api_root_id>[^/]+)/status/(?P<job_id>[^/]+)/$")
@register_handler(
r"^/taxii2/(?P<api_root_id>[^/]+)/status/(?P<job_id>[^/]+)/$",
handles_own_auth=True,
)
def job_handler(self, api_root_id, job_id):
try:
api_root = self.persistence.get_api_root(api_root_id=api_root_id)
except DoesNotExistError:
if context.account is None:
raise Unauthorized()
raise NotFound()
if context.account is None and not api_root.is_public:
raise Unauthorized()
try:
job = self.persistence.get_job_and_details(
api_root_id=api_root_id, job_id=job_id
Expand Down Expand Up @@ -564,7 +575,10 @@ def collection_handler(self, api_root_id, collection_id_or_alias):
if context.account is None:
raise Unauthorized()
raise NotFound()
if context.account is None and not collection.can_read(context.account):
if context.account is None and not (
collection.can_read(context.account)
or collection.can_write(context.account)
):
raise Unauthorized()
response = {
"id": collection.id,
Expand Down
10 changes: 8 additions & 2 deletions opentaxii/taxii2/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class Collection(Entity):
:param str description: human readable plain text description for this collection
:param str alias: human readable collection name that can be used on systems to alias a collection id
:param bool is_public: whether this is a publicly readable collection
:param bool is_public_write: whether this is a publicly writable collection
"""

def __init__(
Expand All @@ -49,6 +50,7 @@ def __init__(
description: str,
alias: str,
is_public: bool,
is_public_write: bool,
):
"""Initialize Collection."""
self.id = id
Expand All @@ -57,6 +59,7 @@ def __init__(
self.description = description
self.alias = alias
self.is_public = is_public
self.is_public_write = is_public_write

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

def can_write(self, account: Optional[Account]):
"""Determine if `account` is allowed to write to this collection."""
return account and (
account.is_admin or "write" in set(account.permissions.get(self.id, []))
return self.is_public_write or (
account
and (
account.is_admin or "write" in set(account.permissions.get(self.id, []))
)
)


Expand Down
18 changes: 16 additions & 2 deletions tests/taxii2/test_taxii2_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,18 +205,26 @@ def test_collection(


@pytest.mark.parametrize("is_public", [True, False])
@pytest.mark.parametrize("is_public_write", [True, False])
@pytest.mark.parametrize("method", ["get", "post", "delete"])
def test_collection_unauthenticated(
client,
method,
is_public,
is_public_write,
):
if is_public:
collection_id = COLLECTIONS[6].id
if method == "get":
expected_status_code = 200
else:
expected_status_code = 405
elif is_public_write:
collection_id = COLLECTIONS[7].id
if method == "get":
expected_status_code = 200
else:
expected_status_code = 405
else:
collection_id = COLLECTIONS[0].id
if method == "get":
Expand All @@ -241,14 +249,15 @@ def test_collection_unauthenticated(


@pytest.mark.parametrize(
["api_root_id", "title", "description", "alias", "is_public"],
["api_root_id", "title", "description", "alias", "is_public", "is_public_write"],
[
pytest.param(
API_ROOTS[0].id, # api_root_id
"my new collection", # title
None, # description
None, # alias
False, # is_public
False, # is_public_write
id="api_root_id, title",
),
pytest.param(
Expand All @@ -257,6 +266,7 @@ def test_collection_unauthenticated(
"my description", # description
None, # alias
True, # is_public
False, # is_public_write
id="api_root_id, title, description",
),
pytest.param(
Expand All @@ -265,26 +275,29 @@ def test_collection_unauthenticated(
"my description", # description
"my-alias", # alias
False, # is_public
True, # is_public_write
id="api_root_id, title, description, alias",
),
],
)
def test_add_collection(
app, api_root_id, title, description, alias, is_public, db_api_roots, db_collections
app, api_root_id, title, description, alias, is_public, is_public_write, db_api_roots, db_collections
):
collection = app.taxii_server.servers.taxii2.persistence.api.add_collection(
api_root_id=api_root_id,
title=title,
description=description,
alias=alias,
is_public=is_public,
is_public_write=is_public_write,
)
assert collection.id is not None
assert str(collection.api_root_id) == api_root_id
assert collection.title == title
assert collection.description == description
assert collection.alias == alias
assert collection.is_public == is_public
assert collection.is_public_write == is_public_write
db_collection = (
app.taxii_server.servers.taxii2.persistence.api.db.session.query(
taxii2models.Collection
Expand All @@ -297,3 +310,4 @@ def test_add_collection(
assert db_collection.description == description
assert db_collection.alias == alias
assert db_collection.is_public == is_public
assert db_collection.is_public_write == is_public_write
8 changes: 8 additions & 0 deletions tests/taxii2/test_taxii2_collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@
"can_write": False,
"media_types": ["application/stix+json;version=2.1"],
},
{
"id": COLLECTIONS[7].id,
"title": "7Publicwrite",
"description": "public write description",
"can_read": False,
"can_write": True,
"media_types": ["application/stix+json;version=2.1"],
},
]
},
id="good, first",
Expand Down
24 changes: 23 additions & 1 deletion tests/taxii2/test_taxii2_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -1239,11 +1239,13 @@ def test_objects(


@pytest.mark.parametrize("is_public", [True, False])
@pytest.mark.parametrize("is_public_write", [True, False])
@pytest.mark.parametrize("method", ["get", "post", "delete"])
def test_objects_unauthenticated(
client,
method,
is_public,
is_public_write,
):
if is_public:
collection_id = COLLECTIONS[6].id
Expand All @@ -1253,6 +1255,14 @@ def test_objects_unauthenticated(
expected_status_code = 401
else:
expected_status_code = 405
elif is_public_write:
collection_id = COLLECTIONS[7].id
if method == "get":
expected_status_code = 401
elif method == "post":
expected_status_code = 202
else:
expected_status_code = 405
else:
collection_id = COLLECTIONS[0].id
if method == "get":
Expand All @@ -1269,7 +1279,11 @@ def test_objects_unauthenticated(
client.application.taxii_server.servers.taxii2.persistence.api,
"get_collection",
side_effect=GET_COLLECTION_MOCK,
):
), patch.object(
client.application.taxii_server.servers.taxii2.persistence.api,
"add_objects",
side_effect=ADD_OBJECTS_MOCK,
) as add_objects_mock:
kwargs = {
"headers": {
"Accept": "application/taxii+json;version=2.1",
Expand Down Expand Up @@ -1302,3 +1316,11 @@ def test_objects_unauthenticated(
**kwargs,
)
assert response.status_code == expected_status_code
if method == "post" and expected_status_code == 202:
add_objects_mock.assert_called_once_with(
api_root_id=API_ROOTS[0].id,
collection_id=COLLECTIONS[7].id,
objects=kwargs["json"]["objects"],
)
else:
add_objects_mock.assert_not_called()
43 changes: 38 additions & 5 deletions tests/taxii2/test_taxii2_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
import pytest
from opentaxii.persistence.sqldb import taxii2models
from opentaxii.taxii2.utils import taxii2_datetimeformat
from tests.taxii2.utils import (API_ROOTS, GET_JOB_AND_DETAILS_MOCK, JOBS,
config_noop, server_mapping_noop,
from tests.taxii2.utils import (API_ROOTS, GET_API_ROOT_MOCK,
GET_JOB_AND_DETAILS_MOCK, JOBS, config_noop,
server_mapping_noop,
server_mapping_remove_fields)


Expand Down Expand Up @@ -251,6 +252,10 @@ def test_status(
authenticated_client.application.taxii_server.servers.taxii2.persistence.api,
"get_api_roots",
return_value=API_ROOTS,
), patch.object(
authenticated_client.application.taxii_server.servers.taxii2.persistence.api,
"get_api_root",
side_effect=GET_API_ROOT_MOCK,
), patch.object(
authenticated_client.application.taxii_server.servers.taxii2.persistence.api,
"get_job_and_details",
Expand Down Expand Up @@ -278,14 +283,42 @@ def test_status(
assert content == expected_content


@pytest.mark.parametrize("is_public", [True, False])
@pytest.mark.parametrize("method", ["get", "post", "delete"])
def test_status_unauthenticated(
client,
method,
is_public,
):
func = getattr(client, method)
response = func(f"/taxii2/{API_ROOTS[0].id}/status/{JOBS[0].id}/")
assert response.status_code == 401
if is_public:
api_root_id = API_ROOTS[1].id
job_id = JOBS[2].id
if method == "get":
expected_status_code = 200
else:
expected_status_code = 405
else:
api_root_id = API_ROOTS[0].id
job_id = JOBS[0].id
if method == "get":
expected_status_code = 401
else:
expected_status_code = 405
with patch.object(
client.application.taxii_server.servers.taxii2.persistence.api,
"get_api_root",
side_effect=GET_API_ROOT_MOCK,
), patch.object(
client.application.taxii_server.servers.taxii2.persistence.api,
"get_job_and_details",
side_effect=GET_JOB_AND_DETAILS_MOCK,
):
func = getattr(client, method)
response = func(
f"/taxii2/{api_root_id}/status/{job_id}/",
headers={"Accept": "application/taxii+json;version=2.1"},
)
assert response.status_code == expected_status_code


def test_job_cleanup(app, db_jobs):
Expand Down

0 comments on commit aea56b7

Please sign in to comment.