Deactivate all extensions flag (#2206)

* feat: allow all extension deactivation

* doc: updated comment

* fix: make sure `register_routes` executes after installed extensions are checked

* chore: code format

* fix: do not run migration on deactivated extensions

* fix: make sure the deactivated extension list is loaded in time

* feat: register extension routes if extension never loaded before

* fix: move `load_disabled_extension_list`

* doc: disable by default
This commit is contained in:
Vlad Stan 2024-01-22 12:18:12 +02:00 committed by GitHub
parent 0d2447faf3
commit 26ca8c71d7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 57 additions and 24 deletions

View file

@ -141,6 +141,9 @@ LNBITS_ADMIN_USERS=""
# Extensions only admin can access
LNBITS_ADMIN_EXTENSIONS="ngrok, admin"
# Start LNbits core only. The extensions are not loaded.
# LNBITS_EXTENSIONS_DEACTIVATE_ALL=true
# Disable account creation for new users
# LNBITS_ALLOW_NEW_ACCOUNTS=false

View file

@ -10,7 +10,7 @@ import traceback
from hashlib import sha256
from http import HTTPStatus
from pathlib import Path
from typing import Callable, List
from typing import Callable, List, Optional
from fastapi import FastAPI, HTTPException, Request
from fastapi.exceptions import RequestValidationError
@ -35,7 +35,7 @@ from lnbits.tasks import cancel_all_tasks, create_permanent_task
from lnbits.utils.cache import cache
from lnbits.wallets import get_wallet_class, set_wallet_class
from .commands import db_versions, load_disabled_extension_list, migrate_databases
from .commands import db_versions, migrate_databases
from .core import init_core_routers
from .core.db import core_app_extra
from .core.services import check_admin_settings, check_webpush_settings
@ -112,7 +112,6 @@ def create_app() -> FastAPI:
add_ratelimit_middleware(app)
register_startup(app)
register_routes(app)
register_async_tasks(app)
register_exception_handlers(app)
register_shutdown(app)
@ -189,8 +188,7 @@ async def check_installed_extensions(app: FastAPI):
persist state. Zips that are missing will be re-downloaded.
"""
shutil.rmtree(os.path.join("lnbits", "upgrades"), True)
await load_disabled_extension_list()
installed_extensions = await build_all_installed_extensions_list()
installed_extensions = await build_all_installed_extensions_list(False)
for ext in installed_extensions:
try:
@ -212,7 +210,9 @@ async def check_installed_extensions(app: FastAPI):
logger.info(f"{ext.id} ({ext.installed_version})")
async def build_all_installed_extensions_list() -> List[InstallableExtension]:
async def build_all_installed_extensions_list(
include_deactivated: Optional[bool] = True,
) -> List[InstallableExtension]:
"""
Returns a list of all the installed extensions plus the extensions that
MUST be installed by default (see LNBITS_EXTENSIONS_DEFAULT_INSTALL).
@ -237,7 +237,17 @@ async def build_all_installed_extensions_list() -> List[InstallableExtension]:
)
installed_extensions.append(ext_info)
return installed_extensions
if include_deactivated:
return installed_extensions
if settings.lnbits_extensions_deactivate_all:
return []
return [
e
for e in installed_extensions
if e.id not in settings.lnbits_deactivated_extensions
]
def check_installed_extension_files(ext: InstallableExtension) -> bool:
@ -273,7 +283,7 @@ def register_routes(app: FastAPI) -> None:
"""Register FastAPI routes / LNbits extensions."""
init_core_routers(app)
for ext in get_valid_extensions():
for ext in get_valid_extensions(False):
try:
register_ext_routes(app, ext)
except Exception as e:
@ -383,6 +393,9 @@ def register_startup(app: FastAPI):
# check extensions after restart
await check_installed_extensions(app)
# register core and extension routes
register_routes(app)
if settings.lnbits_admin_ui:
initialize_server_logger()

View file

@ -136,7 +136,11 @@ async def migrate_databases():
core_version = current_versions.get("core", 0)
await run_migration(conn, core_migrations, "core", core_version)
for ext in get_valid_extensions():
# here is the first place we can be sure that the
# `installed_extensions` table has been created
await load_disabled_extension_list()
for ext in get_valid_extensions(False):
current_version = current_versions.get(ext.code, 0)
try:
await migrate_extension_database(ext, current_version)

