fix: policies

This commit is contained in:
Vlad Stan
2026-07-08 11:55:20 +03:00
parent e0fc3c89d1
commit 9d6069a043
8 changed files with 119 additions and 54 deletions
+1 -1
View File
@@ -82,7 +82,7 @@ class ExtensionPermission(BaseModel):
id: str id: str
label: str | None = None label: str | None = None
description: str | None = None description: str | None = None
policy: dict[str, Any] | None = None policies: list[Any] | None = None
@staticmethod @staticmethod
def list_from_config(config_json: Mapping[str, Any]) -> list[ExtensionPermission]: def list_from_config(config_json: Mapping[str, Any]) -> list[ExtensionPermission]:
+52 -28
View File
@@ -242,8 +242,18 @@ class ExtensionHostAPI:
from lnbits.core.models.payments import CreateInvoice from lnbits.core.models.payments import CreateInvoice
from lnbits.core.services.payments import create_payment_request from lnbits.core.services.payments import create_payment_request
table, wallet_field = self._public_invoice_wallet_source() row: dict[str, Any] | None = None
row = await storage_get_public_row(self.extension_id, table, request.source_id) 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: if not row:
raise PermissionError("Public invoice source was not found.") raise PermissionError("Public invoice source was not found.")
@@ -401,8 +411,8 @@ class ExtensionHostAPI:
async def http_request(self, request: HttpRequest) -> HttpResponse: async def http_request(self, request: HttpRequest) -> HttpResponse:
from ..client.http import send_extension_http_request from ..client.http import send_extension_http_request
policy = self.permission_policies.get("http.request") or {} policies = self.permission_policies.get("http.request") or []
return await send_extension_http_request(self.extension_id, policy, request) return await send_extension_http_request(self.extension_id, policies, request)
@extension_api_method( @extension_api_method(
method_id="extension.api.request", method_id="extension.api.request",
@@ -417,10 +427,10 @@ class ExtensionHostAPI:
async def extension_api_request(self, request: ExtensionApiRequest) -> HttpResponse: async def extension_api_request(self, request: ExtensionApiRequest) -> HttpResponse:
from ..client.extensions import send_extension_api_request 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( return await send_extension_api_request(
self.extension_id, self.extension_id,
policy, policies,
self.user_id, self.user_id,
self.access_token, self.access_token,
request, request,
@@ -469,9 +479,9 @@ class ExtensionHostAPI:
@staticmethod @staticmethod
def _permission_data( def _permission_data(
permissions: Iterable[Any], permissions: Iterable[Any],
) -> tuple[set[str], dict[str, dict[str, Any]]]: ) -> tuple[set[str], dict[str, list[Any]]]:
permission_ids: set[str] = set() permission_ids: set[str] = set()
policies: dict[str, dict[str, Any]] = {} policies: dict[str, list[Any]] = {}
for permission in permissions: for permission in permissions:
if isinstance(permission, str): if isinstance(permission, str):
@@ -479,28 +489,27 @@ class ExtensionHostAPI:
continue continue
permission_id: str | None = None permission_id: str | None = None
policy: Any = None permission_policies: Any = None
if isinstance(permission, Mapping): if isinstance(permission, Mapping):
permission_id = permission.get("id") # type: ignore[assignment] permission_id = permission.get("id") # type: ignore[assignment]
policy = permission.get("policy") permission_policies = permission.get("policies")
else: else:
permission_id = getattr(permission, "id", None) permission_id = getattr(permission, "id", None)
policy = getattr(permission, "policy", None) permission_policies = getattr(permission, "policies", None)
if not permission_id: if not permission_id:
continue continue
permission_ids.add(permission_id) permission_ids.add(permission_id)
if isinstance(policy, dict): if isinstance(permission_policies, list):
policies[permission_id] = policy policies[permission_id] = permission_policies
return permission_ids, policies return permission_ids, policies
def _public_storage_fields(self, table: str) -> set[str]: def _public_storage_fields(self, table: str) -> set[str]:
policy = self.permission_policies.get("ext.storage.read_public") or {} tables = self.permission_policies.get("ext.storage.read_public")
tables = policy.get("tables") if not isinstance(tables, list) or not tables:
if not isinstance(tables, list):
raise PermissionError( raise PermissionError(
"Public storage reads require a tables policy for " "Public storage reads require policies for "
"'ext.storage.read_public'." "'ext.storage.read_public'."
) )
@@ -520,19 +529,34 @@ class ExtensionHostAPI:
raise PermissionError(f"Storage table '{table}' is not publicly readable.") raise PermissionError(f"Storage table '{table}' is not publicly readable.")
def _public_invoice_wallet_source(self) -> tuple[str, str]: def _public_invoice_wallet_sources(self) -> list[dict[str, str]]:
policy = self.permission_policies.get("wallet.create_invoice_public") or {} policies = self.permission_policies.get("wallet.create_invoice_public")
table = policy.get("table") if not isinstance(policies, list) or not policies:
wallet_field = policy.get("wallet_field") raise PermissionError("Public invoice creation requires a policies list.")
if not isinstance(table, str) or not table:
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( 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: return sources
raise PermissionError(
"Public invoice creation requires a wallet field policy."
)
return table, wallet_field
def require_permission(self, permission: str | None) -> None: def require_permission(self, permission: str | None) -> None:
if permission and permission not in self.permissions: if permission and permission not in self.permissions:
+6 -1
View File
@@ -116,7 +116,12 @@ class CreateInvoiceRequest(BaseModel):
class CreateInvoicePublicRequest(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) amount: float = Field(..., gt=0)
currency: str = Field(..., min_length=1, max_length=8) currency: str = Field(..., min_length=1, max_length=8)
memo: str = Field("", max_length=512) memo: str = Field("", max_length=512)
+5 -8
View File
@@ -31,7 +31,7 @@ _FORBIDDEN_RESPONSE_HEADERS = {
async def send_extension_api_request( async def send_extension_api_request(
caller_extension_id: str, caller_extension_id: str,
policy: dict[str, Any], policies: list[Any],
user_id: str | None, user_id: str | None,
access_token: str | None, access_token: str | None,
request: ExtensionApiRequest, request: ExtensionApiRequest,
@@ -42,7 +42,7 @@ async def send_extension_api_request(
raise PermissionError("Extension API requests require an account access token.") raise PermissionError("Extension API requests require an account access token.")
target_extension_id = _target_extension_id(request.extension_id) 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) _require_method_access(caller_extension_id, target_extension_id, access, request)
await _require_enabled_extension(target_extension_id, user_id) await _require_enabled_extension(target_extension_id, user_id)
@@ -81,16 +81,13 @@ def _target_extension_id(extension_id: str) -> str:
return target return target
def _target_extension_access( def _target_extension_access(policies: list[Any], target_extension_id: str) -> set[str]:
policy: dict[str, Any], target_extension_id: str if not isinstance(policies, list) or not policies:
) -> set[str]:
extensions = policy.get("extensions")
if not isinstance(extensions, list) or not extensions:
raise PermissionError( raise PermissionError(
"Extension API requests require a non-empty extensions policy." "Extension API requests require a non-empty extensions policy."
) )
for extension in extensions: for extension in policies:
if isinstance(extension, str): if isinstance(extension, str):
extension_id = extension extension_id = extension
access = ["read"] access = ["read"]
+6 -6
View File
@@ -30,10 +30,10 @@ _FORBIDDEN_RESPONSE_HEADERS = {
async def send_extension_http_request( async def send_extension_http_request(
extension_id: str, extension_id: str,
policy: dict[str, Any], policies: list[Any],
request: HttpRequest, request: HttpRequest,
) -> HttpResponse: ) -> HttpResponse:
allowed_origins = _allowed_origins(policy) allowed_origins = _allowed_origins(policies)
origin = _request_origin(request.url) origin = _request_origin(request.url)
if origin not in allowed_origins: if origin not in allowed_origins:
raise PermissionError( raise PermissionError(
@@ -68,13 +68,13 @@ async def send_extension_http_request(
raise ValueError("HTTP request failed.") from exc raise ValueError("HTTP request failed.") from exc
def _allowed_origins(policy: dict[str, Any]) -> set[str]: def _allowed_origins(policies: list[Any]) -> set[str]:
hosts = policy.get("hosts") if not isinstance(policies, list) or not policies:
if not isinstance(hosts, list) or not hosts:
raise PermissionError("HTTP requests require a non-empty hosts policy.") raise PermissionError("HTTP requests require a non-empty hosts policy.")
origins: set[str] = set() 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: if not isinstance(host, str) or not host:
continue continue
origins.add(_request_origin(host)) origins.add(_request_origin(host))
+19 -8
View File
@@ -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: async def _wasm_invoice_paid_owner_id(extension: Any, payment: Any) -> str | None:
source_id = _payment_source_id(payment) source_id = _payment_source_id(payment)
source_table = _wasm_public_invoice_source_table(extension.config) source_tables = _wasm_public_invoice_source_tables(extension.config)
if not source_id or not source_table: if not source_id or not source_tables:
return None return None
from lnbits.core.wasm_ext.storage.crud import storage_get_row_owner_id 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: 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 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 [] permissions = config.get("permissions") or []
for permission in permissions: for permission in permissions:
if not isinstance(permission, dict): if not isinstance(permission, dict):
continue continue
if permission.get("id") != "wallet.create_invoice_public": if permission.get("id") != "wallet.create_invoice_public":
continue continue
policy = permission.get("policy") or {} policies = permission.get("policies")
table = policy.get("table") if not isinstance(policies, list):
return table if isinstance(table, str) and table else None return []
return None 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: def _wasm_invoice_paid_export(config: dict[str, Any]) -> str | None:
+24 -2
View File
@@ -790,6 +790,7 @@ window.PageExtensions = {
badges: [], badges: [],
descriptions, descriptions,
fieldGroups: [], fieldGroups: [],
invoicePolicies: [],
extensionAccess: [] extensionAccess: []
} }
@@ -809,6 +810,10 @@ window.PageExtensions = {
})) }))
} }
if (permission.id === 'wallet.create_invoice_public') {
item.invoicePolicies = this.publicInvoicePolicies(permission)
}
return item return item
}, },
permissionRisk(permissions) { permissionRisk(permissions) {
@@ -891,7 +896,7 @@ window.PageExtensions = {
return index === -1 ? order.length : index return index === -1 ? order.length : index
}, },
publicStorageFieldGroups(permission) { publicStorageFieldGroups(permission) {
const tables = permission.policy?.tables const tables = permission.policies
if (!Array.isArray(tables)) return [] if (!Array.isArray(tables)) return []
return tables return tables
.map(table => { .map(table => {
@@ -907,8 +912,25 @@ window.PageExtensions = {
}) })
.filter(Boolean) .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) { extensionApiPermissionTargets(permission) {
const extensions = permission.policy?.extensions const extensions = permission.policies
if (!Array.isArray(extensions)) return [] if (!Array.isArray(extensions)) return []
return extensions return extensions
.map(extension => { .map(extension => {
+6
View File
@@ -529,6 +529,12 @@
class="text-caption q-mb-xs" class="text-caption q-mb-xs"
v-text="description" v-text="description"
></p> ></p>
<p
v-for="policy of permission.invoicePolicies"
:key="policy.table + ':' + policy.walletField"
class="text-caption q-mb-xs"
v-text="publicInvoicePolicySentence(policy)"
></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> <span v-text="group.table"></span>