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 sys
import traceback import traceback
from http import HTTPStatus from http import HTTPStatus
from pathlib import Path
import httpx import httpx
from bolt11 import decode as bolt11_decode 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)]) @extension_router.post("", dependencies=[Depends(check_admin)])
async def api_install_extension(data: CreateExtension): async def api_install_extension(data: CreateExtension):
release = await InstallableExtension.get_extension_release( 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) user_ext = UserExtension(user=account_id.id, extension=ext_id, active=False)
await create_user_extension(user_ext) 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: if account_id.is_admin_id or not ext.requires_payment:
user_ext.active = True user_ext.active = True
await update_user_extension(user_ext) await update_user_extension(user_ext)
+76 -85
View File
@@ -262,39 +262,81 @@ window.PageExtensions = {
extension.inProgress = false extension.inProgress = false
}) })
}, },
async enableExtensionForUser(extension) { async loadWasmCapabilities(extension) {
if (extension.extensionType === 'wasm') { const {data} = await LNbits.api.request(
const wasmHost = this.extensions.find(ext => ext.id === 'wasm') 'GET',
if (!wasmHost || !wasmHost.isInstalled || !wasmHost.isActive) { `/wasm/api/v1/extensions/${extension.id}/capabilities`,
Quasar.Notify.create({ this.g.user.wallets[0].adminkey
type: 'warning', )
message: const capabilities = data || {}
'Enable the WASM! host extension before using this extension.' extension.permissions = capabilities.permissions || []
}) extension.paymentTags = capabilities.payment_tags || []
return extension.grantedPermissions = capabilities.granted_permissions || []
} extension.grantedPaymentTags = capabilities.granted_payment_tags || []
if (extension.permissions && extension.permissions.length) { extension._grantedPermissions = extension.grantedPermissions.slice()
if (!extension._grantedPermissions) { extension._grantedPaymentTags = extension.grantedPaymentTags.slice()
Quasar.Notify.create({ return capabilities
type: 'warning', },
message: 'Save permissions before enabling this extension.' showWasmPermissionsDialog(extension, data = {}) {
}) this.permissionsDialog.extension = extension
return this.permissionsDialog.checked = (
} data.granted_permissions ||
} extension._grantedPermissions ||
if (extension.paymentTags && extension.paymentTags.length) { extension.grantedPermissions ||
if ( []
!extension._grantedPaymentTags || ).slice()
!extension._grantedPaymentTags.length this.permissionsDialog.missing = data.missing_permissions || []
) { this.permissionsDialog.tags = (
Quasar.Notify.create({ data.granted_payment_tags ||
type: 'warning', extension._grantedPaymentTags ||
message: 'Select payment tags before enabling this extension.' extension.grantedPaymentTags ||
}) []
return ).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) { if (extension.isPaymentRequired) {
this.showPayToEnable(extension) this.showPayToEnable(extension)
return return
@@ -348,22 +390,6 @@ window.PageExtensions = {
this.selectedExtension.payToEnable.showQRCode = false this.selectedExtension.payToEnable.showQRCode = false
this.showPayToEnableDialog = true 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() { cancelPermissionsDialog() {
this.permissionsDialog.show = false this.permissionsDialog.show = false
this.permissionsDialog.extension = null this.permissionsDialog.extension = null
@@ -380,47 +406,12 @@ window.PageExtensions = {
}) })
return 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 { try {
const {data} = await LNbits.api.request( const data = await this.loadWasmCapabilities(extension)
'GET', this.showWasmPermissionsDialog(extension, data)
`/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()
}
} catch (err) { } catch (err) {
LNbits.utils.notifyApiError(err) LNbits.utils.notifyApiError(err)
} }
this.permissionsDialog.show = true
}, },
async confirmPermissionsDialog() { async confirmPermissionsDialog() {
const ext = this.permissionsDialog.extension const ext = this.permissionsDialog.extension
+1 -2
View File
@@ -291,8 +291,7 @@
" "
flat flat
color="primary" color="primary"
type="a" @click="openExtension(extension)"
:href="extension.id + '/'"
:label="$t('open')" :label="$t('open')"
></q-btn> ></q-btn>
<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 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 @pytest.mark.anyio
async def test_extension_api_pay_to_enable_and_catalog_views(mocker, admin_user): async def test_extension_api_pay_to_enable_and_catalog_views(mocker, admin_user):
regular_user = await create_user_account( regular_user = await create_user_account(