diff --git a/.env.example b/.env.example index 1c478cd99..060748a97 100644 --- a/.env.example +++ b/.env.example @@ -5,11 +5,15 @@ QUART_DEBUG=true HOST=127.0.0.1 PORT=5000 - LNBITS_ALLOWED_USERS="" LNBITS_ADMIN_USERS="" +# Extensions only admin can access +LNBITS_ADMIN_EXTENSIONS="ngrok" LNBITS_DEFAULT_WALLET_NAME="LNbits wallet" +# Disable extensions for all users, use "all" to disable all extensions +LNBITS_DISABLED_EXTENSIONS="amilk" + # Database: to use SQLite, specify LNBITS_DATA_FOLDER # to use PostgreSQL, specify LNBITS_DATABASE_URL=postgres://... # to use CockroachDB, specify LNBITS_DATABASE_URL=cockroachdb://... @@ -18,8 +22,6 @@ LNBITS_DEFAULT_WALLET_NAME="LNbits wallet" LNBITS_DATA_FOLDER="./data" # LNBITS_DATABASE_URL="postgres://user:password@host:port/databasename" -# disable selected extensions, or use "all" to disable all extensions -LNBITS_DISABLED_EXTENSIONS="amilk,ngrok" LNBITS_FORCE_HTTPS=true LNBITS_SERVICE_FEE="0.0" diff --git a/lnbits/decorators.py b/lnbits/decorators.py index fc92594ed..9eee1afae 100644 --- a/lnbits/decorators.py +++ b/lnbits/decorators.py @@ -13,7 +13,7 @@ from starlette.requests import Request from lnbits.core.crud import get_user, get_wallet_for_key from lnbits.core.models import User, Wallet from lnbits.requestvars import g -from lnbits.settings import LNBITS_ALLOWED_USERS, LNBITS_ADMIN_USERS +from lnbits.settings import LNBITS_ALLOWED_USERS, LNBITS_ADMIN_USERS, LNBITS_ADMIN_EXTENSIONS class KeyChecker(SecurityBase): @@ -122,6 +122,7 @@ async def get_key_type( # 0: admin # 1: invoice # 2: invalid + pathname = r['path'].split('/')[1] if not api_key_header and not api_key_query: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST) @@ -131,7 +132,10 @@ async def get_key_type( try: checker = WalletAdminKeyChecker(api_key=token) await checker.__call__(r) - return WalletTypeInfo(0, checker.wallet) + wallet = WalletTypeInfo(0, checker.wallet) + if (LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS) and (LNBITS_ADMIN_EXTENSIONS and pathname in LNBITS_ADMIN_EXTENSIONS): + raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized.") + return wallet except HTTPException as e: if e.status_code == HTTPStatus.BAD_REQUEST: raise @@ -143,7 +147,10 @@ async def get_key_type( try: checker = WalletInvoiceKeyChecker(api_key=token) await checker.__call__(r) - return WalletTypeInfo(1, checker.wallet) + wallet = WalletTypeInfo(0, checker.wallet) + if (LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS) and (LNBITS_ADMIN_EXTENSIONS and pathname in LNBITS_ADMIN_EXTENSIONS): + raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized.") + return wallet except HTTPException as e: if e.status_code == HTTPStatus.BAD_REQUEST: raise diff --git a/lnbits/helpers.py b/lnbits/helpers.py index dfd9d53de..d26163759 100644 --- a/lnbits/helpers.py +++ b/lnbits/helpers.py @@ -15,6 +15,7 @@ import lnbits.settings as settings class Extension(NamedTuple): code: str is_valid: bool + is_admin_only: bool name: Optional[str] = None short_description: Optional[str] = None icon: Optional[str] = None @@ -25,6 +26,7 @@ class Extension(NamedTuple): class ExtensionManager: def __init__(self): self._disabled: List[str] = settings.LNBITS_DISABLED_EXTENSIONS + self._admin_only: List[str] = [x.strip(' ') for x in settings.LNBITS_ADMIN_EXTENSIONS] self._extension_folders: List[str] = [ x[1] for x in os.walk(os.path.join(settings.LNBITS_PATH, "extensions")) ][0] @@ -47,14 +49,17 @@ class ExtensionManager: ) as json_file: config = json.load(json_file) is_valid = True + is_admin_only = True if extension in self._admin_only else False except Exception: config = {} is_valid = False + is_admin_only = False output.append( Extension( extension, is_valid, + is_admin_only, config.get("name"), config.get("short_description"), config.get("icon"), diff --git a/lnbits/settings.py b/lnbits/settings.py index b204e28f1..26699bc02 100644 --- a/lnbits/settings.py +++ b/lnbits/settings.py @@ -29,6 +29,7 @@ LNBITS_ALLOWED_USERS: List[str] = env.list( "LNBITS_ALLOWED_USERS", default=[], subcast=str ) LNBITS_ADMIN_USERS: List[str] = env.list("LNBITS_ADMIN_USERS", default=[], subcast=str) +LNBITS_ADMIN_EXTENSIONS: List[str] = env.list("LNBITS_ADMIN_EXTENSIONS", default=[], subcast=str) LNBITS_DISABLED_EXTENSIONS: List[str] = env.list( "LNBITS_DISABLED_EXTENSIONS", default=[], subcast=str ) diff --git a/lnbits/static/js/base.js b/lnbits/static/js/base.js index 13f683882..d49eec16f 100644 --- a/lnbits/static/js/base.js +++ b/lnbits/static/js/base.js @@ -111,7 +111,7 @@ window.LNbits = { '/wallet?' + (userId ? 'usr=' + userId + '&' : '') + 'nme=' + walletName }, updateWallet: function (walletName, userId, walletId) { - window.location.href = `/wallet?usr=${userId}&wal=${walletId}&nme=${walletName}` + window.location.href = `/wallet?usr=${userId}&wal=${walletId}&nme=${walletName}` }, deleteWallet: function (walletId, userId) { window.location.href = '/deletewallet?usr=' + userId + '&wal=' + walletId @@ -123,6 +123,7 @@ window.LNbits = { [ 'code', 'isValid', + 'isAdminOnly', 'name', 'shortDescription', 'icon', @@ -135,7 +136,12 @@ window.LNbits = { return obj }, user: function (data) { - var obj = {id: data.id, email: data.email, extensions: data.extensions, wallets: data.wallets} + var obj = { + id: data.id, + email: data.email, + extensions: data.extensions, + wallets: data.wallets + } var mapWallet = this.wallet obj.wallets = obj.wallets .map(function (obj) { @@ -153,16 +159,23 @@ window.LNbits = { return obj }, wallet: function (data) { - newWallet = {id: data.id, name: data.name, adminkey: data.adminkey, inkey: data.inkey} + newWallet = { + id: data.id, + name: data.name, + adminkey: data.adminkey, + inkey: data.inkey + } newWallet.msat = data.balance_msat newWallet.sat = Math.round(data.balance_msat / 1000) - newWallet.fsat = new Intl.NumberFormat(window.LOCALE).format(newWallet.sat) + newWallet.fsat = new Intl.NumberFormat(window.LOCALE).format( + newWallet.sat + ) newWallet.url = ['/wallet?usr=', data.user, '&wal=', data.id].join('') return newWallet }, payment: function (data) { obj = { - checking_id:data.id, + checking_id: data.id, pending: data.pending, amount: data.amount, fee: data.fee, @@ -174,8 +187,8 @@ window.LNbits = { extra: data.extra, wallet_id: data.wallet_id, webhook: data.webhook, - webhook_status: data.webhook_status, - } + webhook_status: data.webhook_status + } obj.date = Quasar.utils.date.formatDate( new Date(obj.time * 1000), @@ -225,7 +238,8 @@ window.LNbits = { Quasar.plugins.Notify.create({ timeout: 5000, type: types[error.response.status] || 'warning', - message: error.response.data.message || error.response.data.detail || null, + message: + error.response.data.message || error.response.data.detail || null, caption: [error.response.status, ' ', error.response.statusText] .join('') @@ -368,6 +382,10 @@ window.windowMixin = { .filter(function (obj) { return !obj.hidden }) + .filter(function (obj) { + if (window.user.admin) return obj + return !obj.isAdminOnly + }) .map(function (obj) { if (user) { obj.isEnabled = user.extensions.indexOf(obj.code) !== -1