fix: stricter public reads
This commit is contained in:
@@ -52,6 +52,7 @@ from .models import (
|
|||||||
StorageGetResponse,
|
StorageGetResponse,
|
||||||
StoragePaginatedRequest,
|
StoragePaginatedRequest,
|
||||||
StoragePaginatedResponse,
|
StoragePaginatedResponse,
|
||||||
|
StoragePublicPaginatedRequest,
|
||||||
StorageSetRequest,
|
StorageSetRequest,
|
||||||
StorageSetResponse,
|
StorageSetResponse,
|
||||||
UserWalletSummary,
|
UserWalletSummary,
|
||||||
@@ -128,7 +129,7 @@ class ExtensionHostAPI:
|
|||||||
async def storage_get_public(
|
async def storage_get_public(
|
||||||
self, request: StorageGetRequest
|
self, request: StorageGetRequest
|
||||||
) -> StorageGetResponse:
|
) -> StorageGetResponse:
|
||||||
public_fields = self._public_storage_fields(request.table)
|
public_fields = self._public_storage_policy(request.table)["public_fields"]
|
||||||
row = await storage_get_public_row(self.extension_id, request.table, request.id)
|
row = await storage_get_public_row(self.extension_id, request.table, request.id)
|
||||||
if not row:
|
if not row:
|
||||||
return StorageGetResponse()
|
return StorageGetResponse()
|
||||||
@@ -239,14 +240,23 @@ class ExtensionHostAPI:
|
|||||||
require_auth=False,
|
require_auth=False,
|
||||||
)
|
)
|
||||||
async def storage_get_public_paginated(
|
async def storage_get_public_paginated(
|
||||||
self, request: StoragePaginatedRequest
|
self, request: StoragePublicPaginatedRequest
|
||||||
) -> StoragePaginatedResponse:
|
) -> StoragePaginatedResponse:
|
||||||
public_fields = self._public_storage_fields(request.table)
|
policy = self._public_storage_policy(request.table)
|
||||||
self._validate_public_storage_query_fields(request, public_fields)
|
public_fields = policy["public_fields"]
|
||||||
|
source_id_field = policy["source_id_field"]
|
||||||
|
if not isinstance(source_id_field, str) or not source_id_field:
|
||||||
|
raise PermissionError(
|
||||||
|
"Public paginated storage reads require a source ID field policy."
|
||||||
|
)
|
||||||
|
|
||||||
|
filters = self._public_storage_paginated_filters(
|
||||||
|
request, public_fields, source_id_field
|
||||||
|
)
|
||||||
page = await storage_get_public_paginated_rows(
|
page = await storage_get_public_paginated_rows(
|
||||||
self.extension_id,
|
self.extension_id,
|
||||||
request.table,
|
request.table,
|
||||||
request.filters,
|
filters,
|
||||||
search=request.search,
|
search=request.search,
|
||||||
search_fields=request.search_fields,
|
search_fields=request.search_fields,
|
||||||
sort_by=request.sort_by,
|
sort_by=request.sort_by,
|
||||||
@@ -737,7 +747,7 @@ class ExtensionHostAPI:
|
|||||||
|
|
||||||
return permission_ids, policies
|
return permission_ids, policies
|
||||||
|
|
||||||
def _public_storage_fields(self, table: str) -> set[str]:
|
def _public_storage_policy(self, table: str) -> dict[str, Any]:
|
||||||
tables = self.permission_policies.get("ext.storage.read_public")
|
tables = self.permission_policies.get("ext.storage.read_public")
|
||||||
if not isinstance(tables, list) or not tables:
|
if not isinstance(tables, list) or not tables:
|
||||||
raise PermissionError(
|
raise PermissionError(
|
||||||
@@ -757,24 +767,56 @@ class ExtensionHostAPI:
|
|||||||
raise PermissionError(
|
raise PermissionError(
|
||||||
f"Public storage table '{table}' has no valid public fields."
|
f"Public storage table '{table}' has no valid public fields."
|
||||||
)
|
)
|
||||||
return set(public_fields)
|
source_id_field = table_policy.get("source_id_field")
|
||||||
|
if source_id_field is not None and (
|
||||||
|
not isinstance(source_id_field, str) or not source_id_field
|
||||||
|
):
|
||||||
|
raise PermissionError(
|
||||||
|
f"Public storage table '{table}' has no valid source ID field."
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"public_fields": set(public_fields),
|
||||||
|
"source_id_field": source_id_field,
|
||||||
|
}
|
||||||
|
|
||||||
raise PermissionError(f"Storage table '{table}' is not publicly readable.")
|
raise PermissionError(f"Storage table '{table}' is not publicly readable.")
|
||||||
|
|
||||||
def _validate_public_storage_query_fields(
|
def _validate_public_storage_query_fields(
|
||||||
self, request: StoragePaginatedRequest, public_fields: set[str]
|
self,
|
||||||
|
request: StoragePaginatedRequest,
|
||||||
|
public_fields: set[str],
|
||||||
|
allowed_private_fields: set[str] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
allowed_private_fields = allowed_private_fields or set()
|
||||||
query_fields = set(request.filters)
|
query_fields = set(request.filters)
|
||||||
query_fields.update(request.search_fields)
|
query_fields.update(request.search_fields)
|
||||||
if request.sort_by:
|
if request.sort_by:
|
||||||
query_fields.add(request.sort_by)
|
query_fields.add(request.sort_by)
|
||||||
private_fields = sorted(query_fields - public_fields)
|
private_fields = sorted(query_fields - public_fields - allowed_private_fields)
|
||||||
if private_fields:
|
if private_fields:
|
||||||
raise PermissionError(
|
raise PermissionError(
|
||||||
"Public storage query uses non-public fields: "
|
"Public storage query uses non-public fields: "
|
||||||
+ ", ".join(private_fields)
|
+ ", ".join(private_fields)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _public_storage_paginated_filters(
|
||||||
|
self,
|
||||||
|
request: StoragePublicPaginatedRequest,
|
||||||
|
public_fields: set[str],
|
||||||
|
source_id_field: str,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
self._validate_public_storage_query_fields(
|
||||||
|
request, public_fields, {source_id_field}
|
||||||
|
)
|
||||||
|
filters = dict(request.filters)
|
||||||
|
requested_source_id = filters.get(source_id_field)
|
||||||
|
if requested_source_id is not None and requested_source_id != request.source_id:
|
||||||
|
raise PermissionError(
|
||||||
|
"Public storage source filter does not match source_id."
|
||||||
|
)
|
||||||
|
filters[source_id_field] = request.source_id
|
||||||
|
return filters
|
||||||
|
|
||||||
async def _public_storage_append_policy(
|
async def _public_storage_append_policy(
|
||||||
self, table: str, source_id: str
|
self, table: str, source_id: str
|
||||||
) -> tuple[dict[str, Any], str]:
|
) -> tuple[dict[str, Any], str]:
|
||||||
|
|||||||
@@ -109,6 +109,10 @@ class StoragePaginatedRequest(BaseModel):
|
|||||||
return values
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
class StoragePublicPaginatedRequest(StoragePaginatedRequest):
|
||||||
|
source_id: str = Field(..., min_length=1, max_length=512)
|
||||||
|
|
||||||
|
|
||||||
class StoragePaginatedResponse(BaseModel):
|
class StoragePaginatedResponse(BaseModel):
|
||||||
rows_json: str = "[]"
|
rows_json: str = "[]"
|
||||||
total: int = 0
|
total: int = 0
|
||||||
|
|||||||
@@ -210,29 +210,47 @@ def _public_storage_grant_is_subset(
|
|||||||
) -> bool:
|
) -> bool:
|
||||||
requested_tables = _public_storage_tables(requested_policies)
|
requested_tables = _public_storage_tables(requested_policies)
|
||||||
granted_tables = _public_storage_tables(granted_policies)
|
granted_tables = _public_storage_tables(granted_policies)
|
||||||
for table_name, granted_fields in granted_tables.items():
|
for table_name, granted_policy in granted_tables.items():
|
||||||
requested_fields = requested_tables.get(table_name)
|
requested_policy = requested_tables.get(table_name)
|
||||||
if requested_fields is None or not granted_fields.issubset(requested_fields):
|
if requested_policy is None:
|
||||||
|
return False
|
||||||
|
if not granted_policy["public_fields"].issubset(
|
||||||
|
requested_policy["public_fields"]
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
requested_source_id_field = requested_policy["source_id_field"]
|
||||||
|
if (
|
||||||
|
requested_source_id_field
|
||||||
|
and granted_policy["source_id_field"] != requested_source_id_field
|
||||||
|
):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _public_storage_tables(policies: list[Any] | None) -> dict[str, set[str]]:
|
def _public_storage_tables(policies: list[Any] | None) -> dict[str, dict[str, Any]]:
|
||||||
tables: dict[str, set[str]] = {}
|
tables: dict[str, dict[str, Any]] = {}
|
||||||
for policy in _policy_list(policies):
|
for policy in _policy_list(policies):
|
||||||
if not isinstance(policy, dict):
|
if not isinstance(policy, dict):
|
||||||
continue
|
continue
|
||||||
table_name = policy.get("table_name")
|
table_name = policy.get("table_name")
|
||||||
public_fields = policy.get("public_fields")
|
public_fields = policy.get("public_fields")
|
||||||
|
source_id_field = policy.get("source_id_field")
|
||||||
if (
|
if (
|
||||||
not isinstance(table_name, str)
|
not isinstance(table_name, str)
|
||||||
or table_name in tables
|
or table_name in tables
|
||||||
or not isinstance(public_fields, list)
|
or not isinstance(public_fields, list)
|
||||||
|
or (
|
||||||
|
source_id_field is not None
|
||||||
|
and (not isinstance(source_id_field, str) or not source_id_field)
|
||||||
|
)
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
fields = {field for field in public_fields if isinstance(field, str) and field}
|
fields = {field for field in public_fields if isinstance(field, str) and field}
|
||||||
if fields:
|
if fields:
|
||||||
tables[table_name] = fields
|
tables[table_name] = {
|
||||||
|
"public_fields": fields,
|
||||||
|
"source_id_field": source_id_field,
|
||||||
|
}
|
||||||
return tables
|
return tables
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -549,6 +549,8 @@ window.localisation.en = {
|
|||||||
extension_permission_ext_storage_append_public_sources:
|
extension_permission_ext_storage_append_public_sources:
|
||||||
'Allowed append targets',
|
'Allowed append targets',
|
||||||
extension_permission_ext_storage_read_public: 'Read public extension storage',
|
extension_permission_ext_storage_read_public: 'Read public extension storage',
|
||||||
|
extension_permission_ext_storage_read_public_source_required:
|
||||||
|
'required to read',
|
||||||
extension_permission_ext_storage_write: 'Write extension storage',
|
extension_permission_ext_storage_write: 'Write extension storage',
|
||||||
extension_permission_ext_storage_read_write: 'Read & Write extension storage',
|
extension_permission_ext_storage_read_write: 'Read & Write extension storage',
|
||||||
extension_permission_extension_api_request: 'Use other extensions',
|
extension_permission_extension_api_request: 'Use other extensions',
|
||||||
|
|||||||
@@ -174,7 +174,12 @@
|
|||||||
: table.public_fields.filter(
|
: table.public_fields.filter(
|
||||||
field => typeof field === 'string' && field
|
field => typeof field === 'string' && field
|
||||||
)
|
)
|
||||||
return tableName ? {table: tableName, fields} : null
|
const sourceIdField =
|
||||||
|
typeof table === 'string' ||
|
||||||
|
typeof table?.source_id_field !== 'string'
|
||||||
|
? ''
|
||||||
|
: table.source_id_field
|
||||||
|
return tableName ? {table: tableName, fields, sourceIdField} : null
|
||||||
})
|
})
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,28 +9,31 @@
|
|||||||
>
|
>
|
||||||
<template v-slot:header>
|
<template v-slot:header>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label class="text-weight-medium">
|
<div class="row items-center q-col-gutter-x-md q-row-gutter-sm">
|
||||||
<span v-text="permission.label"></span>
|
<q-item-label
|
||||||
</q-item-label>
|
class="text-weight-medium col-auto"
|
||||||
</q-item-section>
|
style="max-width: 100%"
|
||||||
<q-item-section
|
>
|
||||||
v-if="permission.risk.level !== 'low' || permission.badges.length"
|
<span v-text="permission.label"></span>
|
||||||
side
|
</q-item-label>
|
||||||
top
|
<div
|
||||||
>
|
v-if="permission.risk.level !== 'low' || permission.badges.length"
|
||||||
<div class="row items-center justify-end q-gutter-xs">
|
class="row items-center q-gutter-xs col-auto q-mb-xs"
|
||||||
<q-badge
|
style="max-width: 100%"
|
||||||
v-for="badge of permission.badges"
|
>
|
||||||
:key="badge.key"
|
<q-badge
|
||||||
outline
|
v-for="badge of permission.badges"
|
||||||
color="primary"
|
:key="badge.key"
|
||||||
v-text="badge.label"
|
outline
|
||||||
></q-badge>
|
color="primary"
|
||||||
<q-badge
|
v-text="badge.label"
|
||||||
v-if="permission.risk.level !== 'low'"
|
></q-badge>
|
||||||
:color="permission.risk.color"
|
<q-badge
|
||||||
v-text="permission.risk.label"
|
v-if="permission.risk.level !== 'low'"
|
||||||
></q-badge>
|
:color="permission.risk.color"
|
||||||
|
v-text="permission.risk.label"
|
||||||
|
></q-badge>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</template>
|
</template>
|
||||||
@@ -57,7 +60,24 @@
|
|||||||
></p>
|
></p>
|
||||||
<ul v-if="permission.fieldGroups.length" class="q-my-sm q-pl-md">
|
<ul v-if="permission.fieldGroups.length" class="q-my-sm q-pl-md">
|
||||||
<li v-for="group of permission.fieldGroups" :key="group.table">
|
<li v-for="group of permission.fieldGroups" :key="group.table">
|
||||||
<span v-text="group.table"></span>
|
<div class="row items-center q-gutter-xs">
|
||||||
|
<span v-text="group.table"></span>
|
||||||
|
<template v-if="group.sourceIdField">
|
||||||
|
<q-badge
|
||||||
|
color="warning"
|
||||||
|
text-color="dark"
|
||||||
|
v-text="group.sourceIdField"
|
||||||
|
></q-badge>
|
||||||
|
<span
|
||||||
|
class="text-caption text-grey"
|
||||||
|
v-text="
|
||||||
|
$t(
|
||||||
|
'extension_permission_ext_storage_read_public_source_required'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
></span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
<ul v-if="group.fields.length" class="q-pl-md">
|
<ul v-if="group.fields.length" class="q-pl-md">
|
||||||
<li
|
<li
|
||||||
v-for="field of group.fields"
|
v-for="field of group.fields"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from lnbits.core.wasm_ext.api.models import (
|
|||||||
PayInvoiceRequest,
|
PayInvoiceRequest,
|
||||||
StorageAppendPublicRequest,
|
StorageAppendPublicRequest,
|
||||||
StorageGetRequest,
|
StorageGetRequest,
|
||||||
StoragePaginatedRequest,
|
StoragePublicPaginatedRequest,
|
||||||
WalletBalanceRequest,
|
WalletBalanceRequest,
|
||||||
WebsocketPublishRequest,
|
WebsocketPublishRequest,
|
||||||
)
|
)
|
||||||
@@ -79,6 +79,7 @@ async def test_host_api_filters_public_paginated_storage_rows(
|
|||||||
policies=[
|
policies=[
|
||||||
{
|
{
|
||||||
"table_name": "messages",
|
"table_name": "messages",
|
||||||
|
"source_id_field": "thread_id",
|
||||||
"public_fields": ["id", "thread_id", "message"],
|
"public_fields": ["id", "thread_id", "message"],
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -87,15 +88,16 @@ async def test_host_api_filters_public_paginated_storage_rows(
|
|||||||
)
|
)
|
||||||
|
|
||||||
response = await api.storage_get_public_paginated(
|
response = await api.storage_get_public_paginated(
|
||||||
StoragePaginatedRequest(
|
StoragePublicPaginatedRequest(
|
||||||
table="messages",
|
table="messages",
|
||||||
filters={"thread_id": "thread-1"},
|
filters={},
|
||||||
search="hello",
|
search="hello",
|
||||||
search_fields=["message"],
|
search_fields=["message"],
|
||||||
sort_by="id",
|
sort_by="id",
|
||||||
descending=False,
|
descending=False,
|
||||||
limit=25,
|
limit=25,
|
||||||
offset=0,
|
offset=0,
|
||||||
|
source_id="thread-1",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -103,11 +105,55 @@ async def test_host_api_filters_public_paginated_storage_rows(
|
|||||||
{"id": "message-1", "thread_id": "thread-1", "message": "Hello"}
|
{"id": "message-1", "thread_id": "thread-1", "message": "Hello"}
|
||||||
]
|
]
|
||||||
assert response.total == 1
|
assert response.total == 1
|
||||||
storage_mock.assert_awaited_once()
|
storage_mock.assert_awaited_once_with(
|
||||||
|
"demoext",
|
||||||
|
"messages",
|
||||||
|
{"thread_id": "thread-1"},
|
||||||
|
search="hello",
|
||||||
|
search_fields=["message"],
|
||||||
|
sort_by="id",
|
||||||
|
descending=False,
|
||||||
|
limit=25,
|
||||||
|
offset=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_host_api_public_paginated_storage_rejects_private_query_fields():
|
async def test_host_api_public_paginated_storage_rejects_private_query_fields():
|
||||||
|
api = ExtensionHostAPI(
|
||||||
|
"demoext",
|
||||||
|
[
|
||||||
|
ExtensionPermission(
|
||||||
|
id="ext.storage.read_public",
|
||||||
|
policies=[
|
||||||
|
{
|
||||||
|
"table_name": "messages",
|
||||||
|
"source_id_field": "thread_id",
|
||||||
|
"public_fields": ["id", "message"],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(PermissionError, match="non-public fields"):
|
||||||
|
await api.storage_get_public_paginated(
|
||||||
|
StoragePublicPaginatedRequest(
|
||||||
|
table="messages",
|
||||||
|
filters={"admin_note": "secret"},
|
||||||
|
search=None,
|
||||||
|
search_fields=[],
|
||||||
|
sort_by=None,
|
||||||
|
descending=False,
|
||||||
|
limit=25,
|
||||||
|
offset=0,
|
||||||
|
source_id="thread-1",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_host_api_public_paginated_storage_requires_source_policy():
|
||||||
api = ExtensionHostAPI(
|
api = ExtensionHostAPI(
|
||||||
"demoext",
|
"demoext",
|
||||||
[
|
[
|
||||||
@@ -123,17 +169,52 @@ async def test_host_api_public_paginated_storage_rejects_private_query_fields():
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(PermissionError, match="non-public fields"):
|
with pytest.raises(PermissionError, match="source ID field policy"):
|
||||||
await api.storage_get_public_paginated(
|
await api.storage_get_public_paginated(
|
||||||
StoragePaginatedRequest(
|
StoragePublicPaginatedRequest(
|
||||||
table="messages",
|
table="messages",
|
||||||
filters={"admin_note": "secret"},
|
filters={},
|
||||||
search=None,
|
search=None,
|
||||||
search_fields=[],
|
search_fields=[],
|
||||||
sort_by=None,
|
sort_by=None,
|
||||||
descending=False,
|
descending=False,
|
||||||
limit=25,
|
limit=25,
|
||||||
offset=0,
|
offset=0,
|
||||||
|
source_id="thread-1",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_host_api_public_paginated_storage_rejects_conflicting_source_filter():
|
||||||
|
api = ExtensionHostAPI(
|
||||||
|
"demoext",
|
||||||
|
[
|
||||||
|
ExtensionPermission(
|
||||||
|
id="ext.storage.read_public",
|
||||||
|
policies=[
|
||||||
|
{
|
||||||
|
"table_name": "messages",
|
||||||
|
"source_id_field": "thread_id",
|
||||||
|
"public_fields": ["id", "message"],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(PermissionError, match="does not match source_id"):
|
||||||
|
await api.storage_get_public_paginated(
|
||||||
|
StoragePublicPaginatedRequest(
|
||||||
|
table="messages",
|
||||||
|
filters={"thread_id": "thread-2"},
|
||||||
|
search=None,
|
||||||
|
search_fields=[],
|
||||||
|
sort_by=None,
|
||||||
|
descending=False,
|
||||||
|
limit=25,
|
||||||
|
offset=0,
|
||||||
|
source_id="thread-1",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ def test_validate_wasm_permissions_stores_narrower_policy_grant():
|
|||||||
"policies": [
|
"policies": [
|
||||||
{
|
{
|
||||||
"table_name": "tip_jars",
|
"table_name": "tip_jars",
|
||||||
|
"source_id_field": "wallet_id",
|
||||||
"public_fields": ["id", "title", "description"],
|
"public_fields": ["id", "title", "description"],
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -80,6 +81,7 @@ def test_validate_wasm_permissions_stores_narrower_policy_grant():
|
|||||||
policies=[
|
policies=[
|
||||||
{
|
{
|
||||||
"table_name": "tip_jars",
|
"table_name": "tip_jars",
|
||||||
|
"source_id_field": "wallet_id",
|
||||||
"public_fields": ["id", "title"],
|
"public_fields": ["id", "title"],
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -95,6 +97,7 @@ def test_validate_wasm_permissions_stores_narrower_policy_grant():
|
|||||||
policies=[
|
policies=[
|
||||||
{
|
{
|
||||||
"table_name": "tip_jars",
|
"table_name": "tip_jars",
|
||||||
|
"source_id_field": "wallet_id",
|
||||||
"public_fields": ["id", "title"],
|
"public_fields": ["id", "title"],
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -102,6 +105,42 @@ def test_validate_wasm_permissions_stores_narrower_policy_grant():
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_wasm_permissions_rejects_public_read_source_field_omission():
|
||||||
|
ext_info = make_installable_extension("demoext")
|
||||||
|
extension_config = _wasm_config(
|
||||||
|
"demoext",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "ext.storage.read_public",
|
||||||
|
"policies": [
|
||||||
|
{
|
||||||
|
"table_name": "messages",
|
||||||
|
"source_id_field": "conversation_id",
|
||||||
|
"public_fields": ["id", "body"],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="broader policies"):
|
||||||
|
validate_wasm_extension_permissions(
|
||||||
|
ext_info,
|
||||||
|
[
|
||||||
|
ExtensionPermission(
|
||||||
|
id="ext.storage.read_public",
|
||||||
|
policies=[
|
||||||
|
{
|
||||||
|
"table_name": "messages",
|
||||||
|
"public_fields": ["id", "body"],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
extension_config,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_validate_wasm_permissions_allows_narrower_public_append_grant():
|
def test_validate_wasm_permissions_allows_narrower_public_append_grant():
|
||||||
ext_info = make_installable_extension("demoext")
|
ext_info = make_installable_extension("demoext")
|
||||||
extension_config = _wasm_config(
|
extension_config = _wasm_config(
|
||||||
|
|||||||
Reference in New Issue
Block a user