Skip to content

Commit

Permalink
Merge pull request #228 from eclecticiq/public-collection-access
Browse files Browse the repository at this point in the history
Public collection access
  • Loading branch information
erwin-eiq authored May 24, 2022
2 parents ff9fade + 34dff02 commit 80cd524
Show file tree
Hide file tree
Showing 17 changed files with 429 additions and 71 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.5.0 (2022-05-24)
------------------
* Add support for publicly readable taxii 2 collections

0.4.0 (2022-05-20)
------------------
* Move next_param handling into `OpenTAXII2PersistenceAPI`
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.4.0'
__version__ = '0.5.0'
5 changes: 5 additions & 0 deletions opentaxii/cli/persistence.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ def add_collection():
"-d", "--description", required=False, help="Description of the collection"
)
parser.add_argument("-a", "--alias", required=False, help="alias of the collection")
parser.add_argument(
"--public", action="store_true", help="allow public read access"
)
parser.set_defaults(public=False)

args = parser.parse_args()
with app.app_context():
Expand All @@ -142,6 +146,7 @@ def add_collection():
title=args.title,
description=args.description,
alias=args.alias,
is_public=args.public,
)


Expand Down
17 changes: 14 additions & 3 deletions opentaxii/persistence/sqldb/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -504,9 +504,11 @@ def parse_next_param(next_param: str) -> Dict:
"""
Parse provided `next_param` into kwargs to be used to filter stix objects.
"""
date_added_str, obj_id = base64.b64decode(next_param.encode()).decode().split("|")
date_added_str, obj_id = (
base64.b64decode(next_param.encode()).decode().split("|")
)
date_added = datetime.datetime.strptime(
date_added_str.split('+')[0], "%Y-%m-%dT%H:%M:%S.%f"
date_added_str.split("+")[0], "%Y-%m-%dT%H:%M:%S.%f"
).replace(tzinfo=datetime.timezone.utc)
return {"id": obj_id, "date_added": date_added}

Expand Down Expand Up @@ -630,6 +632,7 @@ def get_collections(self, api_root_id: str) -> List[entities.Collection]:
title=obj.title,
description=obj.description,
alias=obj.alias,
is_public=obj.is_public,
)
for obj in query.all()
]
Expand Down Expand Up @@ -660,6 +663,7 @@ def get_collection(
title=obj.title,
description=obj.description,
alias=obj.alias,
is_public=obj.is_public,
)

