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