feat(auth): implement API key authentication and management
Introduce a new API key authentication mechanism to support service-to-service communication (e.g., Airflow, CI/CD) without browser-based JWT login. - Add `APIKey` model and `APIKeyPrincipal` dependency for RBAC. - Implement `require_api_key_or_jwt` dependency with environment scoping. - Add admin CRUD endpoints for API key lifecycle management. - Add API key management UI in System Settings. - Update maintenance API to support explicit `environment_id` and API key auth. - Add i18n support for API keys and connection settings. - Include Python and Bash usage examples in the `examples/` directory. - Update technical specifications and documentation.
This commit is contained in:
@@ -41,16 +41,29 @@ def client():
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
def _mock_auth_overrides():
|
||||
"""Apply dependency overrides for authentication.
|
||||
|
||||
Creates a mock admin user so that all RBAC checks pass.
|
||||
The Admin role has full access via has_permission()'s special case.
|
||||
"""
|
||||
@pytest.fixture(autouse=True)
|
||||
def auth_mock(client, mock_db):
|
||||
"""Create a valid API key with maintenance permissions and mock JWT user."""
|
||||
from src.core.auth.api_key import generate_api_key
|
||||
from src.models.api_key import APIKey
|
||||
from src.dependencies import get_current_user
|
||||
from src.app import app
|
||||
|
||||
# Create admin user with Admin role (full access)
|
||||
# Create API key for mutation endpoints
|
||||
raw, prefix, key_hash = generate_api_key()
|
||||
api_key = APIKey(
|
||||
key_hash=key_hash,
|
||||
prefix=prefix,
|
||||
name="Test API Key",
|
||||
permissions=["maintenance:start", "maintenance:end", "maintenance:end_all"],
|
||||
active=True,
|
||||
)
|
||||
mock_db.add(api_key)
|
||||
mock_db.commit()
|
||||
|
||||
client.headers.update({"X-API-Key": raw})
|
||||
|
||||
# Mock JWT user for read-only endpoints
|
||||
mock_role = MagicMock()
|
||||
mock_role.name = "Admin"
|
||||
mock_role.permissions = []
|
||||
@@ -64,19 +77,10 @@ def _mock_auth_overrides():
|
||||
|
||||
app.dependency_overrides[get_current_user] = _mock_get_current_user
|
||||
|
||||
|
||||
def _clear_overrides():
|
||||
"""Clear dependency overrides."""
|
||||
from src.app import app
|
||||
app.dependency_overrides = {}
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def auth_mock():
|
||||
"""Auto-apply auth mocks for all tests."""
|
||||
_mock_auth_overrides()
|
||||
yield
|
||||
_clear_overrides()
|
||||
|
||||
client.headers.pop("X-API-Key", None)
|
||||
app.dependency_overrides = {}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -164,6 +168,7 @@ class TestStartEndpoint:
|
||||
"start_time": (datetime.now(timezone.utc) + timedelta(hours=1)).isoformat(),
|
||||
"end_time": (datetime.now(timezone.utc) + timedelta(hours=3)).isoformat(),
|
||||
"message": "Test maintenance",
|
||||
"environment_id": "test-env",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 202
|
||||
@@ -194,6 +199,7 @@ class TestStartEndpoint:
|
||||
"tables": ["raw.sales"],
|
||||
"start_time": (datetime.now(timezone.utc) + timedelta(hours=3)).isoformat(),
|
||||
"end_time": (datetime.now(timezone.utc) + timedelta(hours=1)).isoformat(),
|
||||
"environment_id": "test-env",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
@@ -208,6 +214,7 @@ class TestStartEndpoint:
|
||||
json={
|
||||
"tables": tables,
|
||||
"start_time": (datetime.now(timezone.utc) + timedelta(hours=1)).isoformat(),
|
||||
"environment_id": "test-env",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 422
|
||||
@@ -222,6 +229,7 @@ class TestStartEndpoint:
|
||||
"tables": ["raw.sales"],
|
||||
"start_time": (datetime.now(timezone.utc) + timedelta(hours=1)).isoformat(),
|
||||
"end_time": (datetime.now(timezone.utc) + timedelta(hours=3)).isoformat(),
|
||||
"environment_id": "test-env",
|
||||
}
|
||||
|
||||
# First call creates event
|
||||
|
||||
Reference in New Issue
Block a user