fix: policies
This commit is contained in:
@@ -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]:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user