Skip to content

feature/auth#27

Merged
davemoore- merged 67 commits into
mainfrom
feature/auth
Mar 31, 2026
Merged

feature/auth#27
davemoore- merged 67 commits into
mainfrom
feature/auth

Conversation

@davemoore-

Copy link
Copy Markdown
Collaborator

v1.2.0

Features

  • Added per-user authentication with JWT sessions and pass-through Elasticsearch credentials for the studio deployment. Users log in with their own ES credentials; username/password login creates a session API key, while API-key login reuses the provided API key. Controlled by AUTH_ENABLED (default: true). (Issue 4) (2a9b84a, 7866176)
  • Added authentication for the MCP Server via Authorization header (Basic, ApiKey, Bearer schemes). (Issue 5) (fb62d8e)
  • Added native TLS support for the Server and MCP Server. Controlled by TLS_ENABLED (default: true). (Issue 3) (4d6afd4)
  • Added @meta.created_via channel-tracking to asset documents ("server", "mcp", "api"), and @meta.updated_via where assets support update semantics. (Issue 2) (1e773f8)
  • Added login screen to the UI with username/password and API key authentication modes. (Issue 6) (fcec321, 220d26e)

Improvements

  • Refactored Elasticsearch client to support per-request clients alongside singleton service-account clients, enabling authenticated operations per user. (Issue 1) (7866176)
  • The judgements rated-human / rated-ai filter now uses @meta.updated_via (channel), while retaining @meta.updated_by checks for compatibility with existing data. (635198f)
  • Added index template migration 1.2.0 to add created_via and updated_via fields to existing indices. (Issue 7) (ad3919e)
  • Quickstart script now supports interactive TLS certificate generation and authentication setup. (468957f, cdd0159)

Breaking changes

  • AUTH_ENABLED now exists and defaults to true. When enabled, API routes require login and AUTH_JWT_SECRET must be configured. (468957f)
  • TLS_ENABLED now exists and defaults to true. When enabled, TLS_CERT_FILE and TLS_KEY_FILE are required at startup. (468957f)

Bug fixes

  • Fixed a race condition in the UI login flow and improved redirect behavior after authentication. (e8ea664, f267a41, ef185ba)
  • Fixed MCP Server connection behavior when TLS is enabled. (8065cfa)
  • Fixed chat panel regressions and related UI bugs. (b8a49bb)

- AssetCreate/AssetUpdate: enrich_meta adds created_via/updated_via from context['via']
- Validate via in {server,mcp,api}, default to 'api' when not provided
- Preserve @meta rejection behavior
- EvaluationCreate: add created_via with same validation/default
- Index templates: add @meta.created_via and @meta.updated_via (keyword) where @meta exists
- Judgements search: rated-ai/rated-human use @meta.updated_via (mcp vs not mcp) instead of updated_by
- Judgements set: script and upsert include created_via/updated_via
- Flask: pass via='server' to all create/update/set API calls
- FastMCP: pass via='mcp' to all create/update/set API calls
- Setup: pass via for ledger writes, add 1.2.0 migration for existing indices
- Agent: pass via='server' for conversation updates
- Unit tests: via validation/default, judgements filter, evaluation created_via

Made-with: Cursor
- src/server/tls.py: Central TLS config loader (TLS_ENABLED default true,
  require TLS_CERT_FILE + TLS_KEY_FILE when enabled, clear startup errors)
- src/server/flask.py: Use TLS config, ssl_context tuple, run via python -m
- src/server/fastmcp.py: Same TLS behavior for MCP HTTP transport
- .env-reference: TLS_ENABLED/TLS_CERT_FILE/TLS_KEY_FILE docs
- webpack.config.js: DEV_API_URL for optional dev TLS/proxy
- Dockerfile-server, Dockerfile-server-mcp: Document cert volume mount
- docker-compose.yml: Cert volume mounts, TLS_ENABLED=false for default
- scripts/quickstart.sh: --generate-tls-cert for self-signed local dev
- docs/docs/v1.1/reference/security.md: Native TLS docs, self-signed guide
- tests/unit/test_tls.py: Unit tests for TLS config
- tests/docker-compose.yml: TLS_ENABLED=false for integration tests

Made-with: Cursor
- Add es_studio_endpoint() returning endpoint config without credentials
- Add es_from_credentials(api_key or username/password, optional cloud_id/url)
  for constructing Elasticsearch client for studio endpoint
