Repository navigation
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
Changes from 4 commits
ccb5c00
fe98a66
153b4f8
16604fa
af94779
f0346b0
3b60ae5
b55c8b7
e6c500d
a8cdb16
6156b9d
bfb17f6
fa1f9c8
78d133a
a2e5ebb
0be1e79
20f6d9a
d0178c6
5ce8807
626b311
d29e41f
56368a7
b6c87c4
60a5462
1671871
e261388
08c5ba2
b528141
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,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
+76
to
+79
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, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,14 +19,18 @@ class IdentitySettings: | |
| audience: str | ||
| jwks_url: str = "" | ||
| bootstrap_admin_token: str = "" | ||
| secret_key: str = "change-me-in-production" | ||
|
|
||
| @classmethod | ||
| def from_env(cls) -> IdentitySettings: | ||
| from backend.config import get_settings | ||
|
|
||
| return cls( | ||
| issuer=os.getenv("OIDC_ISSUER", "").rstrip("/"), | ||
| audience=os.getenv("OIDC_AUDIENCE", ""), | ||
| jwks_url=os.getenv("OIDC_JWKS_URL", ""), | ||
| bootstrap_admin_token=os.getenv("BOOTSTRAP_ADMIN_TOKEN", ""), | ||
| secret_key=get_settings().secret_key, | ||
| ) | ||
|
|
||
|
|
||
|
|
@@ -129,6 +133,18 @@ async def get_request_identity(request: Request) -> RequestIdentity: | |
| "Bearer token required", | ||
| headers={"WWW-Authenticate": "Bearer"}, | ||
| ) | ||
| try: | ||
| local_claims = jwt.decode(token, resolved.secret_key, algorithms=["HS256"]) | ||
| except JWTError: | ||
| local_claims = None | ||
| if local_claims and local_claims.get("auth_method") == "local": | ||
| return RequestIdentity( | ||
| subject="local-admin", | ||
| name=local_claims.get("name") or "本地管理员", | ||
| username=local_claims.get("username"), | ||
| is_platform_admin=True, | ||
| auth_method="local", | ||
| ) | ||
|
Comment on lines
+141
to
+152
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 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -u
printf '%s\n' '--- candidate files ---'
fd -t f -i 'identity|settings|security|auth|token' . | head -200
printf '%s\n' '--- identity structure ---'
ast-grep outline backend/security/identity.py --view expanded 2>/dev/null || true
printf '%s\n' '--- relevant references ---'
rg -n -C 4 'secret_key|change-me-in-production|auth_method|local-admin|jwt\.encode|jwt\.decode|fleet token|fleet_token' backend .github 2>/dev/null | head -400
printf '%s\n' '--- identity.py ---'
cat -n backend/security/identity.py | sed -n '1,240p'
printf '%s\n' '--- settings definitions ---'
rg -n -C 8 'class Settings|secret_key|change-me-in-production' . -g '*.py' | head -300Repository: 2233admin/opencli-Razormind Length of output: 40869 🏁 Script executed: #!/bin/bash
set -u
printf '%s\n' '--- identity security tests ---'
cat -n tests/unit/security/test_identity.py | sed -n '1,280p'
printf '%s\n' '--- local auth tests ---'
cat -n tests/unit/security/test_local_auth.py | sed -n '1,240p'
printf '%s\n' '--- fleet auth implementation ---'
cat -n backend/security/fleet_auth.py | sed -n '1,260p'
printf '%s\n' '--- application wiring and protected routes ---'
rg -n -C 5 'FleetAuth|identity_dependency|get_request_identity|Depends\(' backend -g '*.py' | head -500
printf '%s\n' '--- complete relevant config fields ---'
cat -n backend/config.py | sed -n '1,180p'Repository: 2233admin/opencli-Razormind Length of output: 50383 🏁 Script executed: #!/bin/bash
set -u
printf '%s\n' '--- settings and local-login contract ---'
cat -n backend/config.py | sed -n '17,95p'
cat -n backend/api/v1/identity.py | sed -n '1,110p'
cat -n backend/security/local_auth.py | sed -n '1,95p'
printf '%s\n' '--- focused authentication tests and documentation ---'
rg -n -C 5 'local|secret_key|API_AUTH_TOKEN|LOCAL_ADMIN|change-me-in-production|local-admin' \
tests/unit/security tests/integration docs README.md .env.example docker-compose.yml 2>/dev/null | head -350
printf '%s\n' '--- standalone HS256 behavior probe (does not import or execute repository code) ---'
python3 - <<'PY'
import base64, hashlib, hmac, json, time
def b64(value):
return base64.urlsafe_b64encode(value).rstrip(b"=").decode()
def forge(claims, secret):
header = b64(b'{"alg":"HS256","typ":"JWT"}')
payload = b64(json.dumps(claims, separators=(",", ":")).encode())
signing_input = f"{header}.{payload}".encode()
signature = b64(hmac.new(secret.encode(), signing_input, hashlib.sha256).digest())
return f"{header}.{payload}.{signature}"
def jose_like_decode(token, secret):
header, payload, signature = token.split(".")
expected = hmac.new(
secret.encode(), f"{header}.{payload}".encode(), hashlib.sha256
).digest()
actual = base64.urlsafe_b64decode(signature + "=" * (-len(signature) % 4))
if not hmac.compare_digest(actual, expected):
raise ValueError("bad signature")
claims = json.loads(base64.urlsafe_b64decode(payload + "=" * (-len(payload) % 4)))
if claims.get("exp", time.time() + 1) < time.time():
raise ValueError("expired")
return claims
secret = "change-me-in-production"
for claims in (
{"auth_method": "local", "username": "attacker"},
{"auth_method": "local", "sub": "local-admin", "username": "attacker"},
{"auth_method": "local", "sub": "other", "username": "attacker"},
):
token = forge(claims, secret)
decoded = jose_like_decode(token, secret)
identity_branch_accepts = bool(decoded) and decoded.get("auth_method") == "local"
fleet_local_session_accepts = (
decoded.get("auth_method") == "local"
and decoded.get("sub") == "local-admin"
)
print({
"claims": claims,
"signature_valid": True,
"identity_branch_accepts": identity_branch_accepts,
"fleet_local_session_accepts": fleet_local_session_accepts,
})
PYRepository: 2233admin/opencli-Razormind Length of output: 30693 Reject local JWTs with the default signing key.
🤖 Prompt for AI Agents |
||
| if resolved.bootstrap_admin_token and hmac.compare_digest( | ||
| token, resolved.bootstrap_admin_token | ||
| ): | ||
|
|
||
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.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
Repository: 2233admin/opencli-Razormind
Length of output: 41617
🏁 Script executed:
Repository: 2233admin/opencli-Razormind
Length of output: 50383
🏁 Script executed:
Repository: 2233admin/opencli-Razormind
Length of output: 16874
🏁 Script executed:
Repository: 2233admin/opencli-Razormind
Length of output: 50383
Make local workspace provisioning safe for concurrent first requests.
Use database-native upserts, or catch
IntegrityErrorinside savepoints and re-query the rows. Add an integration test with separate sessions for parallel first requests.🤖 Prompt for AI Agents