Files
arma-server-web-manager/phases/phase-02-auth-security.md
Khoa (Revenovich) Tran Gia e02db3ddde feat: add Java→Python migration plan with 9 self-contained phase files
Converts Spring Boot 4.0.3 ARMA Server Web GUI to FastAPI/Python.
Each phase file is fully self-contained: lists Java source files to
read, output files to create, implementation patterns, REST endpoint
contracts, and a completion checklist. A future agent can execute any
single phase without rescanning the Java project.

Phases:
- 01: Foundation — SQLAlchemy models, Alembic, settings, base schemas
- 02: Auth & Users — JWT middleware, RBAC, user CRUD
- 03: CFG parser + server process — server.cfg round-trip, start/stop
- 04: Server settings — general/network/logging/security/difficulty
- 05: Mod management — mod CRUD, presets, settings, WebSocket progress
- 06: Steam integration — SteamCMD queue, Workshop API, python-a2s
- 07: Missions, CDLC, Discord, APScheduler jobs
- 08: Middleware & polish — global exception handler, SPA redirect, structlog
- 09: Testing — pytest-asyncio, respx, 80% coverage target
2026-04-14 15:06:56 +07:00

7.8 KiB

Phase 2 — Authentication + Security

Status: PENDING Depends on: Phase 1 complete Next phase: phase-03-cfg-parser-process.md


Goal

