Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion core/pystac/validation/stac_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,15 +142,23 @@ class JsonSchemaSTACValidator(STACValidator):
the validator will retrieve the JSON schemas for validation.
Defaults to an instance of
:class:`~pystac.validation.schema_uri_map.DefaultSchemaUriMap`
allow_list: Optional list of base urls that are the validator can read from
when validating. Defaults to ``None`` which means that the validator
can read from anywhere.

Note:
This class requires the ``jsonschema`` library to be installed.
"""

schema_uri_map: SchemaUriMap
schema_cache: dict[str, dict[str, Any]]
allow_list: list[str] | None = None

def __init__(self, schema_uri_map: SchemaUriMap | None = None) -> None:
def __init__(
self,
schema_uri_map: SchemaUriMap | None = None,
allow_list: list[str] | None = None,
) -> None:
if not HAS_JSONSCHEMA:
raise ImportError("Cannot instantiate, requires jsonschema package")

Expand All @@ -159,11 +167,18 @@ def __init__(self, schema_uri_map: SchemaUriMap | None = None) -> None:
else:
self.schema_uri_map = DefaultSchemaUriMap()

self.allow_list = self.allow_list if allow_list is None else allow_list
self.schema_cache = get_local_schema_cache()

def _get_schema(self, schema_uri: str) -> dict[str, Any]:
if schema_uri not in self.schema_cache:
try:
if self.allow_list is not None and not any(
[schema_uri.startswith(base_url) for base_url in self.allow_list]
):
raise ValueError(
f"{schema_uri=} is not in the allow-list {self.allow_list}"
)
s = json.loads(pystac.StacIO.default().read_text(schema_uri))
except Exception as error:
raise GetSchemaError(schema_uri, error) from error
Expand Down Expand Up @@ -302,3 +317,14 @@ def validate_extension(
self._validate_from_uri(stac_dict, stac_object_type, schema_uri, href)

return schema_uri


class SecureJsonSchemaSTACValidator(JsonSchemaSTACValidator):

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ended up adding this because it seems like nice quality of life, but I am pretty +0 on it.

"""Validate STAC, but only allow fetching schemas from domains that
are standard to the STAC organization.
"""

allow_list: list[str] = [
"https://schemas.stacspec.org/",
"https://stac-extensions.github.io/",
]
5 changes: 5 additions & 0 deletions docs/api/validation/stac_validator.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ pystac.validation.stac\_validator
:undoc-members:
:noindex:

.. autoclass:: pystac.validation.stac_validator.SecureJsonSchemaSTACValidator
:members:
:undoc-members:
:noindex:

.. automodule:: pystac.validation.stac_validator
:members:
:undoc-members:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
interactions:
- request:
body: null
headers: {}
method: GET
uri: https://stac-extensions.github.io/projection/v2.0.0/schema.json
response:
body:
string: "{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"$id\":
\"https://stac-extensions.github.io/projection/v2.0.0/schema.json\",\n \"title\":
\"Projection Extension\",\n \"description\": \"STAC Projection Extension
for STAC Items.\",\n \"$comment\": \"This schema succeeds if the proj: fields
are not used at all, please keep this in mind.\",\n \"oneOf\": [\n {\n
\ \"$comment\": \"This is the schema for STAC Items.\",\n \"allOf\":
[\n {\n \"$ref\": \"#/definitions/stac_extensions\"\n },\n
\ {\n \"type\": \"object\",\n \"required\": [\n \"type\",\n
\ \"properties\",\n \"assets\"\n ],\n \"properties\":
{\n \"type\": {\n \"const\": \"Feature\"\n },\n
\ \"properties\": {\n \"$ref\": \"#/definitions/fields\"\n
\ },\n \"assets\": {\n \"type\": \"object\",\n
\ \"additionalProperties\": {\n \"$ref\": \"#/definitions/fields\"\n
\ }\n }\n }\n }\n ]\n },\n
\ {\n \"$comment\": \"This is the schema for STAC Collections.\",\n
\ \"allOf\": [\n {\n \"type\": \"object\",\n \"required\":
[\n \"type\"\n ],\n \"properties\": {\n \"type\":
{\n \"const\": \"Collection\"\n },\n \"assets\":
{\n \"type\": \"object\",\n \"additionalProperties\":
{\n \"$ref\": \"#/definitions/fields\"\n }\n },\n
\ \"item_assets\": {\n \"type\": \"object\",\n \"additionalProperties\":
{\n \"$ref\": \"#/definitions/fields\"\n }\n }\n
\ }\n },\n {\n \"$ref\": \"#/definitions/stac_extensions\"\n
\ }\n ]\n }\n ],\n \"definitions\": {\n \"stac_extensions\":
{\n \"type\": \"object\",\n \"required\": [\n \"stac_extensions\"\n
\ ],\n \"properties\": {\n \"stac_extensions\": {\n \"type\":
\"array\",\n \"contains\": {\n \"const\": \"https://stac-extensions.github.io/projection/v2.0.0/schema.json\"\n
\ }\n }\n }\n },\n \"fields\": {\n \"type\":
\"object\",\n \"properties\": {\n \"proj:code\":{\n \"title\":\"Projection
code\",\n \"type\":[\n \"string\",\n \"null\"\n
\ ]\n },\n \"proj:wkt2\":{\n \"title\":\"Coordinate
Reference System in WKT2 format\",\n \"type\":[\n \"string\",\n
\ \"null\"\n ]\n },\n \"proj:projjson\":
{\n \"title\":\"Coordinate Reference System in PROJJSON format\",\n
\ \"oneOf\": [\n {\n \"$ref\": \"https://proj.org/schemas/v0.7/projjson.schema.json\"\n
\ },\n {\n \"type\": \"null\"\n }\n
\ ]\n },\n \"proj:geometry\":{\n \"$ref\":
\"https://geojson.org/schema/Geometry.json\"\n },\n \"proj:bbox\":{\n
\ \"title\":\"Extent\",\n \"type\":\"array\",\n \"oneOf\":
[\n {\n \"minItems\":4,\n \"maxItems\":4\n
\ },\n {\n \"minItems\":6,\n \"maxItems\":6\n
\ }\n ],\n \"items\":{\n \"type\":\"number\"\n
\ }\n },\n \"proj:centroid\":{\n \"title\":\"Centroid\",\n
\ \"type\":\"object\",\n \"required\": [\n \"lat\",\n
\ \"lon\"\n ],\n \"properties\": {\n \"lat\":
{\n \"type\": \"number\",\n \"minimum\": -90,\n
\ \"maximum\": 90\n },\n \"lon\": {\n \"type\":
\"number\",\n \"minimum\": -180,\n \"maximum\":
180\n }\n }\n },\n \"proj:shape\":{\n \"title\":\"Shape\",\n
\ \"type\":\"array\",\n \"minItems\":2,\n \"maxItems\":2,\n
\ \"items\":{\n \"type\":\"integer\"\n }\n },\n
\ \"proj:transform\":{\n \"title\":\"Transform\",\n \"type\":\"array\",\n
\ \"oneOf\": [\n {\n \"minItems\":6,\n \"maxItems\":6\n
\ },\n {\n \"minItems\":9,\n \"maxItems\":9\n
\ }\n ],\n \"items\":{\n \"type\":\"number\"\n
\ }\n }\n },\n \"patternProperties\": {\n \"^(?!proj:)\":
{}\n },\n \"additionalProperties\": false\n }\n }\n}"
headers: {}
status:
code: 200
message: OK
version: 1
17 changes: 17 additions & 0 deletions tests/validation/test_validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from pystac.serialization.common_properties import merge_common_properties
from pystac.utils import get_opt
from pystac.validation import GetSchemaError, JsonSchemaSTACValidator
from pystac.validation.stac_validator import SecureJsonSchemaSTACValidator
from tests.utils import TestCases
from tests.utils.test_cases import ExampleInfo

Expand Down Expand Up @@ -259,3 +260,19 @@ def test_404_schema_url(requests_mock: Mocker, item: pystac.Item) -> None:
GetSchemaError, match="http://pystac-extensions.test/a-fake.schema.json"
):
item.validate()


@pytest.mark.vcr()
def test_secure_validator_allows_trusted_schema(item: pystac.Item) -> None:
schema_uri = "https://stac-extensions.github.io/projection/v2.0.0/schema.json"
item.stac_extensions.append(schema_uri)

item.validate(validator=SecureJsonSchemaSTACValidator())


def test_secure_validator_rejects_untrusted_schema(item: pystac.Item) -> None:
schema_uri = "https://untrusted.example/schema.json"
item.stac_extensions.append(schema_uri)

with pytest.raises(GetSchemaError, match="not in the allow-list"):
item.validate(validator=SecureJsonSchemaSTACValidator())
Loading