@@ -10,6 +10,7 @@ from pathlib import Path
|
||||
from typing import Annotated, Literal
|
||||
from fastapi import FastAPI, File, Form, Header, HTTPException, Request, UploadFile
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse, JSONResponse, Response
|
||||
from sqlalchemy import func, select, text
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
@@ -80,6 +81,14 @@ async def security(request, call_next):
|
||||
})
|
||||
return response
|
||||
|
||||
# Register after security so CORS also decorates its early authentication errors.
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=['https://git.sadentel.net'],
|
||||
allow_methods=['GET', 'POST', 'OPTIONS'],
|
||||
allow_headers=['X-API-Key', 'X-Viewer-Token', 'Content-Type'],
|
||||
)
|
||||
|
||||
@app.exception_handler(HTTPException)
|
||||
async def http_error(request, exc):
|
||||
detail = exc.detail if isinstance(exc.detail, dict) else {'code': 'http_error', 'message': str(exc.detail)}
|
||||
|
||||
@@ -163,3 +163,65 @@ def test_parser_wall_timeout_is_reported(client,ascii_schematic,monkeypatch):
|
||||
process(job)
|
||||
status=client.get(f"/api/v1/schematics/{body['id']}/status",headers={'X-API-Key':KEY}).json()
|
||||
assert status['status']=='failed' and status['error']['code']=='parse_timeout'
|
||||
|
||||
|
||||
CORS_ORIGIN = 'https://git.sadentel.net'
|
||||
|
||||
@pytest.mark.parametrize('requested_headers', ['x-api-key', 'X-API-Key, Content-Type'])
|
||||
def test_gitea_preflight_without_api_key(client, requested_headers):
|
||||
response = client.options('/api/v1/schematics/render', headers={
|
||||
'Origin': CORS_ORIGIN,
|
||||
'Access-Control-Request-Method': 'POST',
|
||||
'Access-Control-Request-Headers': requested_headers,
|
||||
})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.headers['access-control-allow-origin'] == CORS_ORIGIN
|
||||
assert {'POST', 'OPTIONS'} <= set(response.headers['access-control-allow-methods'].split(', '))
|
||||
allowed = {h.strip().lower() for h in response.headers['access-control-allow-headers'].split(',')}
|
||||
assert {'x-api-key', 'content-type'} <= allowed
|
||||
assert 'Origin' in response.headers['vary']
|
||||
assert 'access-control-allow-credentials' not in response.headers
|
||||
|
||||
|
||||
@pytest.mark.parametrize('origin', ['https://evil.example', 'https://git.sadentel.net.evil.example', 'http://git.sadentel.net'])
|
||||
def test_cors_rejects_other_origins(client, origin):
|
||||
response = client.options('/api/v1/schematics/render', headers={
|
||||
'Origin': origin,
|
||||
'Access-Control-Request-Method': 'POST',
|
||||
'Access-Control-Request-Headers': 'x-api-key',
|
||||
})
|
||||
assert response.status_code == 400
|
||||
assert 'access-control-allow-origin' not in response.headers
|
||||
|
||||
|
||||
@pytest.mark.parametrize('key', [None, 'invalid-key', KEY])
|
||||
def test_gitea_post_retains_authentication_and_cors(client, ascii_schematic, key):
|
||||
headers = {'Origin': CORS_ORIGIN}
|
||||
if key is not None: headers['X-API-Key'] = key
|
||||
response = client.post('/api/v1/schematics/render', headers=headers,
|
||||
files={'file': ('design.SchDoc', ascii_schematic)}, data={'access': 'private'})
|
||||
assert response.status_code == (202 if key == KEY else 401)
|
||||
assert response.headers['access-control-allow-origin'] == CORS_ORIGIN
|
||||
|
||||
|
||||
def test_gitea_private_status_read(client, ascii_schematic):
|
||||
created = upload(client, ascii_schematic).json()
|
||||
endpoint = f"/api/v1/schematics/{created['id']}/status"
|
||||
preflight = client.options(endpoint, headers={
|
||||
'Origin': CORS_ORIGIN, 'Access-Control-Request-Method': 'GET',
|
||||
'Access-Control-Request-Headers': 'x-viewer-token',
|
||||
})
|
||||
assert preflight.status_code == 200
|
||||
response = client.get(endpoint, headers={'Origin': CORS_ORIGIN, 'X-Viewer-Token': created['viewer_token']})
|
||||
assert response.status_code == 200
|
||||
assert response.headers['access-control-allow-origin'] == CORS_ORIGIN
|
||||
assert client.get(endpoint, headers={'Origin': CORS_ORIGIN}).status_code == 404
|
||||
|
||||
|
||||
def test_cors_rejects_unneeded_methods_and_headers(client):
|
||||
for method, header in [('DELETE', 'x-api-key'), ('POST', 'x-unapproved-header')]:
|
||||
response = client.options('/api/v1/schematics/render', headers={
|
||||
'Origin': CORS_ORIGIN, 'Access-Control-Request-Method': method,
|
||||
'Access-Control-Request-Headers': header,
|
||||
})
|
||||
assert response.status_code == 400
|
||||
|
||||
مرجع در شماره جدید
مسدود کردن کاربر