mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-07-15 15:05:48 +00:00
add key-import button to interface
This commit is contained in:
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
121
src/ahriman/web/views/service/pgp.py
Normal file
121
src/ahriman/web/views/service/pgp.py
Normal 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()
|
@ -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))
|
||||
|
||||
|
@ -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))
|
||||
|
||||
|
@ -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}")
|
||||
|
||||
|
59
src/ahriman/web/views/service/update.py
Normal file
59
src/ahriman/web/views/service/update.py
Normal 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()
|
@ -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
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user