add key-import button to interface

This commit is contained in:
2022-11-25 02:12:05 +02:00
parent 766081d212
commit 5073c80af1
41 changed files with 727 additions and 177 deletions

View File

@ -229,7 +229,7 @@ def _set_key_import_parser(root: SubParserAction) -> argparse.ArgumentParser:
"fail in case if key is not known for build user. This subcommand can be used "
"in order to import the PGP key to user keychain.",
formatter_class=_formatter)
parser.add_argument("--key-server", help="key server for key import", default="pgp.mit.edu")
parser.add_argument("--key-server", help="key server for key import", default="keyserver.ubuntu.com")
parser.add_argument("key", help="PGP key to import from public server")
parser.set_defaults(handler=handlers.KeyImport, architecture=[""], lock=None, report=False)
return parser

View File

@ -62,7 +62,7 @@ class Auth(LazyLogging):
Returns:
str: login control as html code to insert
"""
return """<button type="button" class="btn btn-link" data-bs-toggle="modal" data-bs-target="#loginForm" style="text-decoration: none"><i class="bi bi-box-arrow-in-right"></i> login</button>"""
return """<button type="button" class="btn btn-link" data-bs-toggle="modal" data-bs-target="#login-modal" style="text-decoration: none"><i class="bi bi-box-arrow-in-right"></i> login</button>"""
@classmethod
def load(cls: Type[Auth], configuration: Configuration, database: SQLite) -> Auth:

View File

@ -118,7 +118,7 @@ class GPG(LazyLogging):
"""
key = key if key.startswith("0x") else f"0x{key}"
try:
response = requests.get(f"http://{server}/pks/lookup", params={
response = requests.get(f"https://{server}/pks/lookup", params={
"op": "get",
"options": "mr",
"search": key

View File

@ -24,7 +24,7 @@ import uuid
from multiprocessing import Process, Queue
from threading import Lock, Thread
from typing import Callable, Dict, Iterable, Tuple
from typing import Callable, Dict, Iterable, Optional, Tuple
from ahriman.core.configuration import Configuration
from ahriman.core.log import LazyLogging
@ -78,6 +78,17 @@ class Spawn(Thread, LazyLogging):
result = callback(args, architecture)
queue.put((process_id, result))
def key_import(self, key: str, server: Optional[str]) -> None:
"""
import key to service cache
Args:
key(str): key to import
server(str): PGP key server
"""
kwargs = {} if server is None else {"key-server": server}
self.spawn_process("key-import", key, **kwargs)
def packages_add(self, packages: Iterable[str], *, now: bool) -> None:
"""
add packages
@ -86,12 +97,10 @@ class Spawn(Thread, LazyLogging):
packages(Iterable[str]): packages list to add
now(bool): build packages now
"""
if not packages:
return self.spawn_process("repo-update")
kwargs = {"source": PackageSource.AUR.value} # avoid abusing by building non-aur packages
if now:
kwargs["now"] = ""
return self.spawn_process("package-add", *packages, **kwargs)
self.spawn_process("package-add", *packages, **kwargs)
def packages_remove(self, packages: Iterable[str]) -> None:
"""
@ -102,6 +111,12 @@ class Spawn(Thread, LazyLogging):
"""
self.spawn_process("package-remove", *packages)
def packages_update(self, ) -> None:
"""
run full repository update
"""
self.spawn_process("repo-update")
def spawn_process(self, command: str, *args: str, **kwargs: str) -> None:
"""
spawn external ahriman process with supplied arguments

View File

@ -20,9 +20,8 @@
import aiohttp_jinja2
import logging
from aiohttp.web import middleware, Request
from aiohttp.web_exceptions import HTTPClientError, HTTPException, HTTPServerError, HTTPUnauthorized
from aiohttp.web_response import json_response, StreamResponse
from aiohttp.web import HTTPClientError, HTTPException, HTTPServerError, HTTPUnauthorized, Request, StreamResponse, \
json_response, middleware
from ahriman.web.middlewares import HandlerType, MiddlewareType

View File

@ -22,9 +22,11 @@ from pathlib import Path
from ahriman.web.views.index import IndexView
from ahriman.web.views.service.add import AddView
from ahriman.web.views.service.pgp import PGPView
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.service.update import UpdateView
from ahriman.web.views.status.logs import LogsView
from ahriman.web.views.status.package import PackageView
from ahriman.web.views.status.packages import PackagesView
@ -47,13 +49,16 @@ def setup_routes(application: Application, static_path: Path) -> None:
* ``POST /api/v1/service/add`` add new packages to repository
* ``GET /api/v1/service/pgp`` fetch PGP key from the keyserver
* ``POST /api/v1/service/pgp`` import PGP key from the keyserver
* ``POST /api/v1/service/remove`` remove existing package from repository
* ``POST /api/v1/service/request`` request to add new packages to repository
* ``GET /api/v1/service/search`` search for substring in AUR
* ``POST /api/v1/service/update`` update packages in repository, actually it is just alias for add
* ``POST /api/v1/service/update`` update all packages in repository
* ``GET /api/v1/packages`` get all known packages
* ``POST /api/v1/packages`` force update every package from repository
@ -84,13 +89,16 @@ def setup_routes(application: Application, static_path: Path) -> None:
application.router.add_post("/api/v1/service/add", AddView)
application.router.add_get("/api/v1/service/pgp", PGPView, allow_head=True)
application.router.add_post("/api/v1/service/pgp", PGPView)
application.router.add_post("/api/v1/service/remove", RemoveView)
application.router.add_post("/api/v1/service/request", RequestView)
application.router.add_get("/api/v1/service/search", SearchView, allow_head=False)
application.router.add_post("/api/v1/service/update", AddView)
application.router.add_post("/api/v1/service/update", UpdateView)
application.router.add_get("/api/v1/packages", PackagesView, allow_head=True)
application.router.add_post("/api/v1/packages", PackagesView)

View File

@ -20,16 +20,18 @@
from __future__ import annotations
from aiohttp.web import Request, View
from typing import Any, Dict, List, Optional, Type
from typing import Any, Callable, Dict, List, Optional, Type, TypeVar
from ahriman.core.auth import Auth
from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.spawn import Spawn
from ahriman.core.status.watcher import Watcher
from ahriman.models.user_access import UserAccess
T = TypeVar("T", str, List[str])
class BaseView(View):
"""
base web view to make things typed
@ -46,17 +48,6 @@ class BaseView(View):
configuration: Configuration = self.request.app["configuration"]
return configuration
@property
def database(self) -> SQLite:
"""
get database instance
Returns:
SQLite: database instance
"""
database: SQLite = self.request.app["database"]
return database
@property
def service(self) -> Watcher:
"""
@ -104,6 +95,29 @@ class BaseView(View):
permission: UserAccess = getattr(cls, f"{request.method.upper()}_PERMISSION", UserAccess.Full)
return permission
@staticmethod
def get_non_empty(extractor: Callable[[str], Optional[T]], key: str) -> T:
"""
get non-empty value from request parameters
Args:
extractor(Callable[[str], T]): function to get value by key
key(str): key to extract value
Returns:
T: extracted values if it is presented and not empty
Raises:
KeyError: in case if key was not found or value is empty
"""
try:
value = extractor(key)
if not value:
raise KeyError(key)
except Exception:
raise KeyError(f"Key {key} is missing or empty")
return value
async def extract_data(self, list_keys: Optional[List[str]] = None) -> Dict[str, Any]:
"""
extract json data from either json or form data

View File

@ -17,7 +17,7 @@
# 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 HTTPNoContent
from aiohttp.web import HTTPBadRequest, HTTPNoContent
from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView
@ -62,8 +62,11 @@ class AddView(BaseView):
< Server: Python/3.10 aiohttp/3.8.3
<
"""
data = await self.extract_data(["packages"])
packages = data.get("packages", [])
try:
data = await self.extract_data(["packages"])
packages = self.get_non_empty(lambda key: [package for package in data[key] if package], "packages")
except Exception as e:
raise HTTPBadRequest(reason=str(e))
self.spawner.packages_add(packages, now=True)

View File

@ -0,0 +1,121 @@
#
# 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, HTTPNotFound, Response, json_response
from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView
class PGPView(BaseView):
"""
pgp key management 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
"""
POST_PERMISSION = UserAccess.Full
GET_PERMISSION = HEAD_PERMISSION = UserAccess.Reporter
async def get(self) -> Response:
"""
retrieve key from the key server. It supports two query parameters: ``key`` - pgp key fingerprint and
``server`` which points to valid PGP key server
Returns:
Response: 200 with key body on success
Raises:
HTTPBadRequest: if bad data is supplied
HTTPNotFound: if key wasn't found or service was unable to fetch it
Examples:
Example of command by using curl::
$ curl -v -H 'Accept: application/json' 'http://example.com/api/v1/service/pgp?key=0xE989490C&server=keyserver.ubuntu.com'
> GET /api/v1/service/pgp?key=0xE989490C&server=keyserver.ubuntu.com HTTP/1.1
> Host: example.com
> User-Agent: curl/7.86.0
> Accept: application/json
>
< HTTP/1.1 200 OK
< Content-Type: application/json; charset=utf-8
< Content-Length: 3275
< Date: Fri, 25 Nov 2022 22:54:02 GMT
< Server: Python/3.10 aiohttp/3.8.3
<
{"key": "key"}
"""
try:
key = self.get_non_empty(self.request.query.getone, "key")
server = self.get_non_empty(self.request.query.getone, "server")
except Exception as e:
raise HTTPBadRequest(reason=str(e))
try:
key = self.service.repository.sign.key_download(server, key)
except Exception:
raise HTTPNotFound()
return json_response({"key": key})
async def post(self) -> None:
"""
store key to the local service environment
JSON body must be supplied, the following model is used::
{
"key": "0x8BE91E5A773FB48AC05CC1EDBED105AED6246B39", # key fingerprint to import
"server": "keyserver.ubuntu.com" # optional pgp server address
}
Raises:
HTTPBadRequest: if bad data is supplied
HTTPNoContent: in case of success response
Examples:
Example of command by using curl::
$ curl -v -H 'Content-Type: application/json' 'http://example.com/api/v1/service/pgp' -d '{"key": "0xE989490C"}'
> POST /api/v1/service/pgp HTTP/1.1
> Host: example.com
> User-Agent: curl/7.86.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 21
>
< HTTP/1.1 204 No Content
< Date: Fri, 25 Nov 2022 22:55:56 GMT
< Server: Python/3.10 aiohttp/3.8.3
<
"""
data = await self.extract_data()
try:
key = self.get_non_empty(data.get, "key")
except Exception as e:
raise HTTPBadRequest(reason=str(e))
self.spawner.key_import(key, data.get("server"))
raise HTTPNoContent()

View File

@ -65,7 +65,7 @@ class RemoveView(BaseView):
"""
try:
data = await self.extract_data(["packages"])
packages = data["packages"]
packages = self.get_non_empty(lambda key: [package for package in data[key] if package], "packages")
except Exception as e:
raise HTTPBadRequest(reason=str(e))

View File

@ -65,7 +65,7 @@ class RequestView(BaseView):
"""
try:
data = await self.extract_data(["packages"])
packages = data["packages"]
packages = self.get_non_empty(lambda key: [package for package in data[key] if package], "packages")
except Exception as e:
raise HTTPBadRequest(reason=str(e))

View File

@ -17,7 +17,7 @@
# 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 HTTPNotFound, Response, json_response
from aiohttp.web import HTTPBadRequest, HTTPNotFound, Response, json_response
from typing import Callable, List
from ahriman.core.alpm.remote import AUR
@ -45,6 +45,7 @@ class SearchView(BaseView):
Response: 200 with found package bases and descriptions sorted by base
Raises:
HTTPBadRequest: in case if bad data is supplied
HTTPNotFound: if no packages found
Examples:
@ -64,8 +65,12 @@ class SearchView(BaseView):
<
[{"package": "ahriman", "description": "ArcH linux ReposItory MANager"}, {"package": "ahriman-git", "description": "ArcH Linux ReposItory MANager"}]
"""
search: List[str] = self.request.query.getall("for", default=[])
packages = AUR.multisearch(*search, pacman=self.service.repository.pacman)
try:
search: List[str] = self.get_non_empty(lambda key: self.request.query.getall(key, default=[]), "for")
packages = AUR.multisearch(*search, pacman=self.service.repository.pacman)
except Exception as e:
raise HTTPBadRequest(reason=str(e))
if not packages:
raise HTTPNotFound(reason=f"No packages found for terms: {search}")

View File

@ -0,0 +1,59 @@
#
# 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 HTTPNoContent
from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView
class UpdateView(BaseView):
"""
update repository web view
Attributes:
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
"""
POST_PERMISSION = UserAccess.Full
async def post(self) -> None:
"""
run repository update. No parameters supported here
Raises:
HTTPNoContent: in case of success response
Examples:
Example of command by using curl::
$ curl -v -XPOST 'http://example.com/api/v1/service/update'
> POST /api/v1/service/update HTTP/1.1
> Host: example.com
> User-Agent: curl/7.86.0
> Accept: */*
>
< HTTP/1.1 204 No Content
< Date: Fri, 25 Nov 2022 22:57:56 GMT
< Server: Python/3.10 aiohttp/3.8.3
<
"""
self.spawner.packages_update()
raise HTTPNoContent()

View File

@ -17,8 +17,7 @@
# 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 aiohttp.web_exceptions import HTTPNotFound
from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound, Response, json_response
from ahriman.core.exceptions import UnknownPackageError
from ahriman.models.log_record_id import LogRecordId

View File

@ -17,8 +17,7 @@
# 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 HTTPFound
from aiohttp.web_exceptions import HTTPUnauthorized
from aiohttp.web import HTTPFound, HTTPUnauthorized
from ahriman.core.auth.helpers import check_authorized, forget
from ahriman.models.user_access import UserAccess