feat: harden OAuth state secret validation, DCR file permissions, and policy defaults
docker / test (pull_request) Successful in 24s
lint / lint (pull_request) Successful in 37s
lint / lint (push) Successful in 1m26s
test / test (push) Successful in 1m40s
test / test (pull_request) Successful in 34s
docker / lint (pull_request) Successful in 1m59s
docker / docker-test (pull_request) Successful in 14s
docker / docker-publish (pull_request) Has been skipped
docker / test (pull_request) Successful in 24s
lint / lint (pull_request) Successful in 37s
lint / lint (push) Successful in 1m26s
test / test (push) Successful in 1m40s
test / test (pull_request) Successful in 34s
docker / lint (pull_request) Successful in 1m59s
docker / docker-test (pull_request) Successful in 14s
docker / docker-publish (pull_request) Has been skipped
- Enforce 32-char minimum on OAUTH_STATE_SECRET at startup (config.py) - Write DCR client registry with owner-only (0o600) permissions before atomic replace - Flip policy.yaml default write action from allow → deny - Add CLAUDE.md with architecture, commands, and AGENTS.md contract summary - Add .pre-commit-config.yaml mirroring `make lint` checks - Update .gitignore: add .venv, .claude, .mypy_cache, .ruff_cache, .coverage.* - Extend docs: audit log rotation guidance, OAUTH_STATE_SECRET and DCR_STORAGE_PATH notes - Tests: short-secret rejection, 32-char acceptance, POSIX permission check for DCR store Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -106,3 +106,33 @@ def test_write_mode_allows_all_token_repos(monkeypatch: pytest.MonkeyPatch) -> N
|
||||
reset_settings()
|
||||
settings = get_settings()
|
||||
assert settings.write_allow_all_token_repos is True
|
||||
|
||||
|
||||
def _oauth_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Apply a minimal, valid OAuth-mode environment."""
|
||||
monkeypatch.setenv("GITEA_URL", "https://gitea.example.com")
|
||||
monkeypatch.setenv("OAUTH_MODE", "true")
|
||||
monkeypatch.setenv("GITEA_OAUTH_CLIENT_ID", "test-client-id")
|
||||
monkeypatch.setenv("GITEA_OAUTH_CLIENT_SECRET", "test-client-secret")
|
||||
monkeypatch.setenv("STARTUP_VALIDATE_GITEA", "false")
|
||||
|
||||
|
||||
def test_oauth_state_secret_too_short_is_rejected(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""OAUTH_STATE_SECRET shorter than 32 characters must fail validation."""
|
||||
_oauth_env(monkeypatch)
|
||||
monkeypatch.setenv("OAUTH_STATE_SECRET", "short-secret")
|
||||
|
||||
reset_settings()
|
||||
with pytest.raises(ValidationError, match="at least 32 characters"):
|
||||
get_settings()
|
||||
|
||||
|
||||
def test_oauth_state_secret_minimum_length_accepted(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""A 32+ character OAUTH_STATE_SECRET passes validation."""
|
||||
_oauth_env(monkeypatch)
|
||||
monkeypatch.setenv("OAUTH_STATE_SECRET", "x" * 32)
|
||||
|
||||
reset_settings()
|
||||
settings = get_settings()
|
||||
assert settings.oauth_mode is True
|
||||
assert len(settings.oauth_state_secret) == 32
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import stat
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
@@ -537,6 +539,29 @@ def test_dcr_registry_persists_registered_clients(tmp_path):
|
||||
assert reloaded.get(response["client_id"]) is not None
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
os.name != "posix", reason="POSIX permission bits are not enforced on this platform"
|
||||
)
|
||||
def test_dcr_storage_is_written_owner_only(tmp_path):
|
||||
"""The persisted DCR store must not be readable beyond its owner (0o600)."""
|
||||
storage_path = tmp_path / "dcr_clients.json"
|
||||
registry = OAuthClientRegistry(storage_path)
|
||||
request = OAuthRegistrationRequest.model_validate(
|
||||
{
|
||||
"client_name": "perm-client",
|
||||
"redirect_uris": ["http://127.0.0.1:8080/callback"],
|
||||
"token_endpoint_auth_method": "none",
|
||||
"grant_types": ["authorization_code"],
|
||||
"response_types": ["code"],
|
||||
}
|
||||
)
|
||||
|
||||
registry.register(request)
|
||||
|
||||
mode = stat.S_IMODE(os.stat(storage_path).st_mode)
|
||||
assert mode == 0o600
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Config validation tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user