* chore: update linting workflows to include dev branch in merge_group * Update README.md Add 1.0 banner * Update README.md * chore: update package versions in pyproject.toml files * Refactor "created_at" column type for consistency and fix cancel middleware (#2316) * chore: update linting workflows to include dev branch in merge_group * Update README.md Add 1.0 banner * Update README.md * chore: update package versions in pyproject.toml files * refactor: update "created_at" column type to use the "sa" module for consistency * Update README.md Add 1.0 banner * chore: Remove unused import in ToolCallingAgent.py * fix: adapt RequestCancelledMiddleware to handle cancelled requests * chore: Remove unused import in test_helper_components.py * refactor: Declare queue variable with explicit type in RequestCancelledMiddleware --------- Co-authored-by: Rodrigo Nader <rodrigosilvanader@gmail.com> * chore: Update AstraDB.py imports and method signature for search_documents * chore: Update package versions in pyproject.toml files * chore: Update run-name in release.yml for Langflow Release * fix: add call to _add_documents_to_vector_store in AstraDB component * chore: Fix missing parentheses in RequestCancelledMiddleware * chore: Update pydantic-settings and tenacity versions The commit updates the versions of the `pydantic-settings` and `tenacity` packages in the `poetry.lock` file. The `pydantic-settings` version is updated from 2.3.3 to 2.3.4, and the `tenacity` version is updated from 8.4.1 to 8.4.2. * Update README.md Add 1.0 banner * fix fetch data to work even with autologin true * format code * deactivate stop button until we have a better solution (#2337) * consistent auth error status code * [Fix] unhandled http errors in background tasks (#2326) * handle exceptions for background task * revert changes that is not related to this HTTP handler exception * Refactor model GoogleGenerativeAIModel (#2251) * refactor model GoogleGenerativeAIModel * adds model options --------- Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org> * Fix .env values not being honored in CLI (#2336) * chore: Update launch.json to include environment file The launch.json file was updated to include the environment file path for the "run" command in the "Python: Flask" configuration. This change ensures that the necessary environment variables are loaded when running the backend base of Langflow frontend. The previous configuration had the environment variables set in the "env" field, but it has been removed as it is redundant with the new environment file inclusion. * chore: Update dotenv import and environment variable handling This commit updates the import statement for the `dotenv` module in the `__main__.py` file. It adds the `dotenv_values` function to the import statement to enable loading environment variables from a file. Additionally, it introduces a new section of code that maps environment variables to their corresponding variables and types, allowing for more flexible and dynamic configuration. The commit also updates the `run` function to update variables based on environment variables, if they are present. This change improves the handling of environment variables and enhances the configurability of the application. * deactivate stop button until we have a better solution (#2337) * consistent auth error status code * [Fix] unhandled http errors in background tasks (#2326) * handle exceptions for background task * revert changes that is not related to this HTTP handler exception * Refactor model GoogleGenerativeAIModel (#2251) * refactor model GoogleGenerativeAIModel * adds model options --------- Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org> --------- Co-authored-by: ming luo <itestmycode@gmail.com> Co-authored-by: Ítalo Johnny <italojohnnydosanjos@gmail.com> * Update GitHub Actions workflows and dependencies (#2341) * chore: Add GitHub Actions workflow for testing documentation build * Fix server start command in GitHub Actions workflows * chore: Bump langflow and langflow-base versions * chore: Update GitHub Actions workflow for docs_test * chore: Update typing import in __main__.py * Fix user authentication and authorization issues (#2343) --------- Co-authored-by: Rodrigo Nader <rodrigosilvanader@gmail.com> Co-authored-by: anovazzi1 <otavio2204@gmail.com> Co-authored-by: ming luo <itestmycode@gmail.com> Co-authored-by: Ítalo Johnny <italojohnnydosanjos@gmail.com>
232 lines
8.5 KiB
Python
232 lines
8.5 KiB
Python
from datetime import datetime
|
|
|
|
import pytest
|
|
|
|
from langflow.services.auth.utils import create_super_user, get_password_hash
|
|
from langflow.services.database.models.user import UserUpdate
|
|
from langflow.services.database.models.user.model import User
|
|
from langflow.services.database.utils import session_getter
|
|
from langflow.services.deps import get_db_service, get_settings_service
|
|
|
|
|
|
@pytest.fixture
|
|
def super_user(client):
|
|
settings_manager = get_settings_service()
|
|
auth_settings = settings_manager.auth_settings
|
|
with session_getter(get_db_service()) as session:
|
|
return create_super_user(
|
|
db=session,
|
|
username=auth_settings.SUPERUSER,
|
|
password=auth_settings.SUPERUSER_PASSWORD,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def super_user_headers(client, super_user):
|
|
settings_service = get_settings_service()
|
|
auth_settings = settings_service.auth_settings
|
|
login_data = {
|
|
"username": auth_settings.SUPERUSER,
|
|
"password": auth_settings.SUPERUSER_PASSWORD,
|
|
}
|
|
response = client.post("/api/v1/login", data=login_data)
|
|
assert response.status_code == 200
|
|
tokens = response.json()
|
|
a_token = tokens["access_token"]
|
|
return {"Authorization": f"Bearer {a_token}"}
|
|
|
|
|
|
@pytest.fixture
|
|
def deactivated_user():
|
|
with session_getter(get_db_service()) as session:
|
|
user = User(
|
|
username="deactivateduser",
|
|
password=get_password_hash("testpassword"),
|
|
is_active=False,
|
|
is_superuser=False,
|
|
last_login_at=datetime.now(),
|
|
)
|
|
session.add(user)
|
|
session.commit()
|
|
session.refresh(user)
|
|
return user
|
|
|
|
|
|
def test_user_waiting_for_approval(
|
|
client,
|
|
):
|
|
# Create a user that is not active and has never logged in
|
|
with session_getter(get_db_service()) as session:
|
|
user = User(
|
|
username="waitingforapproval",
|
|
password=get_password_hash("testpassword"),
|
|
is_active=False,
|
|
last_login_at=None,
|
|
)
|
|
session.add(user)
|
|
session.commit()
|
|
|
|
login_data = {"username": "waitingforapproval", "password": "testpassword"}
|
|
response = client.post("/api/v1/login", data=login_data)
|
|
assert response.status_code == 400
|
|
assert response.json()["detail"] == "Waiting for approval"
|
|
|
|
|
|
def test_deactivated_user_cannot_login(client, deactivated_user):
|
|
login_data = {"username": deactivated_user.username, "password": "testpassword"}
|
|
response = client.post("/api/v1/login", data=login_data)
|
|
assert response.status_code == 401, response.json()
|
|
assert response.json()["detail"] == "Inactive user", response.text
|
|
|
|
|
|
def test_deactivated_user_cannot_access(client, deactivated_user, logged_in_headers):
|
|
# Assuming the headers for deactivated_user
|
|
response = client.get("/api/v1/users", headers=logged_in_headers)
|
|
assert response.status_code == 403, response.json()
|
|
assert response.json()["detail"] == "The user doesn't have enough privileges", response.text
|
|
|
|
|
|
def test_data_consistency_after_update(client, active_user, logged_in_headers, super_user_headers):
|
|
user_id = active_user.id
|
|
update_data = UserUpdate(is_active=False)
|
|
|
|
response = client.patch(f"/api/v1/users/{user_id}", json=update_data.model_dump(), headers=super_user_headers)
|
|
assert response.status_code == 200, response.json()
|
|
|
|
# Fetch the updated user from the database
|
|
response = client.get("/api/v1/users/whoami", headers=logged_in_headers)
|
|
assert response.status_code == 401, response.json()
|
|
assert response.json()["detail"] == "User not found or is inactive."
|
|
|
|
|
|
def test_data_consistency_after_delete(client, test_user, super_user_headers):
|
|
user_id = test_user.get("id")
|
|
response = client.delete(f"/api/v1/users/{user_id}", headers=super_user_headers)
|
|
assert response.status_code == 200, response.json()
|
|
|
|
# Attempt to fetch the deleted user from the database
|
|
response = client.get("/api/v1/users", headers=super_user_headers)
|
|
assert response.status_code == 200
|
|
assert all(user["id"] != user_id for user in response.json()["users"])
|
|
|
|
|
|
def test_inactive_user(client):
|
|
# Create a user that is not active and has a last_login_at value
|
|
with session_getter(get_db_service()) as session:
|
|
user = User(
|
|
username="inactiveuser",
|
|
password=get_password_hash("testpassword"),
|
|
is_active=False,
|
|
last_login_at=datetime(2023, 1, 1, 0, 0, 0),
|
|
)
|
|
session.add(user)
|
|
session.commit()
|
|
|
|
login_data = {"username": "inactiveuser", "password": "testpassword"}
|
|
response = client.post("/api/v1/login", data=login_data)
|
|
assert response.status_code == 401
|
|
assert response.json()["detail"] == "Inactive user"
|
|
|
|
|
|
def test_add_user(client, test_user):
|
|
assert test_user["username"] == "testuser"
|
|
|
|
|
|
# This is not used in the Frontend at the moment
|
|
# def test_read_current_user(client: TestClient, active_user):
|
|
# # First we need to login to get the access token
|
|
# login_data = {"username": "testuser", "password": "testpassword"}
|
|
# response = client.post("/api/v1/login", data=login_data)
|
|
# assert response.status_code == 200
|
|
|
|
# headers = {"Authorization": f"Bearer {response.json()['access_token']}"}
|
|
|
|
# response = client.get("/api/v1/user", headers=headers)
|
|
# assert response.status_code == 200, response.json()
|
|
# assert response.json()["username"] == "testuser"
|
|
|
|
|
|
def test_read_all_users(client, super_user_headers):
|
|
response = client.get("/api/v1/users", headers=super_user_headers)
|
|
assert response.status_code == 200, response.json()
|
|
assert isinstance(response.json()["users"], list)
|
|
|
|
|
|
def test_normal_user_cant_read_all_users(client, logged_in_headers):
|
|
response = client.get("/api/v1/users", headers=logged_in_headers)
|
|
assert response.status_code == 403, response.json()
|
|
assert response.json() == {"detail": "The user doesn't have enough privileges"}
|
|
|
|
|
|
def test_patch_user(client, active_user, logged_in_headers):
|
|
user_id = active_user.id
|
|
update_data = UserUpdate(
|
|
username="newname",
|
|
)
|
|
|
|
response = client.patch(f"/api/v1/users/{user_id}", json=update_data.model_dump(), headers=logged_in_headers)
|
|
assert response.status_code == 200, response.json()
|
|
update_data = UserUpdate(
|
|
profile_image="new_image",
|
|
)
|
|
|
|
response = client.patch(f"/api/v1/users/{user_id}", json=update_data.model_dump(), headers=logged_in_headers)
|
|
assert response.status_code == 200, response.json()
|
|
|
|
|
|
def test_patch_reset_password(client, active_user, logged_in_headers):
|
|
user_id = active_user.id
|
|
update_data = UserUpdate(
|
|
password="newpassword",
|
|
)
|
|
|
|
response = client.patch(
|
|
f"/api/v1/users/{user_id}/reset-password",
|
|
json=update_data.model_dump(),
|
|
headers=logged_in_headers,
|
|
)
|
|
assert response.status_code == 200, response.json()
|
|
# Now we need to test if the new password works
|
|
login_data = {"username": active_user.username, "password": "newpassword"}
|
|
response = client.post("/api/v1/login", data=login_data)
|
|
assert response.status_code == 200
|
|
|
|
|
|
def test_patch_user_wrong_id(client, active_user, logged_in_headers):
|
|
user_id = "wrong_id"
|
|
update_data = UserUpdate(
|
|
username="newname",
|
|
)
|
|
|
|
response = client.patch(f"/api/v1/users/{user_id}", json=update_data.model_dump(), headers=logged_in_headers)
|
|
assert response.status_code == 422, response.json()
|
|
json_response = response.json()
|
|
detail = json_response["detail"]
|
|
error = detail[0]
|
|
assert error["loc"] == ["path", "user_id"]
|
|
assert error["type"] == "uuid_parsing"
|
|
|
|
|
|
def test_delete_user(client, test_user, super_user_headers):
|
|
user_id = test_user["id"]
|
|
response = client.delete(f"/api/v1/users/{user_id}", headers=super_user_headers)
|
|
assert response.status_code == 200
|
|
assert response.json() == {"detail": "User deleted"}
|
|
|
|
|
|
def test_delete_user_wrong_id(client, test_user, super_user_headers):
|
|
user_id = "wrong_id"
|
|
response = client.delete(f"/api/v1/users/{user_id}", headers=super_user_headers)
|
|
assert response.status_code == 422
|
|
json_response = response.json()
|
|
detail = json_response["detail"]
|
|
error = detail[0]
|
|
assert error["loc"] == ["path", "user_id"]
|
|
assert error["type"] == "uuid_parsing"
|
|
|
|
|
|
def test_normal_user_cant_delete_user(client, test_user, logged_in_headers):
|
|
user_id = test_user["id"]
|
|
response = client.delete(f"/api/v1/users/{user_id}", headers=logged_in_headers)
|
|
assert response.status_code == 403
|
|
assert response.json() == {"detail": "The user doesn't have enough privileges"}
|