Working JWT login/logout, BCrypt password verification, the full AswgAuthority enum (37 permissions), JWT middleware enforcing auth on all /api/** routes, user CRUD service, and the auth + user REST endpoints.

After this phase: POST /api/v1/auth returns a JWT token; all other /api/** endpoints return 401 without a valid token.


Java Source Files to Read

<JAVA_SRC> = E:\TestScript\ARMA-Server-Web-Gui\src\main\java\pl\bartlomiejstepien\armaserverwebgui\

File What to extract
<JAVA_SRC>application/auth/AuthService.java authenticate(), logout() logic
<JAVA_SRC>application/auth/JwtToken.java JWT payload fields
<JAVA_SRC>application/security/AswgAuthority.java All 37 authority code strings
<JAVA_SRC>application/security/jwt/JwtService.java createJwt(), validateJwt(), blacklisting logic
<JAVA_SRC>application/security/jwt/filter/JwtFilter.java Permit-all path list
<JAVA_SRC>application/security/jwt/cleaner/InvalidJwtCleaner.java Scheduled cleanup logic
<JAVA_SRC>application/security/AuthenticationFacade.java getCurrentUser() pattern
<JAVA_SRC>application/security/AswgAuthenticationEntryPoint.java 401 response shape
<JAVA_SRC>application/config/SecurityConfig.java Dual enabled/disabled modes, permit-all paths
<JAVA_SRC>domain/user/UserService.java All method signatures
<JAVA_SRC>domain/user/UserServiceImpl.java Full implementation
<JAVA_SRC>domain/user/dto/ (all files) DTO field names
<JAVA_SRC>web/AuthRestController.java Exact paths, request/response JSON
<JAVA_SRC>web/UserRestController.java Exact paths, request/response JSON
<JAVA_SRC>web/request/PasswordChangeRequest.java Fields

Output Files to Create

src/application/__init__.py
src/application/security/__init__.py
src/application/security/jwt_service.py
src/application/security/auth_service.py
src/application/security/permissions.py       (AswgAuthority enum + has_permission factory)
src/application/security/authentication.py    (get_current_user dependency)
src/application/security/password.py          (BCrypt wrapper via passlib)
src/application/middleware/__init__.py
src/application/middleware/jwt_middleware.py
src/domain/__init__.py
src/domain/user/__init__.py
src/domain/user/user_service.py
src/domain/user/user_loader_service.py
src/domain/user/user_session_service.py
src/domain/user/models.py
src/web/__init__.py
src/web/schemas/__init__.py
src/web/schemas/common.py          (BaseSchema with camelCase alias, RestErrorResponse)
src/web/schemas/auth.py
src/web/schemas/users.py
src/web/auth_router.py
src/web/user_router.py

Also update src/main.py:

  • Register auth_router and user_router
  • Add JwtMiddleware
  • Create default user on startup in lifespan

Implementation Notes

src/web/schemas/common.py — BaseSchema (used by ALL schemas in all phases)

from pydantic import BaseModel, ConfigDict
from pydantic.alias_generators import to_camel

class BaseSchema(BaseModel):
    model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)

class RestErrorResponse(BaseSchema):
    code: str
    message: str

Every schema in every phase must inherit BaseSchema to preserve camelCase JSON for the Angular frontend.

src/application/security/permissions.py

Extract all 37 codes from AswgAuthority.java. Example:

from enum import StrEnum
from fastapi import Depends, HTTPException

class AswgAuthority(StrEnum):
    SERVER_START_STOP = "SERVER_START_STOP"
    GENERAL_SETTINGS_VIEW = "GENERAL_SETTINGS_VIEW"
    # ... all 37 values

def has_permission(authority: AswgAuthority):
    async def _check(current_user=Depends(get_current_user)):
        user_codes = {a.code for a in current_user.authorities}
        if authority.value not in user_codes:
            raise HTTPException(status_code=403)
        return current_user
    return Depends(_check)

Usage: @router.get("/properties", dependencies=[has_permission(AswgAuthority.GENERAL_SETTINGS_VIEW)])

src/application/security/jwt_service.py

Algorithm: HS256. JWT payload fields (from JwtToken.java): sub (username), authorities (list of code strings), iss, iat, exp.

Blacklisting: on logout, insert token into invalid_jwt_token table. On every validation, check the table. Scheduled cleanup (InvalidJwtCleaner equivalent): delete rows where expiration_datetime < now().

src/application/middleware/jwt_middleware.py

Permit-all paths from SecurityConfig.java / JwtFilter.java:

PERMIT_ALL_EXACT = {
    ("POST", "/api/v1/auth"),
    ("POST", "/api/v1/auth/logout"),
    ("GET",  "/api/v1/actuator/health"),
    ("GET",  "/api/v1/actuator/info"),
}
PERMIT_ALL_PREFIX = ["/api/v1/ws/", "/api/v1/logging/logs-sse"]

When settings.security_enabled = False: skip all JWT checks, attach a superuser principal to request.state.user.

On JWT failure return JSON matching AswgAuthenticationEntryPoint:

  • Expired token: {"code":"AUTH_TOKEN_EXPIRED","message":"..."} HTTP 401
  • Missing token: {"code":"AUTH_TOKEN_REQUIRED","message":"..."} HTTP 401
  • Bad token: {"code":"BAD_AUTH_TOKEN","message":"..."} HTTP 401

Auth REST endpoints (from AuthRestController.java)

POST /api/v1/auth           body: {username, password}  → {token: "..."}         200
GET  /api/v1/auth/myself    (authenticated)             → UserProfileResponse     200
POST /api/v1/auth/logout    (authenticated)             → 200 (no body)

User REST endpoints (from UserRestController.java)

GET    /api/v1/users              → list[UserResponse]
POST   /api/v1/users              body: CreateUserRequest  → UserResponse         201
PUT    /api/v1/users/{id}         body: UpdateUserRequest  → UserResponse         200
DELETE /api/v1/users/{id}                                  → 200 (no body)
POST   /api/v1/users/{id}/password-change  body: {password} → 200 (no body)

Default user creation in lifespan (src/main.py update)

async with SessionLocal() as session:
    user_repo = UserRepository(session)
    existing = await user_repo.find_by_username(settings.default_user_username)
    if not existing:
        hashed = password_service.encode(settings.default_user_password)
        await user_repo.create(username=settings.default_user_username,
                               password=hashed, authorities=list(AswgAuthority))
    elif settings.default_user_reset:
        hashed = password_service.encode(settings.default_user_password)
        await user_repo.update_password(existing.id, hashed)

Completion Checklist

  • AswgAuthority enum has exactly 37 permission codes (count against Java source)
  • has_permission() factory works as a Depends()
  • JWT is created and validated with HS256
  • Expired and blacklisted tokens are rejected with correct error JSON
  • JWT middleware blocks all /api/** without a valid token
  • Permit-all paths respond without token (health, login, logout, SSE, WS)
  • security_enabled=False bypasses all auth checks
  • POST /api/v1/auth → JWT for valid credentials, 401 for bad
  • GET /api/v1/auth/myself → current user profile JSON
  • POST /api/v1/auth/logout → blacklists token
  • All user CRUD endpoints return correct status codes
  • Default user created on first startup
  • Password reset works when aswg.default-user.reset=true

Contract for Phase 3

Phase 3 imports:

  • from src.application.security.permissions import has_permission, AswgAuthority
  • from src.application.security.authentication import CurrentUser, get_current_user
  • from src.web.schemas.common import BaseSchema, RestErrorResponse