View file

@ -54,8 +54,6 @@ async def stop_extension_background_work(
"""
Stop background work for extension (like asyncio.Tasks, WebSockets, etc).
Extensions SHOULD expose a DELETE enpoint at the root level of their API.
This function tries first to call the endpoint using `http`
and if it fails it tries using `https`.
"""
async with httpx.AsyncClient() as client:
try:

View file

@ -1,4 +1,5 @@
import asyncio
import sys
from http import HTTPStatus
from pathlib import Path
from typing import Annotated, List, Optional, Union
@ -11,7 +12,7 @@ from fastapi.routing import APIRouter
from loguru import logger
from pydantic.types import UUID4
from lnbits.core.db import db
from lnbits.core.db import core_app_extra, db
from lnbits.core.helpers import to_valid_user_id
from lnbits.core.models import User
from lnbits.decorators import check_admin, check_user_exists
@ -74,9 +75,6 @@ async def extensions_install(
):
await toggle_extension(enable, disable, user.id)
# Update user as his extensions have been updated
if enable or disable:
user = await get_user(user.id) # type: ignore
try:
installed_exts: List["InstallableExtension"] = await get_installed_extensions()
installed_exts_ids = [e.id for e in installed_exts]
@ -103,20 +101,28 @@ async def extensions_install(
try:
ext_id = activate or deactivate
all_extensions = get_valid_extensions()
ext = next((e for e in all_extensions if e.code == ext_id), None)
if ext_id and user.admin:
if deactivate and deactivate not in settings.lnbits_deactivated_extensions:
settings.lnbits_deactivated_extensions += [deactivate]
elif activate:
# if extension never loaded (was deactivated on server startup)
if ext_id not in sys.modules.keys():
# run extension start-up routine
core_app_extra.register_new_ext_routes(ext)
settings.lnbits_deactivated_extensions = list(
filter(
lambda e: e != activate, settings.lnbits_deactivated_extensions
)
)
await update_installed_extension_state(
ext_id=ext_id, active=activate is not None
)
all_extensions = list(map(lambda e: e.code, get_valid_extensions()))
all_ext_ids = list(map(lambda e: e.code, all_extensions))
inactive_extensions = await get_inactive_extensions()
db_version = await get_dbversions()
extensions = list(
@ -131,7 +137,7 @@ async def extensions_install(
"dependencies": ext.dependencies,
"isInstalled": ext.id in installed_exts_ids,
"hasDatabaseTables": ext.id in db_version,
"isAvailable": ext.id in all_extensions,
"isAvailable": ext.id in all_ext_ids,
"isAdminOnly": ext.id in settings.lnbits_admin_extensions,
"isActive": ext.id not in inactive_extensions,
"latestRelease": (

View file

@ -604,11 +604,23 @@ class CreateExtension(BaseModel):
source_repo: str
def get_valid_extensions() -> List[Extension]:
return [
def get_valid_extensions(include_deactivated: Optional[bool] = True) -> List[Extension]:
valid_extensions = [
extension for extension in ExtensionManager().extensions if extension.is_valid
]
if include_deactivated:
return valid_extensions
if settings.lnbits_extensions_deactivate_all:
return []
return [
e
for e in valid_extensions
if e.code not in settings.lnbits_deactivated_extensions
]
def version_parse(v: str):
"""

View file

@ -71,11 +71,7 @@ def template_renderer(additional_folders: Optional[List] = None) -> Jinja2Templa
settings.lnbits_node_ui and get_node_class() is not None
)
t.env.globals["LNBITS_NODE_UI_AVAILABLE"] = get_node_class() is not None
t.env.globals["EXTENSIONS"] = [
e
for e in get_valid_extensions()
if e.code not in settings.lnbits_deactivated_extensions
]
t.env.globals["EXTENSIONS"] = get_valid_extensions(False)
if settings.lnbits_custom_logo:
t.env.globals["USE_CUSTOM_LOGO"] = settings.lnbits_custom_logo

View file

@ -347,6 +347,7 @@ class EnvSettings(LNbitsSettings):
log_rotation: str = Field(default="100 MB")
log_retention: str = Field(default="3 months")
server_startup_time: int = Field(default=time())
lnbits_extensions_deactivate_all: bool = Field(default=False)
@property
def has_default_extension_path(self) -> bool: