dynamic html load (#63)

* dynamic html load
* split by classes
This commit is contained in:
2022-05-20 22:29:36 +03:00
committed by GitHub
parent 5674b7b388
commit 47de715d7d
72 changed files with 720 additions and 706 deletions

View File

@ -89,10 +89,13 @@ class AuthorizationPolicy(aiohttp_security.AbstractAuthorizationPolicy): # type
return await self.validator.verify_access(user.username, permission, context)
def auth_handler() -> MiddlewareType:
def auth_handler(allow_read_only: bool) -> MiddlewareType:
"""
authorization and authentication middleware
Args:
allow_read_only: allow
Returns:
MiddlewareType: built middleware
"""
@ -102,10 +105,14 @@ def auth_handler() -> MiddlewareType:
permission = await permission_method(request)
elif isinstance(handler, types.MethodType): # additional wrapper for static resources
handler_instance = getattr(handler, "__self__", None)
permission = UserAccess.Safe if isinstance(handler_instance, StaticResource) else UserAccess.Write
permission = UserAccess.Unauthorized if isinstance(handler_instance, StaticResource) else UserAccess.Full
else:
permission = UserAccess.Full
if permission == UserAccess.Unauthorized: # explicit if elif else for better code coverage
pass
elif allow_read_only and UserAccess.Read.permits(permission):
pass
else:
permission = UserAccess.Write
if permission != UserAccess.Safe:
await aiohttp_security.check_permission(request, permission, request.path)
return await handler(request)
@ -133,6 +140,6 @@ def setup_auth(application: web.Application, validator: Auth) -> web.Application
identity_policy = aiohttp_security.SessionIdentityPolicy()
aiohttp_security.setup(application, identity_policy, authorization_policy)
application.middlewares.append(auth_handler())
application.middlewares.append(auth_handler(validator.allow_read_only))
return application

View File

@ -25,7 +25,6 @@ from ahriman.web.views.service.add import AddView
from ahriman.web.views.service.remove import RemoveView
from ahriman.web.views.service.request import RequestView
from ahriman.web.views.service.search import SearchView
from ahriman.web.views.status.ahriman import AhrimanView
from ahriman.web.views.status.package import PackageView
from ahriman.web.views.status.packages import PackagesView
from ahriman.web.views.status.status import StatusView
@ -55,9 +54,6 @@ def setup_routes(application: Application, static_path: Path) -> None:
* POST /service-api/v1/update update packages in repository, actually it is just alias for add
* GET /status-api/v1/ahriman get current service status
* POST /status-api/v1/ahriman update service status
* GET /status-api/v1/packages get all known packages
* POST /status-api/v1/packages force update every package from repository
@ -65,7 +61,8 @@ def setup_routes(application: Application, static_path: Path) -> None:
* GET /status-api/v1/package/:base get package base status
* POST /status-api/v1/package/:base update package base status
* GET /status-api/v1/status get web service status itself
* GET /status-api/v1/status get service status itself
* POST /status-api/v1/status update service status itself
* GET /user-api/v1/login OAuth2 handler for login
* POST /user-api/v1/login login to service
@ -90,9 +87,6 @@ def setup_routes(application: Application, static_path: Path) -> None:
application.router.add_post("/service-api/v1/update", AddView)
application.router.add_get("/status-api/v1/ahriman", AhrimanView, allow_head=True)
application.router.add_post("/status-api/v1/ahriman", AhrimanView)
application.router.add_get("/status-api/v1/packages", PackagesView, allow_head=True)
application.router.add_post("/status-api/v1/packages", PackagesView)
@ -101,6 +95,7 @@ def setup_routes(application: Application, static_path: Path) -> None:
application.router.add_post("/status-api/v1/packages/{package}", PackageView)
application.router.add_get("/status-api/v1/status", StatusView, allow_head=True)
application.router.add_post("/status-api/v1/status", StatusView)
application.router.add_get("/user-api/v1/login", LoginView)
application.router.add_post("/user-api/v1/login", LoginView)

View File

@ -101,7 +101,7 @@ class BaseView(View):
Returns:
UserAccess: extracted permission
"""
permission: UserAccess = getattr(cls, f"{request.method.upper()}_PERMISSION", UserAccess.Write)
permission: UserAccess = getattr(cls, f"{request.method.upper()}_PERMISSION", UserAccess.Full)
return permission
async def extract_data(self, list_keys: Optional[List[str]] = None) -> Dict[str, Any]:

View File

@ -21,9 +21,7 @@ import aiohttp_jinja2
from typing import Any, Dict
from ahriman import version
from ahriman.core.auth.helpers import authorized_userid
from ahriman.core.util import pretty_datetime
from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView
@ -34,37 +32,19 @@ class IndexView(BaseView):
It uses jinja2 templates for report generation, the following variables are allowed:
* architecture - repository architecture, string, required
* auth - authorization descriptor, required
* authenticated - alias to check if user can see the page, boolean, required
* control - HTML to insert for login control, HTML string, required
* enabled - whether authorization is enabled by configuration or not, boolean, required
* username - authenticated username if any, string, null means not authenticated
* index_url - url to the repository index, string, optional
* packages - sorted list of packages properties, required
* base, string
* depends, sorted list of strings
* groups, sorted list of strings
* licenses, sorted list of strings
* packages, sorted list of strings
* status, string based on enum value
* status_color, string based on enum value
* timestamp, pretty printed datetime, string
* version, string
* web_url, string
* repository - repository name, string, required
* service - service status properties, required
* status, string based on enum value
* status_color, string based on enum value
* timestamp, pretty printed datetime, string
* version - ahriman version, string, required
Attributes:
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
HEAD_PERMISSION(UserAccess): (class attribute) head permissions of self
"""
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Safe
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Unauthorized
@aiohttp_jinja2.template("build-status.jinja2")
async def get(self) -> Dict[str, Any]:
@ -74,43 +54,15 @@ class IndexView(BaseView):
Returns:
Dict[str, Any]: parameters for jinja template
"""
# some magic to make it jinja-friendly
packages = [
{
"base": package.base,
"depends": package.depends,
"groups": package.groups,
"licenses": package.licenses,
"packages": list(sorted(package.packages)),
"status": status.status.value,
"status_color": status.status.bootstrap_color(),
"timestamp": pretty_datetime(status.timestamp),
"version": package.version,
"web_url": package.remote.web_url if package.remote is not None else None,
} for package, status in sorted(self.service.packages, key=lambda item: item[0].base)
]
service = {
"status": self.service.status.status.value,
"status_color": self.service.status.status.badges_color(),
"timestamp": pretty_datetime(self.service.status.timestamp),
}
# auth block
auth_username = await authorized_userid(self.request)
authenticated = not self.validator.enabled or self.validator.safe_build_status or auth_username is not None
auth = {
"authenticated": authenticated,
"control": self.validator.auth_control,
"enabled": self.validator.enabled,
"username": auth_username,
}
return {
"architecture": self.service.architecture,
"auth": auth,
"index_url": self.configuration.get("web", "index_url", fallback=None),
"packages": packages,
"repository": self.service.repository.name,
"service": service,
"version": version.__version__,
}

View File

@ -31,7 +31,7 @@ class AddView(BaseView):
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
"""
POST_PERMISSION = UserAccess.Write
POST_PERMISSION = UserAccess.Full
async def post(self) -> None:
"""

View File

@ -31,7 +31,7 @@ class RemoveView(BaseView):
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
"""
POST_PERMISSION = UserAccess.Write
POST_PERMISSION = UserAccess.Full
async def post(self) -> None:
"""

View File

@ -31,7 +31,7 @@ class RequestView(BaseView):
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
"""
POST_PERMISSION = UserAccess.Read
POST_PERMISSION = UserAccess.Reporter
async def post(self) -> None:
"""

View File

@ -35,7 +35,7 @@ class SearchView(BaseView):
HEAD_PERMISSION(UserAccess): (class attribute) head permissions of self
"""
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Read
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Reporter
async def get(self) -> Response:
"""

View File

@ -1,71 +0,0 @@
#
# Copyright (c) 2021-2022 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView
class AhrimanView(BaseView):
"""
service status web view
Attributes:
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
HEAD_PERMISSION(UserAccess): (class attribute) head permissions of self
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
"""
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Read
POST_PERMISSION = UserAccess.Write
async def get(self) -> Response:
"""
get current service status
Returns:
Response: 200 with service status object
"""
return json_response(self.service.status.view())
async def post(self) -> None:
"""
update service status
JSON body must be supplied, the following model is used::
{
"status": "unknown", # service status string, must be valid ``BuildStatusEnum``
}
Raises:
HTTPBadRequest: if bad data is supplied
HTTPNoContent: in case of success response
"""
try:
data = await self.extract_data()
status = BuildStatusEnum(data["status"])
except Exception as e:
raise HTTPBadRequest(reason=str(e))
self.service.update_self(status)
raise HTTPNoContent()

View File

@ -37,7 +37,7 @@ class PackageView(BaseView):
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
"""
DELETE_PERMISSION = POST_PERMISSION = UserAccess.Write
DELETE_PERMISSION = POST_PERMISSION = UserAccess.Full
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Read
async def get(self) -> Response:

View File

@ -34,7 +34,7 @@ class PackagesView(BaseView):
"""
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Read
POST_PERMISSION = UserAccess.Write
POST_PERMISSION = UserAccess.Full
async def get(self) -> Response:
"""

View File

@ -17,9 +17,10 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from aiohttp.web import Response, json_response
from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response
from ahriman import version
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.counters import Counters
from ahriman.models.internal_status import InternalStatus
from ahriman.models.user_access import UserAccess
@ -33,9 +34,11 @@ class StatusView(BaseView):
Attributes:
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
HEAD_PERMISSION(UserAccess): (class attribute) head permissions of self
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
"""
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Read
POST_PERMISSION = UserAccess.Full
async def get(self) -> Response:
"""
@ -46,9 +49,34 @@ class StatusView(BaseView):
"""
counters = Counters.from_packages(self.service.packages)
status = InternalStatus(
status=self.service.status,
architecture=self.service.architecture,
packages=counters,
repository=self.service.repository.name,
version=version.__version__)
return json_response(status.view())
async def post(self) -> None:
"""
update service status
JSON body must be supplied, the following model is used::
{
"status": "unknown", # service status string, must be valid ``BuildStatusEnum``
}
Raises:
HTTPBadRequest: if bad data is supplied
HTTPNoContent: in case of success response
"""
try:
data = await self.extract_data()
status = BuildStatusEnum(data["status"])
except Exception as e:
raise HTTPBadRequest(reason=str(e))
self.service.update_self(status)
raise HTTPNoContent()

View File

@ -34,7 +34,7 @@ class LoginView(BaseView):
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
"""
GET_PERMISSION = POST_PERMISSION = UserAccess.Safe
GET_PERMISSION = POST_PERMISSION = UserAccess.Unauthorized
async def get(self) -> None:
"""

View File

@ -32,7 +32,7 @@ class LogoutView(BaseView):
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
"""
POST_PERMISSION = UserAccess.Safe
POST_PERMISSION = UserAccess.Unauthorized
async def post(self) -> None:
"""