[feat] access control lists (with access tokens) (#2864)

This commit is contained in:
Vlad Stan 2025-01-16 17:25:27 +02:00 committed by GitHub
parent f415a92914
commit b164317121
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 2131 additions and 67 deletions

View file

@ -0,0 +1,138 @@
from uuid import uuid4
import jwt
import pytest
import shortuuid
from fastapi import Request
from fastapi.exceptions import HTTPException
from httpx import AsyncClient
from pydantic.types import UUID4
from lnbits.core.crud.users import delete_account
from lnbits.core.models import User
from lnbits.core.models.users import AccessTokenPayload
from lnbits.decorators import check_user_exists
from lnbits.settings import AuthMethods, Settings, settings
@pytest.mark.anyio
async def test_check_user_exists_with_valid_access_token(
http_client: AsyncClient, user_alan: User
):
# Login to get a valid access token
response = await http_client.post(
"/api/v1/auth", json={"username": user_alan.username, "password": "secret1234"}
)
assert response.status_code == 200, "Alan logs in OK"
access_token = response.json()["access_token"]
assert access_token is not None
request = Request({"type": "http", "path": "/some/path", "method": "GET"})
user = await check_user_exists(request, access_token=access_token)
assert user.id == user_alan.id
assert request.scope["user_id"] == user.id
@pytest.mark.anyio
async def test_check_user_exists_with_invalid_access_token():
request = Request({"type": "http", "path": "/some/path", "method": "GET"})
with pytest.raises(HTTPException) as exc_info:
await check_user_exists(request, access_token="invalid_token")
assert exc_info.value.status_code == 401
assert exc_info.value.detail == "Invalid access token."
@pytest.mark.anyio
async def test_check_user_exists_with_missing_access_token():
request = Request({"type": "http", "path": "/some/path", "method": "GET"})
with pytest.raises(HTTPException) as exc_info:
await check_user_exists(request, access_token=None)
assert exc_info.value.status_code == 401
assert exc_info.value.detail == "Missing user ID or access token."
@pytest.mark.anyio
async def test_check_user_exists_with_valid_user_id(user_alan: User):
request = Request({"type": "http", "path": "/some/path", "method": "GET"})
user = await check_user_exists(request, access_token=None, usr=UUID4(user_alan.id))
assert user.id == user_alan.id
@pytest.mark.anyio
async def test_check_user_exists_with_invalid_user_id():
request = Request({"type": "http", "path": "/some/path", "method": "GET"})
with pytest.raises(HTTPException) as exc_info:
await check_user_exists(request, access_token=None, usr=uuid4())
assert exc_info.value.status_code == 401
assert exc_info.value.detail == "User not found."
@pytest.mark.anyio
async def test_check_user_exists_with_user_not_allowed(user_alan: User):
settings.lnbits_admin_users = []
request = Request({"type": "http", "path": "/some/path", "method": "GET"})
settings.lnbits_allowed_users = ["only_this_user_id"]
with pytest.raises(HTTPException) as exc_info:
await check_user_exists(request, access_token=None, usr=UUID4(user_alan.id))
assert exc_info.value.status_code == 401
assert exc_info.value.detail == "User not allowed."
@pytest.mark.anyio
async def test_check_user_exists_after_user_deletion(http_client: AsyncClient):
# Register a new user
tiny_id = shortuuid.uuid()[:8]
register_response = await http_client.post(
"/api/v1/auth/register",
json={
"username": f"u21.{tiny_id}",
"password": "secret1234",
"password_repeat": "secret1234",
"email": f"u21.{tiny_id}@lnbits.com",
},
)
assert register_response.status_code == 200, "User registers OK"
access_token = register_response.json()["access_token"]
assert access_token is not None
payload: dict = jwt.decode(access_token, settings.auth_secret_key, ["HS256"])
access_token_payload = AccessTokenPayload(**payload)
# Get the user ID
user_id = access_token_payload.usr
assert user_id, "User ID is not None"
# Delete the user
await delete_account(user_id)
# Attempt to check user existence with the deleted user's access token
request = Request({"type": "http", "path": "/some/path", "method": "GET"})
with pytest.raises(HTTPException) as exc_info:
await check_user_exists(request, access_token=access_token)
assert exc_info.value.status_code == 401
assert exc_info.value.detail == "User not found."
@pytest.mark.anyio
async def test_check_user_exists_with_user_id_only_allowed(
user_alan: User, settings: Settings
):
settings.auth_allowed_methods = [AuthMethods.user_id_only.value]
request = Request({"type": "http", "path": "/some/path", "method": "GET"})
user = await check_user_exists(request, access_token=None, usr=UUID4(user_alan.id))
assert user.id == user_alan.id
assert request.scope["user_id"] == user.id
@pytest.mark.anyio
async def test_check_user_exists_with_user_id_only_not_allowed(user_alan: User):
settings.auth_allowed_methods = []
request = Request({"type": "http", "path": "/some/path", "method": "GET"})
with pytest.raises(HTTPException) as exc_info:
await check_user_exists(request, access_token=None, usr=UUID4(user_alan.id))
assert exc_info.value.status_code == 401
assert exc_info.value.detail == "Missing user ID or access token."