-
Notifications
You must be signed in to change notification settings - Fork 4
feat: simplify local administrator login #78
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
f0346b0
3b60ae5
b55c8b7
e6c500d
a8cdb16
6156b9d
bfb17f6
fa1f9c8
78d133a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,7 +3,7 @@ | |
| from sqlalchemy.ext.asyncio import AsyncSession | ||
|
|
||
| from backend.database import get_db | ||
| from backend.models.identity import User, Workspace, WorkspaceMembership, WorkspaceRole | ||
| from backend.models.identity import Team, User, Workspace, WorkspaceMembership, WorkspaceRole | ||
| from backend.models.workflow import Project | ||
| from backend.schemas.common import ApiResponse | ||
| from backend.schemas.workflow_asset import ProjectRead | ||
|
|
@@ -54,6 +54,51 @@ async def _get_or_create_user( | |
| raise HTTPException(status.HTTP_409_CONFLICT, "Disabled user cannot join a Workspace") | ||
| return user | ||
|
|
||
| async def _ensure_local_admin_workspace( | ||
| db: AsyncSession, | ||
| identity: RequestIdentity, | ||
| ) -> None: | ||
| if identity.auth_method != "local": | ||
| return | ||
|
|
||
| user = await db.scalar(select(User).where(User.subject == identity.subject)) | ||
| if user is None: | ||
| user = User( | ||
| subject=identity.subject, | ||
| display_name=identity.name or "本地管理员", | ||
| ) | ||
| db.add(user) | ||
| await db.flush() | ||
|
|
||
| workspace = await db.scalar(select(Workspace).where(Workspace.slug == "opencli-default")) | ||
| if workspace is None: | ||
| workspace = Workspace(name="OpenCLI 工作区", slug="opencli-default") | ||
| db.add(workspace) | ||
| await db.flush() | ||
|
|
||
| membership = await db.scalar( | ||
| select(WorkspaceMembership) | ||
| .where(WorkspaceMembership.workspace_id == workspace.id) | ||
| .where(WorkspaceMembership.user_id == user.id) | ||
| ) | ||
| if membership is None: | ||
| db.add( | ||
| WorkspaceMembership( | ||
| workspace_id=workspace.id, | ||
| user_id=user.id, | ||
| role=WorkspaceRole.ADMIN, | ||
| ) | ||
| ) | ||
|
|
||
| team = await db.scalar( | ||
| select(Team) | ||
| .where(Team.workspace_id == workspace.id) | ||
| .where(Team.slug == "default") | ||
| ) | ||
| if team is None: | ||
| db.add(Team(workspace_id=workspace.id, name="默认团队", slug="default")) | ||
| await db.flush() | ||
|
Comment on lines
+64
to
+100
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- target file ---'
wc -l backend/api/v1/workspaces.py
cat -n backend/api/v1/workspaces.py | sed -n '1,180p'
printf '%s\n' '--- candidate files ---'
git ls-files | rg '(^|/)(models?|schema|schemas|workspace|team|membership|conftest|test)' | head -200
printf '%s\n' '--- symbols and constraints ---'
rg -n --glob '*.py' 'class (User|Workspace|WorkspaceMembership|Team)|WorkspaceRole|opencli-default|slug.*unique|UniqueConstraint|IntegrityError|commit\(' backend tests 2>/dev/null | head -300Repository: 2233admin/opencli-Razormind Length of output: 41617 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- request transaction and dependency handling ---'
rg -n --glob '*.py' 'get_db|AsyncSession|yield db|db\.commit|db\.rollback|HTTPException|IntegrityError' backend tests 2>/dev/null | head -300
printf '%s\n' '--- all references to provisioning endpoint/helper ---'
rg -n --glob '*.py' 'ensure|provision|workspace|default.*team|local admin|本地管理员' backend tests 2>/dev/null | head -300Repository: 2233admin/opencli-Razormind Length of output: 50383 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- AST outlines ---'
ast-grep outline backend/models/identity.py
ast-grep outline backend/database.py
ast-grep outline tests/integration/test_local_workspace_api.py
ast-grep outline tests/conftest.py
printf '%s\n' '--- identity model source ---'
cat -n backend/models/identity.py
printf '%s\n' '--- database dependency source ---'
cat -n backend/database.py | sed -n '1,180p'
printf '%s\n' '--- local workspace integration test ---'
cat -n tests/integration/test_local_workspace_api.py
printf '%s\n' '--- relevant test fixtures ---'
cat -n tests/conftest.py | sed -n '1,130p'Repository: 2233admin/opencli-Razormind Length of output: 16874 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- migration definitions for identity tables ---'
rg -n --glob '*.py' 'CREATE TABLE|users|workspaces|workspace_memberships|teams|uq_|UniqueConstraint' alembic migrations backend 2>/dev/null | head -240
printf '%s\n' '--- route registration and auth identity behavior ---'
rg -n --glob '*.py' 'list_accessible_workspaces|governance/workspaces|auth_method|RequestIdentity|local' backend/main.py backend backend/security tests/integration/test_local_workspace_api.py 2>/dev/null | head -260Repository: 2233admin/opencli-Razormind Length of output: 50383 Make local workspace provisioning safe for concurrent first requests. Use database-native upserts, or catch 🤖 Prompt for AI Agents |
||
|
|
||
|
|
||
| @router.get( | ||
| "/governance/workspaces", | ||
|
|
@@ -63,6 +108,7 @@ async def list_accessible_workspaces( | |
| identity: RequestIdentity = Depends(get_request_identity), | ||
| db: AsyncSession = Depends(get_db), | ||
| ) -> ApiResponse: | ||
| await _ensure_local_admin_workspace(db, identity) | ||
| rows = ( | ||
| ( | ||
| await db.execute( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,8 @@ | |
|
|
||
| from pydantic_settings import BaseSettings, SettingsConfigDict | ||
|
|
||
| from backend.security.local_auth import DEFAULT_LOCAL_ADMIN_PASSWORD_HASH | ||
|
|
||
|
|
||
| class Settings(BaseSettings): | ||
| model_config = SettingsConfigDict( | ||
|
|
@@ -59,6 +61,11 @@ class Settings(BaseSettings): | |
| # guard only allows on a localhost bind. Env: API_AUTH_TOKEN. | ||
| api_auth_token: str = "" | ||
|
|
||
| # Local-first account used by the NAS/server deployment. The password hash | ||
| # is persisted in .env after the user changes the default password. | ||
| local_admin_username: str = "admin" | ||
| local_admin_password_hash: str = DEFAULT_LOCAL_ADMIN_PASSWORD_HASH | ||
|
Comment on lines
+64
to
+67
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win Block remote access while the default password is active. The documented Reject non-loopback startup while 🤖 Prompt for AI Agents |
||
|
|
||
| # CLI channel binary allowlist (ADR-0005, audit P0-4). The cli channel is | ||
| # an arbitrary-binary-execution surface, so it only runs binaries the | ||
| # operator explicitly listed here. Comma-separated binary paths/names, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: 2233admin/opencli-Razormind
Length of output: 21100
🏁 Script executed:
Repository: 2233admin/opencli-Razormind
Length of output: 38288
🏁 Script executed:
Repository: 2233admin/opencli-Razormind
Length of output: 478
🏁 Script executed:
Repository: 2233admin/opencli-Razormind
Length of output: 468
🌐 Web query:
Pydantic 2 default str validation accepts newline characters python-dotenv dotenv_values multiline unquoted value parsing separate assignment💡 Result:
Pydantic v2 and python-dotenv handle newline characters in string values differently, depending on the context of their operation. Pydantic v2 Pydantic's default string validation treats newline characters (\n) as valid components of a string [1][2]. When you define a field as str in a Pydantic model, it accepts strings containing newlines without modification by default [1][3]. While you can apply constraints using Annotated with StringConstraints (e.g., min_length, max_length, pattern) [4][5], these constraints do not automatically strip or invalidate newline characters [6]. If you need to enforce a single-line format, you must use a regex pattern (e.g., pattern=r'^[^\n]*$') within the string constraints [4]. python-dotenv The python-dotenv library (specifically dotenv_values) has specific rules for parsing multiline values: 1. Quoted Values: If a value is wrapped in single ('') or double ("") quotes, python-dotenv supports multiline values [7][8]. You can either place an actual newline inside the quoted string in your.env file or use an escaped literal like \n (e.g., FOO="line1\nline2") [7][8][9]. 2. Unquoted Values: Unquoted values in a.env file do not support multiline parsing [10]. The parser for unquoted values (parse_unquoted_value) stops at line breaks, as it is designed to treat the newline character as a delimiter for the end of the assignment [10]. 3. Parsing Behavior: When dotenv_values encounters a multiline structure that it cannot parse (such as an unquoted value spanning multiple lines or incorrectly formatted quotes), it may result in the assignment failing or the key being associated with a None value, rather than treating the newline as part of the string [7][8]. In summary, Pydantic is agnostic toward newlines in strings, while python-dotenv requires explicit quoting to preserve or parse newline characters within values during file loading [7][8].
Citations:
use_attribute_docstringspydantic/pydantic#11225Reject
\rand\nin every string setting before_update_env_file.ConfigPatchaccepts these characters._update_env_filewrites them as physical line breaks, so a value such as\nDATABASE_URL=...creates a separate dotenv assignment that a later reload can apply. Add an API test that submits this value and confirms a validation error with no file change.🤖 Prompt for AI Agents