- Error when no credentials; prefer cloud_id when available else url
- Update API modules to accept optional es_client, fallback to es('studio') when None:
  workspaces, displays, scenarios, strategies, judgements, benchmarks,
  evaluations, conversations, setup
- content.py accepts optional es_client for consistency; uses content cluster
  service-account by default
- Update utils.search_assets(), search_tags() to accept optional es_client
- Worker behavior unchanged (uses es('studio') by default)
- Add unit tests for es_client, es_from_credentials, es_studio_endpoint
- Add unit tests for API es_client fallback/override
- Add integration test for workspaces CRUD with explicit es_client
- Fix setup test monkeypatches for new es_client signatures

Made-with: Cursor
- Add es_studio_endpoint() returning endpoint config without credentials
- Add es_from_credentials(api_key or username/password, optional cloud_id/url)
  for constructing Elasticsearch client for studio endpoint
- Error when no credentials; prefer cloud_id when available else url
- Update API modules to accept optional es_client, fallback to es('studio') when None:
  workspaces, displays, scenarios, strategies, judgements, benchmarks,
  evaluations, conversations, setup
- content.py accepts optional es_client for consistency; uses content cluster
  service-account by default
- Update utils.search_assets(), search_tags() to accept optional es_client
- Worker behavior unchanged (uses es('studio') by default)
- Add unit tests for es_client, es_from_credentials, es_studio_endpoint
- Add unit tests for API es_client fallback/override
- Add integration test for workspaces CRUD with explicit es_client
- Fix setup test monkeypatches for new es_client signatures

Made-with: Cursor
- Add src/server/auth.py:
  - validate_credentials() for Basic/ApiKey against _security/_authenticate
  - create_session_api_key() calling POST /_security/api_key
  - encode_jwt() / decode_jwt() for session tokens
  - AUTH_ENABLED config (default true)