def add_collection(
Expand All @@ -668,6 +672,7 @@ def add_collection(
title: str,
description: Optional[str] = None,
alias: Optional[str] = None,
is_public: bool = False,
) -> entities.Collection:
"""
Add a new collection.
Expand All @@ -676,11 +681,16 @@ def add_collection(
:param str title: Title of the new 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
:return: The added Collection entity.
"""
collection = taxii2models.Collection(
api_root_id=api_root_id, title=title, description=description, alias=alias
api_root_id=api_root_id,
title=title,
description=description,
alias=alias,
is_public=is_public,
)
self.db.session.add(collection)
self.db.session.commit()
Expand All @@ -691,6 +701,7 @@ def add_collection(
title=collection.title,
description=collection.description,
alias=collection.alias,
is_public=collection.is_public,
)

def _objects_query(self, collection_id: str, ordered: bool) -> Query:
Expand Down
7 changes: 5 additions & 2 deletions opentaxii/persistence/sqldb/taxii2models.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class Job(Base):
details = relationship("JobDetail", back_populates="job")

__table_args__ = (
sqlalchemy.Index('ix_opentaxii_job_api_root_id_id', api_root_id, id),
sqlalchemy.Index("ix_opentaxii_job_api_root_id_id", api_root_id, id),
)

@classmethod
Expand Down Expand Up @@ -118,6 +118,7 @@ class Collection(Base):
title = sqlalchemy.Column(sqlalchemy.String(100), nullable=False, index=True)
description = sqlalchemy.Column(sqlalchemy.Text)
alias = sqlalchemy.Column(sqlalchemy.String(100), nullable=True)
is_public = sqlalchemy.Column(sqlalchemy.Boolean, nullable=False)

api_root = relationship("ApiRoot", back_populates="collections")
objects = relationship("STIXObject", back_populates="collection")
Expand All @@ -138,7 +139,9 @@ class STIXObject(Base):
pk = sqlalchemy.Column(GUID, primary_key=True, default=uuid.uuid4)
id = sqlalchemy.Column(sqlalchemy.String(100), index=True)
collection_id = sqlalchemy.Column(
GUID, sqlalchemy.ForeignKey("opentaxii_collection.id", ondelete="CASCADE"), index=True,
GUID,
sqlalchemy.ForeignKey("opentaxii_collection.id", ondelete="CASCADE"),
index=True,
)
type = sqlalchemy.Column(sqlalchemy.String(50), index=True)
spec_version = sqlalchemy.Column(sqlalchemy.String(10), index=True) # STIX version
Expand Down
43 changes: 37 additions & 6 deletions opentaxii/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,11 @@ def get_endpoint(self, relative_path: str) -> Optional[Callable[[], Response]]:
if endpoint:
return functools.partial(self.handle_request, endpoint)

def check_authentication(self):
def check_authentication(self, endpoint: Callable[[], Response]):
"""Check if account is authenticated, unless endpoint handles that itself."""
if endpoint.func.handles_own_auth:
# Endpoint will handle auth checks itself
return
if context.account is None:
raise Unauthorized()

Expand Down Expand Up @@ -458,7 +462,7 @@ def check_allowed_methods(self, endpoint: Callable[[], Response]):
raise MethodNotAllowed(valid_methods=endpoint.func.registered_valid_methods)

def handle_request(self, endpoint: Callable[[], Response]):
self.check_authentication()
self.check_authentication(endpoint)
self.check_content_length()
self.check_allowed_methods(endpoint)
self.check_headers(endpoint)
Expand Down Expand Up @@ -547,15 +551,20 @@ def collections_handler(self, api_root_id):
return make_taxii2_response(response)

@register_handler(
r"^/(?P<api_root_id>[^/]+)/collections/(?P<collection_id_or_alias>[^/]+)/$"
r"^/(?P<api_root_id>[^/]+)/collections/(?P<collection_id_or_alias>[^/]+)/$",
handles_own_auth=True,
)
def collection_handler(self, api_root_id, collection_id_or_alias):
try:
collection = self.persistence.get_collection(
api_root_id=api_root_id, collection_id_or_alias=collection_id_or_alias
)
except DoesNotExistError:
if context.account is None:
raise Unauthorized()
raise NotFound()
if context.account is None and not collection.can_read(context.account):
raise Unauthorized()
response = {
"id": collection.id,
"title": collection.title,
Expand All @@ -570,7 +579,8 @@ def collection_handler(self, api_root_id, collection_id_or_alias):
return make_taxii2_response(response)

@register_handler(
r"^/(?P<api_root_id>[^/]+)/collections/(?P<collection_id_or_alias>[^/]+)/manifest/$"
r"^/(?P<api_root_id>[^/]+)/collections/(?P<collection_id_or_alias>[^/]+)/manifest/$",
handles_own_auth=True,
)
def manifest_handler(self, api_root_id, collection_id_or_alias):
filter_params = validate_list_filter_params(request.args, self.persistence.api)
Expand All @@ -581,6 +591,8 @@ def manifest_handler(self, api_root_id, collection_id_or_alias):
**filter_params,
)
except (DoesNotExistError, NoReadPermission):
if context.account is None:
raise Unauthorized()
raise NotFound()
if manifest:
response = {
Expand Down Expand Up @@ -615,6 +627,7 @@ def manifest_handler(self, api_root_id, collection_id_or_alias):
r"^/(?P<api_root_id>[^/]+)/collections/(?P<collection_id_or_alias>[^/]+)/objects/$",
("GET", "POST"),
valid_content_types=("application/taxii+json;version=2.1",),
handles_own_auth=True,
)
def objects_handler(self, api_root_id, collection_id_or_alias):
if request.method == "GET":
Expand All @@ -631,6 +644,8 @@ def objects_get_handler(self, api_root_id, collection_id_or_alias):
**filter_params,
)
except (DoesNotExistError, NoReadPermission):
if context.account is None:
raise Unauthorized()
raise NotFound()
if objects:
response = {
Expand Down Expand Up @@ -672,6 +687,8 @@ def objects_post_handler(self, api_root_id, collection_id_or_alias):
data=request.get_json(),
)
except (DoesNotExistError, NoWritePermission):
if context.account is None:
raise Unauthorized()
raise NotFound()
response = {
"id": job.id,
Expand Down Expand Up @@ -701,6 +718,7 @@ def objects_post_handler(self, api_root_id, collection_id_or_alias):
@register_handler(
r"^/(?P<api_root_id>[^/]+)/collections/(?P<collection_id_or_alias>[^/]+)/objects/(?P<object_id>[^/]+)/$",
("GET", "DELETE"),
handles_own_auth=True,
)
def object_handler(self, api_root_id, collection_id_or_alias, object_id):
if request.method == "GET":
Expand All @@ -713,7 +731,9 @@ def object_handler(self, api_root_id, collection_id_or_alias, object_id):
)

