fix: policies

This commit is contained in:
Vlad Stan
2026-07-09 16:36:05 +03:00
parent 114229fb15
commit d9189e62af
8 changed files with 119 additions and 54 deletions
+1 -1
View File
@@ -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]:
+52 -28
View File
@@ -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:
+6 -1
View File
@@ -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)
+5 -8
View File
@@ -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"]
+6 -6
View File
@@ -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))
+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:
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:
+24 -2
View File
@@ -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 => {
+6
View File
@@ -529,6 +529,12 @@
class="text-caption q-mb-xs"
v-text="description"
></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">
<li v-for="group of permission.fieldGroups" :key="group.table">
<span v-text="group.table"></span>