lnbits-legend/lnbits/core/views/extension_api.py
Vlad Stan 44b458ebb8
[fix] check user extension access (#2519)
* feat: check user extension access
* fix: handle upgraded extensions
2024-05-22 11:10:35 +02:00

269 lines
8.4 KiB
Python

from http import HTTPStatus
from typing import (
List,
Optional,
)
from bolt11 import decode as bolt11_decode
from fastapi import (
APIRouter,
Depends,
HTTPException,
)
from loguru import logger
from lnbits.core.db import core_app_extra
from lnbits.core.helpers import (
migrate_extension_database,
stop_extension_background_work,
)
from lnbits.core.models import (
User,
)
from lnbits.decorators import (
check_access_token,
check_admin,
)
from lnbits.extension_manager import (
CreateExtension,
Extension,
ExtensionRelease,
InstallableExtension,
fetch_github_release_config,
fetch_release_payment_info,
get_valid_extensions,
)
from lnbits.settings import settings
from ..crud import (
add_installed_extension,
delete_dbversion,
delete_installed_extension,
drop_extension_db,
get_dbversions,
get_installed_extension,
get_installed_extensions,
)
extension_router = APIRouter(
tags=["Extension Managment"],
prefix="/api/v1/extension",
)
@extension_router.post("")
async def api_install_extension(
data: CreateExtension,
user: User = Depends(check_admin),
access_token: Optional[str] = Depends(check_access_token),
):
release = await InstallableExtension.get_extension_release(
data.ext_id, data.source_repo, data.archive, data.version
)
if not release:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Release not found"
)
if not release.is_version_compatible:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Incompatible extension version"
)
release.payment_hash = data.payment_hash
ext_info = InstallableExtension(
id=data.ext_id, name=data.ext_id, installed_release=release, icon=release.icon
)
try:
installed_ext = await get_installed_extension(data.ext_id)
ext_info.payments = installed_ext.payments if installed_ext else []
await ext_info.download_archive()
ext_info.extract_archive()
extension = Extension.from_installable_ext(ext_info)
db_version = (await get_dbversions()).get(data.ext_id, 0)
await migrate_extension_database(extension, db_version)
await add_installed_extension(ext_info)
if extension.is_upgrade_extension:
# call stop while the old routes are still active
await stop_extension_background_work(data.ext_id, user.id, access_token)
settings.lnbits_deactivated_extensions.add(data.ext_id)
# mount routes for the new version
core_app_extra.register_new_ext_routes(extension)
ext_info.notify_upgrade(extension.upgrade_hash)
return extension
except AssertionError as exc:
raise HTTPException(HTTPStatus.BAD_REQUEST, str(exc)) from exc
except Exception as exc:
logger.warning(exc)
ext_info.clean_extension_files()
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail=(
f"Failed to install extension {ext_info.id} "
f"({ext_info.installed_version})."
),
) from exc
@extension_router.delete("/{ext_id}")
async def api_uninstall_extension(
ext_id: str,
user: User = Depends(check_admin),
access_token: Optional[str] = Depends(check_access_token),
):
installed_extensions = await get_installed_extensions()
extensions = [e for e in installed_extensions if e.id == ext_id]
if len(extensions) == 0:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"Unknown extension id: {ext_id}",
)
# check that other extensions do not depend on this one
for valid_ext_id in [ext.code for ext in get_valid_extensions()]:
installed_ext = next(
(ext for ext in installed_extensions if ext.id == valid_ext_id), None
)
if installed_ext and ext_id in installed_ext.dependencies:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=(
f"Cannot uninstall. Extension '{installed_ext.name}' "
"depends on this one."
),
)
try:
# call stop while the old routes are still active
await stop_extension_background_work(ext_id, user.id, access_token)
settings.lnbits_deactivated_extensions.add(ext_id)
for ext_info in extensions:
ext_info.clean_extension_files()
await delete_installed_extension(ext_id=ext_info.id)
logger.success(f"Extension '{ext_id}' uninstalled.")
except Exception as exc:
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(exc)
) from exc
@extension_router.get("/{ext_id}/releases", dependencies=[Depends(check_admin)])
async def get_extension_releases(ext_id: str):
try:
extension_releases: List[ExtensionRelease] = (
await InstallableExtension.get_extension_releases(ext_id)
)
installed_ext = await get_installed_extension(ext_id)
if not installed_ext:
return extension_releases
for release in extension_releases:
payment_info = installed_ext.find_existing_payment(release.pay_link)
if payment_info:
release.paid_sats = payment_info.amount
return extension_releases
except Exception as exc:
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(exc)
) from exc
@extension_router.put("/invoice", dependencies=[Depends(check_admin)])
async def get_extension_invoice(data: CreateExtension):
try:
assert data.cost_sats, "A non-zero amount must be specified"
release = await InstallableExtension.get_extension_release(
data.ext_id, data.source_repo, data.archive, data.version
)
assert release, "Release not found"
assert release.pay_link, "Pay link not found for release"
payment_info = await fetch_release_payment_info(
release.pay_link, data.cost_sats
)
assert payment_info and payment_info.payment_request, "Cannot request invoice"
invoice = bolt11_decode(payment_info.payment_request)
assert invoice.amount_msat is not None, "Invoic amount is missing"
invoice_amount = int(invoice.amount_msat / 1000)
assert (
invoice_amount == data.cost_sats
), f"Wrong invoice amount: {invoice_amount}."
assert (
payment_info.payment_hash == invoice.payment_hash
), "Wroong invoice payment hash"
return payment_info
except AssertionError as exc:
raise HTTPException(HTTPStatus.BAD_REQUEST, str(exc)) from exc
except Exception as exc:
logger.warning(exc)
raise HTTPException(
HTTPStatus.INTERNAL_SERVER_ERROR, "Cannot request invoice"
) from exc
@extension_router.get(
"/release/{org}/{repo}/{tag_name}",
dependencies=[Depends(check_admin)],
)
async def get_extension_release(org: str, repo: str, tag_name: str):
try:
config = await fetch_github_release_config(org, repo, tag_name)
if not config:
return {}
return {
"min_lnbits_version": config.min_lnbits_version,
"is_version_compatible": config.is_version_compatible(),
"warning": config.warning,
}
except Exception as exc:
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(exc)
) from exc
@extension_router.delete(
"/{ext_id}/db",
dependencies=[Depends(check_admin)],
)
async def delete_extension_db(ext_id: str):
try:
db_version = (await get_dbversions()).get(ext_id, None)
if not db_version:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"Unknown extension id: {ext_id}",
)
await drop_extension_db(ext_id=ext_id)
await delete_dbversion(ext_id=ext_id)
logger.success(f"Database removed for extension '{ext_id}'")
except HTTPException as ex:
logger.error(ex)
raise ex
except Exception as exc:
logger.error(exc)
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail=f"Cannot delete data for extension '{ext_id}'",
) from exc