def object_get_handler(self, api_root_id, collection_id_or_alias, object_id):
filter_params = validate_object_filter_params(request.args, self.persistence.api)
filter_params = validate_object_filter_params(
request.args, self.persistence.api
)
try:
versions, more, next_param = self.persistence.get_object(
api_root_id=api_root_id,
Expand All @@ -722,6 +742,8 @@ def object_get_handler(self, api_root_id, collection_id_or_alias, object_id):
**filter_params,
)
except (DoesNotExistError, NoReadPermission):
if context.account is None:
raise Unauthorized()
raise NotFound()
if versions:
response = {
Expand Down Expand Up @@ -764,8 +786,12 @@ def object_delete_handler(self, api_root_id, collection_id_or_alias, object_id):
**filter_params,
)
except (DoesNotExistError, NoReadNoWritePermission):
if context.account is None:
raise Unauthorized()
raise NotFound()
except (NoReadPermission, NoWritePermission):
if context.account is None:
raise Unauthorized()
raise Forbidden()
return make_taxii2_response("")

Expand All @@ -774,9 +800,12 @@ def object_delete_handler(self, api_root_id, collection_id_or_alias, object_id):
r"^/(?P<api_root_id>[^/]+)/collections/(?P<collection_id_or_alias>[^/]+)"
r"/objects/(?P<object_id>[^/]+)/versions/$"
),
handles_own_auth=True,
)
def versions_handler(self, api_root_id, collection_id_or_alias, object_id):
filter_params = validate_versions_filter_params(request.args, self.persistence.api)
filter_params = validate_versions_filter_params(
request.args, self.persistence.api
)
try:
versions, more = self.persistence.get_versions(
api_root_id=api_root_id,
Expand All @@ -785,6 +814,8 @@ def versions_handler(self, api_root_id, collection_id_or_alias, object_id):
**filter_params,
)
except (DoesNotExistError, NoReadPermission):
if context.account is None:
raise Unauthorized()
raise NotFound()
if versions:
response = {
Expand Down
17 changes: 14 additions & 3 deletions opentaxii/taxii2/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,22 +34,33 @@ class Collection(Entity):
:param str title: human readable plain text name used to identify this collection
: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
"""

def __init__(
self, id: str, api_root_id: str, title: str, description: str, alias: str
self,
id: str,
api_root_id: str,
title: str,
description: str,
alias: str,
is_public: bool,
):
"""Initialize Collection."""
self.id = id
self.api_root_id = api_root_id
self.title = title
self.description = description
self.alias = alias
self.is_public = is_public

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

def can_write(self, account: Optional[Account]):
Expand Down
2 changes: 2 additions & 0 deletions opentaxii/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ def register_handler(
valid_methods: Optional[Tuple[str]] = None,
valid_accept_mimetypes: Optional[Tuple[str]] = None,
valid_content_types: Optional[Tuple[str]] = None,
handles_own_auth: bool = False,
):
"""
Register decorated method as handler function for `url_re`.
Expand Down Expand Up @@ -345,6 +346,7 @@ def inner(*args, **kwargs):
inner.registered_valid_methods = valid_methods
inner.registered_valid_accept_mimetypes = valid_accept_mimetypes
inner.registered_valid_content_types = valid_content_types
inner.handles_own_auth = handles_own_auth
return inner

return inner_decorator
Loading

0 comments on commit 80cd524

Please sign in to comment.