mirror of
https://github.com/langflow-ai/langflow.git
synced 2026-07-23 20:22:46 +08:00
fix: remove hardcoded default superuser password (#13822)
* fix: remove hardcoded default superuser password * fix: harden legacy superuser credential handling * fix: update superuser CI paths * [autofix.ci] apply automated fixes * fix: have images default auto_login to false have images default auto_login to false * fix: reject legacy superuser password * fix: align docker auth setup docs * docs: fix custom docker auth examples * docs: include auth env in docker upgrade examples --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Adam Aghili <Adam.Aghili@ibm.com>
This commit is contained in:
@ -98,8 +98,8 @@ LANGFLOW_REMOVE_API_KEYS=
|
||||
LANGFLOW_CACHE_TYPE=
|
||||
|
||||
# Set LANGFLOW_AUTO_LOGIN to false if you want to disable auto login
|
||||
# and use the login form to login. LANGFLOW_SUPERUSER and LANGFLOW_SUPERUSER_PASSWORD
|
||||
# must be set if AUTO_LOGIN is set to false
|
||||
# and use the login form to login. LANGFLOW_SUPERUSER_PASSWORD
|
||||
# must be set if AUTO_LOGIN is set to false. LANGFLOW_SUPERUSER is optional.
|
||||
# Values: true, false
|
||||
LANGFLOW_AUTO_LOGIN=
|
||||
|
||||
|
||||
@ -201,7 +201,7 @@
|
||||
"filename": "docker_example/docker-compose.yml",
|
||||
"hashed_secret": "e80c4f90316c87b6b24d03890493c8d1c7c1c99d",
|
||||
"is_verified": false,
|
||||
"line_number": 24,
|
||||
"line_number": 25,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
@ -2837,7 +2837,7 @@
|
||||
"filename": "src/backend/tests/conftest.py",
|
||||
"hashed_secret": "8bb6118f8fd6935ad0876a3be34a717d32708ffd",
|
||||
"is_verified": false,
|
||||
"line_number": 515,
|
||||
"line_number": 519,
|
||||
"is_secret": false
|
||||
},
|
||||
{
|
||||
@ -2845,7 +2845,7 @@
|
||||
"filename": "src/backend/tests/conftest.py",
|
||||
"hashed_secret": "61fbb5a12cd7b1f1fe1624120089efc0cd299e43",
|
||||
"is_verified": false,
|
||||
"line_number": 725,
|
||||
"line_number": 729,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
@ -2939,16 +2939,6 @@
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"src/backend/tests/locust/langflow_setup_test.py": [
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": "src/backend/tests/locust/langflow_setup_test.py",
|
||||
"hashed_secret": "e80c4f90316c87b6b24d03890493c8d1c7c1c99d",
|
||||
"is_verified": false,
|
||||
"line_number": 205,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"src/backend/tests/unit/agentic/flows/test_langflow_assistant.py": [
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
@ -3943,7 +3933,7 @@
|
||||
"filename": "src/backend/tests/unit/test_auth_settings.py",
|
||||
"hashed_secret": "0352a8acc949c7df21fec16e566ba9a74e797a97",
|
||||
"is_verified": false,
|
||||
"line_number": 65,
|
||||
"line_number": 71,
|
||||
"is_secret": false
|
||||
},
|
||||
{
|
||||
@ -3951,7 +3941,7 @@
|
||||
"filename": "src/backend/tests/unit/test_auth_settings.py",
|
||||
"hashed_secret": "920f8f5815b381ea692e9e7c2f7119f2b1aa620a",
|
||||
"is_verified": false,
|
||||
"line_number": 77,
|
||||
"line_number": 83,
|
||||
"is_secret": false
|
||||
},
|
||||
{
|
||||
@ -3959,7 +3949,7 @@
|
||||
"filename": "src/backend/tests/unit/test_auth_settings.py",
|
||||
"hashed_secret": "81f344a7686a80b4c5293e8fdc0b0160c82c06a8",
|
||||
"is_verified": false,
|
||||
"line_number": 83,
|
||||
"line_number": 89,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
@ -4044,7 +4034,7 @@
|
||||
"filename": "src/backend/tests/unit/test_setup_superuser_flow.py",
|
||||
"hashed_secret": "6b7065a54fddbb64d6fbe6c4ce55cc668e8a3511",
|
||||
"is_verified": false,
|
||||
"line_number": 192,
|
||||
"line_number": 348,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
@ -6267,7 +6257,7 @@
|
||||
"filename": "src/frontend/tests/core/features/user-flow-state-cleanup.spec.ts",
|
||||
"hashed_secret": "66904568acfa1e59b8b072cbe602e0ba3688019c",
|
||||
"is_verified": false,
|
||||
"line_number": 41,
|
||||
"line_number": 46,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
@ -9287,5 +9277,5 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"generated_at": "2026-06-24T23:54:39Z"
|
||||
"generated_at": "2026-06-25T20:04:57Z"
|
||||
}
|
||||
|
||||
@ -132,5 +132,6 @@ WORKDIR /app
|
||||
|
||||
ENV LANGFLOW_HOST=0.0.0.0
|
||||
ENV LANGFLOW_PORT=7860
|
||||
ENV LANGFLOW_AUTO_LOGIN=false
|
||||
|
||||
CMD ["langflow", "run"]
|
||||
|
||||
@ -111,5 +111,6 @@ WORKDIR /app
|
||||
|
||||
ENV LANGFLOW_HOST=0.0.0.0
|
||||
ENV LANGFLOW_PORT=7860
|
||||
ENV LANGFLOW_AUTO_LOGIN=false
|
||||
|
||||
CMD ["python", "-m", "langflow", "run", "--backend-only"]
|
||||
|
||||
@ -134,5 +134,6 @@ WORKDIR /app
|
||||
|
||||
ENV LANGFLOW_HOST=0.0.0.0
|
||||
ENV LANGFLOW_PORT=7860
|
||||
ENV LANGFLOW_AUTO_LOGIN=false
|
||||
|
||||
CMD ["langflow-base", "run"]
|
||||
|
||||
@ -121,6 +121,7 @@ WORKDIR /app
|
||||
ENV LANGFLOW_HOST=0.0.0.0
|
||||
ENV LANGFLOW_PORT=7860
|
||||
ENV LANGFLOW_EVENT_DELIVERY=polling
|
||||
ENV LANGFLOW_AUTO_LOGIN=false
|
||||
|
||||
USER 1000
|
||||
CMD ["python", "-m", "langflow", "run", "--host", "0.0.0.0", "--backend-only"]
|
||||
|
||||
@ -122,5 +122,6 @@ WORKDIR /app
|
||||
|
||||
ENV LANGFLOW_HOST=0.0.0.0
|
||||
ENV LANGFLOW_PORT=7860
|
||||
ENV LANGFLOW_AUTO_LOGIN=false
|
||||
|
||||
CMD ["langflow", "run"]
|
||||
|
||||
@ -21,7 +21,15 @@ This guide will help you get LangFlow up and running using Docker and Docker Com
|
||||
cd langflow/docker_example
|
||||
```
|
||||
|
||||
3. Run the Docker Compose file:
|
||||
3. Create a `.env` file with the LangFlow admin password:
|
||||
|
||||
```sh
|
||||
LANGFLOW_SUPERUSER_PASSWORD=replace-with-a-strong-password
|
||||
```
|
||||
|
||||
The default admin username is `langflow`.
|
||||
|
||||
4. Run the Docker Compose file:
|
||||
|
||||
```sh
|
||||
docker compose up
|
||||
@ -40,6 +48,7 @@ The `langflow` service uses the `langflowai/langflow:latest` Docker image and ex
|
||||
Environment variables:
|
||||
|
||||
- `LANGFLOW_DATABASE_URL`: The connection string for the PostgreSQL database.
|
||||
- `LANGFLOW_SUPERUSER_PASSWORD`: The initial admin password. This value is required in `.env`.
|
||||
- `LANGFLOW_CONFIG_DIR`: The directory where LangFlow stores logs, file storage, monitor data, and secret keys.
|
||||
|
||||
Volumes:
|
||||
|
||||
@ -8,6 +8,7 @@ services:
|
||||
- postgres
|
||||
environment:
|
||||
- LANGFLOW_DATABASE_URL=postgresql://langflow:langflow@postgres:5432/langflow
|
||||
- LANGFLOW_SUPERUSER_PASSWORD=${LANGFLOW_SUPERUSER_PASSWORD:?set LANGFLOW_SUPERUSER_PASSWORD in .env}
|
||||
# This variable defines where the logs, file storage, monitor data and secret keys are stored.
|
||||
- LANGFLOW_CONFIG_DIR=/app/langflow
|
||||
volumes:
|
||||
|
||||
@ -11,7 +11,7 @@ who = requests.get(f"{base}/api/v1/users/whoami", headers=headers, timeout=30)
|
||||
who.raise_for_status()
|
||||
user_id = who.json()["id"]
|
||||
|
||||
# Must differ from the current password (default superuser is often langflow/langflow).
|
||||
# Must differ from the current password.
|
||||
payload = {"password": "DocsExampleResetPass2025!"}
|
||||
|
||||
response = requests.patch(
|
||||
|
||||
@ -20,17 +20,17 @@ This option provides more control over the configuration, including a persistent
|
||||
* [Create a custom Langflow image](#customize-the-langflow-docker-image): Use a Dockerfile to package a custom Langflow Docker image that includes your own code, custom dependencies, or other modifications.
|
||||
* [Upgrade the Langflow Docker image](#upgrade-the-langflow-docker-image): Upgrade to a newer image without losing your database or flows by using persistent volumes and replacing only the container.
|
||||
|
||||
## Quickstart: Start a Langflow container with default values {#quickstart}
|
||||
## Quickstart: Start a local Langflow container {#quickstart}
|
||||
|
||||
With Docker installed and running on your system, run the following command:
|
||||
|
||||
```shell
|
||||
docker run -p 7860:7860 langflowai/langflow:latest
|
||||
docker run -p 7860:7860 -e LANGFLOW_AUTO_LOGIN=true langflowai/langflow:latest
|
||||
```
|
||||
|
||||
Then, access Langflow at `http://localhost:7860/`.
|
||||
|
||||
This container runs a pre-built Docker image with default settings.
|
||||
This starts a pre-built Docker image with automatic login enabled for local development.
|
||||
For more control over the configuration, see [Clone the repo and run the Langflow Docker container](#clone).
|
||||
|
||||
## Clone the repo and run the Langflow Docker container {#clone}
|
||||
@ -57,13 +57,21 @@ The complete Docker Compose configuration is available in `docker_example/docker
|
||||
cd langflow/docker_example
|
||||
```
|
||||
|
||||
3. Run the Docker Compose file:
|
||||
3. Create a `.env` file with the Langflow admin password:
|
||||
|
||||
```text
|
||||
LANGFLOW_SUPERUSER_PASSWORD=replace-with-a-strong-password
|
||||
```
|
||||
|
||||
The default admin username is `langflow`.
|
||||
|
||||
4. Run the Docker Compose file:
|
||||
|
||||
```shell
|
||||
docker compose up
|
||||
```
|
||||
|
||||
4. Access Langflow at `http://localhost:7860/`.
|
||||
5. Access Langflow at `http://localhost:7860/`.
|
||||
|
||||
### Customize your deployment
|
||||
|
||||
@ -82,6 +90,7 @@ For example, to configure the container's database credentials using a `.env` fi
|
||||
# Langflow configuration
|
||||
LANGFLOW_DATABASE_URL=postgresql://myuser:mypassword@postgres:5432/langflow
|
||||
LANGFLOW_CONFIG_DIR=/app/langflow
|
||||
LANGFLOW_SUPERUSER_PASSWORD=replace-with-a-strong-password
|
||||
```
|
||||
|
||||
2. Modify the `docker-compose.yml` file to reference the `.env` file for both the `langflow` and `postgres` services:
|
||||
@ -140,7 +149,7 @@ This Dockerfile uses the official Langflow image as the base, creates a director
|
||||
|
||||
```bash
|
||||
docker build -t myuser/langflow-custom:1.0.0 .
|
||||
docker run -p 7860:7860 myuser/langflow-custom:1.0.0
|
||||
docker run -p 7860:7860 -e LANGFLOW_AUTO_LOGIN=true myuser/langflow-custom:1.0.0
|
||||
```
|
||||
|
||||
5. Push your image to Docker Hub (optional):
|
||||
@ -210,7 +219,7 @@ In this example, Langflow expects `memory.py` to exist in the `/helpers` directo
|
||||
|
||||
```bash
|
||||
docker build -t myuser/langflow-custom:1.0.0 .
|
||||
docker run -p 7860:7860 myuser/langflow-custom:1.0.0
|
||||
docker run -p 7860:7860 -e LANGFLOW_AUTO_LOGIN=true myuser/langflow-custom:1.0.0
|
||||
```
|
||||
|
||||
This approach can be adapted for any other components or custom code you want to add to Langflow by modifying the file paths and component names.
|
||||
@ -229,6 +238,7 @@ For example, this Docker Compose file uses a bind mount for Langflow data (`./la
|
||||
image: langflowai/langflow:1.8.0
|
||||
environment:
|
||||
- LANGFLOW_CONFIG_DIR=/app/langflow
|
||||
- LANGFLOW_SUPERUSER_PASSWORD=${LANGFLOW_SUPERUSER_PASSWORD}
|
||||
volumes:
|
||||
- ./langflow-data:/app/langflow
|
||||
postgres:
|
||||
@ -270,7 +280,7 @@ For example, this Docker Compose file uses a bind mount for Langflow data (`./la
|
||||
With `docker run`, use the same volume mount and the new image tag:
|
||||
|
||||
```bash
|
||||
docker run -p 7860:7860 -v langflow-data:/app/langflow langflowai/langflow:1.8.0
|
||||
docker run -p 7860:7860 -v langflow-data:/app/langflow -e LANGFLOW_SUPERUSER_PASSWORD=replace-with-a-strong-password langflowai/langflow:1.8.0
|
||||
```
|
||||
|
||||
This approach keeps the persistent volumes separate from the Langflow container, so you can upgrade the Langflow application without losing data.
|
||||
@ -278,4 +288,4 @@ This approach keeps the persistent volumes separate from the Langflow container,
|
||||
If you need to upgrade to a custom image based on a Langflow release, such as to add `uv` in `1.8.0`, first build a derived image from the official image, and then follow the same steps above.
|
||||
Set the custom image in your compose file or `docker run`, and then pull and restart.
|
||||
|
||||
For a minimal Dockerfile that adds `uv` to the 1.8.0 image, see the [release notes](/release-notes) (“Docker image no longer includes uv or uvx”).
|
||||
For a minimal Dockerfile that adds `uv` to the 1.8.0 image, see the [release notes](/release-notes) (“Docker image no longer includes uv or uvx”).
|
||||
|
||||
@ -157,9 +157,9 @@ For JWT authentication configuration, including algorithm selection and key mana
|
||||
This variable controls whether authentication is required to access your Langflow server, including the visual editor, API, and Langflow CLI:
|
||||
|
||||
* If `LANGFLOW_AUTO_LOGIN=False`, automatic login is disabled. Users must sign in to the visual editor, authenticate as a superuser to run certain Langflow CLI commands, and use a Langflow API key for Langflow API requests.
|
||||
If `false`, the Langflow team recommends that you also explicitly set [`LANGFLOW_SUPERUSER` and `LANGFLOW_SUPERUSER_PASSWORD`](#langflow-superuser) to avoid using the insecure default values.
|
||||
If `false`, you must explicitly set [`LANGFLOW_SUPERUSER_PASSWORD`](#langflow-superuser) before startup. Set [`LANGFLOW_SUPERUSER`](#langflow-superuser) only if you want to override the default superuser username.
|
||||
|
||||
* If `LANGFLOW_AUTO_LOGIN=True` (default), all API requests require authentication with a Langflow API key, but the visual editor automatically signs in all users as superusers, and Langflow uses _only_ the default [superuser credentials](/api-keys-and-authentication#langflow-superuser).
|
||||
* If `LANGFLOW_AUTO_LOGIN=True` (default), all API requests require authentication with a Langflow API key, but the visual editor automatically signs in all users as superusers, and Langflow uses the configured superuser username with a configured or generated bootstrap password.
|
||||
All users access the same visual editor environment without password protection, they can run all Langflow CLI commands as superusers, and Langflow automatically authenticates internal requests between the backend and frontend based on the users' superuser privileges.
|
||||
If you also want to bypass authentication for Langflow API requests in addition to other bypassed authentication, see [`LANGFLOW_SKIP_AUTH_AUTO_LOGIN`](/api-keys-and-authentication#langflow-skip-auth-auto-login).
|
||||
|
||||
@ -201,10 +201,11 @@ LANGFLOW_SUPERUSER=administrator
|
||||
LANGFLOW_SUPERUSER_PASSWORD=securepassword
|
||||
```
|
||||
|
||||
They are required if `LANGFLOW_AUTO_LOGIN=False`.
|
||||
Otherwise, they aren't relevant.
|
||||
`LANGFLOW_SUPERUSER_PASSWORD` is required if `LANGFLOW_AUTO_LOGIN=False`. `LANGFLOW_SUPERUSER` is optional and defaults to `langflow`.
|
||||
`LANGFLOW_SUPERUSER_PASSWORD` can't use the legacy default value `langflow`; startup fails closed instead.
|
||||
When `LANGFLOW_AUTO_LOGIN=True`, `LANGFLOW_SUPERUSER` selects the auto-login account username and `LANGFLOW_SUPERUSER_PASSWORD` is optional. If the password isn't set, Langflow generates an unknown bootstrap password for the auto-login account.
|
||||
|
||||
When you [start a Langflow server with authentication enabled](#start-a-langflow-server-with-authentication-enabled), if these variables are required but _not_ set, then Langflow uses the default values of `langflow` and `langflow`.
|
||||
When you [start a Langflow server with authentication enabled](#start-a-langflow-server-with-authentication-enabled), if the required password is _not_ set, then startup fails instead of creating a default password.
|
||||
These defaults don't apply when using the Langflow CLI command [`langflow superuser`](/configuration-cli#langflow-superuser).
|
||||
|
||||
### LANGFLOW_SECRET_KEY {#langflow-secret-key}
|
||||
@ -510,7 +511,7 @@ Additionally, you must sign in as a superuser to manage users and [create a Lang
|
||||
Your `.env` file can have other environment variables.
|
||||
This example focuses on authentication variables.
|
||||
|
||||
2. Set `LANGFLOW_SUPERUSER` and `LANGFLOW_SUPERUSER_PASSWORD` to your desired superuser credentials.
|
||||
2. Set `LANGFLOW_SUPERUSER_PASSWORD` to your desired superuser password. Optionally set `LANGFLOW_SUPERUSER` to override the default `langflow` username.
|
||||
|
||||
For a one-time test, you can use basic credentials like `administrator` and `password`.
|
||||
Strong, securely-stored credentials are recommended in genuine development and production environments.
|
||||
@ -538,8 +539,8 @@ Additionally, you must sign in as a superuser to manage users and [create a Lang
|
||||
uv run langflow run --env-file .env
|
||||
```
|
||||
|
||||
Starting Langflow with a `.env` file automatically authenticates you as the superuser set in `LANGFLOW_SUPERUSER` and `LANGFLOW_SUPERUSER_PASSWORD`.
|
||||
If you don't explicitly set these variables, the default values are `langflow` and `langflow` for system auto-login.
|
||||
Starting Langflow with this `.env` file creates or uses the configured superuser.
|
||||
If `LANGFLOW_SUPERUSER_PASSWORD` isn't set, startup fails instead of creating a default password.
|
||||
|
||||
6. Verify the server is running. The default location is `http://localhost:7860`.
|
||||
|
||||
@ -551,7 +552,7 @@ Next, you can add users to your Langflow server to collaborate with others on fl
|
||||
|
||||
If you aren't using the default location, replace `localhost:7860` with your server's address.
|
||||
|
||||
2. Log in with the superuser credentials you set in your `.env` (`LANGFLOW_SUPERUSER` and `LANGFLOW_SUPERUSER_PASSWORD`).
|
||||
2. Log in with the superuser credentials from your `.env`: the `LANGFLOW_SUPERUSER` username, or `langflow` if unset, and the `LANGFLOW_SUPERUSER_PASSWORD` password.
|
||||
|
||||
3. To manage users on your server, navigate to `/admin`, such as `http://localhost:7860/admin`, click your profile icon, and then click **Admin Page**.
|
||||
|
||||
|
||||
@ -73,7 +73,7 @@ For more information, see [Deploy Langflow on Docker](/deployment-docker).
|
||||
2. Pull the latest [Langflow Docker image](https://hub.docker.com/r/langflowai/langflow) and start it:
|
||||
|
||||
```bash
|
||||
docker run -p 7860:7860 langflowai/langflow:latest
|
||||
docker run -p 7860:7860 -e LANGFLOW_AUTO_LOGIN=true langflowai/langflow:latest
|
||||
```
|
||||
|
||||
3. To access Langflow, navigate to `http://localhost:7860/`.
|
||||
@ -199,4 +199,4 @@ To reinstall Langflow and all of its dependencies, run `uv pip install langflow
|
||||
|
||||
* [Quickstart](/get-started-quickstart): Build and run your first flow in minutes.
|
||||
* [Build flows](/concepts-flows): Learn about building flows.
|
||||
* [Troubleshoot Langflow](/troubleshoot): Get help with common Langflow install and startup issues.
|
||||
* [Troubleshoot Langflow](/troubleshoot): Get help with common Langflow install and startup issues.
|
||||
|
||||
@ -67,6 +67,11 @@ For all changes, see the [Changelog](https://github.com/langflow-ai/langflow/rel
|
||||
|
||||
For more information, see [Langflow Extensions overview](../Develop/extensions-overview.mdx).
|
||||
|
||||
- Default superuser password removed
|
||||
|
||||
Langflow no longer creates or accepts the legacy `langflow`/`langflow` default superuser credentials.
|
||||
If `LANGFLOW_AUTO_LOGIN=False`, set `LANGFLOW_SUPERUSER_PASSWORD` to a strong password before startup.
|
||||
|
||||
### New features and enhancements
|
||||
|
||||
- **Langflow Assistant**: build complete flows
|
||||
|
||||
@ -11,7 +11,7 @@ who = requests.get(f"{base}/api/v1/users/whoami", headers=headers, timeout=30)
|
||||
who.raise_for_status()
|
||||
user_id = who.json()["id"]
|
||||
|
||||
# Must differ from the current password (default superuser is often langflow/langflow).
|
||||
# Must differ from the current password.
|
||||
payload = {"password": "DocsExampleResetPass2025!"}
|
||||
|
||||
response = requests.patch(
|
||||
|
||||
@ -20,17 +20,17 @@ This option provides more control over the configuration, including a persistent
|
||||
* [Create a custom Langflow image](#customize-the-langflow-docker-image): Use a Dockerfile to package a custom Langflow Docker image that includes your own code, custom dependencies, or other modifications.
|
||||
* [Upgrade the Langflow Docker image](#upgrade-the-langflow-docker-image): Upgrade to a newer image without losing your database or flows by using persistent volumes and replacing only the container.
|
||||
|
||||
## Quickstart: Start a Langflow container with default values {#quickstart}
|
||||
## Quickstart: Start a local Langflow container {#quickstart}
|
||||
|
||||
With Docker installed and running on your system, run the following command:
|
||||
|
||||
```shell
|
||||
docker run -p 7860:7860 langflowai/langflow:latest
|
||||
docker run -p 7860:7860 -e LANGFLOW_AUTO_LOGIN=true langflowai/langflow:latest
|
||||
```
|
||||
|
||||
Then, access Langflow at `http://localhost:7860/`.
|
||||
|
||||
This container runs a pre-built Docker image with default settings.
|
||||
This starts a pre-built Docker image with automatic login enabled for local development.
|
||||
For more control over the configuration, see [Clone the repo and run the Langflow Docker container](#clone).
|
||||
|
||||
## Clone the repo and run the Langflow Docker container {#clone}
|
||||
@ -57,13 +57,21 @@ The complete Docker Compose configuration is available in `docker_example/docker
|
||||
cd langflow/docker_example
|
||||
```
|
||||
|
||||
3. Run the Docker Compose file:
|
||||
3. Create a `.env` file with the Langflow admin password:
|
||||
|
||||
```text
|
||||
LANGFLOW_SUPERUSER_PASSWORD=replace-with-a-strong-password
|
||||
```
|
||||
|
||||
The default admin username is `langflow`.
|
||||
|
||||
4. Run the Docker Compose file:
|
||||
|
||||
```shell
|
||||
docker compose up
|
||||
```
|
||||
|
||||
4. Access Langflow at `http://localhost:7860/`.
|
||||
5. Access Langflow at `http://localhost:7860/`.
|
||||
|
||||
### Customize your deployment
|
||||
|
||||
@ -82,6 +90,7 @@ For example, to configure the container's database credentials using a `.env` fi
|
||||
# Langflow configuration
|
||||
LANGFLOW_DATABASE_URL=postgresql://myuser:mypassword@postgres:5432/langflow
|
||||
LANGFLOW_CONFIG_DIR=/app/langflow
|
||||
LANGFLOW_SUPERUSER_PASSWORD=replace-with-a-strong-password
|
||||
```
|
||||
|
||||
2. Modify the `docker-compose.yml` file to reference the `.env` file for both the `langflow` and `postgres` services:
|
||||
@ -140,7 +149,7 @@ This Dockerfile uses the official Langflow image as the base, creates a director
|
||||
|
||||
```bash
|
||||
docker build -t myuser/langflow-custom:1.0.0 .
|
||||
docker run -p 7860:7860 myuser/langflow-custom:1.0.0
|
||||
docker run -p 7860:7860 -e LANGFLOW_AUTO_LOGIN=true myuser/langflow-custom:1.0.0
|
||||
```
|
||||
|
||||
5. Push your image to Docker Hub (optional):
|
||||
@ -210,7 +219,7 @@ In this example, Langflow expects `memory.py` to exist in the `/helpers` directo
|
||||
|
||||
```bash
|
||||
docker build -t myuser/langflow-custom:1.0.0 .
|
||||
docker run -p 7860:7860 myuser/langflow-custom:1.0.0
|
||||
docker run -p 7860:7860 -e LANGFLOW_AUTO_LOGIN=true myuser/langflow-custom:1.0.0
|
||||
```
|
||||
|
||||
This approach can be adapted for any other components or custom code you want to add to Langflow by modifying the file paths and component names.
|
||||
@ -229,6 +238,7 @@ For example, this Docker Compose file uses a bind mount for Langflow data (`./la
|
||||
image: langflowai/langflow:1.8.0
|
||||
environment:
|
||||
- LANGFLOW_CONFIG_DIR=/app/langflow
|
||||
- LANGFLOW_SUPERUSER_PASSWORD=${LANGFLOW_SUPERUSER_PASSWORD}
|
||||
volumes:
|
||||
- ./langflow-data:/app/langflow
|
||||
postgres:
|
||||
@ -270,7 +280,7 @@ For example, this Docker Compose file uses a bind mount for Langflow data (`./la
|
||||
With `docker run`, use the same volume mount and the new image tag:
|
||||
|
||||
```bash
|
||||
docker run -p 7860:7860 -v langflow-data:/app/langflow langflowai/langflow:1.8.0
|
||||
docker run -p 7860:7860 -v langflow-data:/app/langflow -e LANGFLOW_SUPERUSER_PASSWORD=replace-with-a-strong-password langflowai/langflow:1.8.0
|
||||
```
|
||||
|
||||
This approach keeps the persistent volumes separate from the Langflow container, so you can upgrade the Langflow application without losing data.
|
||||
@ -278,4 +288,4 @@ This approach keeps the persistent volumes separate from the Langflow container,
|
||||
If you need to upgrade to a custom image based on a Langflow release, such as to add `uv` in `1.8.0`, first build a derived image from the official image, and then follow the same steps above.
|
||||
Set the custom image in your compose file or `docker run`, and then pull and restart.
|
||||
|
||||
For a minimal Dockerfile that adds `uv` to the 1.8.0 image, see the [release notes](/release-notes) (“Docker image no longer includes uv or uvx”).
|
||||
For a minimal Dockerfile that adds `uv` to the 1.8.0 image, see the [release notes](/release-notes) (“Docker image no longer includes uv or uvx”).
|
||||
|
||||
@ -157,9 +157,9 @@ For JWT authentication configuration, including algorithm selection and key mana
|
||||
This variable controls whether authentication is required to access your Langflow server, including the visual editor, API, and Langflow CLI:
|
||||
|
||||
* If `LANGFLOW_AUTO_LOGIN=False`, automatic login is disabled. Users must sign in to the visual editor, authenticate as a superuser to run certain Langflow CLI commands, and use a Langflow API key for Langflow API requests.
|
||||
If `false`, the Langflow team recommends that you also explicitly set [`LANGFLOW_SUPERUSER` and `LANGFLOW_SUPERUSER_PASSWORD`](#langflow-superuser) to avoid using the insecure default values.
|
||||
If `false`, you must explicitly set [`LANGFLOW_SUPERUSER_PASSWORD`](#langflow-superuser) before startup. Set [`LANGFLOW_SUPERUSER`](#langflow-superuser) only if you want to override the default superuser username.
|
||||
|
||||
* If `LANGFLOW_AUTO_LOGIN=True` (default), all API requests require authentication with a Langflow API key, but the visual editor automatically signs in all users as superusers, and Langflow uses _only_ the default [superuser credentials](/api-keys-and-authentication#langflow-superuser).
|
||||
* If `LANGFLOW_AUTO_LOGIN=True` (default), all API requests require authentication with a Langflow API key, but the visual editor automatically signs in all users as superusers, and Langflow uses the configured superuser username with a configured or generated bootstrap password.
|
||||
All users access the same visual editor environment without password protection, they can run all Langflow CLI commands as superusers, and Langflow automatically authenticates internal requests between the backend and frontend based on the users' superuser privileges.
|
||||
If you also want to bypass authentication for Langflow API requests in addition to other bypassed authentication, see [`LANGFLOW_SKIP_AUTH_AUTO_LOGIN`](/api-keys-and-authentication#langflow-skip-auth-auto-login).
|
||||
|
||||
@ -201,10 +201,11 @@ LANGFLOW_SUPERUSER=administrator
|
||||
LANGFLOW_SUPERUSER_PASSWORD=securepassword
|
||||
```
|
||||
|
||||
They are required if `LANGFLOW_AUTO_LOGIN=False`.
|
||||
Otherwise, they aren't relevant.
|
||||
`LANGFLOW_SUPERUSER_PASSWORD` is required if `LANGFLOW_AUTO_LOGIN=False`. `LANGFLOW_SUPERUSER` is optional and defaults to `langflow`.
|
||||
`LANGFLOW_SUPERUSER_PASSWORD` can't use the legacy default value `langflow`; startup fails closed instead.
|
||||
When `LANGFLOW_AUTO_LOGIN=True`, `LANGFLOW_SUPERUSER` selects the auto-login account username and `LANGFLOW_SUPERUSER_PASSWORD` is optional. If the password isn't set, Langflow generates an unknown bootstrap password for the auto-login account.
|
||||
|
||||
When you [start a Langflow server with authentication enabled](#start-a-langflow-server-with-authentication-enabled), if these variables are required but _not_ set, then Langflow uses the default values of `langflow` and `langflow`.
|
||||
When you [start a Langflow server with authentication enabled](#start-a-langflow-server-with-authentication-enabled), if the required password is _not_ set, then startup fails instead of creating a default password.
|
||||
These defaults don't apply when using the Langflow CLI command [`langflow superuser`](/configuration-cli#langflow-superuser).
|
||||
|
||||
### LANGFLOW_SECRET_KEY {#langflow-secret-key}
|
||||
@ -510,7 +511,7 @@ Additionally, you must sign in as a superuser to manage users and [create a Lang
|
||||
Your `.env` file can have other environment variables.
|
||||
This example focuses on authentication variables.
|
||||
|
||||
2. Set `LANGFLOW_SUPERUSER` and `LANGFLOW_SUPERUSER_PASSWORD` to your desired superuser credentials.
|
||||
2. Set `LANGFLOW_SUPERUSER_PASSWORD` to your desired superuser password. Optionally set `LANGFLOW_SUPERUSER` to override the default `langflow` username.
|
||||
|
||||
For a one-time test, you can use basic credentials like `administrator` and `password`.
|
||||
Strong, securely-stored credentials are recommended in genuine development and production environments.
|
||||
@ -538,8 +539,8 @@ Additionally, you must sign in as a superuser to manage users and [create a Lang
|
||||
uv run langflow run --env-file .env
|
||||
```
|
||||
|
||||
Starting Langflow with a `.env` file automatically authenticates you as the superuser set in `LANGFLOW_SUPERUSER` and `LANGFLOW_SUPERUSER_PASSWORD`.
|
||||
If you don't explicitly set these variables, the default values are `langflow` and `langflow` for system auto-login.
|
||||
Starting Langflow with this `.env` file creates or uses the configured superuser.
|
||||
If `LANGFLOW_SUPERUSER_PASSWORD` isn't set, startup fails instead of creating a default password.
|
||||
|
||||
6. Verify the server is running. The default location is `http://localhost:7860`.
|
||||
|
||||
@ -551,7 +552,7 @@ Next, you can add users to your Langflow server to collaborate with others on fl
|
||||
|
||||
If you aren't using the default location, replace `localhost:7860` with your server's address.
|
||||
|
||||
2. Log in with the superuser credentials you set in your `.env` (`LANGFLOW_SUPERUSER` and `LANGFLOW_SUPERUSER_PASSWORD`).
|
||||
2. Log in with the superuser credentials from your `.env`: the `LANGFLOW_SUPERUSER` username, or `langflow` if unset, and the `LANGFLOW_SUPERUSER_PASSWORD` password.
|
||||
|
||||
3. To manage users on your server, navigate to `/admin`, such as `http://localhost:7860/admin`, click your profile icon, and then click **Admin Page**.
|
||||
|
||||
|
||||
@ -73,7 +73,7 @@ For more information, see [Deploy Langflow on Docker](/deployment-docker).
|
||||
2. Pull the latest [Langflow Docker image](https://hub.docker.com/r/langflowai/langflow) and start it:
|
||||
|
||||
```bash
|
||||
docker run -p 7860:7860 langflowai/langflow:latest
|
||||
docker run -p 7860:7860 -e LANGFLOW_AUTO_LOGIN=true langflowai/langflow:latest
|
||||
```
|
||||
|
||||
3. To access Langflow, navigate to `http://localhost:7860/`.
|
||||
@ -199,4 +199,4 @@ To reinstall Langflow and all of its dependencies, run `uv pip install langflow
|
||||
|
||||
* [Quickstart](/get-started-quickstart): Build and run your first flow in minutes.
|
||||
* [Build flows](/concepts-flows): Learn about building flows.
|
||||
* [Troubleshoot Langflow](/troubleshoot): Get help with common Langflow install and startup issues.
|
||||
* [Troubleshoot Langflow](/troubleshoot): Get help with common Langflow install and startup issues.
|
||||
|
||||
@ -67,6 +67,11 @@ For all changes, see the [Changelog](https://github.com/langflow-ai/langflow/rel
|
||||
|
||||
For more information, see [Langflow Extensions overview](../Develop/extensions-overview.mdx).
|
||||
|
||||
- Default superuser password removed
|
||||
|
||||
Langflow no longer creates or accepts the legacy `langflow`/`langflow` default superuser credentials.
|
||||
If `LANGFLOW_AUTO_LOGIN=False`, set `LANGFLOW_SUPERUSER_PASSWORD` to a strong password before startup.
|
||||
|
||||
### New features and enhancements
|
||||
|
||||
- **Langflow Assistant**: build complete flows
|
||||
|
||||
@ -41,7 +41,7 @@ from fastapi import HTTPException
|
||||
from httpx import HTTPError
|
||||
from jwt import InvalidTokenError
|
||||
from lfx.log.logger import configure, logger
|
||||
from lfx.services.settings.constants import DEFAULT_SUPERUSER, DEFAULT_SUPERUSER_PASSWORD
|
||||
from lfx.services.settings.constants import DEFAULT_SUPERUSER
|
||||
from multiprocess import cpu_count
|
||||
from multiprocess.context import Process
|
||||
from packaging import version as pkg_version
|
||||
@ -58,7 +58,7 @@ from langflow.services.auth.utils import get_current_user_from_access_token
|
||||
from langflow.services.database.models.api_key.crud import check_key
|
||||
from langflow.services.database.service import UnsupportedPostgreSQLVersionError, check_postgresql_version_sync
|
||||
from langflow.services.deps import get_db_service, get_settings_service, is_settings_service_initialized, session_scope
|
||||
from langflow.services.utils import initialize_services
|
||||
from langflow.services.utils import get_auto_login_superuser_password, initialize_services
|
||||
from langflow.utils.version import fetch_latest_version, get_version_info
|
||||
from langflow.utils.version import is_pre_release as langflow_is_pre_release
|
||||
|
||||
@ -844,10 +844,10 @@ def print_banner(host: str, port: int, protocol: str) -> None:
|
||||
@app.command()
|
||||
def superuser(
|
||||
username: str = typer.Option(
|
||||
None, help="Username for the superuser. Defaults to 'langflow' when AUTO_LOGIN is enabled."
|
||||
None, help="Username for the superuser. Defaults to the configured AUTO_LOGIN superuser."
|
||||
),
|
||||
password: str = typer.Option(
|
||||
None, help="Password for the superuser. Defaults to 'langflow' when AUTO_LOGIN is enabled."
|
||||
None, help="Password for the superuser. Ignored when AUTO_LOGIN generates the bootstrap password."
|
||||
),
|
||||
log_level: str = typer.Option("error", help="Logging level.", envvar="LANGFLOW_LOG_LEVEL"),
|
||||
auth_token: str = typer.Option(
|
||||
@ -856,7 +856,7 @@ def superuser(
|
||||
) -> None:
|
||||
"""Create a superuser.
|
||||
|
||||
When AUTO_LOGIN is enabled, uses default credentials.
|
||||
When AUTO_LOGIN is enabled, uses configured or generated bootstrap credentials.
|
||||
In production mode, requires authentication.
|
||||
"""
|
||||
configure(log_level=log_level)
|
||||
@ -866,7 +866,7 @@ def superuser(
|
||||
|
||||
async def _create_superuser(username: str, password: str, auth_token: str | None):
|
||||
"""Create a superuser."""
|
||||
await initialize_services()
|
||||
await initialize_services(skip_superuser_setup=True)
|
||||
|
||||
settings_service = get_settings_service()
|
||||
# Check if superuser creation via CLI is enabled
|
||||
@ -876,9 +876,7 @@ async def _create_superuser(username: str, password: str, auth_token: str | None
|
||||
raise typer.Exit(1)
|
||||
|
||||
if settings_service.auth_settings.AUTO_LOGIN:
|
||||
# Force default credentials for AUTO_LOGIN mode
|
||||
username = DEFAULT_SUPERUSER
|
||||
password = DEFAULT_SUPERUSER_PASSWORD.get_secret_value()
|
||||
username = getattr(settings_service.auth_settings, "SUPERUSER", None) or DEFAULT_SUPERUSER
|
||||
else:
|
||||
# Production mode - prompt for credentials if not provided
|
||||
if not username:
|
||||
@ -890,8 +888,6 @@ async def _create_superuser(username: str, password: str, auth_token: str | None
|
||||
|
||||
existing_superusers = []
|
||||
async with session_scope() as session:
|
||||
# Note that the default superuser is created by the initialize_services() function,
|
||||
# but leaving this check here in case we change that behavior
|
||||
existing_superusers = await get_all_superusers(session)
|
||||
is_first_setup = len(existing_superusers) == 0
|
||||
|
||||
@ -905,6 +901,7 @@ async def _create_superuser(username: str, password: str, auth_token: str | None
|
||||
typer.echo("2. Run this command again with --auth-token")
|
||||
raise typer.Exit(1)
|
||||
|
||||
password = get_auto_login_superuser_password(settings_service.auth_settings)
|
||||
typer.echo(f"AUTO_LOGIN enabled. Creating default superuser '{username}'...")
|
||||
# Do not echo the default password to avoid exposing it in logs.
|
||||
# AUTO_LOGIN is false - production mode
|
||||
@ -1065,11 +1062,15 @@ def api_key(
|
||||
async with session_scope() as session:
|
||||
from langflow.services.database.models.user.model import User
|
||||
|
||||
stmt = select(User).where(User.username == DEFAULT_SUPERUSER)
|
||||
superuser_username = auth_settings.SUPERUSER or DEFAULT_SUPERUSER
|
||||
stmt = select(User).where(
|
||||
User.username == superuser_username,
|
||||
User.is_superuser == True, # noqa: E712
|
||||
)
|
||||
superuser = (await session.exec(stmt)).first()
|
||||
if not superuser:
|
||||
typer.echo(
|
||||
"Default superuser not found. This command requires a superuser and AUTO_LOGIN to be enabled."
|
||||
"Auto-login superuser not found. This command requires a superuser and AUTO_LOGIN to be enabled."
|
||||
)
|
||||
return None
|
||||
from langflow.services.database.models.api_key.crud import create_api_key, delete_api_key
|
||||
|
||||
@ -49,7 +49,6 @@ from langflow.services.database.models.folder.constants import (
|
||||
)
|
||||
from langflow.services.database.models.folder.model import Folder, FolderCreate, FolderRead
|
||||
from langflow.services.deps import (
|
||||
get_auth_service,
|
||||
get_settings_service,
|
||||
get_storage_service,
|
||||
get_variable_service,
|
||||
@ -1318,15 +1317,29 @@ async def initialize_auto_login_default_superuser() -> None:
|
||||
settings_service = get_settings_service()
|
||||
if not settings_service.auth_settings.AUTO_LOGIN:
|
||||
return
|
||||
# In AUTO_LOGIN mode, always use the default credentials for initial bootstrapping
|
||||
# without persisting the password in memory after setup.
|
||||
from lfx.services.settings.constants import DEFAULT_SUPERUSER, DEFAULT_SUPERUSER_PASSWORD
|
||||
# In AUTO_LOGIN mode, bootstrap with a configured password if the operator
|
||||
# provided one; otherwise generate an unknown password for the default user.
|
||||
from lfx.services.settings.constants import DEFAULT_SUPERUSER
|
||||
|
||||
username = DEFAULT_SUPERUSER
|
||||
password = DEFAULT_SUPERUSER_PASSWORD.get_secret_value()
|
||||
from langflow.services.database.models.user.crud import get_user_by_username
|
||||
from langflow.services.utils import get_auto_login_superuser_password, get_or_create_super_user
|
||||
|
||||
username = settings_service.auth_settings.SUPERUSER or DEFAULT_SUPERUSER
|
||||
password = get_auto_login_superuser_password(settings_service.auth_settings)
|
||||
|
||||
async with session_scope() as async_session:
|
||||
super_user = await get_auth_service().create_super_user(username, password, db=async_session)
|
||||
super_user = await get_or_create_super_user(
|
||||
async_session,
|
||||
username,
|
||||
password,
|
||||
is_default=True,
|
||||
rotate_legacy_default_password=True,
|
||||
)
|
||||
if super_user is None:
|
||||
super_user = await get_user_by_username(async_session, username)
|
||||
if super_user is None or not super_user.is_superuser:
|
||||
msg = "Auto-login superuser was not initialized."
|
||||
raise RuntimeError(msg)
|
||||
await get_variable_service().initialize_user_variables(super_user.id, async_session)
|
||||
# Initialize agentic variables if agentic experience is enabled
|
||||
from langflow.api.utils.mcp.agentic_mcp import initialize_agentic_user_variables
|
||||
|
||||
@ -13,6 +13,7 @@ from fastapi import HTTPException, Request, WebSocketException, status
|
||||
from jwt import InvalidTokenError
|
||||
from lfx.log.logger import logger
|
||||
from lfx.services.auth.base import BaseAuthService
|
||||
from lfx.services.settings.constants import DEFAULT_SUPERUSER, LEGACY_DEFAULT_SUPERUSER_PASSWORD
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from langflow.helpers.user import get_user_by_flow_id_or_endpoint_name
|
||||
@ -693,6 +694,20 @@ class AuthService(BaseAuthService):
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Waiting for approval")
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Inactive user")
|
||||
|
||||
auth_settings = self.settings.auth_settings
|
||||
auto_login_superuser = auth_settings.SUPERUSER or DEFAULT_SUPERUSER
|
||||
legacy_superuser_usernames = {DEFAULT_SUPERUSER, auto_login_superuser}
|
||||
if username in legacy_superuser_usernames and password == LEGACY_DEFAULT_SUPERUSER_PASSWORD.get_secret_value():
|
||||
if request and request.client:
|
||||
logger.warning(
|
||||
"Login failed: legacy default superuser password is disabled",
|
||||
auth_event="login_failed",
|
||||
reason="legacy_default_password_disabled",
|
||||
auth_id=str(user.id),
|
||||
client_ip=request.client.host,
|
||||
)
|
||||
return None
|
||||
|
||||
if not self.verify_password(password, user.password):
|
||||
if request and request.client:
|
||||
logger.warning(
|
||||
|
||||
@ -4,10 +4,14 @@ import asyncio
|
||||
from enum import Enum
|
||||
from importlib import import_module
|
||||
from pathlib import Path
|
||||
from secrets import token_urlsafe
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from lfx.log.logger import logger
|
||||
from lfx.services.settings.constants import DEFAULT_SUPERUSER, DEFAULT_SUPERUSER_PASSWORD
|
||||
from lfx.services.settings.constants import (
|
||||
DEFAULT_SUPERUSER,
|
||||
LEGACY_DEFAULT_SUPERUSER_PASSWORD,
|
||||
)
|
||||
from lfx.services.settings.feature_flags import FEATURE_FLAGS
|
||||
from sqlalchemy import delete
|
||||
from sqlalchemy import exc as sqlalchemy_exc
|
||||
@ -37,7 +41,58 @@ class SetupSuperuserResult(str, Enum):
|
||||
SUPERUSER_UNCHANGED = "superuser_unchanged"
|
||||
|
||||
|
||||
async def get_or_create_super_user(session: AsyncSession, username, password, is_default):
|
||||
def _secret_value(secret) -> str:
|
||||
if not secret:
|
||||
return ""
|
||||
if hasattr(secret, "get_secret_value"):
|
||||
return secret.get_secret_value()
|
||||
return str(secret)
|
||||
|
||||
|
||||
def get_auto_login_superuser_password(auth_settings) -> str:
|
||||
configured_password = _secret_value(auth_settings.SUPERUSER_PASSWORD)
|
||||
legacy_password = LEGACY_DEFAULT_SUPERUSER_PASSWORD.get_secret_value()
|
||||
if configured_password and configured_password != legacy_password:
|
||||
return configured_password
|
||||
return token_urlsafe(32)
|
||||
|
||||
|
||||
async def _get_superuser_by_username(session: AsyncSession, username: str):
|
||||
from langflow.services.database.models.user.model import User
|
||||
|
||||
stmt = select(User).where(
|
||||
User.username == username,
|
||||
User.is_superuser == True, # noqa: E712
|
||||
)
|
||||
result = await session.exec(stmt)
|
||||
return result.first()
|
||||
|
||||
|
||||
async def _rotate_legacy_default_superuser_password(session: AsyncSession, user, replacement_password: str) -> bool:
|
||||
legacy_password = LEGACY_DEFAULT_SUPERUSER_PASSWORD.get_secret_value()
|
||||
if not legacy_password or not user.password:
|
||||
return False
|
||||
|
||||
auth = get_auth_service()
|
||||
if not auth.verify_password(legacy_password, user.password):
|
||||
return False
|
||||
|
||||
user.password = auth.get_password_hash(replacement_password)
|
||||
session.add(user)
|
||||
await session.commit()
|
||||
await session.refresh(user)
|
||||
await logger.awarning("Rotated legacy default superuser password.")
|
||||
return True
|
||||
|
||||
|
||||
async def get_or_create_super_user(
|
||||
session: AsyncSession,
|
||||
username,
|
||||
password,
|
||||
is_default,
|
||||
*,
|
||||
rotate_legacy_default_password: bool = False,
|
||||
):
|
||||
from langflow.services.database.models.user.model import User
|
||||
|
||||
stmt = select(User).where(User.username == username)
|
||||
@ -46,6 +101,8 @@ async def get_or_create_super_user(session: AsyncSession, username, password, is
|
||||
|
||||
auth = get_auth_service()
|
||||
if user and user.is_superuser:
|
||||
if rotate_legacy_default_password:
|
||||
await _rotate_legacy_default_superuser_password(session, user, password)
|
||||
return None # Superuser already exists
|
||||
|
||||
if user and is_default:
|
||||
@ -89,8 +146,14 @@ async def setup_superuser(settings_service: SettingsService, session: AsyncSessi
|
||||
|
||||
from filelock import FileLock
|
||||
|
||||
username = DEFAULT_SUPERUSER
|
||||
password = DEFAULT_SUPERUSER_PASSWORD.get_secret_value()
|
||||
username = settings_service.auth_settings.SUPERUSER or DEFAULT_SUPERUSER
|
||||
configured_password = _secret_value(settings_service.auth_settings.SUPERUSER_PASSWORD)
|
||||
if configured_password == LEGACY_DEFAULT_SUPERUSER_PASSWORD.get_secret_value():
|
||||
await logger.awarning(
|
||||
"Ignoring legacy default LANGFLOW_SUPERUSER_PASSWORD in AUTO_LOGIN mode; "
|
||||
"generated a random bootstrap password instead."
|
||||
)
|
||||
password = get_auto_login_superuser_password(settings_service.auth_settings)
|
||||
|
||||
# Use file lock similar to starter projects
|
||||
lock_file = Path(gettempdir()) / "langflow_auto_login_superuser.lock"
|
||||
@ -99,7 +162,13 @@ async def setup_superuser(settings_service: SettingsService, session: AsyncSessi
|
||||
try:
|
||||
with lock:
|
||||
# Create user and initialize all related resources
|
||||
super_user = await get_or_create_super_user(session, username, password, is_default=True)
|
||||
super_user = await get_or_create_super_user(
|
||||
session,
|
||||
username,
|
||||
password,
|
||||
is_default=True,
|
||||
rotate_legacy_default_password=True,
|
||||
)
|
||||
if super_user: # Only initialize if user was created
|
||||
from langflow.initial_setup.setup import get_or_create_default_folder
|
||||
from langflow.services.deps import get_variable_service
|
||||
@ -116,6 +185,11 @@ async def setup_superuser(settings_service: SettingsService, session: AsyncSessi
|
||||
_ = await get_or_create_default_folder(session, super_user.id)
|
||||
await logger.adebug("Auto-login superuser initialized successfully")
|
||||
return SetupSuperuserResult.AUTO_LOGIN_INITIALIZED
|
||||
existing_superuser = await _get_superuser_by_username(session, username)
|
||||
if existing_superuser is None:
|
||||
msg = "AUTO_LOGIN is enabled but the configured bootstrap user is not a superuser."
|
||||
await logger.aerror(msg)
|
||||
raise RuntimeError(msg)
|
||||
return SetupSuperuserResult.AUTO_LOGIN_ALREADY_SATISFIED
|
||||
except TimeoutError as exc:
|
||||
# Another worker may be handling it - but a stale/abandoned lock or dead holder
|
||||
@ -125,13 +199,7 @@ async def setup_superuser(settings_service: SettingsService, session: AsyncSessi
|
||||
"(another worker may hold it, or the lock file may be stale). "
|
||||
"Verifying whether the default superuser exists.",
|
||||
)
|
||||
from langflow.services.database.models.user.model import User
|
||||
|
||||
stmt = select(User).where(
|
||||
User.username == username,
|
||||
User.is_superuser == True, # noqa: E712
|
||||
)
|
||||
exists = (await session.exec(stmt)).first() is not None
|
||||
exists = (await _get_superuser_by_username(session, username)) is not None
|
||||
if not exists:
|
||||
msg = (
|
||||
"AUTO_LOGIN is enabled but the default superuser was not initialized: "
|
||||
@ -141,12 +209,13 @@ async def setup_superuser(settings_service: SettingsService, session: AsyncSessi
|
||||
await logger.aerror(msg)
|
||||
raise RuntimeError(msg) from exc
|
||||
return SetupSuperuserResult.AUTO_LOGIN_LOCK_TIMEOUT_SUPERUSER_PRESENT
|
||||
finally:
|
||||
settings_service.auth_settings.reset_credentials()
|
||||
# Remove the default superuser if it exists
|
||||
await teardown_superuser(settings_service, session)
|
||||
# If AUTO_LOGIN is disabled, attempt to use configured credentials
|
||||
# or fall back to default credentials if none are provided.
|
||||
# If AUTO_LOGIN is disabled, require configured credentials.
|
||||
username = settings_service.auth_settings.SUPERUSER or DEFAULT_SUPERUSER
|
||||
password = (settings_service.auth_settings.SUPERUSER_PASSWORD or DEFAULT_SUPERUSER_PASSWORD).get_secret_value()
|
||||
password = _secret_value(settings_service.auth_settings.SUPERUSER_PASSWORD)
|
||||
|
||||
await logger.adebug(f"Setup superuser: username={username}, has_password={bool(password)}")
|
||||
|
||||
@ -155,12 +224,21 @@ async def setup_superuser(settings_service: SettingsService, session: AsyncSessi
|
||||
await logger.aerror(f"Missing credentials: username={username}, password={'set' if password else 'not set'}")
|
||||
raise ValueError(msg)
|
||||
|
||||
is_default = (username == DEFAULT_SUPERUSER) and (password == DEFAULT_SUPERUSER_PASSWORD.get_secret_value())
|
||||
if password == LEGACY_DEFAULT_SUPERUSER_PASSWORD.get_secret_value():
|
||||
msg = "LANGFLOW_SUPERUSER_PASSWORD cannot use the legacy default password"
|
||||
await logger.aerror(msg)
|
||||
raise ValueError(msg)
|
||||
|
||||
is_default = (username == DEFAULT_SUPERUSER) and (password == LEGACY_DEFAULT_SUPERUSER_PASSWORD.get_secret_value())
|
||||
|
||||
try:
|
||||
await logger.adebug(f"Creating/getting superuser: username={username}, is_default={is_default}")
|
||||
user = await get_or_create_super_user(
|
||||
session=session, username=username, password=password, is_default=is_default
|
||||
session=session,
|
||||
username=username,
|
||||
password=password,
|
||||
is_default=is_default,
|
||||
rotate_legacy_default_password=True,
|
||||
)
|
||||
if user is not None:
|
||||
await logger.adebug("Superuser created successfully.")
|
||||
@ -581,7 +659,7 @@ def register_builtin_deployment_mappers() -> None:
|
||||
logger.info("Skipping Watsonx Orchestrate deployment mapper registration: %s", exc)
|
||||
|
||||
|
||||
async def initialize_services(*, fix_migration: bool = False) -> None:
|
||||
async def initialize_services(*, fix_migration: bool = False, skip_superuser_setup: bool = False) -> None:
|
||||
"""Initialize all the services needed."""
|
||||
from langflow.helpers.windows_postgres_helper import configure_windows_postgres_event_loop
|
||||
|
||||
@ -602,13 +680,14 @@ async def initialize_services(*, fix_migration: bool = False) -> None:
|
||||
await initialize_database(fix_migration=fix_migration)
|
||||
db_service = get_db_service()
|
||||
await db_service.initialize_alembic_log_file()
|
||||
async with session_scope() as session:
|
||||
settings_service = get_service(ServiceType.SETTINGS_SERVICE)
|
||||
await setup_superuser(settings_service, session)
|
||||
try:
|
||||
await get_db_service().assign_orphaned_flows_to_superuser()
|
||||
except sqlalchemy_exc.IntegrityError as exc:
|
||||
await logger.awarning(f"Error assigning orphaned flows to the superuser: {exc!s}")
|
||||
settings_service = get_service(ServiceType.SETTINGS_SERVICE)
|
||||
if not skip_superuser_setup:
|
||||
async with session_scope() as session:
|
||||
await setup_superuser(settings_service, session)
|
||||
try:
|
||||
await get_db_service().assign_orphaned_flows_to_superuser()
|
||||
except sqlalchemy_exc.IntegrityError as exc:
|
||||
await logger.awarning(f"Error assigning orphaned flows to the superuser: {exc!s}")
|
||||
|
||||
async with session_scope() as session:
|
||||
await clean_transactions(settings_service, session)
|
||||
|
||||
@ -318,6 +318,8 @@ def distributed_client_fixture(
|
||||
db_path = Path(db_dir) / "test.db"
|
||||
monkeypatch.setenv("LANGFLOW_DATABASE_URL", f"sqlite:///{db_path}")
|
||||
monkeypatch.setenv("LANGFLOW_AUTO_LOGIN", "false")
|
||||
monkeypatch.setenv("LANGFLOW_SUPERUSER", "langflow")
|
||||
monkeypatch.setenv("LANGFLOW_SUPERUSER_PASSWORD", "test-superuser-password")
|
||||
# monkeypatch langflow.services.task.manager.USE_CELERY to True
|
||||
# monkeypatch.setattr(manager, "USE_CELERY", True)
|
||||
monkeypatch.setattr(celery_app, "celery_app", celery_app.make_celery("langflow", Config))
|
||||
@ -470,6 +472,8 @@ async def client_fixture(
|
||||
db_path = Path(db_dir) / "test.db"
|
||||
monkeypatch.setenv("LANGFLOW_DATABASE_URL", f"sqlite:///{db_path}")
|
||||
monkeypatch.setenv("LANGFLOW_AUTO_LOGIN", "false")
|
||||
monkeypatch.setenv("LANGFLOW_SUPERUSER", "langflow")
|
||||
monkeypatch.setenv("LANGFLOW_SUPERUSER_PASSWORD", "test-superuser-password")
|
||||
monkeypatch.setenv("DO_NOT_TRACK", "true")
|
||||
if "load_flows" in request.keywords:
|
||||
shutil.copyfile(
|
||||
|
||||
@ -108,7 +108,7 @@ python langflow_setup_test.py --list-flows
|
||||
|
||||
This will:
|
||||
|
||||
- Use default Langflow credentials (langflow/langflow)
|
||||
- Use `LANGFLOW_SUPERUSER` / `LANGFLOW_SUPERUSER_PASSWORD` or AUTO_LOGIN
|
||||
- Generate API keys
|
||||
- Upload a real starter project flow
|
||||
- Provide credentials for load testing
|
||||
@ -185,7 +185,7 @@ Use with: `--shape ramp100` or `--shape stepramp`
|
||||
|
||||
1. **Health Check**: Verify Langflow is running
|
||||
2. **Flow Selection**: Choose from 40+ real starter project flows
|
||||
3. **Authentication**: Login with default credentials (langflow/langflow)
|
||||
3. **Authentication**: Login with configured credentials or AUTO_LOGIN
|
||||
4. **API Key Generation**: Create API key for load testing
|
||||
5. **Flow Upload**: Upload the selected starter project flow
|
||||
6. **Credential Export**: Provide environment variables for testing
|
||||
@ -240,7 +240,7 @@ The test tracks:
|
||||
### Common Issues
|
||||
|
||||
1. **Setup Failed**: Ensure Langflow is accessible and not in read-only mode
|
||||
2. **Authentication Errors**: Verify default credentials (langflow/langflow) are enabled
|
||||
2. **Authentication Errors**: Set `LANGFLOW_SUPERUSER` and `LANGFLOW_SUPERUSER_PASSWORD`, or enable `LANGFLOW_AUTO_LOGIN`
|
||||
3. **Flow Creation Failed**: Verify the user has permission to create flows
|
||||
4. **Connection Errors**: Check network connectivity and firewall settings
|
||||
5. **Status Code 0 Errors**: Usually indicates connection overload - reduce user count or spawn rate
|
||||
|
||||
@ -19,10 +19,39 @@ Usage:
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
async def authenticate_setup_client(client) -> tuple[str, str, str]:
|
||||
"""Return an access token using explicit credentials or AUTO_LOGIN."""
|
||||
configured_username = os.environ.get("LANGFLOW_SUPERUSER", "")
|
||||
password = os.environ.get("LANGFLOW_SUPERUSER_PASSWORD", "")
|
||||
|
||||
if configured_username and not password:
|
||||
raise RuntimeError("Set LANGFLOW_SUPERUSER_PASSWORD when LANGFLOW_SUPERUSER is set, or unset both.")
|
||||
|
||||
if password:
|
||||
username = configured_username or "langflow"
|
||||
login_response = await client.post(
|
||||
"/api/v1/login",
|
||||
data={"username": username, "password": password},
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
)
|
||||
if login_response.status_code != 200:
|
||||
raise RuntimeError(f"Login failed: {login_response.status_code} - {login_response.text}")
|
||||
return login_response.json()["access_token"], username, "<redacted>"
|
||||
|
||||
auto_login_response = await client.get("/api/v1/auto_login")
|
||||
if auto_login_response.status_code != 200:
|
||||
raise RuntimeError(
|
||||
"Authentication failed. Set LANGFLOW_SUPERUSER_PASSWORD and optionally LANGFLOW_SUPERUSER, "
|
||||
"or enable LANGFLOW_AUTO_LOGIN."
|
||||
)
|
||||
return auto_login_response.json()["access_token"], "auto-login", ""
|
||||
|
||||
|
||||
async def get_starter_projects_from_api(host: str, access_token: str) -> list[dict]:
|
||||
"""Get starter projects from Langflow API."""
|
||||
import httpx
|
||||
@ -200,14 +229,10 @@ async def setup_langflow_environment(host: str, flow_name: str | None = None, in
|
||||
print("Install with: pip install httpx")
|
||||
sys.exit(1)
|
||||
|
||||
# Configuration - use default Langflow credentials
|
||||
username = "langflow"
|
||||
password = "langflow"
|
||||
|
||||
setup_state = {
|
||||
"host": host,
|
||||
"username": username,
|
||||
"password": password,
|
||||
"username": None,
|
||||
"password": None,
|
||||
"user_id": None,
|
||||
"access_token": None,
|
||||
"api_key": None,
|
||||
@ -228,36 +253,21 @@ async def setup_langflow_environment(host: str, flow_name: str | None = None, in
|
||||
print(f" ❌ Health check failed: {e}")
|
||||
raise
|
||||
|
||||
# Step 2: Skip user creation, use default credentials
|
||||
print("2. Using default Langflow credentials...")
|
||||
print(f" ✅ Using username: {username}")
|
||||
|
||||
# Step 3: Login to get JWT token
|
||||
print("3. Authenticating...")
|
||||
login_data = {
|
||||
"username": username,
|
||||
"password": password,
|
||||
}
|
||||
# Step 2: Authenticate with configured credentials or AUTO_LOGIN
|
||||
print("2. Authenticating...")
|
||||
|
||||
try:
|
||||
login_response = await client.post(
|
||||
"/api/v1/login",
|
||||
data=login_data, # OAuth2PasswordRequestForm expects form data
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
)
|
||||
|
||||
if login_response.status_code != 200:
|
||||
raise Exception(f"Login failed: {login_response.status_code} - {login_response.text}")
|
||||
|
||||
tokens = login_response.json()
|
||||
setup_state["access_token"] = tokens["access_token"]
|
||||
access_token, username, password = await authenticate_setup_client(client)
|
||||
setup_state["access_token"] = access_token
|
||||
setup_state["username"] = username
|
||||
setup_state["password"] = password
|
||||
print(" ✅ Authentication successful")
|
||||
except Exception as e:
|
||||
print(f" ❌ Authentication failed: {e}")
|
||||
raise
|
||||
|
||||
# Step 4: Create API key
|
||||
print("4. Creating API key...")
|
||||
# Step 3: Create API key
|
||||
print("3. Creating API key...")
|
||||
headers = {"Authorization": f"Bearer {setup_state['access_token']}"}
|
||||
|
||||
try:
|
||||
@ -274,8 +284,8 @@ async def setup_langflow_environment(host: str, flow_name: str | None = None, in
|
||||
print(f" ❌ API key creation failed: {e}")
|
||||
raise
|
||||
|
||||
# Step 5: Select and load flow from API
|
||||
print("5. Selecting starter project flow...")
|
||||
# Step 4: Select and load flow from API
|
||||
print("4. Selecting starter project flow...")
|
||||
|
||||
# Flow selection logic
|
||||
selected_flow_name = None
|
||||
@ -315,8 +325,8 @@ async def setup_langflow_environment(host: str, flow_name: str | None = None, in
|
||||
print(f" ✅ Selected flow: {setup_state['flow_name']}")
|
||||
print(f" Description: {flow_data.get('description', 'No description')}")
|
||||
|
||||
# Step 6: Upload the selected flow
|
||||
print(f"6. Uploading flow: {setup_state['flow_name']}...")
|
||||
# Step 5: Upload the selected flow
|
||||
print(f"5. Uploading flow: {setup_state['flow_name']}...")
|
||||
|
||||
try:
|
||||
# Prepare flow data for upload
|
||||
@ -469,10 +479,6 @@ Examples:
|
||||
print("Install with: pip install httpx")
|
||||
sys.exit(1)
|
||||
|
||||
# Quick authentication to access the API
|
||||
username = "langflow"
|
||||
password = "langflow"
|
||||
|
||||
async with httpx.AsyncClient(base_url=args.host, timeout=30.0) as client:
|
||||
# Health check
|
||||
try:
|
||||
@ -483,24 +489,15 @@ Examples:
|
||||
print(f"❌ Cannot connect to Langflow at {args.host}: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Login to get access token
|
||||
# Authenticate to get access token
|
||||
try:
|
||||
login_data = {"username": username, "password": password}
|
||||
login_response = await client.post(
|
||||
"/api/v1/login",
|
||||
data=login_data,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
)
|
||||
|
||||
if login_response.status_code != 200:
|
||||
raise Exception(f"Authentication failed: {login_response.status_code}")
|
||||
|
||||
tokens = login_response.json()
|
||||
access_token = tokens["access_token"]
|
||||
access_token, _, _ = await authenticate_setup_client(client)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Authentication failed: {e}")
|
||||
print("Make sure Langflow is running with default credentials (langflow/langflow)")
|
||||
print(
|
||||
"Set LANGFLOW_SUPERUSER_PASSWORD and optionally LANGFLOW_SUPERUSER, or enable LANGFLOW_AUTO_LOGIN."
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# Get flows from API
|
||||
|
||||
@ -180,6 +180,8 @@ async def files_client_fixture(
|
||||
db_path = Path(db_dir) / "test.db"
|
||||
monkeypatch.setenv("LANGFLOW_DATABASE_URL", f"sqlite:///{db_path}")
|
||||
monkeypatch.setenv("LANGFLOW_AUTO_LOGIN", "false")
|
||||
monkeypatch.setenv("LANGFLOW_SUPERUSER", "langflow")
|
||||
monkeypatch.setenv("LANGFLOW_SUPERUSER_PASSWORD", "test-superuser-password")
|
||||
from lfx.services.manager import get_service_manager
|
||||
|
||||
get_service_manager().factories.clear()
|
||||
|
||||
@ -114,6 +114,8 @@ async def files_client_fixture(
|
||||
db_path = Path(db_dir) / "test.db"
|
||||
monkeypatch.setenv("LANGFLOW_DATABASE_URL", f"sqlite:///{db_path}")
|
||||
monkeypatch.setenv("LANGFLOW_AUTO_LOGIN", "false")
|
||||
monkeypatch.setenv("LANGFLOW_SUPERUSER", "langflow")
|
||||
monkeypatch.setenv("LANGFLOW_SUPERUSER_PASSWORD", "test-superuser-password")
|
||||
from lfx.services.manager import get_service_manager
|
||||
|
||||
get_service_manager().factories.clear()
|
||||
@ -784,6 +786,8 @@ async def s3_files_client_fixture(
|
||||
db_path = Path(db_dir) / "test_s3.db"
|
||||
monkeypatch.setenv("LANGFLOW_DATABASE_URL", f"sqlite:///{db_path}")
|
||||
monkeypatch.setenv("LANGFLOW_AUTO_LOGIN", "false")
|
||||
monkeypatch.setenv("LANGFLOW_SUPERUSER", "langflow")
|
||||
monkeypatch.setenv("LANGFLOW_SUPERUSER_PASSWORD", "test-superuser-password")
|
||||
# Configure S3 storage
|
||||
monkeypatch.setenv("LANGFLOW_STORAGE_TYPE", "s3")
|
||||
monkeypatch.setenv(
|
||||
|
||||
@ -19,6 +19,7 @@ from langflow.services.auth.exceptions import (
|
||||
from langflow.services.auth.service import AuthService
|
||||
from langflow.services.database.models.user.model import User
|
||||
from lfx.services.settings.auth import AuthSettings
|
||||
from lfx.services.settings.constants import DEFAULT_SUPERUSER, LEGACY_DEFAULT_SUPERUSER_PASSWORD
|
||||
from pydantic import SecretStr
|
||||
|
||||
|
||||
@ -275,6 +276,72 @@ async def test_authenticate_with_credentials_auto_login_skip_rejects_inactive_su
|
||||
await auth_service.authenticate_with_credentials(token=None, api_key=None, db=AsyncMock())
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_authenticate_user_rejects_legacy_default_password_in_auto_login(
|
||||
auth_service: AuthService,
|
||||
auth_settings: AuthSettings,
|
||||
):
|
||||
auth_settings.AUTO_LOGIN = True
|
||||
auth_settings.SUPERUSER = DEFAULT_SUPERUSER
|
||||
legacy_password = LEGACY_DEFAULT_SUPERUSER_PASSWORD.get_secret_value()
|
||||
default_superuser = User(
|
||||
id=uuid4(),
|
||||
username=DEFAULT_SUPERUSER,
|
||||
password=auth_service.get_password_hash(legacy_password),
|
||||
is_active=True,
|
||||
is_superuser=True,
|
||||
)
|
||||
|
||||
with patch("langflow.services.auth.service.get_user_by_username", new=AsyncMock(return_value=default_superuser)):
|
||||
result = await auth_service.authenticate_user(DEFAULT_SUPERUSER, legacy_password, AsyncMock())
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_authenticate_user_rejects_legacy_default_password_when_auto_login_false(
|
||||
auth_service: AuthService,
|
||||
auth_settings: AuthSettings,
|
||||
):
|
||||
auth_settings.AUTO_LOGIN = False
|
||||
auth_settings.SUPERUSER = DEFAULT_SUPERUSER
|
||||
legacy_password = LEGACY_DEFAULT_SUPERUSER_PASSWORD.get_secret_value()
|
||||
default_superuser = User(
|
||||
id=uuid4(),
|
||||
username=DEFAULT_SUPERUSER,
|
||||
password=auth_service.get_password_hash(legacy_password),
|
||||
is_active=True,
|
||||
is_superuser=True,
|
||||
)
|
||||
|
||||
with patch("langflow.services.auth.service.get_user_by_username", new=AsyncMock(return_value=default_superuser)):
|
||||
result = await auth_service.authenticate_user(DEFAULT_SUPERUSER, legacy_password, AsyncMock())
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_authenticate_user_rejects_legacy_default_username_after_superuser_override(
|
||||
auth_service: AuthService,
|
||||
auth_settings: AuthSettings,
|
||||
):
|
||||
auth_settings.AUTO_LOGIN = True
|
||||
auth_settings.SUPERUSER = "custom_admin"
|
||||
legacy_password = LEGACY_DEFAULT_SUPERUSER_PASSWORD.get_secret_value()
|
||||
default_superuser = User(
|
||||
id=uuid4(),
|
||||
username=DEFAULT_SUPERUSER,
|
||||
password=auth_service.get_password_hash(legacy_password),
|
||||
is_active=True,
|
||||
is_superuser=True,
|
||||
)
|
||||
|
||||
with patch("langflow.services.auth.service.get_user_by_username", new=AsyncMock(return_value=default_superuser)):
|
||||
result = await auth_service.authenticate_user(DEFAULT_SUPERUSER, legacy_password, AsyncMock())
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_create_refresh_token_requires_refresh_type(auth_service: AuthService):
|
||||
invalid_refresh = auth_service.create_token({"sub": str(uuid4()), "type": "access"}, timedelta(minutes=1))
|
||||
|
||||
@ -2,7 +2,7 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from lfx.services.settings.auth import AuthSettings
|
||||
from lfx.services.settings.constants import DEFAULT_SUPERUSER
|
||||
from lfx.services.settings.constants import DEFAULT_SUPERUSER, DEFAULT_SUPERUSER_PASSWORD
|
||||
from pydantic import SecretStr, ValidationError
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ def test_superuser_password_is_secretstr(auto_login, tmp_path: Path):
|
||||
assert isinstance(settings.SUPERUSER_PASSWORD, SecretStr)
|
||||
|
||||
|
||||
def test_auto_login_true_forces_default_and_scrubs_password(tmp_path: Path):
|
||||
def test_auto_login_true_preserves_configured_credentials_and_scrubs_password(tmp_path: Path):
|
||||
cfg_dir = tmp_path.as_posix()
|
||||
settings = AuthSettings(
|
||||
CONFIG_DIR=cfg_dir,
|
||||
@ -21,15 +21,21 @@ def test_auto_login_true_forces_default_and_scrubs_password(tmp_path: Path):
|
||||
SUPERUSER="custom",
|
||||
SUPERUSER_PASSWORD=SecretStr("_changed"),
|
||||
)
|
||||
# Validator forces default username and scrubs password
|
||||
assert settings.SUPERUSER == DEFAULT_SUPERUSER
|
||||
assert settings.SUPERUSER == "custom"
|
||||
assert isinstance(settings.SUPERUSER_PASSWORD, SecretStr)
|
||||
assert settings.SUPERUSER_PASSWORD.get_secret_value() == "langflow"
|
||||
assert settings.SUPERUSER_PASSWORD.get_secret_value() == "_changed"
|
||||
|
||||
# reset_credentials keeps default username (AUTO_LOGIN on) and keeps password scrubbed
|
||||
# reset_credentials preserves the username and scrubs the password even in AUTO_LOGIN mode.
|
||||
settings.reset_credentials()
|
||||
assert settings.SUPERUSER == "custom"
|
||||
assert settings.SUPERUSER_PASSWORD.get_secret_value() == ""
|
||||
|
||||
|
||||
def test_default_superuser_password_is_empty(tmp_path: Path):
|
||||
cfg_dir = tmp_path.as_posix()
|
||||
settings = AuthSettings(CONFIG_DIR=cfg_dir)
|
||||
assert settings.SUPERUSER == DEFAULT_SUPERUSER
|
||||
assert settings.SUPERUSER_PASSWORD.get_secret_value() == "langflow"
|
||||
assert settings.SUPERUSER_PASSWORD.get_secret_value() == DEFAULT_SUPERUSER_PASSWORD.get_secret_value() == ""
|
||||
|
||||
|
||||
def test_auto_login_false_preserves_username_and_scrubs_password_on_reset(tmp_path: Path):
|
||||
|
||||
@ -131,8 +131,8 @@ class TestSuperuserCommand:
|
||||
|
||||
@pytest.mark.skip(reason="Skip -- default superuser is created by initialize_services() function")
|
||||
@pytest.mark.asyncio
|
||||
async def test_auto_login_forces_default_credentials(self, client):
|
||||
"""Test AUTO_LOGIN=true forces default credentials."""
|
||||
async def test_auto_login_uses_bootstrap_superuser(self, client):
|
||||
"""Test AUTO_LOGIN=true bootstraps the configured superuser."""
|
||||
# Since client fixture already creates default user, we need to test in a clean DB scenario
|
||||
# But that's why this test is skipped - the behavior is already handled by initialize_services
|
||||
|
||||
|
||||
@ -3,8 +3,10 @@ from unittest.mock import AsyncMock, patch
|
||||
import pytest
|
||||
from langflow.services.auth.exceptions import InvalidTokenError
|
||||
from langflow.services.database.models.user import User
|
||||
from langflow.services.deps import get_auth_service, session_scope
|
||||
from langflow.services.deps import get_auth_service, get_settings_service, session_scope
|
||||
from lfx.services.settings.constants import DEFAULT_SUPERUSER, LEGACY_DEFAULT_SUPERUSER_PASSWORD
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlmodel import select
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -47,6 +49,44 @@ async def test_login_unsuccessful_wrong_password(client, test_user, async_sessio
|
||||
assert response.json()["detail"] == "Incorrect username or password"
|
||||
|
||||
|
||||
async def test_login_rejects_legacy_default_superuser_password_when_auto_login_enabled(client):
|
||||
settings = get_settings_service()
|
||||
original_auto_login = settings.auth_settings.AUTO_LOGIN
|
||||
original_superuser = settings.auth_settings.SUPERUSER
|
||||
legacy_password = LEGACY_DEFAULT_SUPERUSER_PASSWORD.get_secret_value()
|
||||
settings.auth_settings.AUTO_LOGIN = True
|
||||
settings.auth_settings.SUPERUSER = DEFAULT_SUPERUSER
|
||||
|
||||
async with session_scope() as session:
|
||||
stmt = select(User).where(User.username == DEFAULT_SUPERUSER)
|
||||
user = (await session.exec(stmt)).first()
|
||||
if user is None:
|
||||
user = User(
|
||||
username=DEFAULT_SUPERUSER,
|
||||
password=get_auth_service().get_password_hash(legacy_password),
|
||||
is_active=True,
|
||||
is_superuser=True,
|
||||
)
|
||||
session.add(user)
|
||||
await session.commit()
|
||||
else:
|
||||
user.password = get_auth_service().get_password_hash(legacy_password)
|
||||
user.is_active = True
|
||||
user.is_superuser = True
|
||||
await session.commit()
|
||||
|
||||
try:
|
||||
response = await client.post(
|
||||
"api/v1/login",
|
||||
data={"username": DEFAULT_SUPERUSER, "password": legacy_password},
|
||||
)
|
||||
assert response.status_code == 401
|
||||
assert response.json()["detail"] == "Incorrect username or password"
|
||||
finally:
|
||||
settings.auth_settings.AUTO_LOGIN = original_auto_login
|
||||
settings.auth_settings.SUPERUSER = original_superuser
|
||||
|
||||
|
||||
async def test_session_endpoint_unauthenticated(client):
|
||||
"""Test /session endpoint returns authenticated=False for unauthenticated requests."""
|
||||
response = await client.get("api/v1/session")
|
||||
|
||||
@ -240,6 +240,8 @@ class TestCLISubprocessIntegration:
|
||||
LANGFLOW_DATABASE_URL=sqlite:///{db_path}
|
||||
LANGFLOW_AUTO_SAVING=false
|
||||
LANGFLOW_AUTO_LOGIN=false
|
||||
LANGFLOW_SUPERUSER=langflow
|
||||
LANGFLOW_SUPERUSER_PASSWORD=test-superuser-password
|
||||
LANGFLOW_LOG_LEVEL=ERROR
|
||||
""".strip()
|
||||
)
|
||||
|
||||
@ -1,10 +1,17 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import filelock
|
||||
import pytest
|
||||
from langflow.services.auth.utils import verify_password
|
||||
from langflow.services.database.models.user.model import User
|
||||
from langflow.services.deps import get_auth_service, get_settings_service, session_scope
|
||||
from langflow.services.utils import SetupSuperuserResult, setup_superuser, teardown_superuser
|
||||
from lfx.services.settings.constants import DEFAULT_SUPERUSER, DEFAULT_SUPERUSER_PASSWORD
|
||||
from lfx.services.settings.constants import (
|
||||
DEFAULT_SUPERUSER,
|
||||
DEFAULT_SUPERUSER_PASSWORD,
|
||||
LEGACY_DEFAULT_SUPERUSER_PASSWORD,
|
||||
)
|
||||
from pydantic import SecretStr
|
||||
from sqlmodel import select
|
||||
|
||||
_MOCK_AUTO_LOGIN_LOCK_TIMEOUT_MSG = "mock lock timeout"
|
||||
@ -24,6 +31,8 @@ async def initialized_services(monkeypatch, tmp_path):
|
||||
db_path = tmp_path / "test.db"
|
||||
monkeypatch.setenv("LANGFLOW_DATABASE_URL", f"sqlite:///{db_path}")
|
||||
monkeypatch.setenv("LANGFLOW_AUTO_LOGIN", "false")
|
||||
monkeypatch.setenv("LANGFLOW_SUPERUSER", DEFAULT_SUPERUSER)
|
||||
monkeypatch.setenv("LANGFLOW_SUPERUSER_PASSWORD", "test-superuser-password")
|
||||
|
||||
get_service_manager().factories.clear()
|
||||
get_service_manager().services.clear()
|
||||
@ -54,6 +63,110 @@ async def test_initialize_services_creates_default_superuser_when_auto_login_tru
|
||||
user = (await session.exec(stmt)).first()
|
||||
assert user is not None
|
||||
assert user.is_superuser is True
|
||||
assert verify_password("test-superuser-password", user.password) is True
|
||||
assert verify_password(LEGACY_DEFAULT_SUPERUSER_PASSWORD.get_secret_value(), user.password) is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.timeout(30)
|
||||
async def test_setup_superuser_auto_login_rotates_legacy_default_password(initialized_services): # noqa: ARG001
|
||||
"""AUTO_LOGIN startup rotates old langflow/langflow hashes left by previous releases."""
|
||||
settings = get_settings_service()
|
||||
settings.auth_settings.AUTO_LOGIN = True
|
||||
settings.auth_settings.SUPERUSER = DEFAULT_SUPERUSER
|
||||
settings.auth_settings.SUPERUSER_PASSWORD = DEFAULT_SUPERUSER_PASSWORD
|
||||
legacy_password = LEGACY_DEFAULT_SUPERUSER_PASSWORD.get_secret_value()
|
||||
|
||||
async with session_scope() as session:
|
||||
stmt = select(User).where(User.username == DEFAULT_SUPERUSER)
|
||||
user = (await session.exec(stmt)).first()
|
||||
if user is None:
|
||||
user = User(
|
||||
username=DEFAULT_SUPERUSER,
|
||||
password=get_auth_service().get_password_hash(legacy_password),
|
||||
is_superuser=True,
|
||||
is_active=True,
|
||||
)
|
||||
session.add(user)
|
||||
await session.flush()
|
||||
else:
|
||||
user.password = get_auth_service().get_password_hash(legacy_password)
|
||||
user.is_superuser = True
|
||||
await session.commit()
|
||||
|
||||
async with session_scope() as session:
|
||||
result = await setup_superuser(settings, session)
|
||||
assert result == SetupSuperuserResult.AUTO_LOGIN_ALREADY_SATISFIED
|
||||
|
||||
async with session_scope() as session:
|
||||
user = (await session.exec(select(User).where(User.username == DEFAULT_SUPERUSER))).first()
|
||||
assert user is not None
|
||||
assert verify_password(legacy_password, user.password) is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.timeout(30)
|
||||
async def test_setup_superuser_rotates_legacy_default_password_when_auto_login_false(
|
||||
initialized_services, # noqa: ARG001
|
||||
):
|
||||
"""Authenticated startup rotates old langflow/langflow hashes to the configured password."""
|
||||
settings = get_settings_service()
|
||||
settings.auth_settings.AUTO_LOGIN = False
|
||||
settings.auth_settings.SUPERUSER = DEFAULT_SUPERUSER
|
||||
settings.auth_settings.SUPERUSER_PASSWORD = SecretStr("rotated-production-password")
|
||||
legacy_password = LEGACY_DEFAULT_SUPERUSER_PASSWORD.get_secret_value()
|
||||
|
||||
async with session_scope() as session:
|
||||
stmt = select(User).where(User.username == DEFAULT_SUPERUSER)
|
||||
user = (await session.exec(stmt)).first()
|
||||
if user is None:
|
||||
user = User(
|
||||
username=DEFAULT_SUPERUSER,
|
||||
password=get_auth_service().get_password_hash(legacy_password),
|
||||
is_superuser=True,
|
||||
is_active=True,
|
||||
)
|
||||
session.add(user)
|
||||
await session.flush()
|
||||
user.password = get_auth_service().get_password_hash(legacy_password)
|
||||
user.is_superuser = True
|
||||
user.is_active = True
|
||||
user.last_login_at = datetime.now(timezone.utc)
|
||||
await session.commit()
|
||||
|
||||
async with session_scope() as session:
|
||||
result = await setup_superuser(settings, session)
|
||||
assert result == SetupSuperuserResult.SUPERUSER_UNCHANGED
|
||||
|
||||
async with session_scope() as session:
|
||||
user = (await session.exec(select(User).where(User.username == DEFAULT_SUPERUSER))).first()
|
||||
assert user is not None
|
||||
assert verify_password("rotated-production-password", user.password) is True
|
||||
assert verify_password(legacy_password, user.password) is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.timeout(30)
|
||||
async def test_setup_superuser_auto_login_fails_when_username_is_not_superuser(initialized_services): # noqa: ARG001
|
||||
"""AUTO_LOGIN setup must not accept an existing non-superuser bootstrap username."""
|
||||
settings = get_settings_service()
|
||||
settings.auth_settings.AUTO_LOGIN = True
|
||||
settings.auth_settings.SUPERUSER = "regular-user"
|
||||
settings.auth_settings.SUPERUSER_PASSWORD = DEFAULT_SUPERUSER_PASSWORD
|
||||
|
||||
async with session_scope() as session:
|
||||
user = User(
|
||||
username="regular-user",
|
||||
password=get_auth_service().get_password_hash("regular-password"),
|
||||
is_superuser=False,
|
||||
is_active=True,
|
||||
)
|
||||
session.add(user)
|
||||
await session.commit()
|
||||
|
||||
async with session_scope() as session:
|
||||
with pytest.raises(RuntimeError, match="not a superuser"):
|
||||
await setup_superuser(settings, session)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@ -71,7 +184,7 @@ async def test_teardown_superuser_removes_default_if_never_logged(initialized_se
|
||||
if not user:
|
||||
user = User(
|
||||
username=DEFAULT_SUPERUSER,
|
||||
password=DEFAULT_SUPERUSER_PASSWORD.get_secret_value(),
|
||||
password=get_auth_service().get_password_hash("test-superuser-password"),
|
||||
is_superuser=True,
|
||||
is_active=True,
|
||||
)
|
||||
@ -106,7 +219,7 @@ async def test_teardown_superuser_preserves_logged_in_default(initialized_servic
|
||||
if not user:
|
||||
user = User(
|
||||
username=DEFAULT_SUPERUSER,
|
||||
password=DEFAULT_SUPERUSER_PASSWORD.get_secret_value(),
|
||||
password=get_auth_service().get_password_hash("test-superuser-password"),
|
||||
is_superuser=True,
|
||||
is_active=True,
|
||||
)
|
||||
@ -132,8 +245,8 @@ async def test_teardown_superuser_preserves_logged_in_default(initialized_servic
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.timeout(30)
|
||||
async def test_setup_superuser_with_no_configured_credentials(initialized_services): # noqa: ARG001
|
||||
"""Test setup_superuser behavior when no superuser credentials are configured."""
|
||||
async def test_setup_superuser_with_no_configured_credentials_fails_closed(initialized_services): # noqa: ARG001
|
||||
"""AUTO_LOGIN=False must not create a superuser from predictable fallback credentials."""
|
||||
settings = get_settings_service()
|
||||
settings.auth_settings.AUTO_LOGIN = False
|
||||
settings.auth_settings.SUPERUSER = ""
|
||||
@ -141,23 +254,66 @@ async def test_setup_superuser_with_no_configured_credentials(initialized_servic
|
||||
settings.auth_settings.SUPERUSER_PASSWORD = ""
|
||||
|
||||
async with session_scope() as session:
|
||||
# This should create a default superuser since no credentials are provided
|
||||
result = await setup_superuser(settings, session)
|
||||
assert result == SetupSuperuserResult.SUPERUSER_CREATED
|
||||
with pytest.raises(ValueError, match="Username and password must be set"):
|
||||
await setup_superuser(settings, session)
|
||||
|
||||
# Verify default superuser was created
|
||||
stmt = select(User).where(User.username == DEFAULT_SUPERUSER)
|
||||
user = (await session.exec(stmt)).first()
|
||||
|
||||
@pytest.mark.parametrize("username", [DEFAULT_SUPERUSER, "custom_admin"])
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.timeout(30)
|
||||
async def test_setup_superuser_rejects_legacy_default_password_when_auto_login_false(
|
||||
initialized_services, # noqa: ARG001
|
||||
username,
|
||||
):
|
||||
"""Authenticated startup must not create a superuser with the legacy default password."""
|
||||
settings = get_settings_service()
|
||||
settings.auth_settings.AUTO_LOGIN = False
|
||||
settings.auth_settings.SUPERUSER = username
|
||||
settings.auth_settings.SUPERUSER_PASSWORD = LEGACY_DEFAULT_SUPERUSER_PASSWORD
|
||||
|
||||
async with session_scope() as session:
|
||||
with pytest.raises(ValueError, match="legacy default password"):
|
||||
await setup_superuser(settings, session)
|
||||
|
||||
async with session_scope() as session:
|
||||
user = (await session.exec(select(User).where(User.username == username))).first()
|
||||
assert user is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.timeout(30)
|
||||
async def test_setup_superuser_auto_login_ignores_configured_legacy_default_password(
|
||||
initialized_services, # noqa: ARG001
|
||||
):
|
||||
"""AUTO_LOGIN startup must not persist langflow/langflow even when env config provides it."""
|
||||
settings = get_settings_service()
|
||||
settings.auth_settings.AUTO_LOGIN = True
|
||||
settings.auth_settings.SUPERUSER = DEFAULT_SUPERUSER
|
||||
settings.auth_settings.SUPERUSER_PASSWORD = LEGACY_DEFAULT_SUPERUSER_PASSWORD
|
||||
legacy_password = LEGACY_DEFAULT_SUPERUSER_PASSWORD.get_secret_value()
|
||||
|
||||
async with session_scope() as session:
|
||||
user = (await session.exec(select(User).where(User.username == DEFAULT_SUPERUSER))).first()
|
||||
assert user is not None
|
||||
user.password = get_auth_service().get_password_hash(legacy_password)
|
||||
user.is_superuser = True
|
||||
user.is_active = True
|
||||
await session.commit()
|
||||
|
||||
async with session_scope() as session:
|
||||
assert await setup_superuser(settings, session) == SetupSuperuserResult.AUTO_LOGIN_ALREADY_SATISFIED
|
||||
|
||||
async with session_scope() as session:
|
||||
user = (await session.exec(select(User).where(User.username == DEFAULT_SUPERUSER))).first()
|
||||
assert user is not None
|
||||
assert user.is_superuser is True
|
||||
assert verify_password(legacy_password, user.password) is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.timeout(30)
|
||||
async def test_setup_superuser_with_custom_credentials(initialized_services): # noqa: ARG001
|
||||
"""Test setup_superuser behavior with custom superuser credentials."""
|
||||
from pydantic import SecretStr
|
||||
|
||||
settings = get_settings_service()
|
||||
settings.auth_settings.AUTO_LOGIN = False
|
||||
settings.auth_settings.SUPERUSER = "custom_admin"
|
||||
@ -257,7 +413,7 @@ async def test_setup_superuser_auto_login_lock_timeout_ok_when_superuser_exists(
|
||||
async with session_scope() as session:
|
||||
await get_auth_service().create_super_user(
|
||||
DEFAULT_SUPERUSER,
|
||||
DEFAULT_SUPERUSER_PASSWORD.get_secret_value(),
|
||||
"test-superuser-password",
|
||||
db=session,
|
||||
)
|
||||
|
||||
|
||||
@ -118,6 +118,8 @@ export default defineConfig({
|
||||
env: {
|
||||
LANGFLOW_DATABASE_URL: "sqlite:///./temp",
|
||||
LANGFLOW_AUTO_LOGIN: "true",
|
||||
LANGFLOW_SUPERUSER: "langflow",
|
||||
LANGFLOW_SUPERUSER_PASSWORD: "test-superuser-password", // pragma: allowlist secret
|
||||
LANGFLOW_DEACTIVATE_TRACING: "true",
|
||||
LANGFLOW_LOG_LEVEL: "ERROR",
|
||||
DO_NOT_TRACK: "true",
|
||||
|
||||
@ -50,7 +50,7 @@ test(
|
||||
.fill(TEXTS.authDefaultCredential);
|
||||
await page
|
||||
.getByPlaceholder(TEXTS.placeholderPassword)
|
||||
.fill(TEXTS.authDefaultCredential);
|
||||
.fill(TEXTS.authDefaultPassword);
|
||||
|
||||
await page.evaluate(() => {
|
||||
sessionStorage.removeItem("testMockAutoLogin");
|
||||
@ -280,7 +280,7 @@ test(
|
||||
.fill(TEXTS.authDefaultCredential);
|
||||
await page
|
||||
.getByPlaceholder(TEXTS.placeholderPassword)
|
||||
.fill(TEXTS.authDefaultCredential);
|
||||
.fill(TEXTS.authDefaultPassword);
|
||||
|
||||
await page.evaluate(() => {
|
||||
sessionStorage.removeItem("testMockAutoLogin");
|
||||
|
||||
@ -32,7 +32,7 @@ async function setupAutoLoginOff(page: Page): Promise<void> {
|
||||
.fill(TEXTS.authDefaultCredential);
|
||||
await page
|
||||
.getByPlaceholder(TEXTS.placeholderPassword)
|
||||
.fill(TEXTS.authDefaultCredential);
|
||||
.fill(TEXTS.authDefaultPassword);
|
||||
|
||||
await page.evaluate(() => {
|
||||
sessionStorage.removeItem("testMockAutoLogin");
|
||||
|
||||
@ -56,7 +56,7 @@ test(
|
||||
.fill(TEXTS.authDefaultCredential);
|
||||
await page
|
||||
.getByPlaceholder(TEXTS.placeholderPassword)
|
||||
.fill(TEXTS.authDefaultCredential);
|
||||
.fill(TEXTS.authDefaultPassword);
|
||||
await page.evaluate(() => {
|
||||
sessionStorage.removeItem("testMockAutoLogin");
|
||||
});
|
||||
@ -186,7 +186,7 @@ test(
|
||||
.fill(TEXTS.authDefaultCredential);
|
||||
await page
|
||||
.getByPlaceholder(TEXTS.placeholderPassword)
|
||||
.fill(TEXTS.authDefaultCredential);
|
||||
.fill(TEXTS.authDefaultPassword);
|
||||
await page.evaluate(() => {
|
||||
sessionStorage.removeItem("testMockAutoLogin");
|
||||
});
|
||||
|
||||
@ -41,7 +41,7 @@ test(
|
||||
.fill(TEXTS.authDefaultCredential);
|
||||
await page
|
||||
.getByPlaceholder(TEXTS.placeholderPassword)
|
||||
.fill(TEXTS.authDefaultCredential);
|
||||
.fill(TEXTS.authDefaultPassword);
|
||||
|
||||
await page.evaluate(() => {
|
||||
sessionStorage.removeItem("testMockAutoLogin");
|
||||
|
||||
@ -41,7 +41,7 @@ export const addNewUserAndLogin = async (page: Page) => {
|
||||
.fill(TEXTS.authDefaultCredential);
|
||||
await page
|
||||
.getByPlaceholder(TEXTS.placeholderPassword)
|
||||
.fill(TEXTS.authDefaultCredential);
|
||||
.fill(TEXTS.authDefaultPassword);
|
||||
|
||||
await page.evaluate(() => {
|
||||
sessionStorage.removeItem("testMockAutoLogin");
|
||||
|
||||
@ -56,7 +56,7 @@ export const TEXTS = {
|
||||
|
||||
// ─── Toasts / status messages ───────────────────────────────────────
|
||||
toastProjectDeleted: "Project deleted successfully",
|
||||
toastApiKeySaved: "Success! Your API Key has been saved.",
|
||||
toastApiKeySaved: "Success! Your API Key has been saved.", // pragma: allowlist secret
|
||||
/** Sub-string emitted by the build-flow log when a build finishes
|
||||
* successfully — used as `text=built successfully` in 36 specs. */
|
||||
toastBuiltSuccessfully: "built successfully",
|
||||
@ -64,15 +64,17 @@ export const TEXTS = {
|
||||
// ─── Auth / login screen ────────────────────────────────────────────
|
||||
/** Visible on the sign-in route when LANGFLOW_AUTO_LOGIN=false. */
|
||||
authSignInHeader: "sign in to langflow",
|
||||
/** Default seeded username/password ("langflow"/"langflow"). */
|
||||
/** Default seeded username. */
|
||||
authDefaultCredential: "langflow",
|
||||
/** Explicit Playwright-only superuser password seeded in playwright.config.ts. */
|
||||
authDefaultPassword: "test-superuser-password", // pragma: allowlist secret
|
||||
|
||||
// ─── Form placeholders (getByPlaceholder) ───────────────────────────
|
||||
placeholderUsername: "Username",
|
||||
placeholderPassword: "Password",
|
||||
placeholderPassword: "Password", // pragma: allowlist secret
|
||||
placeholderMessage: "Message",
|
||||
placeholderEmpty: "Empty",
|
||||
placeholderApiKey: "Insert your API Key",
|
||||
placeholderApiKey: "Insert your API Key", // pragma: allowlist secret
|
||||
placeholderSendMessage: "Send a message...",
|
||||
placeholderVariableName: "Enter a name for the variable...",
|
||||
|
||||
|
||||
@ -8,6 +8,6 @@ export const loginLangflow = async (page: Page) => {
|
||||
.fill(TEXTS.authDefaultCredential);
|
||||
await page
|
||||
.getByPlaceholder(TEXTS.placeholderPassword)
|
||||
.fill(TEXTS.authDefaultCredential);
|
||||
.fill(TEXTS.authDefaultPassword);
|
||||
await page.getByRole("button", { name: TEXTS.signIn }).click();
|
||||
};
|
||||
|
||||
@ -71,7 +71,7 @@ class AuthSettings(BaseSettings):
|
||||
AUTO_LOGIN: bool = Field(
|
||||
default=True, # TODO: Set to False in v2.0
|
||||
description=(
|
||||
"Enable automatic login with default credentials. "
|
||||
"Enable automatic login with a configured or generated bootstrap account. "
|
||||
"SECURITY WARNING: This bypasses authentication and should only be used in development environments. "
|
||||
"Set to False in production. This will default to False in v2.0."
|
||||
),
|
||||
@ -185,27 +185,6 @@ class AuthSettings(BaseSettings):
|
||||
# Preserve the configured username but scrub the password from memory to avoid plaintext exposure.
|
||||
self.SUPERUSER_PASSWORD = SecretStr("")
|
||||
|
||||
# If autologin is true, then we need to set the credentials to
|
||||
# the default values
|
||||
# so we need to validate the superuser and superuser_password
|
||||
# fields
|
||||
@field_validator("SUPERUSER", "SUPERUSER_PASSWORD", mode="before")
|
||||
@classmethod
|
||||
def validate_superuser(cls, value, info):
|
||||
# When AUTO_LOGIN is enabled, force superuser to use default values.
|
||||
if info.data.get("AUTO_LOGIN"):
|
||||
logger.debug("Auto login is enabled, forcing superuser to use default values")
|
||||
if info.field_name == "SUPERUSER":
|
||||
if value != DEFAULT_SUPERUSER:
|
||||
logger.debug("Resetting superuser to default value")
|
||||
return DEFAULT_SUPERUSER
|
||||
if info.field_name == "SUPERUSER_PASSWORD":
|
||||
if value != DEFAULT_SUPERUSER_PASSWORD.get_secret_value():
|
||||
logger.debug("Resetting superuser password to default value")
|
||||
return DEFAULT_SUPERUSER_PASSWORD
|
||||
|
||||
return value
|
||||
|
||||
@field_validator("SECRET_KEY", mode="before")
|
||||
@classmethod
|
||||
def get_secret_key(cls, value, info):
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
from pydantic import SecretStr
|
||||
|
||||
DEFAULT_SUPERUSER = "langflow"
|
||||
DEFAULT_SUPERUSER_PASSWORD = SecretStr("langflow")
|
||||
DEFAULT_SUPERUSER_PASSWORD = SecretStr("")
|
||||
# Only used to detect and rotate credentials created by older releases.
|
||||
LEGACY_DEFAULT_SUPERUSER_PASSWORD = SecretStr("langflow")
|
||||
|
||||
VARIABLES_TO_GET_FROM_ENVIRONMENT = [
|
||||
"COMPOSIO_API_KEY",
|
||||
|
||||
Reference in New Issue
Block a user