feat: ask permission
This commit is contained in:
@@ -292,6 +292,16 @@ def list_extension_api_methods(
|
||||
return sorted(methods, key=lambda method: method.method_id)
|
||||
|
||||
|
||||
def extension_api_permission_ids(
|
||||
api_cls: type[ExtensionAPI] = ExtensionAPI,
|
||||
) -> set[str]:
|
||||
return {
|
||||
method.required_permission
|
||||
for method in list_extension_api_methods(api_cls)
|
||||
if method.required_permission
|
||||
}
|
||||
|
||||
|
||||
def get_extension_api_method(
|
||||
method_id: str,
|
||||
api_cls: type[ExtensionAPI] = ExtensionAPI,
|
||||
|
||||
@@ -2,6 +2,7 @@ import asyncio
|
||||
import importlib
|
||||
import json
|
||||
import zipfile
|
||||
from collections.abc import Iterable
|
||||
from pathlib import PurePosixPath
|
||||
from typing import Any
|
||||
|
||||
@@ -20,6 +21,7 @@ from lnbits.core.crud.extensions import (
|
||||
get_installed_extensions,
|
||||
update_installed_extension,
|
||||
)
|
||||
from lnbits.core.extensions.api import extension_api_permission_ids
|
||||
from lnbits.core.helpers import migrate_extension_database
|
||||
from lnbits.db import Connection
|
||||
from lnbits.settings import settings
|
||||
@@ -82,6 +84,32 @@ async def install_extension(
|
||||
return extension
|
||||
|
||||
|
||||
def validate_extension_permissions(
|
||||
ext_id: str,
|
||||
permissions: Iterable[ExtensionPermission],
|
||||
*,
|
||||
strict: bool = True,
|
||||
) -> list[ExtensionPermission]:
|
||||
known_permission_ids = extension_api_permission_ids()
|
||||
normalized_permissions: list[ExtensionPermission] = []
|
||||
unknown_ids: list[str] = []
|
||||
|
||||
for permission in permissions:
|
||||
if permission.id not in known_permission_ids:
|
||||
unknown_ids.append(permission.id)
|
||||
if strict:
|
||||
continue
|
||||
normalized_permissions.append(permission.copy(update={"label": None}))
|
||||
|
||||
if unknown_ids and strict:
|
||||
raise ValueError(
|
||||
f"Extension '{ext_id}' requests unknown permissions: "
|
||||
+ ", ".join(sorted(set(unknown_ids)))
|
||||
)
|
||||
|
||||
return normalized_permissions
|
||||
|
||||
|
||||
def _validate_extension_permissions(
|
||||
ext_info: InstallableExtension,
|
||||
granted_permissions: list[ExtensionPermission] | None,
|
||||
@@ -90,25 +118,25 @@ def _validate_extension_permissions(
|
||||
if extension_config.get("extension_type") != "wasm":
|
||||
return []
|
||||
|
||||
requested_permissions = [
|
||||
ExtensionPermission.parse_obj(permission)
|
||||
for permission in extension_config.get("permissions") or []
|
||||
if isinstance(permission, dict) and permission.get("id")
|
||||
]
|
||||
requested_permissions = validate_extension_permissions(
|
||||
ext_info.id,
|
||||
[
|
||||
ExtensionPermission.parse_obj(permission)
|
||||
for permission in extension_config.get("permissions") or []
|
||||
if isinstance(permission, dict) and permission.get("id")
|
||||
],
|
||||
)
|
||||
if not requested_permissions:
|
||||
return []
|
||||
|
||||
if granted_permissions is None:
|
||||
raise ValueError(
|
||||
f"WASM extension '{ext_info.id}' requires permission approval."
|
||||
)
|
||||
raise ValueError(f"Extension '{ext_info.id}' requires permission approval.")
|
||||
|
||||
requested_ids = {permission.id for permission in requested_permissions}
|
||||
granted_ids = {permission.id for permission in granted_permissions}
|
||||
if requested_ids != granted_ids:
|
||||
raise ValueError(
|
||||
f"WASM extension '{ext_info.id}' was not granted all requested "
|
||||
"permissions."
|
||||
f"Extension '{ext_info.id}' was not granted all requested permissions."
|
||||
)
|
||||
|
||||
return requested_permissions
|
||||
|
||||
@@ -39,6 +39,7 @@ from lnbits.core.services.extensions import (
|
||||
get_valid_extensions,
|
||||
install_extension,
|
||||
uninstall_extension,
|
||||
validate_extension_permissions,
|
||||
)
|
||||
from lnbits.db import Page
|
||||
from lnbits.decorators import (
|
||||
@@ -461,13 +462,19 @@ async def get_extension_release(org: str, repo: str, tag_name: str):
|
||||
if not config:
|
||||
return {}
|
||||
|
||||
permissions = validate_extension_permissions(config.name, config.permissions)
|
||||
|
||||
return {
|
||||
"min_lnbits_version": config.min_lnbits_version,
|
||||
"is_version_compatible": config.is_version_compatible(),
|
||||
"warning": config.warning,
|
||||
"extension_type": config.extension_type,
|
||||
"permissions": [dict(permission) for permission in config.permissions],
|
||||
"permissions": [dict(permission) for permission in permissions],
|
||||
}
|
||||
except ValueError as exc:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST, detail=str(exc)
|
||||
) from exc
|
||||
except Exception as exc:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(exc)
|
||||
@@ -566,6 +573,13 @@ async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
|
||||
extension_data = []
|
||||
for ext in installable_exts:
|
||||
installed_ext = installed_exts_by_id.get(ext.id)
|
||||
permissions = (
|
||||
validate_extension_permissions(
|
||||
installed_ext.id, installed_ext.permissions, strict=False
|
||||
)
|
||||
if installed_ext
|
||||
else []
|
||||
)
|
||||
extension_data.append(
|
||||
{
|
||||
"id": ext.id,
|
||||
@@ -603,11 +617,7 @@ async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
|
||||
),
|
||||
"isPaymentRequired": ext.requires_payment,
|
||||
"isWasm": installed_ext.is_wasm if installed_ext else ext.is_wasm,
|
||||
"permissions": (
|
||||
[dict(permission) for permission in installed_ext.permissions]
|
||||
if installed_ext
|
||||
else []
|
||||
),
|
||||
"permissions": [dict(permission) for permission in permissions],
|
||||
"inProgress": False,
|
||||
"selectedForUpdate": False,
|
||||
}
|
||||
|
||||
@@ -519,6 +519,14 @@ window.localisation.en = {
|
||||
admin_settings: 'Admin Settings',
|
||||
extension_cost: 'This release requires a payment of minimum {cost} sats.',
|
||||
extension_paid_sats: 'You have already paid {paid_sats} sats.',
|
||||
extension_permissions_title: 'Grant extension permissions',
|
||||
extension_permissions_request: 'This extension requests these permissions:',
|
||||
extension_permissions_grant_install: 'Grant and install',
|
||||
extension_permission_ext_storage_read_write:
|
||||
'Read and write extension storage',
|
||||
extension_permission_payments_watch: 'Watch payments',
|
||||
extension_permission_wallet_create_invoice: 'Create invoices',
|
||||
extension_permission_wallet_list: 'List wallets',
|
||||
create_extension: 'Create Extension',
|
||||
release_details_error: 'Cannot get the release details.',
|
||||
pay_from_wallet: 'Pay from Wallet',
|
||||
|
||||
@@ -24,6 +24,11 @@ window.PageExtensions = {
|
||||
selectedExtensionDetails: null,
|
||||
selectedExtensionRepos: null,
|
||||
selectedRelease: null,
|
||||
permissionGrant: {
|
||||
show: false,
|
||||
permissions: [],
|
||||
resolve: null
|
||||
},
|
||||
uninstallAndDropDb: false,
|
||||
maxStars: 5,
|
||||
paylinkWebsocket: null,
|
||||
@@ -664,12 +669,13 @@ window.PageExtensions = {
|
||||
if (release.grantedPermissions) {
|
||||
return release.grantedPermissions
|
||||
}
|
||||
const granted = await this.confirmExtensionPermissions(permissions)
|
||||
if (!granted) {
|
||||
const grantedPermissions =
|
||||
await this.confirmExtensionPermissions(permissions)
|
||||
if (!grantedPermissions) {
|
||||
return null
|
||||
}
|
||||
release.grantedPermissions = permissions
|
||||
return permissions
|
||||
release.grantedPermissions = grantedPermissions
|
||||
return grantedPermissions
|
||||
},
|
||||
extensionPermissionsForRelease(release) {
|
||||
return release.permissions || this.selectedExtension?.permissions || []
|
||||
@@ -681,48 +687,43 @@ window.PageExtensions = {
|
||||
)
|
||||
},
|
||||
confirmExtensionPermissions(permissions) {
|
||||
const permissionItems = permissions
|
||||
.map(permission => {
|
||||
const label = this.escapeHtml(permission.label || permission.id)
|
||||
const description = permission.description
|
||||
? `<div class="text-caption text-grey-7">${this.escapeHtml(
|
||||
permission.description
|
||||
)}</div>`
|
||||
: ''
|
||||
return `<li><strong>${label}</strong>${description}</li>`
|
||||
})
|
||||
.join('')
|
||||
|
||||
return new Promise(resolve => {
|
||||
this.$q
|
||||
.dialog({
|
||||
title: 'Grant extension permissions',
|
||||
message: `<p>This WASM extension requests these permissions:</p><ul>${permissionItems}</ul>`,
|
||||
html: true,
|
||||
persistent: true,
|
||||
ok: {
|
||||
label: 'Grant and install',
|
||||
color: 'primary',
|
||||
flat: true
|
||||
},
|
||||
cancel: {
|
||||
label: this.$t('cancel'),
|
||||
color: 'grey',
|
||||
flat: true
|
||||
}
|
||||
})
|
||||
.onOk(() => resolve(true))
|
||||
.onCancel(() => resolve(false))
|
||||
.onDismiss(() => resolve(false))
|
||||
this.selectedRelease = null
|
||||
this.permissionGrant = {
|
||||
show: true,
|
||||
permissions,
|
||||
resolve
|
||||
}
|
||||
this.showManageExtensionDialog = true
|
||||
})
|
||||
},
|
||||
escapeHtml(value) {
|
||||
return String(value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
grantExtensionPermissions() {
|
||||
this.resolveExtensionPermissionDialog(this.permissionGrant.permissions)
|
||||
},
|
||||
cancelExtensionPermissions() {
|
||||
this.resolveExtensionPermissionDialog(null)
|
||||
},
|
||||
onManageExtensionDialogHide() {
|
||||
if (this.permissionGrant.show) {
|
||||
this.resolveExtensionPermissionDialog(null)
|
||||
}
|
||||
},
|
||||
resolveExtensionPermissionDialog(grantedPermissions) {
|
||||
const resolve = this.permissionGrant.resolve
|
||||
this.permissionGrant = {
|
||||
show: false,
|
||||
permissions: [],
|
||||
resolve: null
|
||||
}
|
||||
this.showManageExtensionDialog = false
|
||||
if (resolve) {
|
||||
resolve(grantedPermissions)
|
||||
}
|
||||
},
|
||||
permissionLabel(permission) {
|
||||
const key = `extension_permission_${permission.id.replace(/[^A-Za-z0-9]/g, '_')}`
|
||||
const label = this.$t(key)
|
||||
return label === key ? permission.id : label
|
||||
},
|
||||
async selectAllUpdatableExtensionss() {
|
||||
this.updatableExtensions.forEach(e => (e.selectedForUpdate = true))
|
||||
@@ -737,7 +738,7 @@ window.PageExtensions = {
|
||||
if (ext.isWasm) {
|
||||
Quasar.Notify.create({
|
||||
type: 'warning',
|
||||
message: `Skipping ${ext.id}; WASM updates require permission approval.`
|
||||
message: `Skipping ${ext.id}; this extension update requires permission approval.`
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -455,8 +455,53 @@
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="showManageExtensionDialog" position="top">
|
||||
<q-card v-if="selectedRelease" class="q-pa-lg lnbits__dialog-card">
|
||||
<q-dialog
|
||||
v-model="showManageExtensionDialog"
|
||||
position="top"
|
||||
@hide="onManageExtensionDialogHide"
|
||||
>
|
||||
<q-card v-if="permissionGrant.show" class="q-pa-lg lnbits__dialog-card">
|
||||
<q-card-section>
|
||||
<div class="text-h6" v-text="$t('extension_permissions_title')"></div>
|
||||
<div
|
||||
class="text-body2 q-mt-sm"
|
||||
v-text="$t('extension_permissions_request')"
|
||||
></div>
|
||||
</q-card-section>
|
||||
|
||||
<q-list bordered separator class="q-mt-md">
|
||||
<q-item
|
||||
v-for="permission of permissionGrant.permissions"
|
||||
:key="permission.id"
|
||||
>
|
||||
<q-item-section>
|
||||
<q-item-label v-text="permissionLabel(permission)"></q-item-label>
|
||||
<q-item-label
|
||||
v-if="permission.description"
|
||||
caption
|
||||
v-text="permission.description"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
flat
|
||||
color="grey"
|
||||
v-text="$t('cancel')"
|
||||
@click="cancelExtensionPermissions"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
flat
|
||||
color="primary"
|
||||
class="q-ml-auto"
|
||||
v-text="$t('extension_permissions_grant_install')"
|
||||
@click="grantExtensionPermissions"
|
||||
></q-btn>
|
||||
</div>
|
||||
</q-card>
|
||||
<q-card v-else-if="selectedRelease" class="q-pa-lg lnbits__dialog-card">
|
||||
<q-card-section>
|
||||
<div v-if="selectedRelease.paymentRequest">
|
||||
<lnbits-qrcode
|
||||
|
||||
Reference in New Issue
Block a user