diff --git a/lnbits/core/models/extensions.py b/lnbits/core/models/extensions.py index 262cafe13..35ae8a864 100644 --- a/lnbits/core/models/extensions.py +++ b/lnbits/core/models/extensions.py @@ -81,7 +81,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" >

+