Compare commits

...
Author SHA1 Message Date
Arc fa1fec87c7 fundle 2026-05-01 12:32:44 +01:00
Arc 74a1dd5ea6 trans 2026-05-01 12:31:03 +01:00
Arc ef47f1660d make 2026-05-01 12:28:43 +01:00
Arc 4341c329fd extend to repo manifests 2026-05-01 12:16:39 +01:00
Arc fdf3ab2688 init 2026-05-01 12:10:11 +01:00
25 changed files with 245 additions and 11 deletions
+66 -5
View File
@@ -43,6 +43,8 @@ class ExplicitRelease(BaseModel):
details_link: str | None details_link: str | None
paid_features: str | None paid_features: str | None
pay_link: str | None pay_link: str | None
admin_only: bool = False
super_user_only: bool = False
def is_version_compatible(self): def is_version_compatible(self):
return is_lnbits_version_ok(self.min_lnbits_version, self.max_lnbits_version) return is_lnbits_version_ok(self.min_lnbits_version, self.max_lnbits_version)
@@ -52,6 +54,8 @@ class GitHubRelease(BaseModel):
id: str id: str
organisation: str organisation: str
repository: str repository: str
admin_only: bool = False
super_user_only: bool = False
class Manifest(BaseModel): class Manifest(BaseModel):
@@ -83,6 +87,8 @@ class ExtensionConfig(BaseModel):
warning: str | None = "" warning: str | None = ""
min_lnbits_version: str | None min_lnbits_version: str | None
max_lnbits_version: str | None max_lnbits_version: str | None
admin_only: bool = False
super_user_only: bool = False
def is_version_compatible(self) -> bool: def is_version_compatible(self) -> bool:
return is_lnbits_version_ok(self.min_lnbits_version, self.max_lnbits_version) return is_lnbits_version_ok(self.min_lnbits_version, self.max_lnbits_version)
@@ -195,6 +201,8 @@ class ExtensionRelease(BaseModel):
cost_sats: int | None = None cost_sats: int | None = None
paid_sats: int | None = 0 paid_sats: int | None = 0
payment_hash: str | None = None payment_hash: str | None = None
admin_only: bool = False
super_user_only: bool = False
@property @property
def archive_url(self) -> str: def archive_url(self) -> str:
@@ -263,6 +271,8 @@ class ExtensionRelease(BaseModel):
paid_features=e.paid_features, paid_features=e.paid_features,
repo=e.repo, repo=e.repo,
icon=e.icon, icon=e.icon,
admin_only=e.admin_only,
super_user_only=e.super_user_only,
) )
@classmethod @classmethod
@@ -289,6 +299,8 @@ class ExtensionRelease(BaseModel):
release.min_lnbits_version = config.min_lnbits_version release.min_lnbits_version = config.min_lnbits_version
release.max_lnbits_version = config.max_lnbits_version release.max_lnbits_version = config.max_lnbits_version
release.is_version_compatible = config.is_version_compatible() release.is_version_compatible = config.is_version_compatible()
release.admin_only = config.admin_only
release.super_user_only = config.super_user_only
release.icon = icon_to_github_url(f"{org}/{repo}", config.tile) release.icon = icon_to_github_url(f"{org}/{repo}", config.tile)
@@ -336,6 +348,8 @@ class ExtensionMeta(BaseModel):
paid_features: str | None = None paid_features: str | None = None
has_paid_release: bool = False has_paid_release: bool = False
has_free_release: bool = False has_free_release: bool = False
admin_only: bool = False
super_user_only: bool = False
class InstallableExtension(BaseModel): class InstallableExtension(BaseModel):
@@ -399,6 +413,16 @@ class InstallableExtension(BaseModel):
return False return False
return self.meta.pay_to_enable.required is True return self.meta.pay_to_enable.required is True
@property
def is_admin_only(self) -> bool:
return settings.is_admin_extension(self.id) or bool(
self.meta and self.meta.admin_only
)
@property
def is_super_user_only(self) -> bool:
return bool(self.meta and self.meta.super_user_only)
async def download_archive(self): async def download_archive(self):
logger.info(f"Downloading extension {self.name} ({self.installed_version}).") logger.info(f"Downloading extension {self.name} ({self.installed_version}).")
ext_zip_file = self.zip_path ext_zip_file = self.zip_path
@@ -454,6 +478,16 @@ class InstallableExtension(BaseModel):
self.name = config_json.get("name") self.name = config_json.get("name")
self.short_description = config_json.get("short_description") self.short_description = config_json.get("short_description")
if self.meta:
self.meta.admin_only = config_json.get("admin_only", False)
self.meta.super_user_only = config_json.get("super_user_only", False)
if self.meta.installed_release:
self.meta.installed_release.admin_only = config_json.get(
"admin_only", False
)
self.meta.installed_release.super_user_only = config_json.get(
"super_user_only", False
)
if ( if (
self.meta self.meta
@@ -496,6 +530,10 @@ class InstallableExtension(BaseModel):
return return
if not release.is_version_compatible: if not release.is_version_compatible:
return return
if not self.meta:
self.meta = ExtensionMeta()
self.meta.admin_only = release.admin_only
self.meta.super_user_only = release.super_user_only
if not self.meta or not self.meta.latest_release: if not self.meta or not self.meta.latest_release:
meta = self.meta or ExtensionMeta() meta = self.meta or ExtensionMeta()
meta.latest_release = release meta.latest_release = release
@@ -558,6 +596,13 @@ class InstallableExtension(BaseModel):
github_release.organisation, github_release.repository github_release.organisation, github_release.repository
) )
source_repo = f"{github_release.organisation}/{github_release.repository}" source_repo = f"{github_release.organisation}/{github_release.repository}"
admin_only = github_release.admin_only or config.admin_only
super_user_only = github_release.super_user_only or config.super_user_only
latest_extension_release = ExtensionRelease.from_github_release(
source_repo, latest_release
)
latest_extension_release.admin_only = admin_only
latest_extension_release.super_user_only = super_user_only
return InstallableExtension( return InstallableExtension(
id=github_release.id, id=github_release.id,
name=config.name, name=config.name,
@@ -569,9 +614,9 @@ class InstallableExtension(BaseModel):
config.tile, config.tile,
), ),
meta=ExtensionMeta( meta=ExtensionMeta(
latest_release=ExtensionRelease.from_github_release( admin_only=admin_only,
source_repo, latest_release super_user_only=super_user_only,
), latest_release=latest_extension_release,
), ),
) )
except Exception as e: except Exception as e:
@@ -580,7 +625,12 @@ class InstallableExtension(BaseModel):
@classmethod @classmethod
def from_explicit_release(cls, e: ExplicitRelease) -> InstallableExtension: def from_explicit_release(cls, e: ExplicitRelease) -> InstallableExtension:
meta = ExtensionMeta(archive=e.archive, dependencies=e.dependencies) meta = ExtensionMeta(
archive=e.archive,
dependencies=e.dependencies,
admin_only=e.admin_only,
super_user_only=e.super_user_only,
)
return InstallableExtension( return InstallableExtension(
id=e.id, id=e.id,
name=e.name, name=e.name,
@@ -610,6 +660,8 @@ class InstallableExtension(BaseModel):
short_description=config_json.get("short_description"), short_description=config_json.get("short_description"),
icon=config_json.get("tile"), icon=config_json.get("tile"),
meta=ExtensionMeta( meta=ExtensionMeta(
admin_only=config_json.get("admin_only", False),
super_user_only=config_json.get("super_user_only", False),
installed_release=ExtensionRelease( installed_release=ExtensionRelease(
name=ext_id, name=ext_id,
version=version, version=version,
@@ -617,7 +669,9 @@ class InstallableExtension(BaseModel):
source_repo=f"{conf_path}", source_repo=f"{conf_path}",
min_lnbits_version=config_json.get("min_lnbits_version"), min_lnbits_version=config_json.get("min_lnbits_version"),
max_lnbits_version=config_json.get("max_lnbits_version"), max_lnbits_version=config_json.get("max_lnbits_version"),
) admin_only=config_json.get("admin_only", False),
super_user_only=config_json.get("super_user_only", False),
),
), ),
) )
@@ -719,6 +773,13 @@ class InstallableExtension(BaseModel):
repo_releases = await ExtensionRelease.get_github_releases( repo_releases = await ExtensionRelease.get_github_releases(
r.organisation, r.repository r.organisation, r.repository
) )
for repo_release in repo_releases:
repo_release.admin_only = (
repo_release.admin_only or r.admin_only
)
repo_release.super_user_only = (
repo_release.super_user_only or r.super_user_only
)
extension_releases += repo_releases extension_releases += repo_releases
for e in manifest.extensions: for e in manifest.extensions:
+23 -2
View File
@@ -79,7 +79,11 @@ async def api_install_extension(data: CreateExtension):
raise HTTPException(HTTPStatus.BAD_REQUEST, "Incompatible extension version.") raise HTTPException(HTTPStatus.BAD_REQUEST, "Incompatible extension version.")
release.payment_hash = data.payment_hash release.payment_hash = data.payment_hash
ext_meta = ExtensionMeta(installed_release=release) ext_meta = ExtensionMeta(
installed_release=release,
admin_only=release.admin_only,
super_user_only=release.super_user_only,
)
ext_info = InstallableExtension( ext_info = InstallableExtension(
id=data.ext_id, id=data.ext_id,
name=data.ext_id, name=data.ext_id,
@@ -176,6 +180,19 @@ async def api_update_pay_to_enable(
) )
def _check_enable_extension_access(
ext_id: str, ext: InstallableExtension, account_id: AccountId
) -> None:
if ext.is_super_user_only and not settings.is_super_user(account_id.id):
raise HTTPException(
HTTPStatus.FORBIDDEN, f"User not authorized for extension '{ext_id}'."
)
if ext.is_admin_only and not settings.is_admin_user(account_id.id):
raise HTTPException(
HTTPStatus.FORBIDDEN, f"User not authorized for extension '{ext_id}'."
)
@extension_router.put("/{ext_id}/enable") @extension_router.put("/{ext_id}/enable")
async def api_enable_extension( async def api_enable_extension(
ext_id: str, account_id: AccountId = Depends(check_account_id_exists) ext_id: str, account_id: AccountId = Depends(check_account_id_exists)
@@ -191,6 +208,7 @@ async def api_enable_extension(
raise ValueError(f"Extension '{ext_id}' is not installed.") raise ValueError(f"Extension '{ext_id}' is not installed.")
if not ext.active: if not ext.active:
raise ValueError(f"Extension '{ext_id}' is not activated.") raise ValueError(f"Extension '{ext_id}' is not activated.")
_check_enable_extension_access(ext_id, ext, account_id)
user_ext = await get_user_extension(account_id.id, ext_id) user_ext = await get_user_extension(account_id.id, ext_id)
if not user_ext: if not user_ext:
@@ -464,6 +482,8 @@ async def get_extension_release(org: str, repo: str, tag_name: str):
"min_lnbits_version": config.min_lnbits_version, "min_lnbits_version": config.min_lnbits_version,
"is_version_compatible": config.is_version_compatible(), "is_version_compatible": config.is_version_compatible(),
"warning": config.warning, "warning": config.warning,
"admin_only": config.admin_only,
"super_user_only": config.super_user_only,
} }
except Exception as exc: except Exception as exc:
raise HTTPException( raise HTTPException(
@@ -573,7 +593,8 @@ async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
(True for version in db_versions if version.db == ext.id), False (True for version in db_versions if version.db == ext.id), False
), ),
"isAvailable": ext.id in all_ext_ids, "isAvailable": ext.id in all_ext_ids,
"isAdminOnly": ext.id in settings.lnbits_admin_extensions, "isAdminOnly": ext.is_admin_only,
"isSuperUserOnly": ext.is_super_user_only,
"isActive": ext.id not in inactive_extensions, "isActive": ext.id not in inactive_extensions,
"latestRelease": ( "latestRelease": (
dict(ext.meta.latest_release) dict(ext.meta.latest_release)
+14 -1
View File
@@ -14,6 +14,7 @@ from lnbits.core.crud import (
get_account, get_account,
get_account_by_email, get_account_by_email,
get_account_by_username, get_account_by_username,
get_installed_extension,
get_user_active_extensions_ids, get_user_active_extensions_ids,
get_user_from_account, get_user_from_account,
get_wallet_for_key, get_wallet_for_key,
@@ -424,7 +425,19 @@ async def check_user_extension_access(
Check if the user has access to a particular extension. Check if the user has access to a particular extension.
Raises HTTP Forbidden if the user is not allowed. Raises HTTP Forbidden if the user is not allowed.
""" """
if settings.is_admin_extension(ext_id) and not settings.is_admin_user(user_id): ext = await get_installed_extension(ext_id, conn=conn)
is_admin_only = settings.is_admin_extension(ext_id) or bool(
ext and ext.meta and ext.meta.admin_only
)
is_super_user_only = bool(ext and ext.meta and ext.meta.super_user_only)
if is_super_user_only and not settings.is_super_user(user_id):
return SimpleStatus(
success=False,
message=f"User not authorized for extension '{ext_id}'.",
)
if is_admin_only and not settings.is_admin_user(user_id):
return SimpleStatus( return SimpleStatus(
success=False, message=f"User not authorized for extension '{ext_id}'." success=False, message=f"User not authorized for extension '{ext_id}'."
) )
+1 -1
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -191,6 +191,7 @@ window.localisation.br = {
only_admins_can_create_extensions: only_admins_can_create_extensions:
'Apenas contas de administrador podem criar extensões', 'Apenas contas de administrador podem criar extensões',
admin_only: 'Apenas para Administração', admin_only: 'Apenas para Administração',
super_user_only: 'Apenas para superusuário',
make_user_admin: 'Tornar usuário administrador', make_user_admin: 'Tornar usuário administrador',
revoke_admin: 'Revogar Admin', revoke_admin: 'Revogar Admin',
new_version: 'Nova Versão', new_version: 'Nova Versão',
+1
View File
@@ -135,6 +135,7 @@ window.localisation.cn = {
all: '全部', all: '全部',
only_admins_can_install: '(只有管理员账户可以安装扩展)', only_admins_can_install: '(只有管理员账户可以安装扩展)',
admin_only: '仅限管理员', admin_only: '仅限管理员',
super_user_only: '仅限超级用户',
new_version: '新版本', new_version: '新版本',
extension_depends_on: '依赖于:', extension_depends_on: '依赖于:',
extension_rating_soon: '即将推出评分', extension_rating_soon: '即将推出评分',
+1
View File
@@ -140,6 +140,7 @@ window.localisation.cs = {
only_admins_can_install: only_admins_can_install:
'(Pouze administrátorské účty mohou instalovat rozšíření)', '(Pouze administrátorské účty mohou instalovat rozšíření)',
admin_only: 'Pouze pro adminy', admin_only: 'Pouze pro adminy',
super_user_only: 'Pouze pro superuživatele',
new_version: 'Nová verze', new_version: 'Nová verze',
extension_depends_on: 'Závisí na:', extension_depends_on: 'Závisí na:',
extension_rating_soon: 'Hodnocení brzy dostupné', extension_rating_soon: 'Hodnocení brzy dostupné',
+1
View File
@@ -143,6 +143,7 @@ window.localisation.de = {
only_admins_can_install: only_admins_can_install:
'(Nur Administratorkonten können Erweiterungen installieren)', '(Nur Administratorkonten können Erweiterungen installieren)',
admin_only: 'Nur für Admins', admin_only: 'Nur für Admins',
super_user_only: 'Nur für Superuser',
new_version: 'Neue Version', new_version: 'Neue Version',
extension_depends_on: 'Hängt ab von:', extension_depends_on: 'Hängt ab von:',
extension_rating_soon: 'Bewertungen sind bald verfügbar', extension_rating_soon: 'Bewertungen sind bald verfügbar',
+1
View File
@@ -189,6 +189,7 @@ window.localisation.en = {
only_admins_can_create_extensions: only_admins_can_create_extensions:
'Only admin accounts can create extensions', 'Only admin accounts can create extensions',
admin_only: 'Admin Only', admin_only: 'Admin Only',
super_user_only: 'Super User Only',
make_user_admin: 'Make User Admin', make_user_admin: 'Make User Admin',
revoke_admin: 'Revoke Admin', revoke_admin: 'Revoke Admin',
new_version: 'New Version', new_version: 'New Version',
+1
View File
@@ -142,6 +142,7 @@ window.localisation.es = {
only_admins_can_install: only_admins_can_install:
'(Solo las cuentas de administrador pueden instalar extensiones)', '(Solo las cuentas de administrador pueden instalar extensiones)',
admin_only: 'Solo administradores', admin_only: 'Solo administradores',
super_user_only: 'Solo superusuario',
new_version: 'Nueva Versión', new_version: 'Nueva Versión',
extension_depends_on: 'Depende de:', extension_depends_on: 'Depende de:',
extension_rating_soon: 'Calificaciones próximamente', extension_rating_soon: 'Calificaciones próximamente',
+1
View File
@@ -154,6 +154,7 @@ window.localisation.fi = {
all: 'Kaikki', all: 'Kaikki',
only_admins_can_install: '(Vain pääkäyttäjät voivat asentaa laajennuksia)', only_admins_can_install: '(Vain pääkäyttäjät voivat asentaa laajennuksia)',
admin_only: 'Pääkäyttäjille', admin_only: 'Pääkäyttäjille',
super_user_only: 'Vain superkäyttäjälle',
new_version: 'Uusi versio', new_version: 'Uusi versio',
extension_depends_on: 'Edellyttää:', extension_depends_on: 'Edellyttää:',
extension_rating_soon: 'Arvostelut on tulossa pian', extension_rating_soon: 'Arvostelut on tulossa pian',
+1
View File
@@ -145,6 +145,7 @@ window.localisation.fr = {
only_admins_can_install: only_admins_can_install:
'Seuls les comptes administrateurs peuvent installer des extensions', 'Seuls les comptes administrateurs peuvent installer des extensions',
admin_only: 'Réservé aux administrateurs', admin_only: 'Réservé aux administrateurs',
super_user_only: 'Réservé au super utilisateur',
new_version: 'Nouvelle version', new_version: 'Nouvelle version',
extension_depends_on: 'Dépend de :', extension_depends_on: 'Dépend de :',
extension_rating_soon: 'Notes des utilisateurs à venir bientôt', extension_rating_soon: 'Notes des utilisateurs à venir bientôt',
+1
View File
@@ -142,6 +142,7 @@ window.localisation.it = {
only_admins_can_install: only_admins_can_install:
'Solo gli account amministratore possono installare estensioni.', 'Solo gli account amministratore possono installare estensioni.',
admin_only: 'Solo amministratore', admin_only: 'Solo amministratore',
super_user_only: 'Solo superutente',
new_version: 'Nuova Versione', new_version: 'Nuova Versione',
extension_depends_on: 'Dipende da:', extension_depends_on: 'Dipende da:',
extension_rating_soon: 'Valutazioni in arrivo', extension_rating_soon: 'Valutazioni in arrivo',
+1
View File
@@ -137,6 +137,7 @@ window.localisation.jp = {
only_admins_can_install: only_admins_can_install:
'(管理者アカウントのみが拡張機能をインストールできます)', '(管理者アカウントのみが拡張機能をインストールできます)',
admin_only: '管理者のみ', admin_only: '管理者のみ',
super_user_only: 'スーパー ユーザーのみ',
new_version: '新しいバージョン', new_version: '新しいバージョン',
extension_depends_on: '依存先:', extension_depends_on: '依存先:',
extension_rating_soon: '評価は近日公開', extension_rating_soon: '評価は近日公開',
+1
View File
@@ -139,6 +139,7 @@ window.localisation.kr = {
all: '전체', all: '전체',
only_admins_can_install: '(관리자 계정만이 확장 기능을 설치할 수 있습니다)', only_admins_can_install: '(관리자 계정만이 확장 기능을 설치할 수 있습니다)',
admin_only: '관리자 전용', admin_only: '관리자 전용',
super_user_only: '슈퍼유저 전용',
new_version: '새로운 버전', new_version: '새로운 버전',
extension_depends_on: '의존성 존재:', extension_depends_on: '의존성 존재:',
extension_rating_soon: '평점 기능도 곧 구현됩니다', extension_rating_soon: '평점 기능도 곧 구현됩니다',
+1
View File
@@ -143,6 +143,7 @@ window.localisation.nl = {
only_admins_can_install: only_admins_can_install:
'Alleen beheerdersaccounts kunnen extensies installeren', 'Alleen beheerdersaccounts kunnen extensies installeren',
admin_only: 'Alleen beheerder', admin_only: 'Alleen beheerder',
super_user_only: 'Alleen supergebruiker',
new_version: 'Nieuwe Versie', new_version: 'Nieuwe Versie',
extension_depends_on: 'Afhankelijk van:', extension_depends_on: 'Afhankelijk van:',
extension_rating_soon: 'Beoordelingen binnenkort beschikbaar', extension_rating_soon: 'Beoordelingen binnenkort beschikbaar',
+1
View File
@@ -141,6 +141,7 @@ window.localisation.pi = {
all: 'Arr', all: 'Arr',
only_admins_can_install: '(Only admin accounts can install extensions)', only_admins_can_install: '(Only admin accounts can install extensions)',
admin_only: "Cap'n Only", admin_only: "Cap'n Only",
super_user_only: "Big Cap'n Only",
new_version: 'New Version', new_version: 'New Version',
extension_depends_on: 'Depends on:', extension_depends_on: 'Depends on:',
extension_rating_soon: "Ratings a'comin' soon", extension_rating_soon: "Ratings a'comin' soon",
+1
View File
@@ -140,6 +140,7 @@ window.localisation.pl = {
only_admins_can_install: only_admins_can_install:
'Tylko konta administratorów mogą instalować rozszerzenia', 'Tylko konta administratorów mogą instalować rozszerzenia',
admin_only: 'Tylko dla administratora', admin_only: 'Tylko dla administratora',
super_user_only: 'Tylko dla superużytkownika',
new_version: 'Nowa wersja', new_version: 'Nowa wersja',
extension_depends_on: 'Zależy od:', extension_depends_on: 'Zależy od:',
extension_rating_soon: 'Oceny będą dostępne wkrótce', extension_rating_soon: 'Oceny będą dostępne wkrótce',
+1
View File
@@ -141,6 +141,7 @@ window.localisation.pt = {
only_admins_can_install: only_admins_can_install:
'Apenas contas de administrador podem instalar extensões.', 'Apenas contas de administrador podem instalar extensões.',
admin_only: 'Apenas para administradores', admin_only: 'Apenas para administradores',
super_user_only: 'Apenas para superusuário',
new_version: 'Nova Versão', new_version: 'Nova Versão',
extension_depends_on: 'Depende de:', extension_depends_on: 'Depende de:',
extension_rating_soon: 'Avaliações em breve', extension_rating_soon: 'Avaliações em breve',
+1
View File
@@ -138,6 +138,7 @@ window.localisation.sk = {
only_admins_can_install: only_admins_can_install:
'(Iba administrátorské účty môžu inštalovať rozšírenia)', '(Iba administrátorské účty môžu inštalovať rozšírenia)',
admin_only: 'Iba pre administrátorov', admin_only: 'Iba pre administrátorov',
super_user_only: 'Iba pre superužívateľa',
new_version: 'Nová verzia', new_version: 'Nová verzia',
extension_depends_on: 'Závisí na:', extension_depends_on: 'Závisí na:',
extension_rating_soon: 'Hodnotenia budú čoskoro dostupné', extension_rating_soon: 'Hodnotenia budú čoskoro dostupné',
+1
View File
@@ -139,6 +139,7 @@ window.localisation.we = {
all: 'Pob', all: 'Pob',
only_admins_can_install: 'Dim ond cyfrifon gweinyddwr all osod estyniadau', only_admins_can_install: 'Dim ond cyfrifon gweinyddwr all osod estyniadau',
admin_only: 'Dim ond Gweinyddwr', admin_only: 'Dim ond Gweinyddwr',
super_user_only: 'Dim ond Uwchddefnyddiwr',
new_version: 'Fersiwn Newydd', new_version: 'Fersiwn Newydd',
extension_depends_on: 'Dibynnu ar:', extension_depends_on: 'Dibynnu ar:',
extension_rating_soon: 'Sgôr yn dod yn fuan', extension_rating_soon: 'Sgôr yn dod yn fuan',
+10 -2
View File
@@ -307,7 +307,13 @@
:label="$t('disable')" :label="$t('disable')"
></q-btn> ></q-btn>
<q-badge <q-badge
v-if="extension.isAdminOnly && !g.user.admin" v-if="extension.isSuperUserOnly && !g.user.super_user"
v-text="$t('super_user_only')"
>
</q-badge>
<q-badge
v-else-if="extension.isAdminOnly && !g.user.admin"
v-text="$t('admin_only')" v-text="$t('admin_only')"
> >
</q-badge> </q-badge>
@@ -316,7 +322,9 @@
v-else-if=" v-else-if="
extension.isInstalled && extension.isInstalled &&
extension.isActive && extension.isActive &&
!g.user.extensions.includes(extension.id) !g.user.extensions.includes(extension.id) &&
(!extension.isAdminOnly || g.user.admin) &&
(!extension.isSuperUserOnly || g.user.super_user)
" "
flat flat
color="primary" color="primary"
+71
View File
@@ -20,7 +20,9 @@ from lnbits.core.models.extensions import (
Extension, Extension,
ExtensionConfig, ExtensionConfig,
ExtensionRelease, ExtensionRelease,
GitHubRelease,
InstallableExtension, InstallableExtension,
Manifest,
PayToEnableInfo, PayToEnableInfo,
ReleasePaymentInfo, ReleasePaymentInfo,
UserExtensionInfo, UserExtensionInfo,
@@ -152,6 +154,8 @@ async def test_extension_api_install_details_and_release_endpoints(mocker):
short_description="Config", short_description="Config",
min_lnbits_version="0.1.0", min_lnbits_version="0.1.0",
max_lnbits_version=None, max_lnbits_version=None,
admin_only=True,
super_user_only=True,
) )
mocker.patch.object( mocker.patch.object(
ExtensionConfig, ExtensionConfig,
@@ -160,6 +164,8 @@ async def test_extension_api_install_details_and_release_endpoints(mocker):
) )
release_info = await get_extension_release("org", ext_id, "v1.0.0") release_info = await get_extension_release("org", ext_id, "v1.0.0")
assert release_info["is_version_compatible"] is True assert release_info["is_version_compatible"] is True
assert release_info["admin_only"] is True
assert release_info["super_user_only"] is True
@pytest.mark.anyio @pytest.mark.anyio
@@ -258,6 +264,8 @@ async def test_extension_api_pay_to_enable_and_catalog_views(mocker, admin_user)
catalog_entry = make_installable_extension( catalog_entry = make_installable_extension(
ext_id, ext_id,
pay_to_enable=PayToEnableInfo(required=True, amount=21, wallet=admin_wallet.id), pay_to_enable=PayToEnableInfo(required=True, amount=21, wallet=admin_wallet.id),
admin_only=True,
super_user_only=True,
) )
mocker.patch.object( mocker.patch.object(
InstallableExtension, InstallableExtension,
@@ -267,6 +275,8 @@ async def test_extension_api_pay_to_enable_and_catalog_views(mocker, admin_user)
catalog = await extensions(AccountId(id=regular_user.id)) catalog = await extensions(AccountId(id=regular_user.id))
catalog_item = next(item for item in catalog if item["id"] == ext_id) catalog_item = next(item for item in catalog if item["id"] == ext_id)
assert catalog_item["payToEnable"]["wallet"] is None assert catalog_item["payToEnable"]["wallet"] is None
assert catalog_item["isAdminOnly"] is True
assert catalog_item["isSuperUserOnly"] is True
@pytest.mark.anyio @pytest.mark.anyio
@@ -428,3 +438,64 @@ async def test_extension_api_review_endpoints(mocker):
CreateExtensionReview(tag=ext_id, name="Alice", rating=900, comment="Great") CreateExtensionReview(tag=ext_id, name="Alice", rating=900, comment="Great")
) )
assert payment_request.payment_hash.startswith("hash_") assert payment_request.payment_hash.startswith("hash_")
@pytest.mark.anyio
async def test_extension_api_enable_rejects_admin_and_super_only_extensions(
admin_user,
):
regular_user = await create_user_account(
Account(
id=uuid4().hex,
username=f"user_{uuid4().hex[:8]}",
email=f"user_{uuid4().hex[:8]}@lnbits.com",
)
)
admin_only_ext = f"admin_{uuid4().hex[:8]}"
super_only_ext = f"super_{uuid4().hex[:8]}"
await create_installed_extension(
make_installable_extension(admin_only_ext, admin_only=True)
)
await create_installed_extension(
make_installable_extension(super_only_ext, super_user_only=True)
)
with pytest.raises(HTTPException, match="User not authorized"):
await api_enable_extension(admin_only_ext, AccountId(id=regular_user.id))
with pytest.raises(HTTPException, match="User not authorized"):
await api_enable_extension(super_only_ext, AccountId(id=admin_user.id))
@pytest.mark.anyio
async def test_repo_manifest_flags_apply_to_repo_releases(mocker):
ext_id = f"repo_{uuid4().hex[:8]}"
release = make_extension_release(ext_id)
manifest = Manifest(
repos=[
GitHubRelease(
id=ext_id,
organisation="lnbits",
repository="tunnel_me_out",
admin_only=True,
super_user_only=True,
)
]
)
mocker.patch.object(
InstallableExtension,
"fetch_manifest",
mocker.AsyncMock(return_value=manifest),
)
mocker.patch.object(
ExtensionRelease,
"get_github_releases",
mocker.AsyncMock(return_value=[release]),
)
releases = await InstallableExtension.get_extension_releases(ext_id)
assert len(releases) == 1
assert releases[0].admin_only is True
assert releases[0].super_user_only is True
+6
View File
@@ -141,9 +141,13 @@ def make_installable_extension(
pay_to_enable: PayToEnableInfo | None = None, pay_to_enable: PayToEnableInfo | None = None,
dependencies: list[str] | None = None, dependencies: list[str] | None = None,
payments: list[ReleasePaymentInfo] | None = None, payments: list[ReleasePaymentInfo] | None = None,
admin_only: bool = False,
super_user_only: bool = False,
) -> InstallableExtension: ) -> InstallableExtension:
release = make_extension_release(ext_id, version) release = make_extension_release(ext_id, version)
release.is_version_compatible = compatible release.is_version_compatible = compatible
release.admin_only = admin_only
release.super_user_only = super_user_only
return InstallableExtension( return InstallableExtension(
id=ext_id, id=ext_id,
name=f"Extension {ext_id}", name=f"Extension {ext_id}",
@@ -156,6 +160,8 @@ def make_installable_extension(
pay_to_enable=pay_to_enable, pay_to_enable=pay_to_enable,
dependencies=dependencies or [], dependencies=dependencies or [],
payments=payments or [], payments=payments or [],
admin_only=admin_only,
super_user_only=super_user_only,
), ),
) )
+37
View File
@@ -8,6 +8,7 @@ from fastapi.exceptions import HTTPException
from httpx import AsyncClient from httpx import AsyncClient
from pydantic.types import UUID4 from pydantic.types import UUID4
from lnbits.core.crud.extensions import create_installed_extension
from lnbits.core.crud.users import delete_account from lnbits.core.crud.users import delete_account
from lnbits.core.models import User from lnbits.core.models import User
from lnbits.core.models.users import AccessTokenPayload from lnbits.core.models.users import AccessTokenPayload
@@ -18,10 +19,12 @@ from lnbits.decorators import (
check_extension_builder, check_extension_builder,
check_first_install, check_first_install,
check_user_exists, check_user_exists,
check_user_extension_access,
optional_user_id, optional_user_id,
) )
from lnbits.helpers import create_access_token from lnbits.helpers import create_access_token
from lnbits.settings import AuthMethods, Settings, settings from lnbits.settings import AuthMethods, Settings, settings
from tests.helpers import make_installable_extension
@pytest.mark.anyio @pytest.mark.anyio
@@ -225,3 +228,37 @@ async def test_check_extension_builder_requires_admin_when_disabled_for_users(
admin_user = user_alan.copy(deep=True) admin_user = user_alan.copy(deep=True)
admin_user.admin = True admin_user.admin = True
await check_extension_builder(admin_user) await check_extension_builder(admin_user)
@pytest.mark.anyio
async def test_check_user_extension_access_honors_extension_metadata(
settings: Settings, user_alan: User, admin_user: User
):
admin_only_ext = f"admin_{uuid4().hex[:8]}"
super_only_ext = f"super_{uuid4().hex[:8]}"
await create_installed_extension(
make_installable_extension(admin_only_ext, admin_only=True)
)
await create_installed_extension(
make_installable_extension(super_only_ext, super_user_only=True)
)
regular_status = await check_user_extension_access(user_alan.id, admin_only_ext)
assert regular_status.success is False
admin_status = await check_user_extension_access(admin_user.id, admin_only_ext)
assert admin_status.success is True
admin_super_status = await check_user_extension_access(
admin_user.id, super_only_ext
)
assert admin_super_status.success is False
previous_super_user = settings.super_user
settings.super_user = admin_user.id
try:
super_status = await check_user_extension_access(admin_user.id, super_only_ext)
assert super_status.success is True
finally:
settings.super_user = previous_super_user