mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-04-24 07:17:17 +00:00
render httpunauthorized as html in plain http requests
This commit is contained in:
parent
f2c23bad17
commit
4462eba860
31
package/share/ahriman/templates/error.jinja2
Normal file
31
package/share/ahriman/templates/error.jinja2
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Error</title>
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
<link rel="shortcut icon" href="/static/favicon.ico">
|
||||||
|
|
||||||
|
{% include "utils/style.jinja2" %}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="d-flex flex-row align-items-center">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-12 text-center">
|
||||||
|
<span class="display-1 d-block">{{ code }}</span>
|
||||||
|
<div class="mb-4 lead">{{ reason }}</div>
|
||||||
|
<a class="btn btn-link" style="text-decoration: none" href="/" title="home"><i class="bi bi-house"></i> home</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% include "utils/bootstrap-scripts.jinja2" %}
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
1
setup.py
1
setup.py
@ -66,6 +66,7 @@ setup(
|
|||||||
("share/ahriman/templates", [
|
("share/ahriman/templates", [
|
||||||
"package/share/ahriman/templates/build-status.jinja2",
|
"package/share/ahriman/templates/build-status.jinja2",
|
||||||
"package/share/ahriman/templates/email-index.jinja2",
|
"package/share/ahriman/templates/email-index.jinja2",
|
||||||
|
"package/share/ahriman/templates/error.jinja2",
|
||||||
"package/share/ahriman/templates/repo-index.jinja2",
|
"package/share/ahriman/templates/repo-index.jinja2",
|
||||||
"package/share/ahriman/templates/shell",
|
"package/share/ahriman/templates/shell",
|
||||||
"package/share/ahriman/templates/telegram-index.jinja2",
|
"package/share/ahriman/templates/telegram-index.jinja2",
|
||||||
|
@ -17,10 +17,11 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
import aiohttp_jinja2
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from aiohttp.web import middleware, Request
|
from aiohttp.web import middleware, Request
|
||||||
from aiohttp.web_exceptions import HTTPClientError, HTTPException, HTTPServerError
|
from aiohttp.web_exceptions import HTTPClientError, HTTPException, HTTPServerError, HTTPUnauthorized
|
||||||
from aiohttp.web_response import json_response, StreamResponse
|
from aiohttp.web_response import json_response, StreamResponse
|
||||||
|
|
||||||
from ahriman.web.middlewares import HandlerType, MiddlewareType
|
from ahriman.web.middlewares import HandlerType, MiddlewareType
|
||||||
@ -43,6 +44,11 @@ def exception_handler(logger: logging.Logger) -> MiddlewareType:
|
|||||||
async def handle(request: Request, handler: HandlerType) -> StreamResponse:
|
async def handle(request: Request, handler: HandlerType) -> StreamResponse:
|
||||||
try:
|
try:
|
||||||
return await handler(request)
|
return await handler(request)
|
||||||
|
except HTTPUnauthorized as e:
|
||||||
|
if is_templated_unauthorized(request):
|
||||||
|
context = {"code": e.status_code, "reason": e.reason}
|
||||||
|
return aiohttp_jinja2.render_template("error.jinja2", request, context, status=e.status_code)
|
||||||
|
return json_response(data={"error": e.reason}, status=e.status_code)
|
||||||
except HTTPClientError as e:
|
except HTTPClientError as e:
|
||||||
return json_response(data={"error": e.reason}, status=e.status_code)
|
return json_response(data={"error": e.reason}, status=e.status_code)
|
||||||
except HTTPServerError as e:
|
except HTTPServerError as e:
|
||||||
@ -55,3 +61,17 @@ def exception_handler(logger: logging.Logger) -> MiddlewareType:
|
|||||||
return json_response(data={"error": str(e)}, status=500)
|
return json_response(data={"error": str(e)}, status=500)
|
||||||
|
|
||||||
return handle
|
return handle
|
||||||
|
|
||||||
|
|
||||||
|
def is_templated_unauthorized(request: Request) -> bool:
|
||||||
|
"""
|
||||||
|
check if the request is eligible for rendering html template
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request(Request): source request to check
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True in case if response should be rendered as html and False otherwise
|
||||||
|
"""
|
||||||
|
return request.path in ("/api/v1/login", "/api/v1/logout") \
|
||||||
|
and "application/json" not in request.headers.getall("accept", [])
|
||||||
|
@ -335,6 +335,7 @@ def test_walk(resource_path_root: Path) -> None:
|
|||||||
resource_path_root / "web" / "templates" / "utils" / "style.jinja2",
|
resource_path_root / "web" / "templates" / "utils" / "style.jinja2",
|
||||||
resource_path_root / "web" / "templates" / "build-status.jinja2",
|
resource_path_root / "web" / "templates" / "build-status.jinja2",
|
||||||
resource_path_root / "web" / "templates" / "email-index.jinja2",
|
resource_path_root / "web" / "templates" / "email-index.jinja2",
|
||||||
|
resource_path_root / "web" / "templates" / "error.jinja2",
|
||||||
resource_path_root / "web" / "templates" / "repo-index.jinja2",
|
resource_path_root / "web" / "templates" / "repo-index.jinja2",
|
||||||
resource_path_root / "web" / "templates" / "shell",
|
resource_path_root / "web" / "templates" / "shell",
|
||||||
resource_path_root / "web" / "templates" / "telegram-index.jinja2",
|
resource_path_root / "web" / "templates" / "telegram-index.jinja2",
|
||||||
|
@ -2,12 +2,12 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from aiohttp.web_exceptions import HTTPBadRequest, HTTPInternalServerError, HTTPNoContent
|
from aiohttp.web_exceptions import HTTPBadRequest, HTTPInternalServerError, HTTPNoContent, HTTPUnauthorized
|
||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import AsyncMock
|
from unittest.mock import AsyncMock, MagicMock
|
||||||
|
|
||||||
from ahriman.web.middlewares.exception_handler import exception_handler
|
from ahriman.web.middlewares.exception_handler import exception_handler, is_templated_unauthorized
|
||||||
|
|
||||||
|
|
||||||
def _extract_body(response: Any) -> Any:
|
def _extract_body(response: Any) -> Any:
|
||||||
@ -23,6 +23,37 @@ def _extract_body(response: Any) -> Any:
|
|||||||
return json.loads(getattr(response, "body"))
|
return json.loads(getattr(response, "body"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_templated_unauthorized() -> None:
|
||||||
|
"""
|
||||||
|
must correct check if response should be rendered as template
|
||||||
|
"""
|
||||||
|
response_mock = MagicMock()
|
||||||
|
|
||||||
|
response_mock.path = "/api/v1/login"
|
||||||
|
response_mock.headers.getall.return_value = ["*/*"]
|
||||||
|
assert is_templated_unauthorized(response_mock)
|
||||||
|
|
||||||
|
response_mock.path = "/api/v1/login"
|
||||||
|
response_mock.headers.getall.return_value = ["application/json"]
|
||||||
|
assert not is_templated_unauthorized(response_mock)
|
||||||
|
|
||||||
|
response_mock.path = "/api/v1/logout"
|
||||||
|
response_mock.headers.getall.return_value = ["*/*"]
|
||||||
|
assert is_templated_unauthorized(response_mock)
|
||||||
|
|
||||||
|
response_mock.path = "/api/v1/logout"
|
||||||
|
response_mock.headers.getall.return_value = ["application/json"]
|
||||||
|
assert not is_templated_unauthorized(response_mock)
|
||||||
|
|
||||||
|
response_mock.path = "/api/v1/status"
|
||||||
|
response_mock.headers.getall.return_value = ["*/*"]
|
||||||
|
assert not is_templated_unauthorized(response_mock)
|
||||||
|
|
||||||
|
response_mock.path = "/api/v1/status"
|
||||||
|
response_mock.headers.getall.return_value = ["application/json"]
|
||||||
|
assert not is_templated_unauthorized(response_mock)
|
||||||
|
|
||||||
|
|
||||||
async def test_exception_handler(mocker: MockerFixture) -> None:
|
async def test_exception_handler(mocker: MockerFixture) -> None:
|
||||||
"""
|
"""
|
||||||
must pass success response
|
must pass success response
|
||||||
@ -50,6 +81,36 @@ async def test_exception_handler_success(mocker: MockerFixture) -> None:
|
|||||||
logging_mock.assert_not_called()
|
logging_mock.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_exception_handler_unauthorized(mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must handle unauthorized exception as json response
|
||||||
|
"""
|
||||||
|
request = pytest.helpers.request("", "", "")
|
||||||
|
request_handler = AsyncMock(side_effect=HTTPUnauthorized())
|
||||||
|
mocker.patch("ahriman.web.middlewares.exception_handler.is_templated_unauthorized", return_value=False)
|
||||||
|
render_mock = mocker.patch("aiohttp_jinja2.render_template")
|
||||||
|
|
||||||
|
handler = exception_handler(logging.getLogger())
|
||||||
|
response = await handler(request, request_handler)
|
||||||
|
assert _extract_body(response) == {"error": HTTPUnauthorized().reason}
|
||||||
|
render_mock.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_exception_handler_unauthorized_templated(mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must handle unauthorized exception as json response
|
||||||
|
"""
|
||||||
|
request = pytest.helpers.request("", "", "")
|
||||||
|
request_handler = AsyncMock(side_effect=HTTPUnauthorized())
|
||||||
|
mocker.patch("ahriman.web.middlewares.exception_handler.is_templated_unauthorized", return_value=True)
|
||||||
|
render_mock = mocker.patch("aiohttp_jinja2.render_template")
|
||||||
|
|
||||||
|
handler = exception_handler(logging.getLogger())
|
||||||
|
await handler(request, request_handler)
|
||||||
|
context = {"code": 401, "reason": "Unauthorized"}
|
||||||
|
render_mock.assert_called_once_with("error.jinja2", request, context, status=HTTPUnauthorized.status_code)
|
||||||
|
|
||||||
|
|
||||||
async def test_exception_handler_client_error(mocker: MockerFixture) -> None:
|
async def test_exception_handler_client_error(mocker: MockerFixture) -> None:
|
||||||
"""
|
"""
|
||||||
must handle client exception
|
must handle client exception
|
||||||
|
Loading…
Reference in New Issue
Block a user