- Update src/server/flask.py:
  - POST /api/auth/login, POST /api/auth/logout, GET /api/auth/session
  - before_request auth middleware for /api/* except login and /healthz
  - Decode cookie JWT, create per-request ES client via es_from_credentials
  - Attach g.user and g.es_client; AUTH_ENABLED=false uses service-account
  - Pass g.user and g.es_client to all API route handlers

- Update .env-reference with AUTH_ENABLED, JWT_SECRET, SESSION_EXPIRY

- Add PyJWT dependency

- Add unit tests (auth module, flask middleware/routes)
- Add integration tests for login/session/auth (AUTH_ENABLED=false)
- Set AUTH_ENABLED=false in test docker-compose

Made-with: Cursor
- Add src/server/auth.py:
  - validate_credentials() for Basic/ApiKey against _security/_authenticate
  - create_session_api_key() calling POST /_security/api_key
  - encode_jwt() / decode_jwt() for session tokens
  - AUTH_ENABLED config (default true)

- Update src/server/flask.py:
  - POST /api/auth/login, POST /api/auth/logout, GET /api/auth/session
  - before_request auth middleware for /api/* except login and /healthz
  - Decode cookie JWT, create per-request ES client via es_from_credentials
  - Attach g.user and g.es_client; AUTH_ENABLED=false uses service-account
  - Pass g.user and g.es_client to all API route handlers

- Update .env-reference with AUTH_ENABLED, JWT_SECRET, SESSION_EXPIRY

- Add PyJWT dependency

- Add unit tests (auth module, flask middleware/routes)
- Add integration tests for login/session/auth (AUTH_ENABLED=false)
- Set AUTH_ENABLED=false in test docker-compose

Made-with: Cursor
- Add src/server/auth.py:
  - validate_credentials() for Basic/ApiKey against _security/_authenticate
  - create_session_api_key() calling POST /_security/api_key
  - encode_jwt() / decode_jwt() for session tokens
  - AUTH_ENABLED config (default true)

- Update src/server/flask.py:
  - POST /api/auth/login, POST /api/auth/logout, GET /api/auth/session
  - before_request auth middleware for /api/* except login and /healthz
  - Decode cookie JWT, create per-request ES client via es_from_credentials
  - Attach g.user and g.es_client; AUTH_ENABLED=false uses service-account
  - Pass g.user and g.es_client to all API route handlers

- Update .env-reference with AUTH_ENABLED, JWT_SECRET, SESSION_EXPIRY

- Add PyJWT dependency

- Add unit tests (auth module, flask middleware/routes)
- Add integration tests for login/session/auth (AUTH_ENABLED=false)
- Set AUTH_ENABLED=false in test docker-compose

Made-with: Cursor
- Add es_studio_endpoint() returning endpoint config without credentials
- Add es_from_credentials(api_key or username/password, optional cloud_id/url)
  for constructing Elasticsearch client for studio endpoint
- Error when no credentials; prefer cloud_id when available else url
- Update API modules to accept optional es_client, fallback to es('studio') when None:
  workspaces, displays, scenarios, strategies, judgements, benchmarks,
  evaluations, conversations, setup
- content.py accepts optional es_client for consistency; uses content cluster
  service-account by default
- Update utils.search_assets(), search_tags() to accept optional es_client
- Worker behavior unchanged (uses es('studio') by default)
- Add unit tests for es_client, es_from_credentials, es_studio_endpoint
- Add unit tests for API es_client fallback/override
- Add integration test for workspaces CRUD with explicit es_client
- Fix setup test monkeypatches for new es_client signatures

Made-with: Cursor
- Add Login.js: Basic and API key modes, submit to /api/auth/login, success
  redirect, failure message
- Add AuthContext.js: session check on load via /api/auth/session,
  user/isAuthenticated/login/logout, register 401 handler
- Add auth401handler.js: registry for 401 handler (avoids circular imports)
- Update client.js: Axios 401 interceptor clears auth and redirects to /login
- Update Routes.js: AuthGuard, /login route, redirect when unauthenticated
- Update App.js: wrap with AuthProvider
- Update Page.js: show username and Sign out when authenticated
- Update api.js: auth_login, auth_logout, auth_session
- Incorporate Issue 4 client.py: es_from_credentials, es_studio_endpoint
- When AUTH_ENABLED=false, session returns system user, no login required
- Tests: test_login_accepts_api_key, test_auth_login_returns_system_when_disabled

Made-with: Cursor
- Add mcp_auth module: parse Authorization (Basic, ApiKey, Bearer), validate
  via auth module, create per-request ES client via es_from_credentials
- Add MCPAuthMiddleware: enforce auth per request/tool except /healthz and
  healthz_mcp; AUTH_ENABLED=false uses default user and singleton ES client
- Wire all MCP tools with ctx, user, and es_client from context
- Replace hardcoded user="ai" with authenticated username when auth enabled
- Update fastmcp_proxy.py docs for credential forwarding
- Update security and mcp-tools docs for MCP client credential config
- Unit tests: parse header, enforcement, exemptions, disabled mode, context
- Integration test: healthz exemption (skips if MCP server not running)

Made-with: Cursor
- Add es_studio_endpoint() returning endpoint config without credentials
- Add es_from_credentials(api_key or username/password, optional cloud_id/url)
  for constructing Elasticsearch client for studio endpoint
- Error when no credentials; prefer cloud_id when available else url
- Update API modules to accept optional es_client, fallback to es('studio') when None:
  workspaces, displays, scenarios, strategies, judgements, benchmarks,
  evaluations, conversations, setup
- content.py accepts optional es_client for consistency; uses content cluster
  service-account by default
- Update utils.search_assets(), search_tags() to accept optional es_client
- Worker behavior unchanged (uses es('studio') by default)
- Add unit tests for es_client, es_from_credentials, es_studio_endpoint
- Add unit tests for API es_client fallback/override
- Add integration test for workspaces CRUD with explicit es_client
- Fix setup test monkeypatches for new es_client signatures

Made-with: Cursor
- AssetCreate/AssetUpdate: enrich_meta adds created_via/updated_via from context['via']
- Validate via in {server,mcp,api}, default to 'api' when not provided
- Preserve @meta rejection behavior
- EvaluationCreate: add created_via with same validation/default
- Index templates: add @meta.created_via and @meta.updated_via (keyword) where @meta exists
- Judgements search: rated-ai/rated-human use @meta.updated_via (mcp vs not mcp) instead of updated_by
- Judgements set: script and upsert include created_via/updated_via
- Flask: pass via='server' to all create/update/set API calls
- FastMCP: pass via='mcp' to all create/update/set API calls
- Setup: pass via for ledger writes, add 1.2.0 migration for existing indices
- Agent: pass via='server' for conversation updates
- Unit tests: via validation/default, judgements filter, evaluation created_via

Made-with: Cursor
- src/server/tls.py: Central TLS config loader (TLS_ENABLED default true,
  require TLS_CERT_FILE + TLS_KEY_FILE when enabled, clear startup errors)
- src/server/flask.py: Use TLS config, ssl_context tuple, run via python -m
- src/server/fastmcp.py: Same TLS behavior for MCP HTTP transport
- .env-reference: TLS_ENABLED/TLS_CERT_FILE/TLS_KEY_FILE docs
- webpack.config.js: DEV_API_URL for optional dev TLS/proxy
- Dockerfile-server, Dockerfile-server-mcp: Document cert volume mount
- docker-compose.yml: Cert volume mounts, TLS_ENABLED=false for default
- scripts/quickstart.sh: --generate-tls-cert for self-signed local dev
- docs/docs/v1.1/reference/security.md: Native TLS docs, self-signed guide
- tests/unit/test_tls.py: Unit tests for TLS config
- tests/docker-compose.yml: TLS_ENABLED=false for integration tests

Made-with: Cursor
- Add src/server/auth.py:
  - validate_credentials() for Basic/ApiKey against _security/_authenticate
  - create_session_api_key() calling POST /_security/api_key
  - encode_jwt() / decode_jwt() for session tokens
  - AUTH_ENABLED config (default true)

- Update src/server/flask.py:
  - POST /api/auth/login, POST /api/auth/logout, GET /api/auth/session
  - before_request auth middleware for /api/* except login and /healthz
  - Decode cookie JWT, create per-request ES client via es_from_credentials
  - Attach g.user and g.es_client; AUTH_ENABLED=false uses service-account
  - Pass g.user and g.es_client to all API route handlers

- Update .env-reference with AUTH_ENABLED, JWT_SECRET, SESSION_EXPIRY

- Add PyJWT dependency

- Add unit tests (auth module, flask middleware/routes)
- Add integration tests for login/session/auth (AUTH_ENABLED=false)
- Set AUTH_ENABLED=false in test docker-compose

Made-with: Cursor
- Add mcp_auth module: parse Authorization (Basic, ApiKey, Bearer), validate
  via auth module, create per-request ES client via es_from_credentials
- Add MCPAuthMiddleware: enforce auth per request/tool except /healthz and
  healthz_mcp; AUTH_ENABLED=false uses default user and singleton ES client
- Wire all MCP tools with ctx, user, and es_client from context
- Replace hardcoded user="ai" with authenticated username when auth enabled
- Update fastmcp_proxy.py docs for credential forwarding
- Update security and mcp-tools docs for MCP client credential config
- Unit tests: parse header, enforcement, exemptions, disabled mode, context
- Integration test: healthz exemption (skips if MCP server not running)

Made-with: Cursor
- Add Login.js: Basic and API key modes, submit to /api/auth/login, success
  redirect, failure message
- Add AuthContext.js: session check on load via /api/auth/session,
  user/isAuthenticated/login/logout, register 401 handler
- Add auth401handler.js: registry for 401 handler (avoids circular imports)
- Update client.js: Axios 401 interceptor clears auth and redirects to /login
- Update Routes.js: AuthGuard, /login route, redirect when unauthenticated
- Update App.js: wrap with AuthProvider
- Update Page.js: show username and Sign out when authenticated
- Update api.js: auth_login, auth_logout, auth_session
- Incorporate Issue 4 client.py: es_from_credentials, es_studio_endpoint
- When AUTH_ENABLED=false, session returns system user, no login required
- Tests: test_login_accepts_api_key, test_auth_login_returns_system_when_disabled

Made-with: Cursor
- .env-reference: complete docs for AUTH_ENABLED, JWT_SECRET, SESSION_EXPIRY,
  TLS_ENABLED, TLS_CERT_FILE, TLS_KEY_FILE; clarified service-account roles for
  ELASTICSEARCH_* and CONTENT_*
- docs/v1.1/reference/security.md: add auth config table, align with final
  auth+TLS model
- Add docs: getting-started-auth, getting-started-tls, mcp-client-auth,
  auth-tls-migration
- scripts/quickstart.sh: auth/tls setup prompts and cert generation alignment
- Add unit test test_env_reference.py to verify env vars in .env-reference
- Fix test_api_setup mock for _append_applied_version via param

Made-with: Cursor
The setup API's _append_applied_version was extended with a via parameter
for issue 2 (meta via). The unit test mock must accept this keyword arg.

Made-with: Cursor
Docker Compose may return exit code 1 with 'No such container' due to a
known race on macOS even when containers start successfully. Use
check=False and rely on wait_for_es/wait_for_esrs to verify services.

Made-with: Cursor
- Add _request_with_retry for setup/upgrade HTTP calls to handle transient
  connection resets (Connection reset by peer) after heavy ES operations
- Add retry logic to delete_index_templates in conftest for ES connection issues
- Use retries in clean_data fixture for POST /api/setup

Made-with: Cursor
Add down -v --remove-orphans before up to avoid container name conflicts
and 'No such container' errors. Use --force-recreate on up for reliable
container creation. Add short sleep after down for Docker to settle.

Made-with: Cursor
- Add es_client=None to setup.check(), run(), upgrade() to accept
  the parameter passed from Flask routes (fixes 500 on /api/setup)
- Add pytest-asyncio>=0.24 to avoid Package object collection error
- Add -p no:asyncio for integration tests in CI workflow

Made-with: Cursor
- Add pytest-asyncio>=0.24 to requirements-dev.txt to fix 'Package' object
  has no attribute 'obj' collection error with pytest 8.x
- Add _post_setup_with_retry in conftest for clean_data fixture to handle
  transient 'Remote end closed connection' errors when calling /api/setup

Made-with: Cursor
- pytest.ini: add -p no:asyncio to fix pytest-asyncio collection error
- tests: add esrs-mcp service to docker-compose for integration tests
- tests/conftest: use absolute paths, cwd for docker compose; wait for MCP
- tests/integration/test_mcp_auth: use services fixture, skip when MCP unavailable

Made-with: Cursor
- Add pytest-asyncio>=0.24.0 for pytest 8 compatibility (fixes Package.obj)
- Pre-up docker compose down with --remove-orphans and 5s sleep
- Retry docker compose up up to 3 times on failure
- Add requests_get_retry/requests_post_retry for API calls in setup/upgrade tests

Made-with: Cursor
- test_flask_serves_https_with_valid_certs: HTTPS with self-signed certs, verify=False
- test_flask_serves_http_when_tls_disabled: HTTP when TLS disabled
- test_flask_fails_to_start_with_missing_cert: non-zero exit when cert missing
- test_https_without_trust_fails_verification: SSL error with verify=True

Uses openssl for cert generation, subprocess for Flask, requests for HTTP(S).
Marked with integration_tls. Skip if openssl not available.

Made-with: Cursor
…verify AUTH_ENABLED

- Fix 1: _request_user() extracts username from g.user dict so API modules
  receive string for created_by (Elasticsearch keyword field)
- Fix 2: Add test_decode_expired_token_raises, test_decode_tampered_token_raises,
  test_decode_wrong_secret_raises in test_auth.py
- Fix 3: flask.py already uses auth.AUTH_ENABLED (verified)
- Update evaluations_run to use user string directly (started_by)

Made-with: Cursor
…verify AUTH_ENABLED

- Fix 1: _request_user() extracts username from g.user dict so API modules
  receive string for created_by (Elasticsearch keyword field)
- Fix 2: Add test_decode_expired_token_raises, test_decode_tampered_token_raises,
  test_decode_wrong_secret_raises in test_auth.py
- Fix 3: flask.py already uses auth.AUTH_ENABLED (verified)
- Update evaluations_run to use user string directly (started_by)

Made-with: Cursor
…verify AUTH_ENABLED

- Fix 1: _request_user() extracts username from g.user dict so API modules
  receive string for created_by (Elasticsearch keyword field)
- Fix 2: Add test_decode_expired_token_raises, test_decode_tampered_token_raises,
  test_decode_wrong_secret_raises in test_auth.py
- Fix 3: flask.py already uses auth.AUTH_ENABLED (verified)
- Update evaluations_run to use user string directly (started_by)

Made-with: Cursor
Import auth.AUTH_ENABLED in mcp_auth.py instead of parsing env independently.
mcp_auth_middleware continues to use mcp_auth.AUTH_ENABLED (now delegated).

Made-with: Cursor
…ests

- Move /login route outside AuthGuard and ResourceProvider to prevent 401
  redirect loops when ResourceProvider makes API calls on mount
- Add LoginRoute wrapper to redirect authenticated users away from /login
- Export AuthGuard for testing
- Add Jest, React Testing Library, jest-environment-jsdom
- Add Login.test.js: basic/apikey modes, toggle, disabled submit, error display
- Add AuthContext.test.js: session check, isAuthenticated on 200/401
- Add AuthGuard.test.js: renders children when auth, redirects when not

Made-with: Cursor
Made-with: Cursor
…Display username of humans who made judgements.
@davemoore- davemoore- merged commit 5d2ca72 into main Mar 31, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant