This commit is contained in:
Arc
2026-05-25 14:56:22 +03:00
committed by Vlad Stan
parent 18c31280ef
commit 939a7f242b
4 changed files with 161 additions and 87 deletions
+51
View File
@@ -1,6 +1,8 @@
import json
import sys
import traceback
from http import HTTPStatus
from pathlib import Path
import httpx
from bolt11 import decode as bolt11_decode
@@ -68,6 +70,53 @@ extension_router = APIRouter(
)
def _load_wasm_extension_config(ext_id: str) -> dict:
candidate_dirs = [
Path(settings.lnbits_extensions_path, "extensions", ext_id),
Path(settings.lnbits_path, "lnbits", "extensions", ext_id),
]
for base in candidate_dirs:
conf_path = base / "config.json"
if not conf_path.is_file():
continue
try:
with open(conf_path) as json_file:
config = json.load(json_file)
return config if isinstance(config, dict) else {}
except Exception:
return {}
return {}
def _ensure_wasm_permissions_saved(ext_id: str, user_ext: UserExtension) -> None:
if get_extension_type(ext_id) != "wasm":
return
config = _load_wasm_extension_config(ext_id)
required = [
permission.get("id")
for permission in config.get("permissions", [])
if isinstance(permission, dict) and permission.get("id")
]
granted = user_ext.extra.granted_permissions if user_ext.extra else []
missing = [
permission for permission in required if permission not in (granted or [])
]
if missing:
raise HTTPException(
HTTPStatus.BAD_REQUEST,
"Save WASM permissions before enabling this extension.",
)
payment_tags = config.get("payment_tags", [])
granted_tags = user_ext.extra.granted_payment_tags if user_ext.extra else []
has_granted_payment_tag = any(tag in (granted_tags or []) for tag in payment_tags)
if payment_tags and not has_granted_payment_tag:
raise HTTPException(
HTTPStatus.BAD_REQUEST,
"Select at least one WASM payment tag before enabling this extension.",
)
@extension_router.post("", dependencies=[Depends(check_admin)])
async def api_install_extension(data: CreateExtension):
release = await InstallableExtension.get_extension_release(
@@ -198,6 +247,8 @@ async def api_enable_extension(
user_ext = UserExtension(user=account_id.id, extension=ext_id, active=False)
await create_user_extension(user_ext)
_ensure_wasm_permissions_saved(ext_id, user_ext)
if account_id.is_admin_id or not ext.requires_payment:
user_ext.active = True
await update_user_extension(user_ext)
+76 -85
View File
@@ -262,39 +262,81 @@ window.PageExtensions = {
extension.inProgress = false
})
},
async enableExtensionForUser(extension) {
if (extension.extensionType === 'wasm') {
const wasmHost = this.extensions.find(ext => ext.id === 'wasm')
if (!wasmHost || !wasmHost.isInstalled || !wasmHost.isActive) {
Quasar.Notify.create({
type: 'warning',
message:
'Enable the WASM! host extension before using this extension.'
})
return
}
if (extension.permissions && extension.permissions.length) {
if (!extension._grantedPermissions) {
Quasar.Notify.create({
type: 'warning',
message: 'Save permissions before enabling this extension.'
})
return
}
}
if (extension.paymentTags && extension.paymentTags.length) {
if (
!extension._grantedPaymentTags ||
!extension._grantedPaymentTags.length
) {
Quasar.Notify.create({
type: 'warning',
message: 'Select payment tags before enabling this extension.'
})
return
}
}
async loadWasmCapabilities(extension) {
const {data} = await LNbits.api.request(
'GET',
`/wasm/api/v1/extensions/${extension.id}/capabilities`,
this.g.user.wallets[0].adminkey
)
const capabilities = data || {}
extension.permissions = capabilities.permissions || []
extension.paymentTags = capabilities.payment_tags || []
extension.grantedPermissions = capabilities.granted_permissions || []
extension.grantedPaymentTags = capabilities.granted_payment_tags || []
extension._grantedPermissions = extension.grantedPermissions.slice()
extension._grantedPaymentTags = extension.grantedPaymentTags.slice()
return capabilities
},
showWasmPermissionsDialog(extension, data = {}) {
this.permissionsDialog.extension = extension
this.permissionsDialog.checked = (
data.granted_permissions ||
extension._grantedPermissions ||
extension.grantedPermissions ||
[]
).slice()
this.permissionsDialog.missing = data.missing_permissions || []
this.permissionsDialog.tags = (
data.granted_payment_tags ||
extension._grantedPaymentTags ||
extension.grantedPaymentTags ||
[]
).slice()
this.permissionsDialog.tagOptions =
data.payment_tags || extension.paymentTags || []
this.permissionsDialog.show = true
},
async ensureWasmPermissionsReady(extension) {
if (extension.extensionType !== 'wasm') return true
const wasmHost = this.extensions.find(ext => ext.id === 'wasm')
if (!wasmHost || !wasmHost.isInstalled || !wasmHost.isActive) {
Quasar.Notify.create({
type: 'warning',
message:
'Enable the WASM! host extension before using this extension.'
})
return false
}
let data = {}
try {
data = await this.loadWasmCapabilities(extension)
} catch (err) {
LNbits.utils.notifyApiError(err)
return false
}
const required = (data.permissions || []).map(p => p.id).filter(Boolean)
const missing = required.filter(
permission => !extension.grantedPermissions.includes(permission)
)
const tags = data.payment_tags || []
const missingTags =
tags.length &&
!tags.some(tag => extension.grantedPaymentTags.includes(tag))
if (!missing.length && !missingTags) return true
Quasar.Notify.create({
type: 'warning',
message: 'Save WASM permissions before using this extension.'
})
this.showWasmPermissionsDialog(extension, data)
return false
},
async openExtension(extension) {
if (!(await this.ensureWasmPermissionsReady(extension))) return
window.location.href = `${extension.id}/`
},
async enableExtensionForUser(extension) {
if (!(await this.ensureWasmPermissionsReady(extension))) return
if (extension.isPaymentRequired) {
this.showPayToEnable(extension)
return
@@ -348,22 +390,6 @@ window.PageExtensions = {
this.selectedExtension.payToEnable.showQRCode = false
this.showPayToEnableDialog = true
},
openPermissionsDialog(extension) {
this.permissionsDialog.extension = extension
this.permissionsDialog.checked = extension._grantedPermissions
? extension._grantedPermissions.slice()
: extension.grantedPermissions
? extension.grantedPermissions.slice()
: []
this.permissionsDialog.missing = []
this.permissionsDialog.tags = extension._grantedPaymentTags
? extension._grantedPaymentTags.slice()
: extension.grantedPaymentTags
? extension.grantedPaymentTags.slice()
: []
this.permissionsDialog.tagOptions = []
this.permissionsDialog.show = true
},
cancelPermissionsDialog() {
this.permissionsDialog.show = false
this.permissionsDialog.extension = null
@@ -380,47 +406,12 @@ window.PageExtensions = {
})
return
}
this.permissionsDialog.extension = extension
this.permissionsDialog.checked = extension._grantedPermissions
? extension._grantedPermissions.slice()
: extension.grantedPermissions
? extension.grantedPermissions.slice()
: []
this.permissionsDialog.missing = []
this.permissionsDialog.tags = extension._grantedPaymentTags
? extension._grantedPaymentTags.slice()
: extension.grantedPaymentTags
? extension.grantedPaymentTags.slice()
: []
this.permissionsDialog.tagOptions = []
try {
const {data} = await LNbits.api.request(
'GET',
`/wasm/api/v1/extensions/${extension.id}/capabilities`,
this.g.user.wallets[0].adminkey
)
if (data && Array.isArray(data.permissions)) {
extension.permissions = data.permissions
}
if (data && Array.isArray(data.missing_permissions)) {
this.permissionsDialog.missing = data.missing_permissions
}
if (data && Array.isArray(data.payment_tags)) {
extension.paymentTags = data.payment_tags
this.permissionsDialog.tagOptions = data.payment_tags
}
if (data && Array.isArray(data.granted_permissions)) {
extension.grantedPermissions = data.granted_permissions
this.permissionsDialog.checked = data.granted_permissions.slice()
}
if (data && Array.isArray(data.granted_payment_tags)) {
extension.grantedPaymentTags = data.granted_payment_tags
this.permissionsDialog.tags = data.granted_payment_tags.slice()
}
const data = await this.loadWasmCapabilities(extension)
this.showWasmPermissionsDialog(extension, data)
} catch (err) {
LNbits.utils.notifyApiError(err)
}
this.permissionsDialog.show = true
},
async confirmPermissionsDialog() {
const ext = this.permissionsDialog.extension
+1 -2
View File
@@ -291,8 +291,7 @@
"
flat
color="primary"
type="a"
:href="extension.id + '/'"
@click="openExtension(extension)"
:label="$t('open')"
></q-btn>
<q-btn
+33
View File
@@ -162,6 +162,39 @@ async def test_extension_api_install_details_and_release_endpoints(mocker):
assert release_info["is_version_compatible"] is True
@pytest.mark.anyio
async def test_wasm_extension_enable_requires_saved_permissions(mocker):
user = await create_user_account(
Account(
id=uuid4().hex,
username=f"user_{uuid4().hex[:8]}",
email=f"user_{uuid4().hex[:8]}@lnbits.com",
)
)
ext_id = f"wasm_{uuid4().hex[:8]}"
await create_installed_extension(make_installable_extension(ext_id))
mocker.patch(
"lnbits.core.views.extension_api.get_valid_extensions",
mocker.AsyncMock(return_value=[Extension(code=ext_id, is_valid=True)]),
)
mocker.patch(
"lnbits.core.views.extension_api.get_extension_type",
return_value="wasm",
)
mocker.patch(
"lnbits.core.views.extension_api._load_wasm_extension_config",
return_value={
"extension_type": "wasm",
"permissions": [{"id": "ext.db.read_write"}],
"payment_tags": ["paidtasks"],
},
)
with pytest.raises(HTTPException) as exc:
await api_enable_extension(ext_id, AccountId(id=user.id))
assert exc.value.status_code == 400
@pytest.mark.anyio
async def test_extension_api_pay_to_enable_and_catalog_views(mocker, admin_user):
regular_user = await create_user_account(