From 9d6069a04354543c58e50951d831a03e14fa424d Mon Sep 17 00:00:00 2001
From: Vlad Stan
Date: Tue, 7 Jul 2026 14:26:34 +0300
Subject: [PATCH] fix: policies
---
lnbits/core/models/extensions.py | 2 +-
lnbits/core/wasm_ext/api/host.py | 80 +++++++++++++++--------
lnbits/core/wasm_ext/api/models.py | 7 +-
lnbits/core/wasm_ext/client/extensions.py | 13 ++--
lnbits/core/wasm_ext/client/http.py | 12 ++--
lnbits/core/wasm_ext/wasm/events.py | 27 +++++---
lnbits/static/js/pages/extensions.js | 26 +++++++-
lnbits/templates/pages/extensions.vue | 6 ++
8 files changed, 119 insertions(+), 54 deletions(-)
diff --git a/lnbits/core/models/extensions.py b/lnbits/core/models/extensions.py
index 1fbac449e..db3bfc1c1 100644
--- a/lnbits/core/models/extensions.py
+++ b/lnbits/core/models/extensions.py
@@ -82,7 +82,7 @@ class ExtensionPermission(BaseModel):
id: str
label: str | None = None
description: str | None = None
- policy: dict[str, Any] | None = None
+ policies: list[Any] | None = None
@staticmethod
def list_from_config(config_json: Mapping[str, Any]) -> list[ExtensionPermission]:
diff --git a/lnbits/core/wasm_ext/api/host.py b/lnbits/core/wasm_ext/api/host.py
index 4ff6f9742..ec23f38f8 100644
--- a/lnbits/core/wasm_ext/api/host.py
+++ b/lnbits/core/wasm_ext/api/host.py
@@ -242,8 +242,18 @@ class ExtensionHostAPI:
from lnbits.core.models.payments import CreateInvoice
from lnbits.core.services.payments import create_payment_request
- table, wallet_field = self._public_invoice_wallet_source()
- row = await storage_get_public_row(self.extension_id, table, request.source_id)
+ row: dict[str, Any] | None = None
+ wallet_field = ""
+ for policy in self._public_invoice_wallet_sources():
+ row = await storage_get_public_row(
+ self.extension_id,
+ policy["table"],
+ request.source_id,
+ )
+ if row:
+ wallet_field = policy["wallet_field"]
+ break
+
if not row:
raise PermissionError("Public invoice source was not found.")
@@ -401,8 +411,8 @@ class ExtensionHostAPI:
async def http_request(self, request: HttpRequest) -> HttpResponse:
from ..client.http import send_extension_http_request
- policy = self.permission_policies.get("http.request") or {}
- return await send_extension_http_request(self.extension_id, policy, request)
+ policies = self.permission_policies.get("http.request") or []
+ return await send_extension_http_request(self.extension_id, policies, request)
@extension_api_method(
method_id="extension.api.request",
@@ -417,10 +427,10 @@ class ExtensionHostAPI:
async def extension_api_request(self, request: ExtensionApiRequest) -> HttpResponse:
from ..client.extensions import send_extension_api_request
- policy = self.permission_policies.get("extension.api.request") or {}
+ policies = self.permission_policies.get("extension.api.request") or []
return await send_extension_api_request(
self.extension_id,
- policy,
+ policies,
self.user_id,
self.access_token,
request,
@@ -469,9 +479,9 @@ class ExtensionHostAPI:
@staticmethod
def _permission_data(
permissions: Iterable[Any],
- ) -> tuple[set[str], dict[str, dict[str, Any]]]:
+ ) -> tuple[set[str], dict[str, list[Any]]]:
permission_ids: set[str] = set()
- policies: dict[str, dict[str, Any]] = {}
+ policies: dict[str, list[Any]] = {}
for permission in permissions:
if isinstance(permission, str):
@@ -479,28 +489,27 @@ class ExtensionHostAPI:
continue
permission_id: str | None = None
- policy: Any = None
+ permission_policies: Any = None
if isinstance(permission, Mapping):
permission_id = permission.get("id") # type: ignore[assignment]
- policy = permission.get("policy")
+ permission_policies = permission.get("policies")
else:
permission_id = getattr(permission, "id", None)
- policy = getattr(permission, "policy", None)
+ permission_policies = getattr(permission, "policies", None)
if not permission_id:
continue
permission_ids.add(permission_id)
- if isinstance(policy, dict):
- policies[permission_id] = policy
+ if isinstance(permission_policies, list):
+ policies[permission_id] = permission_policies
return permission_ids, policies
def _public_storage_fields(self, table: str) -> set[str]:
- policy = self.permission_policies.get("ext.storage.read_public") or {}
- tables = policy.get("tables")
- if not isinstance(tables, list):
+ tables = self.permission_policies.get("ext.storage.read_public")
+ if not isinstance(tables, list) or not tables:
raise PermissionError(
- "Public storage reads require a tables policy for "
+ "Public storage reads require policies for "
"'ext.storage.read_public'."
)
@@ -520,19 +529,34 @@ class ExtensionHostAPI:
raise PermissionError(f"Storage table '{table}' is not publicly readable.")
- def _public_invoice_wallet_source(self) -> tuple[str, str]:
- policy = self.permission_policies.get("wallet.create_invoice_public") or {}
- table = policy.get("table")
- wallet_field = policy.get("wallet_field")
- if not isinstance(table, str) or not table:
+ def _public_invoice_wallet_sources(self) -> list[dict[str, str]]:
+ policies = self.permission_policies.get("wallet.create_invoice_public")
+ if not isinstance(policies, list) or not policies:
+ raise PermissionError("Public invoice creation requires a policies list.")
+
+ sources: list[dict[str, str]] = []
+ for source_policy in policies:
+ if not isinstance(source_policy, dict):
+ raise PermissionError(
+ "Public invoice creation policies must be objects."
+ )
+ table = source_policy.get("table")
+ wallet_field = source_policy.get("wallet_field")
+ if not isinstance(table, str) or not table:
+ raise PermissionError(
+ "Public invoice creation requires a storage table policy."
+ )
+ if not isinstance(wallet_field, str) or not wallet_field:
+ raise PermissionError(
+ "Public invoice creation requires a wallet field policy."
+ )
+ sources.append({"table": table, "wallet_field": wallet_field})
+
+ if not sources:
raise PermissionError(
- "Public invoice creation requires a storage table policy."
+ "Public invoice creation requires at least one valid policy."
)
- if not isinstance(wallet_field, str) or not wallet_field:
- raise PermissionError(
- "Public invoice creation requires a wallet field policy."
- )
- return table, wallet_field
+ return sources
def require_permission(self, permission: str | None) -> None:
if permission and permission not in self.permissions:
diff --git a/lnbits/core/wasm_ext/api/models.py b/lnbits/core/wasm_ext/api/models.py
index d8d89e1f5..92038d114 100644
--- a/lnbits/core/wasm_ext/api/models.py
+++ b/lnbits/core/wasm_ext/api/models.py
@@ -116,7 +116,12 @@ class CreateInvoiceRequest(BaseModel):
class CreateInvoicePublicRequest(BaseModel):
- source_id: str = Field(..., min_length=1, max_length=512, description="The source ID (entry id) of the wallet to create the invoice for.")
+ source_id: str = Field(
+ ...,
+ min_length=1,
+ max_length=512,
+ description="The source ID (entry id) of the wallet to create the invoice for.",
+ )
amount: float = Field(..., gt=0)
currency: str = Field(..., min_length=1, max_length=8)
memo: str = Field("", max_length=512)
diff --git a/lnbits/core/wasm_ext/client/extensions.py b/lnbits/core/wasm_ext/client/extensions.py
index e7ce0f26b..92b8741c1 100644
--- a/lnbits/core/wasm_ext/client/extensions.py
+++ b/lnbits/core/wasm_ext/client/extensions.py
@@ -31,7 +31,7 @@ _FORBIDDEN_RESPONSE_HEADERS = {
async def send_extension_api_request(
caller_extension_id: str,
- policy: dict[str, Any],
+ policies: list[Any],
user_id: str | None,
access_token: str | None,
request: ExtensionApiRequest,
@@ -42,7 +42,7 @@ async def send_extension_api_request(
raise PermissionError("Extension API requests require an account access token.")
target_extension_id = _target_extension_id(request.extension_id)
- access = _target_extension_access(policy, target_extension_id)
+ access = _target_extension_access(policies, target_extension_id)
_require_method_access(caller_extension_id, target_extension_id, access, request)
await _require_enabled_extension(target_extension_id, user_id)
@@ -81,16 +81,13 @@ def _target_extension_id(extension_id: str) -> str:
return target
-def _target_extension_access(
- policy: dict[str, Any], target_extension_id: str
-) -> set[str]:
- extensions = policy.get("extensions")
- if not isinstance(extensions, list) or not extensions:
+def _target_extension_access(policies: list[Any], target_extension_id: str) -> set[str]:
+ if not isinstance(policies, list) or not policies:
raise PermissionError(
"Extension API requests require a non-empty extensions policy."
)
- for extension in extensions:
+ for extension in policies:
if isinstance(extension, str):
extension_id = extension
access = ["read"]
diff --git a/lnbits/core/wasm_ext/client/http.py b/lnbits/core/wasm_ext/client/http.py
index e640ba82c..d11444d8c 100644
--- a/lnbits/core/wasm_ext/client/http.py
+++ b/lnbits/core/wasm_ext/client/http.py
@@ -30,10 +30,10 @@ _FORBIDDEN_RESPONSE_HEADERS = {
async def send_extension_http_request(
extension_id: str,
- policy: dict[str, Any],
+ policies: list[Any],
request: HttpRequest,
) -> HttpResponse:
- allowed_origins = _allowed_origins(policy)
+ allowed_origins = _allowed_origins(policies)
origin = _request_origin(request.url)
if origin not in allowed_origins:
raise PermissionError(
@@ -68,13 +68,13 @@ async def send_extension_http_request(
raise ValueError("HTTP request failed.") from exc
-def _allowed_origins(policy: dict[str, Any]) -> set[str]:
- hosts = policy.get("hosts")
- if not isinstance(hosts, list) or not hosts:
+def _allowed_origins(policies: list[Any]) -> set[str]:
+ if not isinstance(policies, list) or not policies:
raise PermissionError("HTTP requests require a non-empty hosts policy.")
origins: set[str] = set()
- for host in hosts:
+ for policy in policies:
+ host = policy.get("host") if isinstance(policy, dict) else policy
if not isinstance(host, str) or not host:
continue
origins.add(_request_origin(host))
diff --git a/lnbits/core/wasm_ext/wasm/events.py b/lnbits/core/wasm_ext/wasm/events.py
index a99708d50..cbcdfbaf1 100644
--- a/lnbits/core/wasm_ext/wasm/events.py
+++ b/lnbits/core/wasm_ext/wasm/events.py
@@ -54,13 +54,17 @@ def _payment_extension_id(payment: Any) -> str | None:
async def _wasm_invoice_paid_owner_id(extension: Any, payment: Any) -> str | None:
source_id = _payment_source_id(payment)
- source_table = _wasm_public_invoice_source_table(extension.config)
- if not source_id or not source_table:
+ source_tables = _wasm_public_invoice_source_tables(extension.config)
+ if not source_id or not source_tables:
return None
from lnbits.core.wasm_ext.storage.crud import storage_get_row_owner_id
- return await storage_get_row_owner_id(extension.id, source_table, source_id)
+ for source_table in source_tables:
+ owner_id = await storage_get_row_owner_id(extension.id, source_table, source_id)
+ if owner_id:
+ return owner_id
+ return None
def _payment_source_id(payment: Any) -> str | None:
@@ -69,17 +73,24 @@ def _payment_source_id(payment: Any) -> str | None:
return source_id if isinstance(source_id, str) and source_id else None
-def _wasm_public_invoice_source_table(config: dict[str, Any]) -> str | None:
+def _wasm_public_invoice_source_tables(config: dict[str, Any]) -> list[str]:
permissions = config.get("permissions") or []
for permission in permissions:
if not isinstance(permission, dict):
continue
if permission.get("id") != "wallet.create_invoice_public":
continue
- policy = permission.get("policy") or {}
- table = policy.get("table")
- return table if isinstance(table, str) and table else None
- return None
+ policies = permission.get("policies")
+ if not isinstance(policies, list):
+ return []
+ return [
+ source_policy["table"]
+ for source_policy in policies
+ if isinstance(source_policy, dict)
+ and isinstance(source_policy.get("table"), str)
+ and source_policy["table"]
+ ]
+ return []
def _wasm_invoice_paid_export(config: dict[str, Any]) -> str | None:
diff --git a/lnbits/static/js/pages/extensions.js b/lnbits/static/js/pages/extensions.js
index d1ffd93fd..30f28c59d 100644
--- a/lnbits/static/js/pages/extensions.js
+++ b/lnbits/static/js/pages/extensions.js
@@ -790,6 +790,7 @@ window.PageExtensions = {
badges: [],
descriptions,
fieldGroups: [],
+ invoicePolicies: [],
extensionAccess: []
}
@@ -809,6 +810,10 @@ window.PageExtensions = {
}))
}
+ if (permission.id === 'wallet.create_invoice_public') {
+ item.invoicePolicies = this.publicInvoicePolicies(permission)
+ }
+
return item
},
permissionRisk(permissions) {
@@ -891,7 +896,7 @@ window.PageExtensions = {
return index === -1 ? order.length : index
},
publicStorageFieldGroups(permission) {
- const tables = permission.policy?.tables
+ const tables = permission.policies
if (!Array.isArray(tables)) return []
return tables
.map(table => {
@@ -907,8 +912,25 @@ window.PageExtensions = {
})
.filter(Boolean)
},
+ publicInvoicePolicies(permission) {
+ const policies = permission.policies
+ if (!Array.isArray(policies)) return []
+ return policies
+ .map(policy => {
+ if (!policy || typeof policy !== 'object') return null
+ const table = policy.table
+ const walletField = policy.wallet_field
+ if (typeof table !== 'string' || !table) return null
+ if (typeof walletField !== 'string' || !walletField) return null
+ return {table, walletField}
+ })
+ .filter(Boolean)
+ },
+ publicInvoicePolicySentence(policy) {
+ return `Invoices will be created using ${policy.walletField} from ${policy.table}.`
+ },
extensionApiPermissionTargets(permission) {
- const extensions = permission.policy?.extensions
+ const extensions = permission.policies
if (!Array.isArray(extensions)) return []
return extensions
.map(extension => {
diff --git a/lnbits/templates/pages/extensions.vue b/lnbits/templates/pages/extensions.vue
index 516e6e67c..0de74659c 100644
--- a/lnbits/templates/pages/extensions.vue
+++ b/lnbits/templates/pages/extensions.vue
@@ -529,6 +529,12 @@
class="text-caption q-mb-xs"
v-text="description"
>
+