Add User API Token Management to Admin API and CLI (#12595)

## Summary

This PR extends the RAGFlow Admin API and CLI with comprehensive user
API token management capabilities. Administrators can now generate,
list, and delete API tokens for users through both the REST API and the
Admin CLI interface.

## Changes

### Backend API (`admin/server/`)

#### New Endpoints
- **POST `/api/v1/admin/users/<username>/new_token`** - Generate a new
API token for a user
- **GET `/api/v1/admin/users/<username>/token_list`** - List all API
tokens for a user
- **DELETE `/api/v1/admin/users/<username>/token/<token>`** - Delete a
specific API token for a user

#### Service Layer Updates (`services.py`)
- Added `get_user_api_key(username)` - Retrieves all API tokens for a
user
- Added `save_api_token(api_token)` - Saves a new API token to the
database
- Added `delete_api_token(username, token)` - Deletes an API token for a
user

### Admin CLI (`admin/client/`)

#### New Commands
- **`GENERATE TOKEN FOR USER <username>;`** - Generate a new API token
for the specified user
- **`LIST TOKENS OF <username>;`** - List all API tokens associated with
a user
- **`DROP TOKEN <token> OF <username>;`** - Delete a specific API token
for a user

### Testing

Added comprehensive test suite in `test/testcases/test_admin_api/`:
- **`test_generate_user_api_key.py`** - Tests for API token generation
- **`test_get_user_api_key.py`** - Tests for listing user API tokens
- **`test_delete_user_api_key.py`** - Tests for deleting API tokens
- **`conftest.py`** - Shared test fixtures and utilities

## Technical Details

### Token Generation
- Tokens are generated using `generate_confirmation_token()` utility
- Each token includes metadata: `tenant_id`, `token`, `beta`,
`create_time`, `create_date`
- Tokens are associated with user tenants automatically

### Security Considerations
- All endpoints require admin authentication (`@check_admin_auth`)
- Tokens are URL-encoded when passed in DELETE requests to handle
special characters
- Proper error handling for unauthorized access and missing resources

### API Response Format
All endpoints follow the standard RAGFlow response format:
```json
{
  "code": 0,
  "data": {...},
  "message": "Success message"
}
```

## Files Changed

- `admin/client/admin_client.py` - CLI token management commands
- `admin/server/routes.py` - New API endpoints
- `admin/server/services.py` - Token management service methods
- `docs/guides/admin/admin_cli.md` - CLI documentation updates
- `test/testcases/test_admin_api/conftest.py` - Test fixtures
- `test/testcases/test_admin_api/test_user_api_key_management/*` - Test
suites

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

---------

Co-authored-by: Alexander Strasser <alexander.strasser@ondewo.com>
Co-authored-by: Hetavi Shah <your.email@example.com>
This commit is contained in:
Hetavi Shah
2026-01-17 12:51:00 +05:30
committed by GitHub
parent bd9163904a
commit 46305ef35e
8 changed files with 1124 additions and 141 deletions

View File

@ -17,6 +17,7 @@
import argparse
import base64
import getpass
import urllib.parse
from cmd import Cmd
from typing import Any, Dict, List
@ -60,6 +61,9 @@ sql_command: list_services
| list_variables
| list_configs
| list_environments
| generate_key
| list_keys
| drop_key
// meta command definition
meta_command: "\\" meta_command_name [meta_args]
@ -107,6 +111,9 @@ VAR: "VAR"i
VARS: "VARS"i
CONFIGS: "CONFIGS"i
ENVS: "ENVS"i
KEY: "KEY"i
KEYS: "KEYS"i
GENERATE: "GENERATE"i
list_services: LIST SERVICES ";"
show_service: SHOW SERVICE NUMBER ";"
@ -144,6 +151,10 @@ list_variables: LIST VARS ";"
list_configs: LIST CONFIGS ";"
list_environments: LIST ENVS ";"
generate_key: GENERATE KEY FOR USER quoted_string ";"
list_keys: LIST KEYS OF quoted_string ";"
drop_key: DROP KEY quoted_string OF quoted_string ";"
show_version: SHOW VERSION ";"
action_list: identifier ("," identifier)*
@ -296,6 +307,19 @@ class AdminTransformer(Transformer):
def list_environments(self, items):
return {"type": "list_environments"}
def generate_key(self, items):
user_name = items[4]
return {"type": "generate_key", "user_name": user_name}
def list_keys(self, items):
user_name = items[3]
return {"type": "list_keys", "user_name": user_name}
def drop_key(self, items):
key = items[2]
user_name = items[4]
return {"type": "drop_key", "key": key, "user_name": user_name}
def action_list(self, items):
return items
@ -362,6 +386,9 @@ SHOW USER PERMISSION <user>
SHOW VERSION
GRANT ADMIN <user>
REVOKE ADMIN <user>
GENERATE KEY FOR USER <user>
LIST KEYS OF <user>
DROP KEY <key> OF <user>
Meta Commands:
\\?, \\h, \\help Show this help
@ -664,6 +691,12 @@ class AdminCLI(Cmd):
self._list_configs(command_dict)
case "list_environments":
self._list_environments(command_dict)
case "generate_key":
self._generate_key(command_dict)
case "list_keys":
self._list_keys(command_dict)
case "drop_key":
self._drop_key(command_dict)
case "meta":
self._handle_meta_command(command_dict)
case _:
@ -796,7 +829,6 @@ class AdminCLI(Cmd):
else:
print(f"Unknown activate status: {activate_status}.")
def _grant_admin(self, command):
user_name_tree: Tree = command["user_name"]
user_name: str = user_name_tree.children[0].strip("'\"")
@ -1044,6 +1076,46 @@ class AdminCLI(Cmd):
else:
print(f"Fail to show version, code: {res_json['code']}, message: {res_json['message']}")
def _generate_key(self, command: dict[str, Any]) -> None:
username_tree: Tree = command["user_name"]
user_name: str = username_tree.children[0].strip("'\"")
print(f"Generating API key for user: {user_name}")
url: str = f"http://{self.host}:{self.port}/api/v1/admin/users/{user_name}/new_token"
response: requests.Response = self.session.post(url)
res_json: dict[str, Any] = response.json()
if response.status_code == 200:
self._print_table_simple(res_json["data"])
else:
print(f"Failed to generate key for user {user_name}, code: {res_json['code']}, message: {res_json['message']}")
def _list_keys(self, command: dict[str, Any]) -> None:
username_tree: Tree = command["user_name"]
user_name: str = username_tree.children[0].strip("'\"")
print(f"Listing API keys for user: {user_name}")
url: str = f"http://{self.host}:{self.port}/api/v1/admin/users/{user_name}/token_list"
response: requests.Response = self.session.get(url)
res_json: dict[str, Any] = response.json()
if response.status_code == 200:
self._print_table_simple(res_json["data"])
else:
print(f"Failed to list keys for user {user_name}, code: {res_json['code']}, message: {res_json['message']}")
def _drop_key(self, command: dict[str, Any]) -> None:
key_tree: Tree = command["key"]
key: str = key_tree.children[0].strip("'\"")
username_tree: Tree = command["user_name"]
user_name: str = username_tree.children[0].strip("'\"")
print(f"Dropping API key for user: {user_name}")
# URL encode the key to handle special characters
encoded_key: str = urllib.parse.quote(key, safe="")
url: str = f"http://{self.host}:{self.port}/api/v1/admin/users/{user_name}/token/{encoded_key}"
response: requests.Response = self.session.delete(url)
res_json: dict[str, Any] = response.json()
if response.status_code == 200:
print(res_json["message"])
else:
print(f"Failed to drop key for user {user_name}, code: {res_json['code']}, message: {res_json['message']}")
def _handle_meta_command(self, command):
meta_command = command["command"]
args = command.get("args", [])

View File

@ -15,8 +15,11 @@
#
import secrets
from typing import Any
from flask import Blueprint, request
from common.time_utils import current_timestamp, datetime_format
from datetime import datetime
from flask import Blueprint, Response, request
from flask_login import current_user, login_required, logout_user
from auth import login_verify, login_admin, check_admin_auth
@ -25,19 +28,20 @@ from services import UserMgr, ServiceMgr, UserServiceMgr, SettingsMgr, ConfigMgr
from roles import RoleMgr
from api.common.exceptions import AdminException
from common.versions import get_ragflow_version
from api.utils.api_utils import generate_confirmation_token
admin_bp = Blueprint('admin', __name__, url_prefix='/api/v1/admin')
admin_bp = Blueprint("admin", __name__, url_prefix="/api/v1/admin")
@admin_bp.route('/ping', methods=['GET'])
@admin_bp.route("/ping", methods=["GET"])
def ping():
return success_response('PONG')
return success_response("PONG")
@admin_bp.route('/login', methods=['POST'])
@admin_bp.route("/login", methods=["POST"])
def login():
if not request.json:
return error_response('Authorize admin failed.' ,400)
return error_response("Authorize admin failed.", 400)
try:
email = request.json.get("email", "")
password = request.json.get("password", "")
@ -46,7 +50,7 @@ def login():
return error_response(str(e), 500)
@admin_bp.route('/logout', methods=['GET'])
@admin_bp.route("/logout", methods=["GET"])
@login_required
def logout():
try:
@ -58,7 +62,7 @@ def logout():
return error_response(str(e), 500)
@admin_bp.route('/auth', methods=['GET'])
@admin_bp.route("/auth", methods=["GET"])
@login_verify
def auth_admin():
try:
@ -67,7 +71,7 @@ def auth_admin():
return error_response(str(e), 500)
@admin_bp.route('/users', methods=['GET'])
@admin_bp.route("/users", methods=["GET"])
@login_required
@check_admin_auth
def list_users():
@ -78,18 +82,18 @@ def list_users():
return error_response(str(e), 500)
@admin_bp.route('/users', methods=['POST'])
@admin_bp.route("/users", methods=["POST"])
@login_required
@check_admin_auth
def create_user():
try:
data = request.get_json()
if not data or 'username' not in data or 'password' not in data:
if not data or "username" not in data or "password" not in data:
return error_response("Username and password are required", 400)
username = data['username']
password = data['password']
role = data.get('role', 'user')
username = data["username"]
password = data["password"]
role = data.get("role", "user")
res = UserMgr.create_user(username, password, role)
if res["success"]:
@ -105,7 +109,7 @@ def create_user():
return error_response(str(e))
@admin_bp.route('/users/<username>', methods=['DELETE'])
@admin_bp.route("/users/<username>", methods=["DELETE"])
@login_required
@check_admin_auth
def delete_user(username):
@ -122,16 +126,16 @@ def delete_user(username):
return error_response(str(e), 500)
@admin_bp.route('/users/<username>/password', methods=['PUT'])
@admin_bp.route("/users/<username>/password", methods=["PUT"])
@login_required
@check_admin_auth
def change_password(username):
try:
data = request.get_json()
if not data or 'new_password' not in data:
if not data or "new_password" not in data:
return error_response("New password is required", 400)
new_password = data['new_password']
new_password = data["new_password"]
msg = UserMgr.update_user_password(username, new_password)
return success_response(None, msg)
@ -141,15 +145,15 @@ def change_password(username):
return error_response(str(e), 500)
@admin_bp.route('/users/<username>/activate', methods=['PUT'])
@admin_bp.route("/users/<username>/activate", methods=["PUT"])
@login_required
@check_admin_auth
def alter_user_activate_status(username):
try:
data = request.get_json()
if not data or 'activate_status' not in data:
if not data or "activate_status" not in data:
return error_response("Activation status is required", 400)
activate_status = data['activate_status']
activate_status = data["activate_status"]
msg = UserMgr.update_user_activate_status(username, activate_status)
return success_response(None, msg)
except AdminException as e:
@ -158,7 +162,7 @@ def alter_user_activate_status(username):
return error_response(str(e), 500)
@admin_bp.route('/users/<username>/admin', methods=['PUT'])
@admin_bp.route("/users/<username>/admin", methods=["PUT"])
@login_required
@check_admin_auth
def grant_admin(username):
@ -173,7 +177,8 @@ def grant_admin(username):
except Exception as e:
return error_response(str(e), 500)
@admin_bp.route('/users/<username>/admin', methods=['DELETE'])
@admin_bp.route("/users/<username>/admin", methods=["DELETE"])
@login_required
@check_admin_auth
def revoke_admin(username):
@ -188,7 +193,8 @@ def revoke_admin(username):
except Exception as e:
return error_response(str(e), 500)
@admin_bp.route('/users/<username>', methods=['GET'])
@admin_bp.route("/users/<username>", methods=["GET"])
@login_required
@check_admin_auth
def get_user_details(username):
@ -202,7 +208,7 @@ def get_user_details(username):
return error_response(str(e), 500)
@admin_bp.route('/users/<username>/datasets', methods=['GET'])
@admin_bp.route("/users/<username>/datasets", methods=["GET"])
@login_required
@check_admin_auth
def get_user_datasets(username):
@ -216,7 +222,7 @@ def get_user_datasets(username):
return error_response(str(e), 500)
@admin_bp.route('/users/<username>/agents', methods=['GET'])
@admin_bp.route("/users/<username>/agents", methods=["GET"])
@login_required
@check_admin_auth
def get_user_agents(username):
@ -230,7 +236,7 @@ def get_user_agents(username):
return error_response(str(e), 500)
@admin_bp.route('/services', methods=['GET'])
@admin_bp.route("/services", methods=["GET"])
@login_required
@check_admin_auth
def get_services():
@ -241,7 +247,7 @@ def get_services():
return error_response(str(e), 500)
@admin_bp.route('/service_types/<service_type>', methods=['GET'])
@admin_bp.route("/service_types/<service_type>", methods=["GET"])
@login_required
@check_admin_auth
def get_services_by_type(service_type_str):
@ -252,7 +258,7 @@ def get_services_by_type(service_type_str):
return error_response(str(e), 500)
@admin_bp.route('/services/<service_id>', methods=['GET'])
@admin_bp.route("/services/<service_id>", methods=["GET"])
@login_required
@check_admin_auth
def get_service(service_id):
@ -263,7 +269,7 @@ def get_service(service_id):
return error_response(str(e), 500)
@admin_bp.route('/services/<service_id>', methods=['DELETE'])
@admin_bp.route("/services/<service_id>", methods=["DELETE"])
@login_required
@check_admin_auth
def shutdown_service(service_id):
@ -274,7 +280,7 @@ def shutdown_service(service_id):
return error_response(str(e), 500)
@admin_bp.route('/services/<service_id>', methods=['PUT'])
@admin_bp.route("/services/<service_id>", methods=["PUT"])
@login_required
@check_admin_auth
def restart_service(service_id):
@ -285,38 +291,38 @@ def restart_service(service_id):
return error_response(str(e), 500)
@admin_bp.route('/roles', methods=['POST'])
@admin_bp.route("/roles", methods=["POST"])
@login_required
@check_admin_auth
def create_role():
try:
data = request.get_json()
if not data or 'role_name' not in data:
if not data or "role_name" not in data:
return error_response("Role name is required", 400)
role_name: str = data['role_name']
description: str = data['description']
role_name: str = data["role_name"]
description: str = data["description"]
res = RoleMgr.create_role(role_name, description)
return success_response(res)
except Exception as e:
return error_response(str(e), 500)
@admin_bp.route('/roles/<role_name>', methods=['PUT'])
@admin_bp.route("/roles/<role_name>", methods=["PUT"])
@login_required
@check_admin_auth
def update_role(role_name: str):
try:
data = request.get_json()
if not data or 'description' not in data:
if not data or "description" not in data:
return error_response("Role description is required", 400)
description: str = data['description']
description: str = data["description"]
res = RoleMgr.update_role_description(role_name, description)
return success_response(res)
except Exception as e:
return error_response(str(e), 500)
@admin_bp.route('/roles/<role_name>', methods=['DELETE'])
@admin_bp.route("/roles/<role_name>", methods=["DELETE"])
@login_required
@check_admin_auth
def delete_role(role_name: str):
@ -327,7 +333,7 @@ def delete_role(role_name: str):
return error_response(str(e), 500)
@admin_bp.route('/roles', methods=['GET'])
@admin_bp.route("/roles", methods=["GET"])
@login_required
@check_admin_auth
def list_roles():
@ -338,7 +344,7 @@ def list_roles():
return error_response(str(e), 500)
@admin_bp.route('/roles/<role_name>/permission', methods=['GET'])
@admin_bp.route("/roles/<role_name>/permission", methods=["GET"])
@login_required
@check_admin_auth
def get_role_permission(role_name: str):
@ -349,54 +355,54 @@ def get_role_permission(role_name: str):
return error_response(str(e), 500)
@admin_bp.route('/roles/<role_name>/permission', methods=['POST'])
@admin_bp.route("/roles/<role_name>/permission", methods=["POST"])
@login_required
@check_admin_auth
def grant_role_permission(role_name: str):
try:
data = request.get_json()
if not data or 'actions' not in data or 'resource' not in data:
if not data or "actions" not in data or "resource" not in data:
return error_response("Permission is required", 400)
actions: list = data['actions']
resource: str = data['resource']
actions: list = data["actions"]
resource: str = data["resource"]
res = RoleMgr.grant_role_permission(role_name, actions, resource)
return success_response(res)
except Exception as e:
return error_response(str(e), 500)
@admin_bp.route('/roles/<role_name>/permission', methods=['DELETE'])
@admin_bp.route("/roles/<role_name>/permission", methods=["DELETE"])
@login_required
@check_admin_auth
def revoke_role_permission(role_name: str):
try:
data = request.get_json()
if not data or 'actions' not in data or 'resource' not in data:
if not data or "actions" not in data or "resource" not in data:
return error_response("Permission is required", 400)
actions: list = data['actions']
resource: str = data['resource']
actions: list = data["actions"]
resource: str = data["resource"]
res = RoleMgr.revoke_role_permission(role_name, actions, resource)
return success_response(res)
except Exception as e:
return error_response(str(e), 500)
@admin_bp.route('/users/<user_name>/role', methods=['PUT'])
@admin_bp.route("/users/<user_name>/role", methods=["PUT"])
@login_required
@check_admin_auth
def update_user_role(user_name: str):
try:
data = request.get_json()
if not data or 'role_name' not in data:
if not data or "role_name" not in data:
return error_response("Role name is required", 400)
role_name: str = data['role_name']
role_name: str = data["role_name"]
res = RoleMgr.update_user_role(user_name, role_name)
return success_response(res)
except Exception as e:
return error_response(str(e), 500)
@admin_bp.route('/users/<user_name>/permission', methods=['GET'])
@admin_bp.route("/users/<user_name>/permission", methods=["GET"])
@login_required
@check_admin_auth
def get_user_permission(user_name: str):
@ -406,19 +412,20 @@ def get_user_permission(user_name: str):
except Exception as e:
return error_response(str(e), 500)
@admin_bp.route('/variables', methods=['PUT'])
@admin_bp.route("/variables", methods=["PUT"])
@login_required
@check_admin_auth
def set_variable():
try:
data = request.get_json()
if not data and 'var_name' not in data:
if not data and "var_name" not in data:
return error_response("Var name is required", 400)
if 'var_value' not in data:
if "var_value" not in data:
return error_response("Var value is required", 400)
var_name: str = data['var_name']
var_value: str = data['var_value']
var_name: str = data["var_name"]
var_value: str = data["var_value"]
SettingsMgr.update_by_name(var_name, var_value)
return success_response(None, "Set variable successfully")
@ -427,7 +434,8 @@ def set_variable():
except Exception as e:
return error_response(str(e), 500)
@admin_bp.route('/variables', methods=['GET'])
@admin_bp.route("/variables", methods=["GET"])
@login_required
@check_admin_auth
def get_variable():
@ -439,9 +447,9 @@ def get_variable():
# get var
data = request.get_json()
if not data and 'var_name' not in data:
if not data and "var_name" not in data:
return error_response("Var name is required", 400)
var_name: str = data['var_name']
var_name: str = data["var_name"]
res = SettingsMgr.get_by_name(var_name)
return success_response(res)
except AdminException as e:
@ -449,7 +457,8 @@ def get_variable():
except Exception as e:
return error_response(str(e), 500)
@admin_bp.route('/configs', methods=['GET'])
@admin_bp.route("/configs", methods=["GET"])
@login_required
@check_admin_auth
def get_config():
@ -461,7 +470,8 @@ def get_config():
except Exception as e:
return error_response(str(e), 500)
@admin_bp.route('/environments', methods=['GET'])
@admin_bp.route("/environments", methods=["GET"])
@login_required
@check_admin_auth
def get_environments():
@ -473,7 +483,69 @@ def get_environments():
except Exception as e:
return error_response(str(e), 500)
@admin_bp.route('/version', methods=['GET'])
@admin_bp.route("/users/<username>/new_token", methods=["POST"])
@login_required
@check_admin_auth
def generate_user_api_key(username: str) -> tuple[Response, int]:
try:
user_details: list[dict[str, Any]] = UserMgr.get_user_details(username)
if not user_details:
return error_response("User not found!", 404)
tenants: list[dict[str, Any]] = UserServiceMgr.get_user_tenants(username)
if not tenants:
return error_response("Tenant not found!", 404)
tenant_id: str = tenants[0]["tenant_id"]
token: str = generate_confirmation_token()
obj: dict[str, Any] = {
"tenant_id": tenant_id,
"token": token,
"beta": generate_confirmation_token().replace("ragflow-", "")[:32],
"create_time": current_timestamp(),
"create_date": datetime_format(datetime.now()),
"update_time": None,
"update_date": None,
}
if not UserMgr.save_api_token(obj):
return error_response("Failed to generate API key!", 500)
return success_response(obj, "API key generated successfully")
except AdminException as e:
return error_response(e.message, e.code)
except Exception as e:
return error_response(str(e), 500)
@admin_bp.route("/users/<username>/token_list", methods=["GET"])
@login_required
@check_admin_auth
def get_user_api_keys(username: str) -> tuple[Response, int]:
try:
api_keys: list[dict[str, Any]] = UserMgr.get_user_api_key(username)
return success_response(api_keys, "Get user API keys")
except AdminException as e:
return error_response(e.message, e.code)
except Exception as e:
return error_response(str(e), 500)
@admin_bp.route("/users/<username>/token/<token>", methods=["DELETE"])
@login_required
@check_admin_auth
def delete_user_api_key(username: str, token: str) -> tuple[Response, int]:
try:
deleted = UserMgr.delete_api_token(username, token)
if deleted:
return success_response(None, "API key deleted successfully")
else:
return error_response("API key not found or could not be deleted", 404)
except AdminException as e:
return error_response(e.message, e.code)
except Exception as e:
return error_response(str(e), 500)
@admin_bp.route("/version", methods=["GET"])
@login_required
@check_admin_auth
def show_version():

View File

@ -17,14 +17,18 @@
import os
import logging
import re
from typing import Any
from werkzeug.security import check_password_hash
from common.constants import ActiveEnum
from api.db.services import UserService
from api.db.joint_services.user_account_service import create_new_user, delete_user_data
from api.db.services.canvas_service import UserCanvasService
from api.db.services.user_service import TenantService
from api.db.services.user_service import TenantService, UserTenantService
from api.db.services.knowledgebase_service import KnowledgebaseService
from api.db.services.system_settings_service import SystemSettingsService
from api.db.services.api_service import APITokenService
from api.db.db_models import APIToken
from api.utils.crypt import decrypt
from api.utils import health_utils
@ -38,13 +42,15 @@ class UserMgr:
users = UserService.get_all_users()
result = []
for user in users:
result.append({
'email': user.email,
'nickname': user.nickname,
'create_date': user.create_date,
'is_active': user.is_active,
'is_superuser': user.is_superuser,
})
result.append(
{
"email": user.email,
"nickname": user.nickname,
"create_date": user.create_date,
"is_active": user.is_active,
"is_superuser": user.is_superuser,
}
)
return result
@staticmethod
@ -53,19 +59,21 @@ class UserMgr:
users = UserService.query_user_by_email(username)
result = []
for user in users:
result.append({
'avatar': user.avatar,
'email': user.email,
'language': user.language,
'last_login_time': user.last_login_time,
'is_active': user.is_active,
'is_anonymous': user.is_anonymous,
'login_channel': user.login_channel,
'status': user.status,
'is_superuser': user.is_superuser,
'create_date': user.create_date,
'update_date': user.update_date
})
result.append(
{
"avatar": user.avatar,
"email": user.email,
"language": user.language,
"last_login_time": user.last_login_time,
"is_active": user.is_active,
"is_anonymous": user.is_anonymous,
"login_channel": user.login_channel,
"status": user.status,
"is_superuser": user.is_superuser,
"create_date": user.create_date,
"update_date": user.update_date,
}
)
return result
@staticmethod
@ -127,8 +135,8 @@ class UserMgr:
# format activate_status before handle
_activate_status = activate_status.lower()
target_status = {
'on': ActiveEnum.ACTIVE.value,
'off': ActiveEnum.INACTIVE.value,
"on": ActiveEnum.ACTIVE.value,
"off": ActiveEnum.INACTIVE.value,
}.get(_activate_status)
if not target_status:
raise AdminException(f"Invalid activate_status: {activate_status}")
@ -138,6 +146,49 @@ class UserMgr:
UserService.update_user(usr.id, {"is_active": target_status})
return f"Turn {_activate_status} user activate status successfully!"
@staticmethod
def get_user_api_key(username: str) -> list[dict[str, Any]]:
# use email to find user. check exist and unique.
user_list: list[Any] = UserService.query_user_by_email(username)
if not user_list:
raise UserNotFoundError(username)
elif len(user_list) > 1:
raise AdminException(f"More than one user with username '{username}' found!")
usr: Any = user_list[0]
# tenant_id is typically the same as user_id for the owner tenant
tenant_id: str = usr.id
# Query all API tokens for this tenant
api_tokens: Any = APITokenService.query(tenant_id=tenant_id)
result: list[dict[str, Any]] = []
for token_obj in api_tokens:
result.append(token_obj.to_dict())
return result
@staticmethod
def save_api_token(api_token: dict[str, Any]) -> bool:
return APITokenService.save(**api_token)
@staticmethod
def delete_api_token(username: str, token: str) -> bool:
# use email to find user. check exist and unique.
user_list: list[Any] = UserService.query_user_by_email(username)
if not user_list:
raise UserNotFoundError(username)
elif len(user_list) > 1:
raise AdminException(f"Exist more than 1 user: {username}!")
usr: Any = user_list[0]
# tenant_id is typically the same as user_id for the owner tenant
tenant_id: str = usr.id
# Delete the API token
deleted_count: int = APITokenService.filter_delete([APIToken.tenant_id == tenant_id, APIToken.token == token])
return deleted_count > 0
@staticmethod
def grant_admin(username: str):
# use email to find user. check exist and unique.
@ -146,6 +197,7 @@ class UserMgr:
raise UserNotFoundError(username)
elif len(user_list) > 1:
raise AdminException(f"Exist more than 1 user: {username}!")
# check activate status different from new
usr = user_list[0]
if usr.is_superuser:
@ -172,7 +224,6 @@ class UserMgr:
class UserServiceMgr:
@staticmethod
def get_user_datasets(username):
# use email to find user.
@ -202,39 +253,43 @@ class UserServiceMgr:
tenant_ids = [m["tenant_id"] for m in tenants]
# filter permitted agents and owned agents
res = UserCanvasService.get_all_agents_by_tenant_ids(tenant_ids, usr.id)
return [{
'title': r['title'],
'permission': r['permission'],
'canvas_category': r['canvas_category'].split('_')[0],
'avatar': r['avatar']
} for r in res]
return [{"title": r["title"], "permission": r["permission"], "canvas_category": r["canvas_category"].split("_")[0], "avatar": r["avatar"]} for r in res]
@staticmethod
def get_user_tenants(email: str) -> list[dict[str, Any]]:
users: list[Any] = UserService.query_user_by_email(email)
if not users:
raise UserNotFoundError(email)
user: Any = users[0]
tenants: list[dict[str, Any]] = UserTenantService.get_tenants_by_user_id(user.id)
return tenants
class ServiceMgr:
@staticmethod
def get_all_services():
doc_engine = os.getenv('DOC_ENGINE', 'elasticsearch')
doc_engine = os.getenv("DOC_ENGINE", "elasticsearch")
result = []
configs = SERVICE_CONFIGS.configs
for service_id, config in enumerate(configs):
config_dict = config.to_dict()
if config_dict['service_type'] == 'retrieval':
if config_dict['extra']['retrieval_type'] != doc_engine:
if config_dict["service_type"] == "retrieval":
if config_dict["extra"]["retrieval_type"] != doc_engine:
continue
try:
service_detail = ServiceMgr.get_service_details(service_id)
if "status" in service_detail:
config_dict['status'] = service_detail['status']
config_dict["status"] = service_detail["status"]
else:
config_dict['status'] = 'timeout'
config_dict["status"] = "timeout"
except Exception as e:
logging.warning(f"Can't get service details, error: {e}")
config_dict['status'] = 'timeout'
if not config_dict['host']:
config_dict['host'] = '-'
if not config_dict['port']:
config_dict['port'] = '-'
config_dict["status"] = "timeout"
if not config_dict["host"]:
config_dict["host"] = "-"
if not config_dict["port"]:
config_dict["port"] = "-"
result.append(config_dict)
return result
@ -250,11 +305,11 @@ class ServiceMgr:
raise AdminException(f"invalid service_index: {service_idx}")
service_config = configs[service_idx]
service_info = {'name': service_config.name, 'detail_func_name': service_config.detail_func_name}
service_info = {"name": service_config.name, "detail_func_name": service_config.detail_func_name}
detail_func = getattr(health_utils, service_info.get('detail_func_name'))
detail_func = getattr(health_utils, service_info.get("detail_func_name"))
res = detail_func()
res.update({'service_name': service_info.get('name')})
res.update({"service_name": service_info.get("name")})
return res
@staticmethod
@ -265,19 +320,21 @@ class ServiceMgr:
def restart_service(service_id: int):
raise AdminException("restart_service: not implemented")
class SettingsMgr:
@staticmethod
def get_all():
settings = SystemSettingsService.get_all()
result = []
for setting in settings:
result.append({
'name': setting.name,
'source': setting.source,
'data_type': setting.data_type,
'value': setting.value,
})
result.append(
{
"name": setting.name,
"source": setting.source,
"data_type": setting.data_type,
"value": setting.value,
}
)
return result
@staticmethod
@ -287,12 +344,14 @@ class SettingsMgr:
raise AdminException(f"Can't get setting: {name}")
result = []
for setting in settings:
result.append({
'name': setting.name,
'source': setting.source,
'data_type': setting.data_type,
'value': setting.value,
})
result.append(
{
"name": setting.name,
"source": setting.source,
"data_type": setting.data_type,
"value": setting.value,
}
)
return result
@staticmethod
@ -308,8 +367,8 @@ class SettingsMgr:
else:
raise AdminException(f"No setting: {name}")
class ConfigMgr:
class ConfigMgr:
@staticmethod
def get_all():
result = []
@ -319,12 +378,13 @@ class ConfigMgr:
result.append(config_dict)
return result
class EnvironmentsMgr:
@staticmethod
def get_all():
result = []
env_kv = {"env": "DOC_ENGINE", "value": os.getenv('DOC_ENGINE')}
env_kv = {"env": "DOC_ENGINE", "value": os.getenv("DOC_ENGINE")}
result.append(env_kv)
env_kv = {"env": "DEFAULT_SUPERUSER_EMAIL", "value": os.getenv("DEFAULT_SUPERUSER_EMAIL", "admin@ragflow.io")}

View File

@ -93,6 +93,21 @@ Commands are case-insensitive and must be terminated with a semicolon(;).
- Changes the user to active or inactive.
- [Example](#example-alter-user-active)
`GENERATE KEY FOR USER <username>;`
- Generates a new API key for the specified user.
- [Example](#example-generate-key)
`LIST KEYS OF <username>;`
- Lists all API keys associated with the specified user.
- [Example](#example-list-keys)
`DROP KEY <key> OF <username>;`
- Deletes a specific API key for the specified user.
- [Example](#example-drop-key)
### Data and Agent Commands
`LIST DATASETS OF <username>;`
@ -345,6 +360,44 @@ Delete done!
Delete user's data at the same time.
<span id="example-generate-key"></span>
- Generate API key for user.
```
admin> generate key for user "example@ragflow.io";
Generating API key for user: example@ragflow.io
+----------------------------------+-------------------------------+---------------+----------------------------------+-----------------------------------------------------+-------------+-------------+
| beta | create_date | create_time | tenant_id | token | update_date | update_time |
+----------------------------------+-------------------------------+---------------+----------------------------------+-----------------------------------------------------+-------------+-------------+
| Es9OpZ6hrnPGeYA3VU1xKUkj6NCb7cp- | Mon, 12 Jan 2026 15:19:11 GMT | 1768227551361 | 5d5ea8a3efc111f0a79b80fa5b90e659 | ragflow-piwVJHEk09M5UN3LS_Xx9HA7yehs3yNOc9GGsD4jzus | None | None |
+----------------------------------+-------------------------------+---------------+----------------------------------+-----------------------------------------------------+-------------+-------------+
```
<span id="example-list-keys"></span>
- List all API keys for user.
```
admin> list keys of "example@ragflow.io";
Listing API keys for user: example@ragflow.io
+----------------------------------+-------------------------------+---------------+-----------+--------+----------------------------------+-----------------------------------------------------+-------------------------------+---------------+
| beta | create_date | create_time | dialog_id | source | tenant_id | token | update_date | update_time |
+----------------------------------+-------------------------------+---------------+-----------+--------+----------------------------------+-----------------------------------------------------+-------------------------------+---------------+
| Es9OpZ6hrnPGeYA3VU1xKUkj6NCb7cp- | Mon, 12 Jan 2026 15:19:11 GMT | 1768227551361 | None | None | 5d5ea8a3efc111f0a79b80fa5b90e659 | ragflow-piwVJHEk09M5UN3LS_Xx9HA7yehs3yNOc9GGsD4jzus | Mon, 12 Jan 2026 15:19:11 GMT | 1768227551361 |
+----------------------------------+-------------------------------+---------------+-----------+--------+----------------------------------+-----------------------------------------------------+-------------------------------+---------------+
```
<span id="example-drop-key"></span>
- Drop API key for user.
```
admin> drop key "ragflow-piwVJHEk09M5UN3LS_Xx9HA7yehs3yNOc9GGsD4jzus" of "example@ragflow.io";
Dropping API key for user: example@ragflow.io
API key deleted successfully
```
<span id="example-list-datasets-of-user"></span>
- List the specified user's dataset.
@ -512,6 +565,21 @@ Commands:
ALTER USER ACTIVE <user> <on/off>
LIST DATASETS OF <user>
LIST AGENTS OF <user>
CREATE ROLE <role>
DROP ROLE <role>
ALTER ROLE <role> SET DESCRIPTION <description>
LIST ROLES
SHOW ROLE <role>
GRANT <action_list> ON <function> TO ROLE <role>
REVOKE <action_list> ON <function> TO ROLE <role>
ALTER USER <user> SET ROLE <role>
SHOW USER PERMISSION <user>
SHOW VERSION
GRANT ADMIN <user>
REVOKE ADMIN <user>
GENERATE KEY FOR USER <user>
LIST KEYS OF <user>
DROP KEY <key> OF <user>
Meta Commands:
\?, \h, \help Show this help
@ -525,4 +593,3 @@ admin> \q
command: \q
Goodbye!
```

View File

@ -0,0 +1,120 @@
#
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import os
import urllib.parse
from typing import Any
import pytest
import requests
from configs import VERSION
# Admin API runs on port 9381
ADMIN_HOST_ADDRESS = os.getenv("ADMIN_HOST_ADDRESS", "http://127.0.0.1:9381")
UNAUTHORIZED_ERROR_MESSAGE = "<!doctype html>\n<html lang=en>\n<title>401 unauthorized</title>\n<h1>unauthorized</h1>\n<p>the server could not verify that you are authorized to access the url requested. you either supplied the wrong credentials (e.g. a bad password), or your browser doesn&#39;t understand how to supply the credentials required.</p>\n"
# password is "admin"
ENCRYPTED_ADMIN_PASSWORD: str = """WBPsJbL/W+1HN+hchm5pgu1YC3yMEb/9MFtsanZrpKEE9kAj4u09EIIVDtIDZhJOdTjz5pp5QW9TwqXBfQ2qzDqVJiwK7HGcNsoPi4wQPCmnLo0fs62QklMlg7l1Q7fjGRgV+KWtvNUce2PFzgrcAGDqRIuA/slSclKUEISEiK4z62rdDgvHT8LyuACuF1lPUY5wV0m/MbmGijRJlgvglAF8BX0BP8rQr8wZeaJdcnAy/keuODCjltMZDL06tYluN7HoiU+qlhBB+ltqG411oO/+vVhBgWsuVVOHd8uMjJEL320GUWUicprDUZvjlLaSSqVyyOiRMHpqAE9eHEecWg=="""
def admin_login(session: requests.Session, email: str = "admin@ragflow.io", password: str = "admin") -> str:
"""Helper function to login as admin and return authorization token"""
url: str = f"{ADMIN_HOST_ADDRESS}/api/{VERSION}/admin/login"
response: requests.Response = session.post(url, json={"email": email, "password": ENCRYPTED_ADMIN_PASSWORD})
res_json: dict[str, Any] = response.json()
if res_json.get("code") != 0:
raise Exception(res_json.get("message"))
# Admin login uses session cookies and Authorization header
# Set Authorization header for subsequent requests
auth: str = response.headers.get("Authorization", "")
if auth:
session.headers.update({"Authorization": auth})
return auth
@pytest.fixture(scope="session")
def admin_session() -> requests.Session:
"""Fixture to create an admin session with login"""
session: requests.Session = requests.Session()
try:
admin_login(session)
except Exception as e:
pytest.skip(f"Admin login failed: {e}")
return session
def generate_user_api_key(session: requests.Session, user_name: str) -> dict[str, Any]:
"""Helper function to generate API key for a user
Returns:
Dict containing the full API response with keys: code, message, data
"""
url: str = f"{ADMIN_HOST_ADDRESS}/api/{VERSION}/admin/users/{user_name}/new_token"
response: requests.Response = session.post(url)
# Some error responses (e.g., 401) may return HTML instead of JSON.
try:
res_json: dict[str, Any] = response.json()
except requests.exceptions.JSONDecodeError:
return {
"code": response.status_code,
"message": response.text,
"data": None,
}
return res_json
def get_user_api_key(session: requests.Session, username: str) -> dict[str, Any]:
"""Helper function to get API keys for a user
Returns:
Dict containing the full API response with keys: code, message, data
"""
url: str = f"{ADMIN_HOST_ADDRESS}/api/{VERSION}/admin/users/{username}/token_list"
response: requests.Response = session.get(url)
try:
res_json: dict[str, Any] = response.json()
except requests.exceptions.JSONDecodeError:
return {
"code": response.status_code,
"message": response.text,
"data": None,
}
return res_json
def delete_user_api_key(session: requests.Session, username: str, token: str) -> dict[str, Any]:
"""Helper function to delete an API key for a user
Returns:
Dict containing the full API response with keys: code, message, data
"""
# URL encode the token to handle special characters
encoded_token: str = urllib.parse.quote(token, safe="")
url: str = f"{ADMIN_HOST_ADDRESS}/api/{VERSION}/admin/users/{username}/token/{encoded_token}"
response: requests.Response = session.delete(url)
try:
res_json: dict[str, Any] = response.json()
except requests.exceptions.JSONDecodeError:
return {
"code": response.status_code,
"message": response.text,
"data": None,
}
return res_json

View File

@ -0,0 +1,191 @@
#
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from typing import Any
import pytest
import requests
from conftest import delete_user_api_key, generate_user_api_key, get_user_api_key, UNAUTHORIZED_ERROR_MESSAGE
from common.constants import RetCode
from configs import EMAIL, HOST_ADDRESS, PASSWORD, VERSION
class TestDeleteUserApiKey:
@pytest.mark.p1
def test_delete_user_api_key_success(self, admin_session: requests.Session) -> None:
"""Test successfully deleting an API key for a user"""
user_name: str = EMAIL
# Generate an API key first
generate_response: dict[str, Any] = generate_user_api_key(admin_session, user_name)
assert generate_response.get("code") == RetCode.SUCCESS, f"Generate should succeed, got code {generate_response.get('code')}"
generated_key: dict[str, Any] = generate_response["data"]
token: str = generated_key["token"]
# Delete the API key
delete_response: dict[str, Any] = delete_user_api_key(admin_session, user_name, token)
# Verify response
assert delete_response.get("code") == RetCode.SUCCESS, f"Delete should succeed, got code {delete_response.get('code')}"
assert "message" in delete_response, "Response should contain message"
message: str = delete_response.get("message", "")
assert message == "API key deleted successfully", f"Message should indicate success, got: {message}"
@pytest.mark.p1
def test_user_api_key_removed_from_list_after_deletion(self, admin_session: requests.Session) -> None:
"""Test that deleted API key is removed from the list"""
user_name: str = EMAIL
# Generate an API key
generate_response: dict[str, Any] = generate_user_api_key(admin_session, user_name)
assert generate_response.get("code") == RetCode.SUCCESS, f"Generate should succeed, got code {generate_response.get('code')}"
generated_key: dict[str, Any] = generate_response["data"]
token: str = generated_key["token"]
# Verify the key exists in the list
get_response_before: dict[str, Any] = get_user_api_key(admin_session, user_name)
assert get_response_before.get("code") == RetCode.SUCCESS, f"Get should succeed, got code {get_response_before.get('code')}"
api_keys_before: list[dict[str, Any]] = get_response_before["data"]
token_found_before: bool = any(key.get("token") == token for key in api_keys_before)
assert token_found_before, "Generated API key should be in the list before deletion"
# Delete the API key
delete_response: dict[str, Any] = delete_user_api_key(admin_session, user_name, token)
assert delete_response.get("code") == RetCode.SUCCESS, f"Delete should succeed, got code {delete_response.get('code')}"
# Verify the key is no longer in the list
get_response_after: dict[str, Any] = get_user_api_key(admin_session, user_name)
assert get_response_after.get("code") == RetCode.SUCCESS, f"Get should succeed, got code {get_response_after.get('code')}"
api_keys_after: list[dict[str, Any]] = get_response_after["data"]
token_found_after: bool = any(key.get("token") == token for key in api_keys_after)
assert not token_found_after, "Deleted API key should not be in the list after deletion"
@pytest.mark.p2
def test_delete_user_api_key_response_structure(self, admin_session: requests.Session) -> None:
"""Test that delete_user_api_key returns correct response structure"""
user_name: str = EMAIL
# Generate an API key
generate_response: dict[str, Any] = generate_user_api_key(admin_session, user_name)
assert generate_response.get("code") == RetCode.SUCCESS, f"Generate should succeed, got code {generate_response.get('code')}"
token: str = generate_response["data"]["token"]
# Delete the API key
delete_response: dict[str, Any] = delete_user_api_key(admin_session, user_name, token)
# Verify response structure
assert delete_response.get("code") == RetCode.SUCCESS, f"Response code should be {RetCode.SUCCESS}, got {delete_response.get('code')}"
assert "message" in delete_response, "Response should contain message"
# Data can be None for delete operations
assert "data" in delete_response, "Response should contain data field"
@pytest.mark.p2
def test_delete_user_api_key_twice(self, admin_session: requests.Session) -> None:
"""Test that deleting the same token twice behaves correctly"""
user_name: str = EMAIL
# Generate an API key
generate_response: dict[str, Any] = generate_user_api_key(admin_session, user_name)
assert generate_response.get("code") == RetCode.SUCCESS, f"Generate should succeed, got code {generate_response.get('code')}"
token: str = generate_response["data"]["token"]
# Delete the API key first time
delete_response1: dict[str, Any] = delete_user_api_key(admin_session, user_name, token)
assert delete_response1.get("code") == RetCode.SUCCESS, f"First delete should succeed, got code {delete_response1.get('code')}"
# Try to delete the same token again
delete_response2: dict[str, Any] = delete_user_api_key(admin_session, user_name, token)
# Second delete should fail since token no longer exists
assert delete_response2.get("code") == RetCode.NOT_FOUND, "Second delete should fail for already deleted token"
assert "message" in delete_response2, "Response should contain message"
@pytest.mark.p2
def test_delete_user_api_key_with_nonexistent_token(self, admin_session: requests.Session) -> None:
"""Test deleting a non-existent API key fails"""
user_name: str = EMAIL
nonexistent_token: str = "ragflow-nonexistent-token-12345"
# Try to delete a non-existent token
delete_response: dict[str, Any] = delete_user_api_key(admin_session, user_name, nonexistent_token)
# Should return error
assert delete_response.get("code") == RetCode.NOT_FOUND, "Delete should fail for non-existent token"
assert "message" in delete_response, "Response should contain message"
message: str = delete_response.get("message", "")
assert message == "API key not found or could not be deleted", f"Message should indicate token not found, got: {message}"
@pytest.mark.p2
def test_delete_user_api_key_with_nonexistent_user(self, admin_session: requests.Session) -> None:
"""Test deleting API key for non-existent user fails"""
nonexistent_user: str = "nonexistent_user_12345@example.com"
token: str = "ragflow-test-token-12345"
# Try to delete token for non-existent user
delete_response: dict[str, Any] = delete_user_api_key(admin_session, nonexistent_user, token)
# Should return error
assert delete_response.get("code") == RetCode.NOT_FOUND, "Delete should fail for non-existent user"
assert "message" in delete_response, "Response should contain message"
message: str = delete_response.get("message", "")
expected_message: str = f"User '{nonexistent_user}' not found"
assert message == expected_message, f"Message should indicate user not found, got: {message}"
@pytest.mark.p2
def test_delete_user_api_key_wrong_user_token(self, admin_session: requests.Session) -> None:
"""Test that deleting a token belonging to another user fails"""
user_name: str = EMAIL
# create second user
url: str = HOST_ADDRESS + f"/{VERSION}/user/register"
user2_email: str = "qa2@ragflow.io"
register_data: dict[str, str] = {"email": user2_email, "nickname": "qa2", "password": PASSWORD}
res: Any = requests.post(url=url, json=register_data)
res: dict[str, Any] = res.json()
if res.get("code") != 0 and "has already registered" not in res.get("message"):
raise Exception(f"Failed to create second user: {res.get("message")}")
# Generate a token for the test user
generate_response: dict[str, Any] = generate_user_api_key(admin_session, user_name)
assert generate_response.get("code") == RetCode.SUCCESS, f"Generate should succeed, got code {generate_response.get('code')}"
token: str = generate_response["data"]["token"]
# Try to delete with the second username
delete_response: dict[str, Any] = delete_user_api_key(admin_session, user2_email, token)
# Should fail because user doesn't exist or token doesn't belong to that user
assert delete_response.get("code") == RetCode.NOT_FOUND, "Delete should fail for wrong user"
assert "message" in delete_response, "Response should contain message"
message: str = delete_response.get("message", "")
expected_message: str = "API key not found or could not be deleted"
assert message == expected_message, f"Message should indicate user not found, got: {message}"
@pytest.mark.p3
def test_delete_user_api_key_without_auth(self) -> None:
"""Test that deleting API key without admin auth fails"""
session: requests.Session = requests.Session()
user_name: str = EMAIL
token: str = "ragflow-test-token-12345"
response: dict[str, Any] = delete_user_api_key(session, user_name, token)
# Verify error response
assert response.get("code") == RetCode.UNAUTHORIZED, "Response code should indicate error"
assert "message" in response, "Response should contain message"
message: str = response.get("message", "").lower()
# The message is an HTML string indicating unauthorized user.
assert message == UNAUTHORIZED_ERROR_MESSAGE

View File

@ -0,0 +1,232 @@
#
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from typing import Any, Dict, List
import pytest
import requests
from common.constants import RetCode
from conftest import generate_user_api_key, get_user_api_key, UNAUTHORIZED_ERROR_MESSAGE
from configs import EMAIL
class TestGenerateUserApiKey:
@pytest.mark.p1
def test_generate_user_api_key_success(self, admin_session: requests.Session) -> None:
"""Test successfully generating API key for a user"""
# Use the test user email (get_user_details expects email)
user_name: str = EMAIL
# Generate API key
response: Dict[str, Any] = generate_user_api_key(admin_session, user_name)
# Verify response code, message, and data
assert response.get("code") == RetCode.SUCCESS, f"Response code should be {RetCode.SUCCESS}, got {response.get('code')}"
assert "message" in response, "Response should contain message"
assert "data" in response, "Response should contain data"
assert response.get("data") is not None, "API key generation should return data"
result: Dict[str, Any] = response["data"]
# Verify response structure
assert "tenant_id" in result, "Response should contain tenant_id"
assert "token" in result, "Response should contain token"
assert "beta" in result, "Response should contain beta"
assert "create_time" in result, "Response should contain create_time"
assert "create_date" in result, "Response should contain create_date"
# Verify token format (should start with "ragflow-")
token: str = result["token"]
assert isinstance(token, str), "Token should be a string"
assert len(token) > 0, "Token should not be empty"
# Verify beta is independently generated
beta: str = result["beta"]
assert isinstance(beta, str), "Beta should be a string"
assert len(beta) == 32, "Beta should be 32 characters"
# Beta should be independent from token (not derived from it)
if token.startswith("ragflow-"):
token_without_prefix: str = token.replace("ragflow-", "")[:32]
assert beta != token_without_prefix, "Beta should be independently generated, not derived from token"
@pytest.mark.p1
def test_generate_user_api_key_appears_in_list(self, admin_session: requests.Session) -> None:
"""Test that generated API key appears in get_user_api_key list"""
user_name: str = EMAIL
# Generate API key
generate_response: Dict[str, Any] = generate_user_api_key(admin_session, user_name)
assert generate_response.get("code") == RetCode.SUCCESS, f"Generate should succeed, got code {generate_response.get('code')}"
generated_key: Dict[str, Any] = generate_response["data"]
token: str = generated_key["token"]
# Get all API keys for the user
get_response: Dict[str, Any] = get_user_api_key(admin_session, user_name)
assert get_response.get("code") == RetCode.SUCCESS, f"Get should succeed, got code {get_response.get('code')}"
api_keys: List[Dict[str, Any]] = get_response["data"]
# Verify the generated key is in the list
assert len(api_keys) > 0, "User should have at least one API key"
token_found: bool = any(key.get("token") == token for key in api_keys)
assert token_found, "Generated API key should appear in the list"
@pytest.mark.p1
def test_generate_user_api_key_response_structure(self, admin_session: requests.Session) -> None:
"""Test that generate_user_api_key returns correct response structure"""
user_name: str = EMAIL
response: Dict[str, Any] = generate_user_api_key(admin_session, user_name)
# Verify response code, message, and data
assert response.get("code") == RetCode.SUCCESS, f"Response code should be {RetCode.SUCCESS}, got {response.get('code')}"
assert "message" in response, "Response should contain message"
assert "data" in response, "Response should contain data"
result: Dict[str, Any] = response["data"]
# Verify all required fields
assert "tenant_id" in result, "Response should have tenant_id"
assert "token" in result, "Response should have token"
assert "beta" in result, "Response should have beta"
assert "create_time" in result, "Response should have create_time"
assert "create_date" in result, "Response should have create_date"
assert "update_time" in result, "Response should have update_time"
assert "update_date" in result, "Response should have update_date"
# Verify field types
assert isinstance(result["tenant_id"], str), "tenant_id should be string"
assert isinstance(result["token"], str), "token should be string"
assert isinstance(result["beta"], str), "beta should be string"
assert isinstance(result["create_time"], (int, type(None))), "create_time should be int or None"
assert isinstance(result["create_date"], (str, type(None))), "create_date should be string or None"
@pytest.mark.p2
def test_generate_user_api_key_multiple_times(self, admin_session: requests.Session) -> None:
"""Test generating multiple API keys for the same user"""
user_name: str = EMAIL
# Generate first API key
response1: Dict[str, Any] = generate_user_api_key(admin_session, user_name)
assert response1.get("code") == RetCode.SUCCESS, f"First generate should succeed, got code {response1.get('code')}"
key1: Dict[str, Any] = response1["data"]
token1: str = key1["token"]
# Generate second API key
response2: Dict[str, Any] = generate_user_api_key(admin_session, user_name)
assert response2.get("code") == RetCode.SUCCESS, f"Second generate should succeed, got code {response2.get('code')}"
key2: Dict[str, Any] = response2["data"]
token2: str = key2["token"]
# Tokens should be different
assert token1 != token2, "Multiple API keys should have different tokens"
# Both should appear in the list
get_response: Dict[str, Any] = get_user_api_key(admin_session, user_name)
assert get_response.get("code") == RetCode.SUCCESS, f"Get should succeed, got code {get_response.get('code')}"
api_keys: List[Dict[str, Any]] = get_response["data"]
tokens: List[str] = [key.get("token") for key in api_keys]
assert token1 in tokens, "First token should be in the list"
assert token2 in tokens, "Second token should be in the list"
@pytest.mark.p2
def test_generate_user_api_key_nonexistent_user(self, admin_session: requests.Session) -> None:
"""Test generating API key for non-existent user fails"""
response: Dict[str, Any] = generate_user_api_key(admin_session, "nonexistent_user_12345")
# Verify error response
assert response.get("code") == RetCode.NOT_FOUND, "Response code should indicate error"
assert "message" in response, "Response should contain message"
message: str = response.get("message", "")
assert message == "User not found!", f"Message should indicate user not found, got: {message}"
@pytest.mark.p2
def test_generate_user_api_key_tenant_id_consistency(self, admin_session: requests.Session) -> None:
"""Test that generated API keys have consistent tenant_id"""
user_name: str = EMAIL
# Generate multiple API keys
response1: Dict[str, Any] = generate_user_api_key(admin_session, user_name)
assert response1.get("code") == RetCode.SUCCESS, f"First generate should succeed, got code {response1.get('code')}"
key1: Dict[str, Any] = response1["data"]
response2: Dict[str, Any] = generate_user_api_key(admin_session, user_name)
assert response2.get("code") == RetCode.SUCCESS, f"Second generate should succeed, got code {response2.get('code')}"
key2: Dict[str, Any] = response2["data"]
# Tenant IDs should be the same for the same user
assert key1["tenant_id"] == key2["tenant_id"], "Same user should have same tenant_id"
@pytest.mark.p2
def test_generate_user_api_key_token_format(self, admin_session: requests.Session) -> None:
"""Test that generated API key has correct format"""
user_name: str = EMAIL
response: Dict[str, Any] = generate_user_api_key(admin_session, user_name)
assert response.get("code") == RetCode.SUCCESS, f"Response code should be {RetCode.SUCCESS}, got {response.get('code')}"
result: Dict[str, Any] = response["data"]
token: str = result["token"]
# Token should be a non-empty string
assert isinstance(token, str), "Token should be a string"
assert len(token) > 0, "Token should not be empty"
# Beta should be independently generated (32 chars, not derived from token)
beta: str = result["beta"]
assert isinstance(beta, str), "Beta should be a string"
assert len(beta) == 32, "Beta should be 32 characters"
# Beta should be independent from token (not derived from it)
if token.startswith("ragflow-"):
token_without_prefix: str = token.replace("ragflow-", "")[:32]
assert beta != token_without_prefix, "Beta should be independently generated, not derived from token"
@pytest.mark.p1
def test_generate_user_api_key_without_auth(self) -> None:
"""Test that generating API key without admin auth fails"""
session: requests.Session = requests.Session()
user_name: str = EMAIL
response: Dict[str, Any] = generate_user_api_key(session, user_name)
# Verify error response
assert response.get("code") == RetCode.UNAUTHORIZED, "Response code should indicate error"
assert "message" in response, "Response should contain message"
message: str = response.get("message", "").lower()
# The message is an HTML string indicating unauthorized user .
assert message == UNAUTHORIZED_ERROR_MESSAGE
@pytest.mark.p3
def test_generate_user_api_key_timestamp_fields(self, admin_session: requests.Session) -> None:
"""Test that generated API key has correct timestamp fields"""
user_name: str = EMAIL
response: Dict[str, Any] = generate_user_api_key(admin_session, user_name)
assert response.get("code") == RetCode.SUCCESS, f"Response code should be {RetCode.SUCCESS}, got {response.get('code')}"
result: Dict[str, Any] = response["data"]
# create_time should be a timestamp (int)
create_time: Any = result.get("create_time")
assert create_time is None or isinstance(create_time, int), "create_time should be int or None"
if create_time is not None:
assert create_time > 0, "create_time should be positive"
# create_date should be a date string
create_date: Any = result.get("create_date")
assert create_date is None or isinstance(create_date, str), "create_date should be string or None"
# update_time and update_date should be None for new keys
assert result.get("update_time") is None, "update_time should be None for new keys"
assert result.get("update_date") is None, "update_date should be None for new keys"

View File

@ -0,0 +1,169 @@
#
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from typing import Any, Dict, List
import pytest
import requests
from conftest import generate_user_api_key, get_user_api_key, UNAUTHORIZED_ERROR_MESSAGE
from common.constants import RetCode
from configs import EMAIL
class TestGetUserApiKey:
@pytest.mark.p1
def test_get_user_api_key_success(self, admin_session: requests.Session) -> None:
"""Test successfully getting API keys for a user with correct response structure"""
user_name: str = EMAIL
# Generate a test API key first
generate_response: Dict[str, Any] = generate_user_api_key(admin_session, user_name)
assert generate_response["code"] == RetCode.SUCCESS, generate_response
generated_key: Dict[str, Any] = generate_response["data"]
generated_token: str = generated_key["token"]
# Get all API keys for the user
get_response: Dict[str, Any] = get_user_api_key(admin_session, user_name)
assert get_response["code"] == RetCode.SUCCESS, get_response
assert "message" in get_response, "Response should contain message"
assert "data" in get_response, "Response should contain data"
api_keys: List[Dict[str, Any]] = get_response["data"]
# Verify response is a list with at least one key
assert isinstance(api_keys, list), "API keys should be returned as a list"
assert len(api_keys) > 0, "User should have at least one API key"
# Verify structure of each API key
for key in api_keys:
assert isinstance(key, dict), "Each API key should be a dictionary"
assert "token" in key, "API key should contain token"
assert "beta" in key, "API key should contain beta"
assert "tenant_id" in key, "API key should contain tenant_id"
assert "create_date" in key, "API key should contain create_date"
# Verify field types
assert isinstance(key["token"], str), "token should be string"
assert isinstance(key["beta"], str), "beta should be string"
assert isinstance(key["tenant_id"], str), "tenant_id should be string"
assert isinstance(key.get("create_date"), (str, type(None))), "create_date should be string or None"
assert isinstance(key.get("update_date"), (str, type(None))), "update_date should be string or None"
# Verify the generated key is in the list
token_found: bool = any(key.get("token") == generated_token for key in api_keys)
assert token_found, "Generated API key should appear in the list"
@pytest.mark.p2
def test_get_user_api_key_nonexistent_user(self, admin_session: requests.Session) -> None:
"""Test getting API keys for non-existent user fails"""
nonexistent_user: str = "nonexistent_user_12345"
response: Dict[str, Any] = get_user_api_key(admin_session, nonexistent_user)
assert response["code"] == RetCode.NOT_FOUND, response
assert "message" in response, "Response should contain message"
message: str = response["message"]
expected_message: str = f"User '{nonexistent_user}' not found"
assert message == expected_message, f"Message should indicate user not found, got: {message}"
@pytest.mark.p2
def test_get_user_api_key_empty_username(self, admin_session: requests.Session) -> None:
"""Test getting API keys with empty username"""
response: Dict[str, Any] = get_user_api_key(admin_session, "")
# Empty username should either return error or empty list
if response["code"] == RetCode.SUCCESS:
assert "data" in response, "Response should contain data"
api_keys: List[Dict[str, Any]] = response["data"]
assert isinstance(api_keys, list), "Should return a list"
assert len(api_keys) == 0, "Empty username should return empty list"
else:
assert "message" in response, "Error response should contain message"
assert len(response["message"]) > 0, "Error message should not be empty"
@pytest.mark.p2
def test_get_user_api_key_token_uniqueness(self, admin_session: requests.Session) -> None:
"""Test that all API keys in the list have unique tokens"""
user_name: str = EMAIL
# Generate multiple API keys
response1: Dict[str, Any] = generate_user_api_key(admin_session, user_name)
assert response1["code"] == RetCode.SUCCESS, response1
response2: Dict[str, Any] = generate_user_api_key(admin_session, user_name)
assert response2["code"] == RetCode.SUCCESS, response2
# Get all API keys
get_response: Dict[str, Any] = get_user_api_key(admin_session, user_name)
assert get_response["code"] == RetCode.SUCCESS, get_response
api_keys: List[Dict[str, Any]] = get_response["data"]
# Verify all tokens are unique
tokens: List[str] = [key.get("token") for key in api_keys if key.get("token")]
assert len(tokens) == len(set(tokens)), "All API keys should have unique tokens"
@pytest.mark.p2
def test_get_user_api_key_tenant_id_consistency(self, admin_session: requests.Session) -> None:
"""Test that all API keys for a user have the same tenant_id"""
user_name: str = EMAIL
# Generate multiple API keys
response1: Dict[str, Any] = generate_user_api_key(admin_session, user_name)
assert response1["code"] == RetCode.SUCCESS, response1
response2: Dict[str, Any] = generate_user_api_key(admin_session, user_name)
assert response2["code"] == RetCode.SUCCESS, response2
# Get all API keys
get_response: Dict[str, Any] = get_user_api_key(admin_session, user_name)
assert get_response["code"] == RetCode.SUCCESS, get_response
api_keys: List[Dict[str, Any]] = get_response["data"]
# Verify all keys have the same tenant_id
tenant_ids: List[str] = [key.get("tenant_id") for key in api_keys if key.get("tenant_id")]
if len(tenant_ids) > 0:
assert all(tid == tenant_ids[0] for tid in tenant_ids), "All API keys should have the same tenant_id"
@pytest.mark.p2
def test_get_user_api_key_beta_format(self, admin_session: requests.Session) -> None:
"""Test that beta field in API keys has correct format (32 characters)"""
user_name: str = EMAIL
# Generate a test API key
generate_response: Dict[str, Any] = generate_user_api_key(admin_session, user_name)
assert generate_response["code"] == RetCode.SUCCESS, generate_response
# Get all API keys
get_response: Dict[str, Any] = get_user_api_key(admin_session, user_name)
assert get_response["code"] == RetCode.SUCCESS, get_response
api_keys: List[Dict[str, Any]] = get_response["data"]
# Verify beta format for all keys
for key in api_keys:
beta: str = key.get("beta", "")
assert isinstance(beta, str), "beta should be a string"
assert len(beta) == 32, f"beta should be 32 characters, got {len(beta)}"
@pytest.mark.p3
def test_get_user_api_key_without_auth(self) -> None:
"""Test that getting API keys without admin auth fails"""
session: requests.Session = requests.Session()
user_name: str = EMAIL
response: Dict[str, Any] = get_user_api_key(session, user_name)
assert response["code"] == RetCode.UNAUTHORIZED, response
assert "message" in response, "Response should contain message"
message: str = response["message"].lower()
assert message == UNAUTHORIZED_ERROR_MESSAGE