Compare commits

...

1563 commits

Author SHA1 Message Date
Leonidas Spyropoulos
8ca61eded2
chore(release): prepare for 6.2.16 2025-01-13 15:52:13 +00:00
Leonidas Spyropoulos
a9bf714dae
fix: bump deps for python 3.13 and vulnerability
pygit2 and watchfiles for precompiled wheels
greenlet for python 3.13 compatibility
python-multipart for security vulnerability

Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2025-01-12 20:39:02 +00:00
Leonidas Spyropoulos
3e3173b5c9
chore: avoid cache for new pacman 7
Pacman 7 introduced sandboxing which breaks cache in containers due to permissions on containers

Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2025-01-12 20:39:02 +00:00
Leonidas Spyropoulos
eca8bbf515
chore(release): prepare for 6.2.15
Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2024-09-15 12:03:17 +03:00
Jelle van der Waa
edc1ab949a perf(captcha): simplify count() query for user ids
Using .count() isn't great as it runs a count query on a subquery which
selects all fields in the Users table. This rewrites it into a simple
SELECT count(ID) from USers query.
2024-09-12 12:29:46 +00:00
Muflone
97cc6196eb fix: reduce the number of subqueries against Packages by preloading the existing dependencies names from AUR 2024-08-21 01:36:15 +02:00
Muflone
77ef87c882 housekeep: code re-formatted by black for lint pipeline 2024-08-20 21:00:46 +00:00
Muflone
a40283cdb2 fix: reduce the number of subqueries against User by loading eagerly the Users from PackageComaintainer 2024-08-20 21:00:46 +00:00
Levente Polyak
4f68532ee2
chore(mariadb): fix mysql deprecation warnings by using mariadb commands
Mariadb has scheduled to remove the deprecated mysql drop-in interface.
Let's adapt which also removes a lot of warnings while spinning up the
service.
2024-08-19 15:26:36 +02:00
Levente Polyak
439ccd4aa3
feat(docker): add full grafana, prometheus, tempo setup for local dev
This is a very useful stack for local development as well, by allowing
to easily access a local grafana instance and look at the accessed
endpoints, query usage and durations etc.
As a nice side effect this also makes sure we have an easy way to
actually test any changes to the opentelemetry integration in an actual
environment instead of just listening to a raw socket.
2024-08-19 15:26:29 +02:00
Levente Polyak
8dcf0b2d97
fix(docker): fix compose race conditions on mariadb_init
We want the dependent services to wait until the initialization service
of mariadb finishes, but also properly accept if it already exited
before a leaf service gets picked up and put into created state. By
using the service_completed_successfully signal, we can ensure precisely
this, without being racy and leading to none booted services.

While at it, remove the compose version identifiers as docker-compose
deprecated them and always warned about when running docker-compose.
2024-08-19 15:26:21 +02:00
Leonidas Spyropoulos
88e8db4404
chore(release): prepare version 6.2.14 2024-08-17 17:28:26 +01:00
Sven-Hendrik Haase
b730f6447d
feat: Add opentelemtry-based tracing
This adds tracing to fastapi, redis, and sqlalchemy. It uses the
recommended OLTP exporter to send the tracing data.
2024-08-17 11:27:26 +01:00
Leonidas Spyropoulos
92f5bbd37f
housekeep: reformat asgi.py 2024-08-17 01:31:43 +01:00
Jelle van der Waa
6c6ecd3971
perf(aurweb): create a context with what is required
The pkgbase/util.py `make_context` helper does a lot of unrelated
expensive queries which are not required for any of the templates. Only
the 404 template shows git_clone_uri_* and pkgbase.
2024-08-16 21:32:22 +02:00
Leonidas Spyropoulos
9b12eaf2b9
chore(release): prepare version 6.2.13 2024-08-16 16:03:40 +01:00
Jelle van der Waa
d1a66a743e
perf(aurweb/pkgbase): use exists() to avoid fetching a row
The previous approach fetched the matching row, by using `exists()`
SQLAlchemy changes the query to a `SELECT 1`.
2024-08-09 16:07:17 +02:00
Jelle van der Waa
b65d6c5e3a
perf(aurweb/pkgbase): only relevant queries when logged in
Don't query for notify, requests and vote information when the user is
not logged in as this information is not shown.
2024-08-09 16:07:17 +02:00
Jelle van der Waa
d393ed2352
fix(templates): hide non-actionable links when not logged in
A non-logged in user cannot vote/enable notifications or submit a
request so hide these links.
2024-08-09 16:07:17 +02:00
Leonidas Spyropoulos
a16fac9b95
fix: revert mysqlclient to 2.2.3 2024-08-09 11:02:13 +01:00
renovate
5dd65846d1
chore(deps): update dependency coverage to v7.6.1 2024-08-05 11:25:17 +00:00
renovate
a1b2d231c3
fix(deps): update dependency aiofiles to v24 2024-08-04 20:25:21 +00:00
renovate
f306b6df7a
fix(deps): update dependency fastapi to ^0.112.0 2024-08-04 12:25:03 +00:00
renovate
0d17895647
fix(deps): update dependency gunicorn to v22 2024-08-04 10:24:33 +00:00
renovate
36a56e9d3c
fix(deps): update all non-major dependencies 2024-08-04 09:24:29 +00:00
Diego Viola
80d3e5f7b6 housekeep: update .editorconfig url
Signed-off-by: Diego Viola <diego.viola@gmail.com>
2024-08-03 11:58:58 +00:00
Leonidas Spyropoulos
2df5a2d5a8
chore(release): prepare version 6.2.12 2024-08-03 10:46:29 +01:00
Leonidas Spyropoulos
a54b6935a1
housekeep: reformat files with pre-hooks
Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2024-08-03 08:15:56 +01:00
Levente Polyak
4d5909256f
fix: add missing indicies on PackageBase ordered columns
Signed-off-by: Levente Polyak <anthraxx@archlinux.org>
2024-08-03 04:45:31 +02:00
Levente Polyak
a5b94a47f3
feat: cache rss feedgen for 5 minutes
The RSS feed should be perfectly fine even when caching them for 5
minutes. This should massively reduce the response times on the
endpoint.

Signed-off-by: Levente Polyak <anthraxx@archlinux.org>
2024-08-03 04:45:24 +02:00
moson
33d31d4117
style: Indicate deleted accounts on requests page
Show "(deleted)" on requests page for user accounts that were removed.

Fixes #505

Signed-off-by: moson <moson@archlinux.org>
2024-06-24 16:35:21 +02:00
Leonidas Spyropoulos
ed878c8c5e
chore(release): prepare for 6.2.11 2024-06-10 11:49:00 +01:00
Leonidas Spyropoulos
77e4979f79
fix: remove the extra spaces in requests textarea
fixes: #503
Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2024-06-10 11:41:19 +01:00
Leonidas Spyropoulos
85af7d6f04
fix: revert Set reply-to header for notifications to ML
The change broke the initial emails to the ML. Not sure why but reverting this now and might look at later

This reverts commit 783422369e.

fixes: #502
2024-06-10 11:40:36 +01:00
Leonidas Spyropoulos
ef0619dc2f
chore(release): prepare for 6.2.10 2024-05-18 20:46:17 +01:00
moson
43b322e739
fix(CI): lint job - fix for python 3.12
Signed-off-by: moson <moson@archlinux.org>
2024-04-28 17:49:08 +02:00
moson
afb7af3e27
housekeep: replace deprecated datetime functions
tests show warnings for deprecated utc functions with python 3.12

Signed-off-by: moson <moson@archlinux.org>
2024-04-25 18:24:16 +02:00
moson
ffddf63975
housekeep: poetry - include python version 3.12
Signed-off-by: moson <moson@archlinux.org>
2024-04-25 07:46:39 +02:00
moson
c6a530f24f
chore(deps): bump pre-commit tools/libs
Prep for python 3.12
Reformat files with latest pre-commit tools

Signed-off-by: moson <moson@archlinux.org>
2024-04-25 07:25:39 +02:00
moson
3220cf886e
fix(CI): Remove "fast-single-thread" tag
Signed-off-by: moson <moson@archlinux.org>
2024-04-08 08:37:41 +02:00
moson
21e2ef5ecb
fix(test): Fix "TestClient"
TestClient changes were reverted with 0.37.2:

https://github.com/encode/starlette/pull/2525
https://github.com/encode/starlette/releases/tag/0.37.2
Signed-off-by: moson <moson@archlinux.org>
2024-04-08 08:37:41 +02:00
moson
6ba06801f7
chore(deps): update dependencies
- Updating pycparser (2.21 -> 2.22)
  - Updating sniffio (1.3.0 -> 1.3.1)
  - Updating typing-extensions (4.8.0 -> 4.11.0)
  - Updating anyio (3.7.1 -> 4.3.0)
  - Updating certifi (2023.11.17 -> 2024.2.2)
  - Updating greenlet (3.0.1 -> 3.0.3)
  - Updating markupsafe (2.1.3 -> 2.1.5)
  - Updating packaging (23.2 -> 24.0)
  - Updating pluggy (1.3.0 -> 1.4.0)
  - Updating pydantic-core (2.14.5 -> 2.16.3)
  - Updating coverage (7.4.0 -> 7.4.4)
  - Updating cryptography (41.0.5 -> 42.0.5)
  - Updating dnspython (2.4.2 -> 2.6.1)
  - Updating execnet (2.0.2 -> 2.1.0)
  - Updating httpcore (1.0.2 -> 1.0.5)
  - Updating lxml (5.1.0 -> 5.2.1)
  - Updating mako (1.3.0 -> 1.3.2)
  - Updating parse (1.20.0 -> 1.20.1)
  - Updating prometheus-client (0.19.0 -> 0.20.0)
  - Updating pydantic (2.5.2 -> 2.6.4)
  - Updating pytest (7.4.4 -> 8.1.1)
  - Updating python-dateutil (2.8.2 -> 2.9.0.post0)
  - Updating redis (5.0.1 -> 5.0.3)
  - Updating urllib3 (2.1.0 -> 2.2.1)
  - Updating asgiref (3.7.2 -> 3.8.1)
  - Updating email-validator (2.1.0.post1 -> 2.1.1)
  - Updating fakeredis (2.20.1 -> 2.21.3)
  - Updating fastapi (0.109.0 -> 0.110.1)
  - Updating filelock (3.13.1 -> 3.13.3)
  - Updating markdown (3.5.2 -> 3.6)
  - Updating mysqlclient (2.2.1 -> 2.2.4)
  - Updating orjson (3.9.12 -> 3.10.0)
  - Updating prometheus-fastapi-instrumentator (6.1.0 -> 7.0.0)
  - Updating protobuf (4.25.2 -> 5.26.1)
  - Updating pygit2 (1.13.3 -> 1.14.1)
  - Updating pytest-asyncio (0.23.3 -> 0.23.6)
  - Updating pytest-cov (4.1.0 -> 5.0.0)
  - Updating tomlkit (0.12.3 -> 0.12.4)
  - Updating uvicorn (0.27.0 -> 0.27.1)
  - Updating werkzeug (3.0.1 -> 3.0.2)
  - Updating starlette (0.35.0 -> 0.37.2)
  - Updating httpx (0.26.0 -> 0.27.0)
  - Updating python-multipart (0.0.6 -> 0.0.9)
  - Updating uvicorn (0.27.1 -> 0.29.0)
  - Updating sqlalchemy (1.4.50 -> 1.4.52)

Signed-off-by: moson <moson@archlinux.org>
2024-04-08 08:37:41 +02:00
moson
21a23c9abe
feat: Limit comment length
Limit the amount of characters that can be entered for a comment.

Signed-off-by: moson <moson@archlinux.org>
2024-02-25 10:46:47 +01:00
moson
d050b626db
feat: Add blacklist check for pkgbase
Also check "pkgbase" against our blacklist.

Signed-off-by: moson <moson@archlinux.org>
2024-02-17 15:55:46 +01:00
moson
057685f304
fix: Fix package info for 404 errors
We try to find packages when a user enters a URL like /somepkg
or accidentally opens /somepkg.git in the browser.

However, it currently also does this for URL's like /pkgbase/doesnotexist
and falsely interprets "pkgbase" part as a package or pkgbase name.
This in combination with a pkgbase that is named "pkgbase" generates
some misleading 404 message for URL's like /pkgbase/doesnotexist.

That being said, we should probably add pkgbase to the blacklist check
as well (we do this for pkgname already) and add things like
"pkgbase" to the blacklist -> Will be picked up in another commit.

Signed-off-by: moson <moson@archlinux.org>
2024-02-17 14:12:09 +01:00
renovate
319c565cb9
fix(deps): update all non-major dependencies 2024-01-23 22:24:28 +00:00
renovate
db6bba8bc8
fix(deps): update dependency feedgen to v1 2024-01-23 21:24:53 +00:00
renovate
a37b9685de
fix(deps): update dependency lxml to v5 2024-01-21 14:24:22 +00:00
moson
6e32cf4275
fix(i18n): Adjust transifex host URL
Fix URL, otherwise the API token won't be picked up from ~/.transifexrc

Signed-off-by: moson <moson@archlinux.org>
2024-01-21 11:40:14 +01:00
moson
76b6971267
chore(deps): Ignore python upgrades with Renovate
Stop Renovate from trying to bump the python version.

Signed-off-by: moson <moson@archlinux.org>
2024-01-21 10:43:12 +01:00
Robin Candau
9818c3f48c chore(i18n): Replace [community] leftover mentions to [extra] 2024-01-21 10:27:57 +01:00
moson
f967c3565a
chore(i18n): Update translations
Pull in updated translations from Transifex: 2023-01-18

Signed-off-by: moson <moson@archlinux.org>
2024-01-21 09:59:05 +01:00
moson
2fcd793a58
fix(test): Fixes for "TestClient" changes
Seems that client is optional according to the ASGI spec.
https://asgi.readthedocs.io/en/latest/specs/www.html

With Starlette 0.35 the TestClient connection  scope is None for "client".
https://github.com/encode/starlette/pull/2377

Signed-off-by: moson <moson@archlinux.org>
2024-01-19 16:37:42 +01:00
renovate
22e1577324
fix(deps): update dependency fastapi to ^0.109.0 2024-01-19 10:26:02 +01:00
moson
baf97bd159
fix(test): FastAPI 0.104.1 - Fix warnings
FastAPI events are deprecated. Use "Lifespan" function instead.

Signed-off-by: moson <moson@archlinux.org>
2023-12-08 14:15:18 +01:00
moson
a0b2e826be
feat: Parse markdown within html block elements
By default, markdown within an HTML block element is not parsed.
Add markdown extension to support markdown text within block
elements.

With this we can annotate our element with a "markdown" attribute:
E.g. <details markdown>*Markdown*</details>
And thus indicate that the content should be parsed.

Signed-off-by: moson <moson@archlinux.org>
2023-12-08 14:14:24 +01:00
moson
1ba9e6eb44
fix: change git-cliff "tag_pattern" option to regex
Changed with v1.4.0
See: https://github.com/orhun/git-cliff/pull/318

Signed-off-by: moson <moson@archlinux.org>
2023-12-08 14:12:48 +01:00
Rafael Fontenelle
1b82887cd6
docs: Change i18n.txt to markdown format 2023-12-08 14:10:32 +01:00
moson
783422369e
feat: Set reply-to header for notifications to ML
We can set the "reply-to" header to the "to" address for any mails
that go out to the aur-requests mailing list.

Signed-off-by: moson <moson@archlinux.org>
2023-11-28 09:33:07 +01:00
moson
4637b2edba
fix(tests): Fix test case for Prometheus metrics
Disable prometheus multiprocess mode in tests to avoid global state:
Depending on the workers which are processing a testfile,
we might run into race issues where tests might influence each other.

We also need to make sure to clear any previously collected values
in case the same worker/process is executing different tests which
evaluate prometheus values.

Signed-off-by: moson <moson@archlinux.org>
2023-11-27 13:21:37 +01:00
moson
027dfbd970
chore(release): prepare for 6.2.9
Signed-off-by: moson <moson@archlinux.org>
2023-11-25 20:30:29 +01:00
moson
8b234c580d
chore(deps): update dependencies
* Updating idna (3.4 -> 3.6)
* Updating annotated-types (0.5.0 -> 0.6.0)
* Updating pydantic-core (2.10.1 -> 2.14.5)
* Updating certifi (2023.7.22 -> 2023.11.17)
* Updating greenlet (3.0.0 -> 3.0.1)
* Updating pydantic (2.4.2 -> 2.5.2)
* Updating charset-normalizer (3.3.0 -> 3.3.2)
* Updating cryptography (41.0.4 -> 41.0.5)
* Updating fastapi (0.103.2 -> 0.104.1)
* Updating mako (1.2.4 -> 1.3.0)
* Updating parse (1.19.1 -> 1.20.0)
* Updating prometheus-client (0.17.1 -> 0.19.0)
* Updating urllib3 (2.0.6 -> 2.1.0)

Fix type annotation for new test function

Signed-off-by: moson <moson@archlinux.org>
2023-11-25 20:23:56 +01:00
renovate
9bf0c61051
fix(deps): update all non-major dependencies 2023-11-25 18:25:05 +00:00
moson
9d5b9c4795
feat: Add "groups" to package details page
Signed-off-by: moson <moson@archlinux.org>
2023-11-25 18:59:43 +01:00
moson
765f989b7d
feat: Allow <del> and <details/summary> tags in comments
* Allow additional html tags: <del> and <details/summary>
* Convert markdown double-tilde (~~) to <del> tags

Signed-off-by: moson <moson@archlinux.org>
2023-11-25 18:41:28 +01:00
Jelle van der Waa
029ce3b418
templates: update Gitlab navbar to point to Arch namespace
Instead of showing your own projects, show the Arch Linux namespace
where all our bugs/projects are.
2023-11-24 18:20:25 +01:00
Jelle van der Waa
3241391af0
templates: update bugs navbar entry to GitLab
Flyspray is no more and all projects are now on our own GitLab instance.
2023-11-12 16:02:16 +01:00
moson
5d302ae00c
feat: Support timezone and language query params
Support setting the timezone as well as the language via query params:
The timezone parameter previously only worked on certain pages.
While we're at it, let's also add the language as a param.
Refactor code for timezone and language functions.
Remove unused AURTZ cookie.

Signed-off-by: moson <moson@archlinux.org>
2023-10-21 10:41:44 +02:00
moson
933654fcbb
fix: Restrict context var override on the package page
Users can (accidentally) override context vars with query params.
This may lead to issues when rendering templates (e.g. "comments=").

Signed-off-by: moson <moson@archlinux.org>
2023-10-21 10:41:43 +02:00
moson
40c1d3e8ee
fix(ci): Don't create error reports from sandbox
We should not try to create issue reports for internal server errors
from a sandbox/review-app environment.

Signed-off-by: moson <moson@archlinux.org>
2023-10-20 15:45:58 +02:00
Hanabishi
2b8c8fc92a fix: make dependency source use superscript tag
Avoid using special characters and use '<sup>' HTML tag instead.
To not rely on user's fonts Unicode coverage.

Closes: #490
Signed-off-by: Hanabishi <1722-hanabishi@users.noreply.gitlab.archlinux.org>
2023-10-18 16:19:58 +00:00
moson
27c51430fb
chore(release): prepare for 6.2.8
Signed-off-by: moson <moson@archlinux.org>
2023-10-15 20:52:57 +02:00
moson
27cd533654
fix: Skip setting existing context values
When setting up a context with user provided variables,
we should not override any existing values previously set.

Signed-off-by: moson <moson@archlinux.org>
2023-10-12 18:09:07 +02:00
moson
2166426d4c
fix(deps): update dependencies
* Updating typing-extensions (4.5.0 -> 4.8.0)
* Installing annotated-types (0.5.0)
* Updating anyio (3.6.2 -> 3.7.1)
* Installing pydantic-core (2.10.1)
* Updating certifi (2023.5.7 -> 2023.7.22)
* Updating cffi (1.15.1 -> 1.16.0)
* Updating greenlet (2.0.2 -> 3.0.0)
* Updating markupsafe (2.1.2 -> 2.1.3)
* Updating packaging (23.1 -> 23.2)
* Updating pluggy (1.0.0 -> 1.3.0)
* Updating pydantic (1.10.7 -> 2.4.2)
* Updating charset-normalizer (3.1.0 -> 3.3.0)
* Updating click (8.1.3 -> 8.1.7)
* Updating coverage (7.2.7 -> 7.3.2)
* Updating cryptography (40.0.2 -> 41.0.4)
* Updating dnspython (2.3.0 -> 2.4.2)
* Updating execnet (1.9.0 -> 2.0.2)
* Updating fastapi (0.100.1 -> 0.103.2)
* Updating httpcore (0.17.0 -> 0.17.3)
* Updating parse (1.19.0 -> 1.19.1)
* Updating prometheus-client (0.16.0 -> 0.17.1)
* Updating pytest (7.4.0 -> 7.4.2)
* Updating redis (4.6.0 -> 5.0.1)
* Updating urllib3 (2.0.2 -> 2.0.6)
* Updating aiofiles (23.1.0 -> 23.2.1)
* Updating alembic (1.11.2 -> 1.12.0)
* Updating fakeredis (2.17.0 -> 2.19.0)
* Updating filelock (3.12.2 -> 3.12.4)
* Updating orjson (3.9.2 -> 3.9.7)
* Updating protobuf (4.23.4 -> 4.24.4)
* Updating pygit2 (1.12.2 -> 1.13.1)
* Updating werkzeug (2.3.6 -> 3.0.0)

Signed-off-by: moson <moson@archlinux.org>
2023-10-05 17:59:14 +02:00
moson
fd3022ff6c
fix: Correct password length message.
Wrong config option was used to display the minimum length error msg.
(username_min_len instead of passwd_min_len)

Signed-off-by: moson <moson@archlinux.org>
2023-10-02 13:47:38 +02:00
moson
9e9ba15813
housekeep: TU rename - Misc
Fix some more test functions

Signed-off-by: moson <moson@archlinux.org>
2023-09-30 16:45:05 +02:00
moson
d2d47254b4
housekeep: TU rename - Table/Column names, scripts
TU_VoteInfo -> VoteInfo
TU_Votes -> Votes
TU_VoteInfo.ActiveTUs -> VoteInfo.ActiveUsers

script: tuvotereminder -> votereminder
Signed-off-by: moson <moson@archlinux.org>
2023-09-30 16:45:05 +02:00
moson
87f6791ea8
housekeep: TU rename - Comments
Changes to comments, function descriptions, etc.

Signed-off-by: moson <moson@archlinux.org>
2023-09-30 16:45:05 +02:00
moson
61f1e5b399
housekeep: TU rename - Test suite
Rename tests: Function names, variables, etc.

Signed-off-by: moson <moson@archlinux.org>
2023-09-30 16:45:05 +02:00
moson
148c882501
housekeep: TU rename - /tu routes
Change /tu to /package-maintainer

Signed-off-by: moson <moson@archlinux.org>
2023-09-30 16:45:04 +02:00
moson
f540c79580
housekeep: TU rename - UI elements
Rename all UI elements and translations.

Signed-off-by: moson <moson@archlinux.org>
2023-09-30 16:45:04 +02:00
moson
1702075875
housekeep: TU rename - code changes
Renaming of symbols. Functions, variables, values, DB values, etc.
Basically everything that is not user-facing.

This only covers "Trusted User" things:
tests, comments, etc. will covered in a following commit.
2023-09-30 16:45:04 +02:00
moson
7466e96449
fix(ci): Exclude review-app jobs for renovate MR's
Signed-off-by: moson <moson@archlinux.org>
2023-09-26 13:47:03 +02:00
moson
0a7b02956f
feat: Indicate dependency source
Dependencies might reside in the AUR or official repositories.
Add "AUR" as superscript letters to indicate if a package/provider
is present in the AUR.

Signed-off-by: moson <moson@archlinux.org>
2023-09-03 14:17:11 +02:00
moson
1433553c05
fix(test): Clear previous prometheus data for test
It could happen that test data is already generated by a previous test.
(running in the same worker)

Make sure we clear everything before performing our checks.

Signed-off-by: moson <moson@archlinux.org>
2023-09-01 22:51:55 +02:00
moson
5699e9bb41
fix(test): Remove file locking and semaphore
All tests within a file run in the same worker and out test DB names
are unique per file as well. We don't really need a locking
mechanism here.

Same is valid for the test-emails. The only potential issue is that it
might try to create the same directory multiple times and thus run
into an error. However, that can be covered by specifying
"exist_ok=True" with os.makedirs such that those errors are ignored.

Signed-off-by: moson <moson@archlinux.org>
2023-09-01 22:51:55 +02:00
moson
9eda6a42c6
feat: Add ansible provisioning step for review-app
Clone infrastructure repository and run playbook to provision our VM
with aurweb.

Signed-off-by: moson <moson@archlinux.org>
2023-08-27 13:54:39 +02:00
Kristian Klausen
6c610b26a3
feat: Add terraform config for review-app[1]
Also removed the logic for deploying to the long gone aur-dev box.

Ansible will be added in a upcoming commit for configurating and
deploying aurweb on the VM.

[1] https://docs.gitlab.com/ee/ci/review_apps/
2023-08-27 12:05:52 +02:00
moson
3005e82f60
fix: Cleanup prometheus metrics for dead workers
The current "cleanup" function that is removing orphan prometheus files
is actually never invoked.
We move this to a default gunicorn config file to register our hook(s).

https://docs.gunicorn.org/en/stable/configure.html
https://docs.gunicorn.org/en/stable/settings.html#child-exit
Signed-off-by: moson <moson@archlinux.org>
2023-08-18 22:04:55 +02:00
Leonidas Spyropoulos
f05f1dbac7
chore(release): prepare for 6.2.7
Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2023-08-04 19:18:38 +03:00
renovate
8ad03522de
fix(deps): update all non-major dependencies 2023-08-04 14:25:22 +00:00
moson
94b62d2949
fix: Check if user exists when editing account
We should check if a user (target) exists before validating permissions.
Otherwise things crash when a TU is trying to edit an account that
does not exist.

Fixes: aurweb-errors#529
Signed-off-by: moson <moson@archlinux.org>
2023-08-04 14:12:50 +02:00
renovate
7a44f37968
fix(deps): update dependency fastapi to v0.100.1 2023-07-27 19:24:28 +00:00
renovate
969b84afe4
fix(deps): update all non-major dependencies 2023-07-25 11:24:30 +00:00
renovate
f74f94b501
fix(deps): update dependency gunicorn to v21 2023-07-24 11:24:26 +00:00
moson
375895f080
feat: Add Prometheus metrics for requests
Adds gauge for requests by type and status

Signed-off-by: moson <moson@archlinux.org>
2023-07-23 22:46:44 +02:00
moson
e45878a058
fix: Fix issue with requests totals
Problem is that we join with PackageBase, thus we are missing
requests for packages that were deleted.

Fixes: #483
Signed-off-by: moson <moson@archlinux.org>
2023-07-23 18:53:58 +02:00
moson
6cd70a5c9f
test: Add tests for user/package statistics
Signed-off-by: moson <moson@archlinux.org>
2023-07-23 13:58:51 +02:00
moson
8699457917
feat: Separate cache expiry for stats and search
Allows us to set different cache eviction timespans  for search queries
and statistics.

Stats and especially "last package updates" should probably be refreshed
more often, whereas we might want to cache search results for a bit
longer.

So this gives us a bit more flexibility playing around with different
settings and tweak things.

Signed-off-by: moson <moson@archlinux.org>
2023-07-23 13:58:50 +02:00
moson
44c158b8c2
feat: Implement statistics class & additional metrics
The new module/class helps us constructing queries and count records to
expose various statistics on the homepage. We also utilize for some new
prometheus metrics (package and user gauges).
Record counts are being cached with Redis.

Signed-off-by: moson <moson@archlinux.org>
2023-07-23 13:58:50 +02:00
moson
347c2ce721
change: Change order of commit validation routine
We currently validate all commits going from latest -> oldest.

It would be nicer to go oldest -> latest so that, in case of errors,
we would indicate which commit "introduced" the problem.

Signed-off-by: moson <moson@archlinux.org>
2023-07-22 10:45:08 +02:00
moson
bc03d8b8f2
fix: Fix middleware checking for accepted terms
The current query is a bit mixed up. The intention was to return the
number of unaccepted records. Now it does also count all records
that were accepted by some other user though.

Let's check the total number of terms vs. the number of accepted
records (by our user) instead.

Signed-off-by: moson <moson@archlinux.org>
2023-07-20 18:21:05 +02:00
moson
5729d6787f
fix: git links in comments for multiple OIDs
The chance of finding multiple object IDs when performing lookups with
a shortened SHA1 hash (7 digits) seems to be quite high.

In those cases pygit2 will throw an error.
Let's catch those exceptions and gracefully handle them.

Fixes: aurweb-errors#496 (and alike)
Signed-off-by: moson <moson@archlinux.org>
2023-07-17 12:45:16 +02:00
renovate
862221f5ce
fix(deps): update all non-major dependencies 2023-07-15 20:27:12 +00:00
moson
27819b4465
fix: /rss lazy load issue & perf improvements
Some fixes for the /rss endpoints

* Load all data in one go:
Previously data was lazy loaded thus it made several sub-queries per
package (> 200 queries for composing the rss data for a single request).
Now we are performing a single SQL query.
(request time improvement: 550ms -> 130ms)
This also fixes aurweb-errors#510 and alike

* Remove some "dead code":
The fields "source, author, link" were never included in the rss output
(wrong or insufficient data passed to the different entry.xyz functions)
Nobody seems to be missing them anyways, so let's remove em.

* Remove "Last-Modified" header:
Obsolete since nginx can/will only handle "If-Modified-Since" requests
in it's current configuration. All requests are passed to fastapi anyways.

Signed-off-by: moson <moson@archlinux.org>
2023-07-13 18:27:02 +02:00
moson
fa1212f2de
fix: translations not containing string formatting
In some translations we might be missing replacement placeholders (%).
This turns out to be problematic when calling the format function.

Wrap the jinja2 format function and just return the string unformatted
when % is missing.

Fixes: #341
Signed-off-by: moson <moson@archlinux.org>
2023-07-10 18:02:20 +02:00
moson
c0bbe21d81
fix(test): correct test for ssh-key parsing
Our set of keys returned by "util.parse_ssh_keys" is unordered so we
have to adapt our test to not rely on a specific order for multiple keys.

Fixes: 5ccfa7c0fd ("fix: same ssh key entered multiple times")
Signed-off-by: moson <moson@archlinux.org>
2023-07-09 16:13:02 +02:00
moson
5ccfa7c0fd
fix: same ssh key entered multiple times
Users might accidentally past their ssh key multiple times
when they try to register or edit their account.

Convert our of list of keys to a set, removing any double keys.

Signed-off-by: moson <moson@archlinux.org>
2023-07-09 14:52:15 +02:00
Leonidas Spyropoulos
225ce23761
chore(release): prepare for 6.2.6
Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2023-07-08 12:54:43 +01:00
moson
4821fc1312
fix: show placeholder for deleted user in comments
show "<deleted-account>" in comment headers in case a user
deleted their account.

Signed-off-by: moson <moson@archlinux.org>
2023-07-08 13:44:24 +02:00
Leonidas Spyropoulos
1f40f6c5a0
housekeep: set current maintainers
Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2023-07-08 12:38:19 +01:00
renovate
81d29b4c66
fix(deps): update dependency fastapi to ^0.100.0 2023-07-08 11:24:29 +00:00
renovate
7cde1ca560
fix(deps): update all non-major dependencies 2023-07-08 09:25:09 +00:00
moson-mo
f3f8c0a871
fix: add recipients to BCC when email is hidden
Package requests are sent to the ML as well as users (CC).
For those who chose to hide their mail address,
we should add them to the BCC list instead.

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-07-08 11:19:02 +02:00
moson
9fe8d524ff
fix(test): MariaDB 11 upgrade, query result order
Fix order of recipients for "FlagNotification" test.
Apply sorting to the recipients query.
(only relevant for tests, but who knows when they change things again)

MariaDB 11 includes some changes related to the
query optimizer. Turns out that this might have effects
on how records are ordered for certain queries.
(in case no ORDER BY clause was specified)

https://mariadb.com/kb/en/mariadb-11-0-0-release-notes/
Signed-off-by: moson <moson@archlinux.org>
2023-07-08 10:32:26 +02:00
moson-mo
814ccf6b04
feat: add Prometheus metrics for Redis cache
Adding a Prometheus counter to be able to monitor cache hits/misses
for search queries

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-07-04 11:57:56 +02:00
moson-mo
3acfb08a0f
feat: cache package search results with Redis
The queries being done on the package search page are quite costly.
(Especially the default one ordered by "Popularity" when navigating to /packages)

Let's add the search results to the Redis cache:
Every result of a search query is being pushed to Redis until we hit our maximum of 50k.
An entry expires after 3 minutes before it's evicted from the cache.
Lifetime an Max values are configurable.

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-07-04 11:57:56 +02:00
moson-mo
7c8b9ba6bc
perf: add index to tweak our default search query
Adds an index on PackageBases.Popularity and PackageBases.Name to
improve performance of our default search query sorted by "Popularity"

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-07-02 13:55:21 +02:00
moson-mo
c41f2e854a
perf: tweak some search queries
We currently sorting on two columns in different tables which is quite
expensive in terms of performance:
MariaDB is first merging the data into some temporary table to apply the
sorting and record limiting.

We can tweak a couple of these queries by changing the "order by" clause
such that they refer to columns within the same table (PackageBases).
So instead performing the second sorting on "Packages.Name", we do
this on "PackageBases.Name" instead.
This should still be "good enough" to produce properly sorted results.

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-07-02 13:21:11 +02:00
Leonidas Spyropoulos
e2c113caee
chore(release): prepare for 6.2.5
Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2023-06-22 19:22:56 +01:00
moson-mo
143575c9de
fix: restore command, remove premature creation of pkgbase
We're currently creating a "PackageBases" when the "restore" command is executed.

This is problematic for pkgbases that never existed before.
In those cases it will create the record but fail in the update.py script.
Thus it leaves an orphan "PackageBases" record in the DB
(which does not have any related "Packages" record(s))

Navigating to such a packages /pkgbase/... URL will result in a crash
since it is not foreseen to have "orphan" pkgbase records.

We can safely remove the early creation of that record because
it'll be taken care of in the update.py script that is being called

We'll also fix some tests. Before it was executing a dummy script
instead of "update.py" which might be a bit misleading
since it did not check the real outcome of our "restore" action.

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-06-16 14:22:22 +02:00
moson-mo
c6c81f0789
housekeep: Amend .gitignore and .dockerignore
Prevent some files/dirs to end up in the repo / docker image:
* directories typically used for python virtualenvs
* files that are being generated by running tests

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-06-16 13:33:39 +02:00
moson-mo
32461f28ea
fix(docker): Suppress error PEP-668
When using docker (compose), we don't create a venv and just install
python packages system-wide.

With python 3.11 (PEP 668) we need to explicitly tell pip to allow this.

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-06-15 14:19:02 +02:00
moson-mo
58158505b0
fix: browser hints for password fields
Co-authored-by: eNV25 <env252525@gmail.com>
Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-06-11 21:04:35 +02:00
moson-mo
ed17486da6
change(git): allow keys/pgp subdir with .asc files
This allows migration of git history for packages dropped from a repo to AUR
in case they contain PGP key material

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-06-11 12:20:02 +02:00
moson-mo
1c11c901a2
feat: switch requests filter for pkgname to "contains"
Use "contains" filtering instead of an exact match
when a package name filter is given.

This makes it easier to find requests for a "group" of packages.

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-06-10 09:40:35 +02:00
Christian Heusel
26b2566b3f
change: print the user name if connecting via ssh
this is similar to the message that gitlab produces:

$ ssh -T aur.archlinux.org
Welcome to AUR, gromit! Interactive shell is disabled.
Try `ssh ssh://aur@aur.archlinux.org help` for a list of commands.

$ ssh -T gitlab.archlinux.org
Welcome to GitLab, @gromit!

Signed-off-by: Christian Heusel <christian@heusel.eu>
2023-06-08 12:47:27 +02:00
Christian Heusel
e9cc2fb437
change: only require .SRCINFO in the latest revision
This is done in order to relax the constraints so that dropping packages
from the official repos can be done with preserving their history.

Its sufficient to also have this present in the latest commit of a push.

Signed-off-by: Christian Heusel <christian@heusel.eu>
2023-06-07 18:54:31 +02:00
Leonidas Spyropoulos
ed2f85ad04
chore(release): prepare for 6.2.4
Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2023-05-27 14:28:48 +01:00
renovate
2709585a70
fix(deps): update dependency fastapi to v0.95.2 2023-05-27 11:24:46 +00:00
renovate
d1a3fee9fe fix(deps): update all non-major dependencies 2023-05-26 21:12:13 +00:00
moson-mo
49e98d64f4
chore: increase default session/cookie timeout
change from 2 to 4 hours.

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-05-26 23:03:38 +02:00
moson-mo
a7882c7533
refactor: remove session_time from user.login
The parameter is not used, we can remove it and adapt the callers.

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-05-26 23:02:38 +02:00
moson-mo
22fe4a988a
fix: make AURSID a session cookie if "remember me" is not checked
This should match more closely the expectation of a user.
A session cookie should vanish on browser close
and you thus they need to authenticate again.

There is no need to bump the expiration of AURSID either,
so we can remove that part.

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-05-26 22:57:47 +02:00
moson-mo
0807ae6b7c
test: add tests for cookie handling
add a bunch of test cases to ensure our cookies work properly

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-05-26 22:57:46 +02:00
moson-mo
d366377231
fix: make AURREMEMBER cookie a permanent one
If it's a session cookie it poses issues for users
whose browsers wipe session cookies after close.
They'd be logged out early even if they chose
the "remember me" option when they log in.

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-05-26 22:57:46 +02:00
moson-mo
57c154a72c
fix: increase expiry for AURLANG cookie; only set when needed
We add a new config option for cookies with a 400 day lifetime.
AURLANG should survive longer for unauthenticated users.
Today they have to set this again after each browser restart.
(for users whose browsers wipe session cookies on close)

authenticated users don't need this cookie
since the setting is saved to the DB

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-05-26 22:57:46 +02:00
moson-mo
638ca7b1d0
chore: remove setting AURLANG and AURTZ on login
We don't need to set these cookies when logging in.
These settings are saved to the DB anyways.
(and they are picked up from there as well for any web requests,
when no cookies are given)

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-05-26 22:57:46 +02:00
moson-mo
edc4ac332d
chore: remove setting AURLANG and AURTZ on account edit
We don't need to set these cookies when an account is edited.
These settings are saved to the DB anyways.
(and they are picked up from there as well for any web requests,
when no cookies are given)

Setting these cookies can even be counter-productive:
Imagine a TU/Dev editing another users account.
They would overwrite their own cookies with the other users TZ/LANG settings.

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-05-26 22:57:46 +02:00
moson-mo
2eacc84cd0
fix: properly evaluate AURREMEMBER cookie
Whenever the AURREMEMBER cookie was defined, regardless of its value,
"remember_me" is always set to True

The get method of a dict returns a string,
converting a value of str "False" into a bool -> True

We have to check AURREMEMBERs value instead.

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-05-26 22:57:46 +02:00
Daniel M. Capella
5fe375bdc3
feat: add link to MergeBaseName in requests.html 2023-05-26 13:26:41 -04:00
renovate
1b41e8572a
fix(deps): update all non-major dependencies 2023-05-26 02:24:39 +00:00
moson-mo
7a88aeb673
chore: update .gitignore for test-emails
emails generated when running tests are stored in test-emails/ dir

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-05-25 11:18:08 +02:00
moson-mo
f24fae0ce6
feat: Add "Requests" filter option for package name
- Add package name textbox for filtering requests (with auto-suggest)
- Make "x pending requests" a link for TU/Dev on the package details page

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-05-25 11:18:08 +02:00
Leonidas Spyropoulos
acdb2864de
Merge branch 'aurblup-update-repo' into 'master'
fix: update repo information with aurblup script / git packaging repo changes

See merge request archlinux/aurweb!710
2023-05-25 10:06:44 +01:00
moson-mo
146943b3b6
housekeep: support new default repos after git migration
community is merged into extra
testing -> core-testing & extra-testing

Announcement: https://archlinux.org/news/git-migration-announcement/

We list "testing" repos first:
See d0b0e4d88b

Co-authored-by: artafinde <artafinde@archlinux.org>
Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-05-18 13:06:21 +02:00
moson-mo
d0b0e4d88b
fix: update repo information with aurblup script
Currently, the "Repo" column in "OfficialProviders" is not updated
when a package is moved from one repository to another.

Note that we only save a package/provides combination once,
hence if a package is available in core and testing at the same time,
it would only put just one record into the OfficialProviders table.

We iterate through the repos one by one and the last value
is kept for mapping a (package/provides) combination to a repo.
Due to that, the repos listed in the "sync-db" config setting
should be ordered such that the "testing" repos are listed first.

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-05-17 18:22:53 +02:00
moson-mo
3253a6ad29
fix(deps): remove urllib3 from dependency list
Previously pinned urllib3 to v1.x. This is not needed though.
The incompatibility of v2.x is with poetry itself, but not aurweb.

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-05-07 09:58:17 +02:00
Daniel M. Capella
d2e8fa0249
chore(deps): "Group all minor and patch updates together"
Treat FastAPI separately due to regular breakage.

Co-authored-by: moson-mo <mo-son@mailbox.org>
2023-05-06 18:03:05 -04:00
Leonidas Spyropoulos
1d627edbe7
chore(release): prepare for 6.2.3
Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2023-05-06 20:34:54 +01:00
moson-mo
b115aedf97
chore(deps): update several dependencies
- Removing rfc3986 (1.5.0)
- Updating coverage (7.2.4 -> 7.2.5)
- Updating fastapi (0.94.1 -> 0.95.1)
- Updating httpcore (0.16.3 -> 0.17.0)
- Updating sqlalchemy (1.4.47 -> 1.4.48)
- Updating httpx (0.23.3 -> 0.24.0)
- Updating prometheus-fastapi-instrumentator (5.11.2 -> 6.0.0)
- Updating protobuf (4.22.3 -> 4.22.4)
- Updating pytest-asyncio (0.20.3 -> 0.21.0)
- Updating requests (2.29.0 -> 2.30.0)
- Updating uvicorn (0.21.1 -> 0.22.0)
- Updating watchfiles (0.18.1 -> 0.19.0)
- Updating werkzeug (2.3.2 -> 2.3.3)

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-05-06 20:29:05 +02:00
Christian Heusel
af4239bcac
replace reference to AUR TU Guidelines with AUR Submission Guidelines
Signed-off-by: Christian Heusel <christian@heusel.eu>
2023-05-06 19:47:01 +02:00
Leonidas Spyropoulos
a8d14e0194
housekeep: remove unused templates and rework existing ones
Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2023-05-01 10:44:45 +01:00
moson-mo
8c5b85db5c
housekeep: remove fix for poetry installer
The problems with the "modern installer" got fixed.
We don't need this workaround anymore.

https://github.com/python-poetry/poetry/issues/7572
Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-05-01 10:23:34 +02:00
moson-mo
b3fcfb7679
doc: improve instructions for setting up a dev/test env
Provide more detailed information how to get started with a dev/test env.

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-05-01 10:23:34 +02:00
Leonidas Spyropoulos
e896edaccc
chore: support for python 3.11 and poetry.lock update
Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2023-04-30 10:12:09 +01:00
moson-mo
bab17a9d26
doc: amend INSTALL instructions
change path for metadata archive files

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-04-29 09:59:34 +02:00
moson-mo
ad61c443f4
fix: restore & move cgit html files
restore files accidentally deleted with PHP cleanup.

1325c71712/web/template/cgit
Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-04-29 09:55:54 +02:00
moson-mo
8ca63075e9
housekeep: remove PHP implementation
removal of the PHP codebase

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-04-28 16:10:32 +02:00
moson-mo
97d0eac303
housekeep: copy static files
we copy static files used by PHP and Python versions into /static

preparation work for the removal of the PHP version

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-04-28 10:53:22 +02:00
Leonidas Spyropoulos
1325c71712
chore: update poetry.lock
Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2023-04-24 09:13:38 +01:00
Leonidas Spyropoulos
6ede837b4f
feat: allow users to hide deleted comments
Closes: #435

Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2023-04-24 09:13:38 +01:00
Leonidas Spyropoulos
174af5f025
chore(release): prepare for 6.2.2
Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2023-03-15 12:09:47 +00:00
moson-mo
993a044680
fix(poetry): use classic installer
The "install" module (v0.6.0) which is being used by poetry 1.4.0
has problems installing certain packages.

Disable the modern installer for now, until things are fixed.

https://github.com/python-poetry/poetry/issues/7572
Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-03-14 17:57:57 +01:00
moson-mo
bf0d4a2be7
fix(deps): bump dependencies
bump all deps except sqlalchemy.

- Updating exceptiongroup (1.1.0 -> 1.1.1)
- Updating pydantic (1.10.5 -> 1.10.6)
- Updating starlette (0.25.0 -> 0.26.1)
- Updating charset-normalizer (3.0.1 -> 3.1.0)
- Updating fastapi (0.92.0 -> 0.94.1)
- Updating setuptools (67.4.0 -> 67.6.0)
- Updating urllib3 (1.26.14 -> 1.26.15)
- Updating alembic (1.9.4 -> 1.10.2)
- Updating fakeredis (2.9.2 -> 2.10.0)
- Updating prometheus-fastapi-instrumentator (5.10.0 -> 5.11.1)
- Updating protobuf (4.22.0 -> 4.22.1)
- Updating pytest-xdist (3.2.0 -> 3.2.1)
- Updating uvicorn (0.20.0 -> 0.21.0)
- Updating filelock (3.9.0 -> 3.9.1)

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-03-14 17:57:56 +01:00
moson-mo
b9df7541b3
fix: add comments in email for direct deletion/merge
TUs and Devs can delete and merge packages directly.
Currently the comments they enter, don't end up in the ML notification.

Include the comment in the notifications for direct deletion / merge

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-03-14 11:17:45 +01:00
moson-mo
7d1827ffc5
feat: cancel button for comment editing
Adds button that allows cancellation while editing a comment

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-03-09 21:48:58 +01:00
moson-mo
52c962a590
fix(deps): fastapi 0.92.0 upgrade
middleware must be added before startup:

fixes: "RuntimeError: Cannot add middleware after an application has started"

https://fastapi.tiangolo.com/release-notes/#0910
Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-03-04 10:29:54 +01:00
moson-mo
c0390240bc
housekeep(deps): bump dependencies
update all poetry deps to the latest version (except of sqlalchemy)

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-03-04 10:27:57 +01:00
moson-mo
7d06c9ab97
fix: encode package name in URL for source files
Package(Base) names might include characters that require url-encoding

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-03-01 18:04:20 +01:00
moson-mo
8aac842307
fix(test): use single-quotes for strings in sql statements
Currently, in the sharness test suites, we use double-quotes
for string literals in SQL statements passed to sqlite3.

With sqlite version 3.41 the usage of double-quotes for string literals
is deactivated by default:
We'll need to switch to single-quotes in our tests.

Ref: Section 6.f. at https://www.sqlite.org/releaselog/3_41_0.html
Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-02-24 10:11:33 +01:00
moson-mo
0c5b4721d6
fix: include package data without "Last Packager"
Data for packages that do not have a "Last Packager"
(e.g. because the user account was deleted)
should still be available from the /rpc and metadata archives.

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-02-21 11:19:02 +01:00
moson-mo
8d2e176c2f
housekeep: stop "pkgmaint" script (cron job)
With the removal of the "setup-repo" command this script becomes obsolete,
because it is not possible to reserve a repo anymore.
Hence we don't need cleanup.

We've also seen issues in case the last packager's user account is removed,
leading to the deletion of a Package.

Let's deactivate this for now.

Issue report: #425

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-02-21 11:19:02 +01:00
moson-mo
b1a9efd552
housekeep(git): remove deprecated "setup-repo" command
Marked as deprecated about 6 years ago.
Time to bury it.

Issue report: #428

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-02-21 11:19:02 +01:00
Leonidas Spyropoulos
68813abcf0
fix(RTL): make RTL layout properly displayed
Closes: #290

Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2023-02-19 02:14:57 +00:00
Leonidas Spyropoulos
45218c4ce7
fix: per-page needs to be non zero
Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2023-02-08 15:13:21 +00:00
Leonidas Spyropoulos
cb16f42a27
fix: validate timezone before use
Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2023-02-06 16:40:43 +00:00
moson-mo
f9a5188fb7
chore(lint): reformatting after black update
Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-02-06 09:15:18 +01:00
moson-mo
2373bdf400
fix(deps): bump pre-commit hooks
Bump hooks with "pre-commit autoupdate".

There is an issue with the latest poetry version and the "isort" hook module
https://github.com/PyCQA/isort/issues/2077

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-02-06 09:12:00 +01:00
Leonidas Spyropoulos
8b25d11a3a
chore(release): prepare for 6.2.1
Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2023-01-27 18:08:54 +00:00
Leonidas Spyropoulos
ef2baad7b3
feat: expand on update.py tests and show on Gitlab UI
Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2023-01-27 17:16:25 +00:00
moson-mo
137ed04d34
test: add tests .SRCINFO parsing and git update routine
Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-01-27 15:40:25 +01:00
moson-mo
97e1f07f71
fix(deps): update srcinfo to 0.1.2
Fixes issue parsing .SRCINFO files

Issue report: #422

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-01-27 14:04:55 +01:00
Leonidas Spyropoulos
2b76b90885
chore(release): prepare for 6.2.0
Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2023-01-26 23:19:04 +00:00
moson-mo
7f9ac28f6e
feat(deps): add watchfiles
When running aurweb with hot-reloading, the CPU consumption is quite high.
This is because it is using "StatReload" for detecting modified files.
(which seems to be rather inefficient)

When "watchfiles" is installed it'll automatically usees that instead and
CPU load goes down to 1%.
watchfiles uses filesystem events for detecting changes and is way more efficient.

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-01-26 12:59:40 +01:00
Leonidas Spyropoulos
255cdcf667
fix:(revert): fix: only try to show dependencies if object exists
This reverts commit 0e44687ab1.

Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2023-01-25 22:17:33 +00:00
moson-mo
ec239ceeb3
feat: add "Last Updated" column to search results
Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-01-25 22:39:36 +01:00
moson-mo
becce1aac4
fix: occasional errors when loading package details
Fixes errors that might occur when loading the package details page.

Problem:
We are querying a list of "Required by" packages.
This list is loaded with all details for a "PackageDependency" record.

Now we also have a reference to some attributes from the
related package (PackageDependency.Package.xxx)

This will effectively trigger the ORM to run another query (lazyload),
to fetch the missing Package data (for each PackageDependency record).

At that point it might have happened that a referenced package
got deleted / updated so that we can't retrieve this data anymore and
our dep.Package object is "None"

Fix:
We can force our query to include Package data right away.
Thus we can avoid running a separate query (per "required by"...)

As a side-effect we get better performance.

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-01-25 22:34:19 +01:00
Leonidas Spyropoulos
6c9be9eb97
fix(deps): update dependencies from renovate
fastapi ^0.89.0
coverage v7
srcinfo ^0.1.0

Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2023-01-25 21:17:50 +00:00
Leonidas Spyropoulos
c176b2b611
feature: increase mandatory coverage to 95%
Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2023-01-25 19:34:52 +00:00
moson-mo
ff0123b54a
fix: save notification state for unchanged comments
When we edit a comment we can enable notifications (if not yet enabled).

We should also do this when the comment text is not changed.

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-01-25 09:42:20 +01:00
moson-mo
36fd58d7a6
fix: show notification box when adding a comment
Currently, the "Enable notifications" checkbox
is only shown when editing a comment.

We should also show it when a new comment is about to be added.

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-01-25 09:42:19 +01:00
moson-mo
65ba735f18
fix: bleach upgrade 6.0
Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-01-23 23:50:04 +01:00
renovate
a2487c20d8
fix(deps): update dependency bleach to v6 2023-01-23 17:24:53 +00:00
Christian Heusel
f41f090ed7 simplify the docker development setup instructions
use `docker compose exec` instead of `docker ps` and `docker exec`

Signed-off-by: Christian Heusel <christian@heusel.eu>
2023-01-15 09:25:22 +00:00
Leonidas Spyropoulos
0e44687ab1 fix: only try to show dependencies if object exists
Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2023-01-14 21:08:34 +00:00
Leonidas Spyropoulos
4d0a982c51 fix: assert offset and per_page are positive
Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2023-01-14 20:57:11 +00:00
moson-mo
f6c4891415
feat: add Support section to Dashboard
Adds the "Support" section (displayed on "Home") to the "Dashboard" page as well.

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-01-14 13:12:33 +01:00
moson-mo
2150f8bc19
fix(docker): nginx health check
nginx health check always results in "unhealthy":

There is no such option "--no-verify" for curl.
We can use "-k" or "--insecure" for disabling SSL checks.

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-01-13 10:26:43 +01:00
moson-mo
ff44eb02de
feat: add link to mailing list article on requests page
Provides a convenient way to check for responses on the
mailing list prior to Accepting/Rejecting requests.

We compute the Message-ID hash that can be used to
link back to the article in the mailing list archive.

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-01-12 12:06:28 +01:00
Kevin Morris
154bb239bf
update-zh_TW translations 2023-01-11 12:25:54 -08:00
Kevin Morris
65d364fe90
update-zh_CN translations 2023-01-11 12:25:53 -08:00
Kevin Morris
ef0e3b9f35
update-zh translations 2023-01-11 12:25:53 -08:00
Kevin Morris
2770952dfb
update-vi translations 2023-01-11 12:25:53 -08:00
Kevin Morris
4cff1e500b
update-uk translations 2023-01-11 12:25:53 -08:00
Kevin Morris
b36cbd526b
update-tr translations 2023-01-11 12:25:52 -08:00
Kevin Morris
5609ddf791
update-sv_SE translations 2023-01-11 12:25:52 -08:00
Kevin Morris
8592bada16
update-sr_RS translations 2023-01-11 12:25:52 -08:00
Kevin Morris
46c925bc82
update-sr translations 2023-01-11 12:25:52 -08:00
Kevin Morris
8ee843b7b1
update-sk translations 2023-01-11 12:25:51 -08:00
Kevin Morris
ebae0d4304
update-ru translations 2023-01-11 12:25:51 -08:00
Kevin Morris
fa20a3b5d8
update-ro translations 2023-01-11 12:25:51 -08:00
Kevin Morris
e7bcf2fc97
update-pt_PT translations 2023-01-11 12:25:51 -08:00
Kevin Morris
bb00a4ecfd
update-pt_BR translations 2023-01-11 12:25:50 -08:00
Kevin Morris
6ee7598211
update-pt translations 2023-01-11 12:25:50 -08:00
Kevin Morris
e572b86fd3
update-pl translations 2023-01-11 12:25:50 -08:00
Kevin Morris
05c6266986
update-nl translations 2023-01-11 12:25:50 -08:00
Kevin Morris
57a2b4b516
update-nb_NO translations 2023-01-11 12:25:49 -08:00
Kevin Morris
d20dbbcf74
update-nb translations 2023-01-11 12:25:49 -08:00
Kevin Morris
e5137e0c42
update-lt translations 2023-01-11 12:25:49 -08:00
Kevin Morris
e6d36101d9
update-ko translations 2023-01-11 12:25:49 -08:00
Kevin Morris
08af8cad8d
update-ja translations 2023-01-11 12:25:49 -08:00
Kevin Morris
a12dbd191a
update-it translations 2023-01-11 12:25:48 -08:00
Kevin Morris
0d950a0c9f
update-is translations 2023-01-11 12:25:48 -08:00
Kevin Morris
3a460faa6e
update-id_ID translations 2023-01-11 12:25:48 -08:00
Kevin Morris
28e8b31211
update-id translations 2023-01-11 12:25:48 -08:00
Kevin Morris
5f71e58db1
update-hu translations 2023-01-11 12:25:47 -08:00
Kevin Morris
bf348fa572
update-hr translations 2023-01-11 12:25:47 -08:00
Kevin Morris
b209cd962c
update-hi_IN translations 2023-01-11 12:25:47 -08:00
Kevin Morris
9385c14f77
update-he translations 2023-01-11 12:25:47 -08:00
Kevin Morris
ff01947f3d
update-fr translations 2023-01-11 12:25:47 -08:00
Kevin Morris
3fa9047864
update-fi_FI translations 2023-01-11 12:25:46 -08:00
Kevin Morris
bce9bedaf4
update-fi translations 2023-01-11 12:25:46 -08:00
Kevin Morris
076245e061
update-et translations 2023-01-11 12:25:46 -08:00
Kevin Morris
aeb38b599d
update-es translations 2023-01-11 12:25:46 -08:00
Kevin Morris
6bf408775c
update-el translations 2023-01-11 12:25:46 -08:00
Kevin Morris
791e715aee
update-de translations 2023-01-11 12:25:45 -08:00
Kevin Morris
5a7a9c2c9f
update-da translations 2023-01-11 12:25:45 -08:00
Kevin Morris
da458ae70a
update-cs translations 2023-01-11 12:25:45 -08:00
Kevin Morris
618a382e6c
update-ca_ES translations 2023-01-11 12:25:45 -08:00
Kevin Morris
d6661403aa
update-ca translations 2023-01-11 12:25:45 -08:00
Kevin Morris
9229220e21
update-bg translations 2023-01-11 12:25:44 -08:00
Kevin Morris
b89fe9eb13
update-az_AZ translations 2023-01-11 12:25:44 -08:00
Kevin Morris
3a13eeb744
update-az translations 2023-01-11 12:25:44 -08:00
Kevin Morris
65266d752b
update-ar translations 2023-01-11 03:09:09 -08:00
Kevin Morris
413de914ca
fix: remove trailing whitespace lint check for ./po
Signed-off-by: Kevin Morris <kevr@0cost.org>
2023-01-10 14:36:31 -08:00
moson-mo
7a9448a3e5
perf: improve packages search-query
Improves performance for queries with large result sets.

The "group by" clause can be removed for all search types but the keywords.

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-01-05 22:00:32 +01:00
moson-mo
d8e91d058c
fix(rpc): provides search should return name match
We need to return packages matching on the name as well.
(A package always provides itself)

Signed-off-by: moson-mo <mo-son@mailbox.org>
2023-01-03 15:58:45 +01:00
moson-mo
2b8dedb3a2
feat: add pagination element below comments
other pages like the "package search" have this as well.

Issue report: #390

Signed-off-by: moson-mo <mo-son@mailbox.org>
2022-11-28 17:01:44 +01:00
moson-mo
8027ff936c
fix: alignment of pagination element
pagination for comments should appear on the right instead of center

Issue report: #390

Signed-off-by: moson-mo <mo-son@mailbox.org>
2022-11-28 16:57:27 +01:00
Leonidas Spyropoulos
c74772cb36
chore: bump to v6.1.9
Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2022-11-27 10:34:07 +00:00
moson-mo
7864ac6dfe
fix: search-by parameter for keyword links
Fixes:
Keyword-links on the package page pass wrong query-parameter.
Thus a name/description search is performed instead of  keywords

Issue report: #397

Signed-off-by: moson-mo <mo-son@mailbox.org>
2022-11-27 10:33:58 +01:00
moson-mo
a08681ba23
fix: Add "Show more..." link for "Required by"
Fix glitch on the package page:
"Show more..." not displayed for the "Required by" list

Fix test case:
Function name does not start with "test" hence it was never executed during test runs

Issue report: #363

Signed-off-by: moson-mo <mo-son@mailbox.org>
2022-11-25 12:24:04 +01:00
moson-mo
a832b3cddb
fix(test): FastAPI 0.87.0 - warning fixes
FastAPI 0.87.0 switched to the httpx library for their TestClient

* cookies need to be defined on the request instance instead of method calls

Signed-off-by: moson-mo <mo-son@mailbox.org>
2022-11-24 22:43:31 +01:00
moson-mo
1216399d53
fix(test): FastAPI 0.87.0 - error fixes
FastAPI 0.87.0 switched to the httpx library for their TestClient

* allow_redirects is deprecated and replaced by follow_redirects

Signed-off-by: moson-mo <mo-son@mailbox.org>
2022-11-24 22:23:37 +01:00
renovate
512ba02389
fix(deps): update dependency fastapi to ^0.87.0 2022-11-23 00:25:31 +00:00
Leonidas Spyropoulos
6b0978b9a5
fix(deps): update dependencies from renovate
Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2022-11-22 21:51:15 +00:00
moson-mo
d5e102e3f4
feat: add "Submitter" field to /rpc info request
Signed-off-by: moson-mo <mo-son@mailbox.org>
2022-11-22 18:46:57 +01:00
Leonidas Spyropoulos
ff92e95f7a
fix: delete associated ssh public keys with account deletion
Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2022-11-22 16:51:09 +00:00
Leonidas Spyropoulos
bce5b81acd
feat: allow filtering requests from maintainers
These are usually easy to handle from TUs so allow to filter for them

Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2022-11-22 16:39:11 +00:00
Leonidas Spyropoulos
500d6b403b
feat: add co-maintainers to RPC
Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2022-11-22 16:32:51 +00:00
moson-mo
bcd808ddc1
feat(rpc): add "by" parameter - comaintainers
Add "by" parameter: comaintainers

Signed-off-by: moson-mo <mo-son@mailbox.org>
2022-11-11 11:32:39 +01:00
moson-mo
efd20ed2c7
feat(rpc): add "by" parameter - keywords
Add "by" parameter: keywords

Signed-off-by: moson-mo <mo-son@mailbox.org>
2022-11-11 11:32:31 +01:00
moson-mo
5484e68b42
feat(rpc): add "by" parameter - submitter
Add "by" parameter: submitter

Signed-off-by: moson-mo <mo-son@mailbox.org>
2022-11-11 11:32:19 +01:00
moson-mo
0583f30a53
feat(rpc): add "by" parameter - groups
Adding "by" parameter to search by "groups"

Signed-off-by: moson-mo <mo-son@mailbox.org>
2022-11-11 11:32:01 +01:00
moson-mo
50287cb066
feat(rpc): add "by" parameters - package relations
This adds new "by" search-parameters: provides, conflicts and replaces

Signed-off-by: moson-mo <mo-son@mailbox.org>
2022-11-11 11:30:44 +01:00
Leonidas Spyropoulos
73f0bddf0b
fix: handle default requests when using pages
The default page shows the pending requests which were working OK if one
used the Filters button. This fixes the case when someone submits by
using the pager (Next, Last etc).

Closes: #405

Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2022-11-08 13:14:42 +00:00
moson-mo
c248a74f80
chore: fix mailing-list URL on passreset page
small addition to the patch provided in #404

Signed-off-by: moson-mo <mo-son@mailbox.org>
2022-11-07 14:36:34 +01:00
Lex Black
4f56a01662
chore: fix mailing-lists urls
Those changed after the migration to mailman3

Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2022-11-04 14:17:08 +00:00
Leonidas Spyropoulos
c0e806072e
chore: bump to v6.1.8
Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2022-11-01 18:31:37 +00:00
Leonidas Spyropoulos
d00371f444
housekeep: bump renovate dependencies
Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2022-11-01 17:24:13 +00:00
Leonidas Spyropoulos
f10c1a0505
perf: add PackageKeywords.PackageBaseID index
This is used on the export for package-meta.v1.gz generation

Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2022-11-01 17:24:13 +00:00
moson-mo
5669821b29
perf: tweak some queries in mkpkglists
We can omit the "distinct" from some queries
because constraints in the DB ensure uniqueness:

* Groups sub-query
PackageGroup: Primary key makes "PackageID" + "GroupID" unique
Groups: Unique index on "Name" column
-> Technically we can't have a package with the same group-name twice

* Licenses sub-query:
PackageLicense -> Primary key makes "PackageID" + "LicenseID" unique
Licenses -> Unique index on "Name" column
-> Technically we can't have a package with the same license-name twice

* Keywords sub-query:
PackageKeywords -> Primary key makes "PackageBaseID" + "KeywordID" unique
(And a Package can only have one PackageBase)
Keywords -> Unique index on "Name" column
-> Technically we can't have a package with the same Keyword twice

* Packages main-query:
We join PackageBases and Users on their primary key columns
(which are guaranteed to be unique)
-> There is no way we could end up with more than one record for a Package

Signed-off-by: moson-mo <mo-son@mailbox.org>
2022-11-01 18:18:06 +01:00
Leonidas Spyropoulos
286834bab1
fix: regression on gzipped filenames from 3dcbee5a
With the 3dcbee5a the filenames inside the .gz archives contained .tmp
at the end. This fixes those by using Gzip Class constructor instead of
the gzip.open method.

Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2022-10-31 14:43:31 +00:00
Mario Oenning
6ee34ab3cb feat: add field "CoMaintainers" to metadata-archives 2022-10-31 09:42:56 +00:00
Mario Oenning
333051ab1f feat: add field "Submitter" to metadata-archives 2022-10-28 16:55:16 +00:00
Leonidas Spyropoulos
48e5dc6763
feat: remove empty lines from ssh_keys text area, and show helpful message
Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2022-10-28 13:43:32 +01:00
Leonidas Spyropoulos
7e06823e58
refactor: remove redundand parenthesis when return tuple
Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2022-10-28 13:43:32 +01:00
Leonidas Spyropoulos
d793193fdf
style: make logging easier to read
Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2022-10-28 13:43:32 +01:00
Mario Oenning
3dcbee5a4f fix: make overwriting of archive files atomic 2022-10-28 12:42:50 +00:00
Leonidas Spyropoulos
524334409a
fix: add production logging.prod.conf to be less verbose
Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2022-10-22 21:58:30 +01:00
Leonidas Spyropoulos
0417603499
housekeep: bump renovate dependencies
email-validator:  1.2.1 -> ^1.3.0
uvicorn:          ^0.18.0 -> ^0.19.0
fastapi:          ^0.83.0 -> ^0.85.0
pytest-asyncio:   ^0.19.0 -> ^0.20.1
pytest-cov        ^3.0.0 -> ^4.0.0

Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2022-10-22 21:48:40 +01:00
Leonidas Spyropoulos
8555e232ae
docs: fix mailing list after migration to mailman3
Closes: #396

Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2022-10-22 20:15:46 +01:00
Leonidas Spyropoulos
9c0f8f053e
chore: rename logging.py and redis.py to avoid circular imports
Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2022-10-22 18:51:38 +01:00
Leonidas Spyropoulos
b757e66997 feature: add filters and stats for requests
Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2022-10-15 15:26:53 +03:00
Kevin Morris
da5a646a73
upgrade: bump to v6.1.7
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-10-11 15:04:25 -07:00
Kevin Morris
18f5e142b9
fix: include orphaned packages in metadata output
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-10-11 14:50:09 -07:00
Kevin Morris
3ae6323a7c
upgrade: bump to v6.1.6
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-09-30 05:19:58 -07:00
Kevin Morris
8657fd336e
feat: GET|POST /account/{name}/delete
Closes #348

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-09-30 05:08:50 -07:00
Kevin Morris
1180565d0c
Merge branch 'upd-metadata-doc' 2022-09-26 01:39:09 -07:00
Kevin Morris
eb0c5605e4
upgrade: bump version to v6.1.5
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-09-26 01:28:38 -07:00
Kevin Morris
e00b0059f7
doc: remove --spec popularity from cron recommendations
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-09-26 01:27:37 -07:00
Leonidas Spyropoulos
0dddaeeb98
fix: remove sessions of suspended users
Fixes: #394

Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2022-09-26 08:59:44 +01:00
moson-mo
137644e919
docs: suggest shallow clone in git-archive.md
we should be suggesting to make a shallow clone to reduce
the amount of data that is being transferred initially

Signed-off-by: moson-mo <mo-son@mailbox.org>
2022-09-25 10:03:05 +02:00
Kevin Morris
30e72d2db5 feat: archive git repository (experimental)
See doc/git-archive.md for general Git archive specifications
See doc/repos/metadata-repo.md for info and direction related to the new Git metadata archive
2022-09-24 16:51:25 +00:00
Kevin Morris
ec3152014b
fix: retry transactions who fail due to deadlocks
In my opinion, this kind of handling of transactions is pretty ugly.
The being said, we have issues with running into deadlocks on aur.al,
so this commit works against that immediate bug.

An ideal solution would be to deal with retrying transactions through
the `db.begin()` scope, so we wouldn't have to explicitly annotate
functions as "retry functions," which is what this commit does.

Closes #376

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-09-13 12:54:08 -07:00
Kevin Morris
f450b5dfc7
upgrade: bump to version v6.1.4
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-09-12 12:29:57 -07:00
Kevin Morris
adc3a21863
fix: add 'unsafe-inline' to script-src CSP
swagger-ui uses inline javascript to bootstrap itself, so we need to
allow unsafe inline because we can't give swagger-ui a nonce to embed.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-09-12 12:28:42 -07:00
Kevin Morris
37c7dee099
fix: produce DeleteNotification a line before handle_request
With this on a single line, the argument ordering and class/func
execution was a bit too RNG causing exceptions to be thrown when
producing a notification based off of a deleted pkgbase object.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-09-12 10:36:50 -07:00
Kevin Morris
624954042b
doc(rpc): include route doc at the top of aurweb.routers.rpc
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-09-12 06:59:52 -07:00
Kevin Morris
17f2c05fd3
feat(rpc): add GET /rpc/v5/suggest/{arg} openapi route
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-09-12 06:50:26 -07:00
Kevin Morris
8e8b746a5b
feat(rpc): add GET /rpc/v5/search/{arg} openapi route
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-09-12 06:50:19 -07:00
Kevin Morris
5e75a00c17
upgrade: bump to version v6.1.3
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-09-11 19:59:16 -07:00
Kevin Morris
9faa7b801d
feat: add cdn.jsdelivr.net to script/style CSP
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-09-11 19:56:29 -07:00
Kevin Morris
df0a4a2be2
feat(rpc): add /rpc/v5/{type} openapi-compatible routes
We will be modeling future RPC implementations on an OpenAPI spec.
While this commit does not completely cohere to OpenAPI in terms
of response data, this is a good start and will allow us to cleanly
document these openapi routes in the current and future.

This commit brings in the new RPC routes:
- GET /rpc/v5/info/{pkgname}
- GET /rpc/v5/info?arg[]=pkg1&arg[]=pkg2
- POST /rpc/v5/info with JSON data `{"arg": ["pkg1", "pkg2"]}`
- GET /rpc/v5/search?arg=keywords&by=valid-by-value
- POST /rpc/v5/search with JSON data `{"by": "valid-by-value", "arg": "keywords"}`

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-09-11 19:11:18 -07:00
renovate
bb6e602e13 fix(deps): update dependency fastapi to ^0.83.0 2022-09-12 01:42:09 +00:00
Kevin Morris
4e0618469d
fix(test): JSONResponse() requires a content argument with fastapi 0.83.0
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-09-11 18:40:31 -07:00
Kevin Morris
b3853e01b8
fix(pre-commit): include migrations in fixes/checks
We want all python files related to the project to be checked, really.
Some of which are still included, but migrations are a core part of
FastAPI aurweb and should be included.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-09-11 18:07:54 -07:00
Kevin Morris
03776c4663
fix(docker): cache & install pre-commit deps during image build
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-09-11 18:00:11 -07:00
Kevin Morris
a2d08e441e
fix(docker): run pre-commit run -a once
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-09-11 17:59:45 -07:00
Kevin Morris
6ad24fc950
Merge branch 'fix-docker-test' 2022-09-11 15:57:08 -07:00
renovate
69d6724749
fix(deps): update dependency redis to v4 2022-09-10 05:25:06 +00:00
renovate
307d944cf1
fix(deps): update dependency protobuf to v4 2022-09-10 03:25:08 +00:00
renovate
3de17311cf
fix(deps): update dependency bleach to v5 2022-09-10 00:25:02 +00:00
renovate
7ad22d8143
fix(deps): update dependency bcrypt to v4 2022-09-07 14:24:55 +00:00
renovate
6ab9663b76
fix(deps): update dependency authlib to v1 2022-09-07 06:25:25 +00:00
renovate
486f8bd61c
fix(deps): update dependency aiofiles to v22 2022-09-07 04:24:53 +00:00
renovate
a39f34d695
chore(deps): update dependency pytest to v7 2022-09-07 03:25:30 +00:00
renovate
bb310bdf65
fix(deps): update dependency uvicorn to ^0.18.0 2022-09-07 02:24:55 +00:00
renovate
a73af3e76d
fix(deps): update dependency hypercorn to ^0.14.0 2022-09-07 01:25:03 +00:00
renovate
a981ae4052
fix(deps): update dependency httpx to ^0.23.0 2022-09-07 00:25:32 +00:00
renovate
cdc7bd618c
fix(deps): update dependency email-validator to v1.2.1 2022-09-06 23:24:49 +00:00
renovate
b38e765dfe
fix(deps): update dependency aiofiles to ^0.8.0 2022-09-06 22:24:52 +00:00
renovate
655402a509
chore(deps): update dependency pytest-asyncio to ^0.19.0 2022-09-06 10:25:02 +00:00
renovate
a84d115fa1
chore(deps): add renovate.json 2022-09-06 08:24:03 +00:00
Leonidas Spyropoulos
310c469ba8
fix: run pre-commit checks instead of flake8 and isort
Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2022-09-06 08:07:05 +01:00
Kevin Morris
25e05830a6
test: test that /packages/{name} produces the package's description
This commit fixes two of our tests in test_templates.py to go along
with our new template modifications, as well as a new test in
test_packages_routes.py which constructs two packages belonging
to the same package base, then tests that viewing their pages
produces their independent descriptions.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-09-05 19:50:41 -07:00
Kevin Morris
0388b12896
fix: package description on /packages/{name} view
...What in the world happened here. We were literally just populating
`pkg` based on `pkgbase.packages.first()`. We should have been focusing
on the package passed by the context, which is always available when
`show_package_details` is true.

Closes #384

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-09-05 19:25:32 -07:00
Kevin Morris
83ddbd220f
test: get /requests displays all requests, including those without a User
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-09-05 02:56:48 -07:00
Kevin Morris
a629098b92
fix: conditional display on Request's 'Filed by' field
Since we support requests which have no associated user, we must
support the case where we are displaying such a request.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-09-05 02:55:20 -07:00
Kevin Morris
7fed5742b8
fix: display requests for TUs which no longer have an associated User
Closes #387

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-09-05 02:43:23 -07:00
Kevin Morris
6435c2b1f1
upgrade: bump to version v6.1.2
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-09-02 15:28:02 -07:00
Kevin Morris
b8a4ce4ceb
fix: include maint/comaint state in pkgbase post's error context
Closes #386

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-09-02 15:12:41 -07:00
Kevin Morris
8a3a7e31ac
upgrade: bump version to v6.1.1
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-08-31 22:01:54 -07:00
Kevin Morris
929bb756a8
ci(lint): add .pre-commit cache for pre-commit
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-08-23 02:32:35 -07:00
Kevin Morris
fbb3e052fe
ci: use cache/virtualenv for test dependencies
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-08-23 02:19:16 -07:00
Kevin Morris
57c0409958
style: set flake8's max-line-length=88
In accordance with black's defined style, we now expect a maximum
of 88 columns for any one particular line.

This change fixes remaining violations of 88 columns in the codebase
(not many), and introduces the modified flake8 configuration.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-08-22 23:44:56 -07:00
Joakim Saario
ce5dbf0eeb
docs(contributing): Update Coding Style 2022-08-22 22:42:10 +02:00
Joakim Saario
de5538a40f
ci(lint): Use pre-commit 2022-08-22 22:42:10 +02:00
Joakim Saario
505eb90479
chore: Add .git-blame-ignore-revs file
The idea is to exclude commits that only contains formatting so that it's
easier to backtrack actual code changes with `git blame`.
2022-08-22 22:41:58 +02:00
Joakim Saario
9c6c13b78a
style: Run pre-commit 2022-08-22 22:40:45 +02:00
Joakim Saario
b47882b114
chore(pre-commit) Use hooks from official repositories
The reason behind this is to make checking and formatting consistent between
contributors and CI. It is also easier to incorporate new hooks, since many
tools already provides pre-commit hooks

In addition this commit also adds `black` and `autoflake` along with a few
other useful hooks from the `pre-commit-hooks` repository.
2022-08-22 22:37:32 +02:00
Kevin Morris
08d485206c
feature: allow co-maintainers to disown their pkg
Derived off of original work done by Leonidas Spyropoulos
at https://gitlab.archlinux.org/archlinux/aurweb/-/merge_requests/503

This revision of that original work finishes off the inconsistencies
mentioned in the original MR and adds a small bit of testing for more
regression checks.

Fixes: #360

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-08-19 18:00:29 -07:00
Kevin Morris
ab2956eef7
feat: add pytest unit of independent user unflagging
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-08-18 16:02:03 -07:00
Kevin Morris
93b4cec932
Merge branch 'show-unflag-link-to-flagger' 2022-08-18 16:01:38 -07:00
Kevin Morris
fd4aaed208
fix: use max-age for all cookie expirations
in addition, remove cookie expiration for AURREMEMBER --
we don't really care about a session time for this cookie, it merely
acts as a flag given out on login to remember what the user selected

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-08-18 15:15:40 -07:00
Kevin Morris
8e43932aa6
fix(doc): re-add Max-Age to list of secure cookie attributes
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-08-18 14:57:42 -07:00
Kevin Morris
4303086c0e
Merged branch 'sameorigin-lax'
Closes #351

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-08-18 14:47:24 -07:00
Joakim Saario
f10732960c
fix: Use SameSite=Lax on cookies 2022-08-18 23:42:33 +02:00
Kevin Morris
fb1fb2ef3b
feat: documentation for web authentication (login, verification)
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-08-17 09:59:56 -07:00
Leon Möller
33bf5df236 fix: show unflag link to flagger
While the flagger is allowed to unflag a package, the link to do so is
hidden from them. Fix by adding the flagger to the unflag list.

Fix #380
2022-08-16 13:19:15 +00:00
Kevin Morris
15d016eb70
fix: secure access to comment edits to user who owns the comment
Found along with the previous commit to be a security hole in our
implementation. This commit resolves an issue regarding comment editing.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-08-15 23:30:34 -07:00
Kevin Morris
7a52da5587
fix: guard POST keywords & allow co-maintainers to see keyword form
This addresses a severe security issue, which is omitted from this
git message for obscurity purposes.

Otherwise, it allows co-maintainers to see the keyword form when
viewing a package they co-maintain.

Closes #378

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-08-15 23:30:15 -07:00
Kevin Morris
7b047578fd
fix: correct kwarg name for approved users of creds.has_credential
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-08-15 19:34:18 -07:00
Kevin Morris
801df832e5
fix(rpc): correct URLPath in package results
This was incorrectly using the particular Package record's name
to format options.snapshot_uri in order to produce URLPath.

It should, instead, use the PackageBase record's name, which
this commit resolves.

Bug reported by thomy2000

Closes #382

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-08-15 10:06:44 -07:00
Kevin Morris
edacde48e5
Merge branch 'paginate-comments' 2022-08-14 19:50:21 -07:00
Kevin Morris
b4e0aea2b7
Merged bugfixes
Brings in: 9497f6e671
Closes #512

Thanks, jelle!

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-08-14 19:25:49 -07:00
Jelle van der Waa
9497f6e671
fix(aurweb): resolve exception in ratelimit
Redis's get() method can return None which makes an RPC request error
out:

  File "/srv/http/aurweb/aurweb/ratelimit.py", line 103, in check_ratelimit
    requests = int(requests.decode())
AttributeError: 'NoneType' object has no attribute 'decode'
2022-08-14 15:43:13 +02:00
Kevin Morris
4565aa38cf
update: Swedish translations
Pulled from Transifex on 08/12/2022 - 08/13/2022.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-08-13 23:51:49 -07:00
Kevin Morris
a82d552e1b
update: migrate new transifex client configuration
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-08-13 23:49:47 -07:00
Kevin Morris
d63615a994
fix(docker): fix ca entrypoint logic and healthcheck
With this commit, it is advised to `rm ./data/root_ca.crt ./data/*.pem`,
as new certificates and a root CA will be generated while utilizing the
step volume.

Closes #367

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-08-13 23:43:04 -07:00
Kevin Morris
6f7ac33166
Revert "feat(db): add an index for SSHPubKeys.PubKey (#2)"
This reverts commit 6c7e274968.

Once again, this does actually cause issues with foreign keys.
Removing it for now and will revisit this.
2022-08-13 23:28:31 -07:00
Kevin Morris
829a8b4b81
Revert "fix(docker): apply chown each time sshd is started"
This reverts commit 952c24783b.

The issue found was actually:
- If `./aur.git` exists within the aurweb repository locally,
  it also ends up in the destination, stopping the aurweb_git_data
  volume from being mounted properly.
2022-08-13 20:56:43 -07:00
Kevin Morris
952c24783b
fix(docker): apply chown each time sshd is started
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-08-13 20:13:07 -07:00
Kevin Morris
6c7e274968
feat(db): add an index for SSHPubKeys.PubKey (#2)
Speeds up SSHPubKeys.PubKey searches in a larger database.

Fixed form of the original commit which was reverted,
1a7f6e1fa9

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-08-13 19:52:50 -07:00
Kevin Morris
5abd5db313
Revert "feat(db): add an index for SSHPubKeys.PubKey"
This reverts commit 1a7f6e1fa9.

This commit broke account creation in some way. We'd still like to
do this, but we need to ensure it does not intrude on other facets.

Extra: We should really work out how this even passed tests; it
should not have.
2022-08-13 19:23:19 -07:00
Kevin Morris
b3d09a4b77
Merge branch 'dummy-data-instructions' 2022-08-13 16:31:47 -07:00
Kevin Morris
1a7f6e1fa9
feat(db): add an index for SSHPubKeys.PubKey
Speeds up SSHPubKeys.PubKey searches in a larger database.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-08-12 22:26:26 -07:00
Kevin Morris
913ce8a4f0
fix(performance): lazily load expensive modules within aurweb.db
Closes #374

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-08-12 22:26:26 -07:00
Jelle van der Waa
0e82916b0a fix(python): don't show maintainer link for non logged in users
Show a plain maintainer text for non logged in users like the submitted,
last packager.

Closes #373
2022-08-10 19:04:59 +00:00
Kevin Morris
9648628a2c
update: requests dependency
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-08-09 16:43:27 -07:00
Leonidas Spyropoulos
2c080b2ea9
feature: add pagination on comments
Fixes: #354

Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2022-08-02 20:27:47 +03:00
Leonidas Spyropoulos
1d6335363c fix: strip whitespace when parsing package keywords
Remove all extra whitespace when parsing Keywords to ensure we don't add
empty keywords in the DB.

Closes: #332

Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2022-08-02 17:06:36 +03:00
Jelle van der Waa
a509e40474 fix(python): use standard dict/list type annotation
Since Python 3.9 list/dict can be used as type hint.
2022-08-02 12:06:58 +00:00
Hugo Osvaldo Barrera
d6fa4ec5a8 Explain how to populate dummy data for TESTING
Signed-off-by: Hugo Osvaldo Barrera <hugo@whynothugo.nl>
2022-07-19 18:55:42 +02:00
Leonidas Spyropoulos
28970ccc91
fix: align text on left
Closes: #368

Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2022-07-17 19:41:19 +01:00
Leonidas Spyropoulos
034e47bc28
fix: hide Unflag package from non-maintainers
Closes: #364
Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2022-07-17 19:37:00 +01:00
Jelle van der Waa
0b03a6871e
fix(docker): document runtime deps 2022-07-04 21:35:41 +02:00
Jelle van der Waa
4a58e1349c
fix(docker): fix typo scheme -> schema 2022-07-04 21:35:06 +02:00
Jelle van der Waa
edef6cc6ac chore(css): drop old vendor prefixes
All of these vendor prefixes are already supported by all browsers for
quite a while.
2022-06-30 21:57:52 +02:00
Jelle van der Waa
ade624c215 doc(README): update contributing guidelines 2022-06-29 10:57:12 +00:00
Jelle van der Waa
98f55879d3 fix(docker): don't run redis with protected mode
For our development setup we run a redis container without a
username/password. Redis recently set protected mode by default which
disallows this, turn it off as it has no security implication.
2022-06-28 22:14:01 +02:00
Jelle van der Waa
8598ea6f74
fix(gitlab-ci): update coverage reporting in CI
Gitlab 14.10 introduced a coverage_report key which obsoletes the old
way of reporting coverage data.
2022-06-27 21:05:05 +02:00
Kristian Klausen
4ddd1dec9c
upgrade: bump to v6.0.28 2022-05-13 00:41:22 +02:00
Leonidas Spyropoulos
0b54488563
fix(poetry): remove mysql-connector dependency
Reverting a8287921

Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2022-05-12 23:26:57 +01:00
Leonidas Spyropoulos
02d114d575
fix: hide email when account's email hidden is set
Fixes: 362
Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2022-05-12 22:51:22 +01:00
Kevin Morris
7a525d7693
change: remove poetry-dynamic-versioning
We've not been using this as it is and its now warning us
about strtobool deprecation changes. Removing it for now.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-03-31 20:59:16 -07:00
Kevin Morris
a553d5d95a
fix: replace distutils.util.strtobool with our own
Reference from
github.com/PostHog/posthog/pull/4631/commits/341c28da0f6d33d6fb12fe443766a2d822ff0097

This fixes a deprecation warning regarding distutil's strtobool.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-03-31 20:59:05 -07:00
Kevin Morris
cf4295a13e
upgrade: bump to v6.0.27
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-03-31 17:45:39 -07:00
Kevin Morris
ed41a4fe19
feat: add paging to package depends & required by
This patch does not include a javascript implementating, but
provides a pure HTML/HTTP method of paging through these lists.

Also fixes erroneous limiting. We now use a hardcoded limit of 20
by default.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-03-30 17:07:40 -07:00
Kevin Morris
d8564e446b
upgrade: bump to v6.0.26
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-03-30 12:30:21 -07:00
Kevin Morris
afd25c248f
fix: remove HEAD and OPTIONS handling from metrics
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-03-14 06:26:37 -07:00
Kevin Morris
790ca4194a
fix: coherenace -> coherence
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-03-14 05:57:06 -07:00
Kevin Morris
7ddce6bb2d
doc: update CONTRIBUTING.md
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-03-14 05:55:19 -07:00
Kevin Morris
c149afb1f1
Merge remote-tracking branch 'fosskers/colin/prework-reformatting' 2022-03-14 05:14:59 -07:00
Kevin Morris
d7cb04b93d
upgrade: bump to v6.0.25
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-03-08 20:35:21 -08:00
Kevin Morris
49c5a3facf
feat: display stats about total & active TUs on proposals
This patch brings in two new features:
- when viewing proposal listings, there is a new Statistics section,
  containing the total and active number of Trusted Users found in the
  database.
- when viewing a proposal directly, the number of active trusted users
  assigned when the proposal was added is now displayed in the details
  section.

Closes #323

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-03-08 20:28:09 -08:00
Kevin Morris
0afa07ed3b
upgrade: bump to v6.0.24
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-03-08 19:16:02 -08:00
Kevin Morris
a1a88ea872
fix(rpc): suggestions should only suggest based on <keyword>%
Previously, Python code was looking for suggestions based on
`%<keyword>%`. This was inconsistent with PHP's suggestion
implementation and cause more records to be bundled with a suggestion,
along with supplying misleading suggestions.

Closes #343

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-03-08 19:00:19 -08:00
Kevin Morris
9791704632
Merge branch 'fix-none-path' 2022-03-08 18:34:38 -08:00
Kevin Morris
2a393f95fa
upgrade: bump to v6.0.23
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-03-08 17:59:00 -08:00
Kevin Morris
e00cf5f124
test: use smtplib.SMTP[_SSL] timeout = notifications.smtp-timeout
A new option has been added for configuration of SMTP timeout:
- notifications.smtp-timeout

During tests, we can change this timeout to be small, so we aren't
depending on hardware-based RNG to pass the timeout.

Without a timeout, users can run into a long-running test for no
particular reason.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-03-08 17:53:31 -08:00
Kevin Morris
13217be939
fix: don't check suspension for ownership changes
People can change comaintainer ownership to suspended users if they
want to.

Suspended users cannot login, so there is no breach of security
here. It does make sense to allow ownership to be changed, imo.

Closes #339

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-03-08 17:51:25 -08:00
Kevin Morris
e2a17fef95
upgrade: bump to v6.0.22
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-03-07 23:57:54 -08:00
Kevin Morris
0f0a2f18ad
Merge branch 'copy-fix' 2022-03-07 23:55:23 -08:00
Kevin Morris
5045f0f3e4
fix: copy.js javascript initialization
Not sure where this works, but it doesn't seem to work on my
browser. Achieved the same by forEaching through the array
returned by querySelectorAll instead.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-03-07 23:53:57 -08:00
Kevin Morris
f11e8de251
upgrade: bump to v6.0.21
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-03-07 23:32:14 -08:00
Kevin Morris
6a243e90db
fix: only reject addvote for users with running proposals
This was incorrectly indiscriminately targetting _any_ proposal
for a particular user.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-03-07 23:23:49 -08:00
Matt Harrison
b80d914cba
fix click to copy when there is more than one copy link on the page.
Fixes issue reported on the mailing list here: https://lists.archlinux.org/pipermail/aur-general/2022-March/036833.html

Thanks to Henry-Joseph Audéoud for diagnosing the issue
https://lists.archlinux.org/pipermail/aur-general/2022-March/036836.html

Also update the event variable to use the local copy instead of the
deprecated global version
https://stackoverflow.com/questions/58341832/event-is-deprecated-what-should-be-used-instead
2022-03-07 12:37:54 -05:00
Kevin Morris
c7c79a152b
upgrade: bump to v6.0.20
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-25 19:44:10 -08:00
Kevin Morris
95c191fb31
Merge branch 'master' of ssh://gitlab.archlinux.org:222/archlinux/aurweb 2022-02-25 19:31:24 -08:00
Kevin Morris
9204b76110
fix: ...do not add to ActiveTUs when voting on a proposal
Straight up bug.

Closes #324

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-25 19:28:03 -08:00
Kevin Morris
1bb4daa36a
doc: merge CodingGuidelines into CONTRIBUTING.md
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-23 18:54:35 -08:00
Kevin Morris
25d74d02c7
Merge remote-tracking branch 'fosskers/colin/docker-usage' 2022-02-23 18:24:58 -08:00
Colin Woodbury
d92f183840
docs(docker): explain how to generate dummy data 2022-02-23 18:12:49 -08:00
Kevin Morris
b63ac7ce91
Merge remote-tracking branch 'fosskers/colin/contributing-tweak' 2022-02-23 17:00:05 -08:00
Kevin Morris
07e479ab50
upgrade: bump to v6.0.19
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-23 14:37:41 -08:00
Kevin Morris
51d4b7f993
fix(rpc): limit Package results, not relationships
...This was an obvious bug in hindsight. Apologies :(

Closes #314

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-23 14:17:41 -08:00
Colin Woodbury
3aa8d523f5
change(rpc): search module reformatting 2022-02-21 16:56:10 -08:00
Colin Woodbury
27f30212e8
docs(docker): note ports and curl usage 2022-02-21 14:40:18 -08:00
Colin Woodbury
7c36379715
docs(docker): basic usage instructions 2022-02-21 14:18:26 -08:00
Colin Woodbury
9f452a62e5
docs: fix link formatting in CONTRIBUTING 2022-02-21 11:56:57 -08:00
Leonidas Spyropoulos
6e837e0c02
fix: always provide a path
891efcd142
2022-02-21 10:25:01 +00:00
Kevin Morris
1e31db47ab
upgrade: bump to v6.0.18
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-19 16:32:49 -08:00
Kevin Morris
80622cc966
fix: suspend check should check Suspended...
This was causing some false negative errors in the update process,
and it clearly not correct -- oops :(

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-19 16:26:31 -08:00
Kevin Morris
4a4fd01563
fix: blanking out particular fields when editing accounts
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-19 16:01:06 -08:00
Kevin Morris
c83c5cdc42
change: log out details about PROMETHEUS_MULTIPROC_DIR
Additionally, respond with a 503 if the var is not set when
/metrics is requested.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-19 12:08:55 -08:00
Kevin Morris
388e64d0af
upgrade: bump to v6.0.17
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-18 17:54:36 -08:00
Kevin Morris
7cc20cd9a4
fix: suspended users should not be able to login
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-18 17:50:35 -08:00
Kevin Morris
e43e1c6d20
upgrade: bump to v6.0.16
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-18 17:17:14 -08:00
Kevin Morris
14347232fd
fix: treat all keywords as lowercase when updating
In addition, treat package search by keywords as lowercase.

Closes #296, #297, #298, #301

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-18 16:58:02 -08:00
Kevin Morris
8387f325f6
fix: resolve null VoteTS columns via migration
Somehow, many aur.al records of PackageVotes do not have a valid VoteTS
value. This migration fixes that issue by setting all null VoteTS
columns to the epoch timestamp.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-18 16:16:07 -08:00
Kevin Morris
1d86b3e210
fix: use a transaction for package query; remove refresh
Closes #284

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-18 15:58:42 -08:00
Kevin Morris
4e641d945c
fix: unset InactivityTS for users on login
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-18 13:53:45 -08:00
Kevin Morris
b2508e5bf8
upgrade: bump to v6.0.15
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-17 18:27:00 -08:00
Kevin Morris
dcaf407536
fix: /packages search result count
We need to query for this after we've applied all filters.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-17 17:55:02 -08:00
Kevin Morris
bfd592299c
change: display default package search parameter values in its form
The previous behavior was carried over from PHP. It has been requested
that we use the true defaults when rendering the default form, making
search a bit more sensible.

Closes #269

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-17 17:54:57 -08:00
Kevin Morris
0bfecb9844
upgrade: bump to v6.0.14
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-17 16:14:31 -08:00
Kevin Morris
2fd9f3436d
Merge branch 'fix-request-autogen' 2022-02-17 16:08:18 -08:00
Kevin Morris
e3864d4b7c
fix: set RequestTS when autogenerating requests
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-17 15:54:04 -08:00
Kevin Morris
361163098f
fix: /packages search ordering links
This was not including other parameters that should be persisted for
users.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-17 15:49:41 -08:00
Kevin Morris
040c9bc3e6
fix: send up to date flag notifications
These were being produced with the db state before the flag was set,
which is not what should be done for flag notifications, as the
notification contains data about the comment and the current flagger.

Closes #292

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-17 15:30:32 -08:00
Kevin Morris
640630faff
upgrade: bump to v6.0.13
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-14 15:45:59 -08:00
Kevin Morris
da0e74a648
Merge branch 'master' of ssh://gitlab.archlinux.org:222/archlinux/aurweb 2022-02-14 15:43:37 -08:00
Kevin Morris
9327594926
upgrade: bump to v6.0.12
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-14 15:42:18 -08:00
Kevin Morris
29061c000c
fix: pkgbase -> package redirection
We were redirecting in some error-cases, which this commit sorts out:
- package count == 1 and package base name != package name
- was redirecting to {name} and not the only associated Package

Now, when we have a package base name that mismatches its only
package, we display the package base page. Otherwise, we redirect
to the first package's page.

Closes #282

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-14 15:38:00 -08:00
Kevin Morris
1671868956
fix: links to cgit should be url encoded
Closes #283

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-13 17:44:40 -08:00
Kevin Morris
708ade4dbf
fix: allow co-maintainers to [un]pin comments on a package
Closes #279

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-11 16:24:42 -08:00
Kevin Morris
35e7486ea3
upgrade: bump to v6.0.11
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-11 00:50:34 -08:00
Kevin Morris
50b726d739
fix: send notifications when users submit comments
Closes #278

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-11 00:43:50 -08:00
Kevin Morris
a2e993119e
fix: correct typo in Bug.md
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-10 14:52:03 -08:00
Kevin Morris
bd13d6904b
change: add explanation of aurweb vs user packages in Bug.md
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-10 14:49:28 -08:00
Kevin Morris
41a6e9740f
upgrade: bump to v6.0.10
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-10 13:55:18 -08:00
Kevin Morris
7485cc231e
change: report unhandled tracebacks to a repository
As repeats of these traceback notifications were annoying some of
the devops staff, and it took coordination to share tracebacks with
developers, this commit removes that responsibility off of devops
by reporting tracebacks to Gitlab repositories in the form of issues.

- removed ServerErrorNotification
- removed notifications.postmaster configuration option
- added notifications.gitlab-instance option
- added notifications.error-project option
- added notifications.error-token option
- added aurweb.exceptions.handle_form_exceptions, a POST route decorator

Issues are filed confidentially. This change will need updates
in infrastructure's ansible configuration before this can be
applied to aur.archlinux.org.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-10 13:44:10 -08:00
Kevin Morris
e2eb3a7ded
fix: restore missing typeahead js on authenticated dashboard
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-10 13:32:37 -08:00
Kevin Morris
0c20e4056e
change(git-cliff): remove space in "bug fixes"
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-10 09:17:37 -08:00
Kevin Morris
e80891f2f2
housekeep: cleanup extra space in test_config.py
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-10 09:16:43 -08:00
Kevin Morris
3af66cafbe
fix(rpc): restore "Too Many Package Results" error
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-10 01:04:25 -08:00
Kevin Morris
86caee74c5
fix(rpc): use max_rpc_results for type=multiinfo result limit
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-10 00:32:35 -08:00
Kevin Morris
f928a49c8a
doc(rpc): Request Types -> Request Methods & reword description
The POST description was ridiculously confusing. This cleans up the
doc a bit and is hopefully a bit more straight-forward.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-10 00:16:32 -08:00
Kevin Morris
3f95ac7db3
fix: correct redirects for package actions & requests
For requests, we always pass a `next` of /requests, leading us
back to the requests page. For a standard package, we get redirected
to the involved pkgbase, or target pkgbase if a merge action was taken.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-08 22:59:01 -08:00
Kevin Morris
c883c71053
upgrade: bump to v6.0.9
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-08 20:14:52 -08:00
Kevin Morris
2cb53411c0
change: remove comaintainers when fulfilling orphan request
Closes FS#50079

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-08 20:03:38 -08:00
Kevin Morris
4ae72af4b5
fix: address missing coverage from previous changes
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-08 17:58:43 -08:00
Kevin Morris
b6321bbdc5
Merge branch 'mr440' 2022-02-08 17:49:50 -08:00
Awal Garg
b119db251b fixup: feat(archives): add .sha256 and construct archives in tmpdir 2022-02-09 07:03:12 +05:30
Kevin Morris
40a0e866e7
feat(archives): add {archive}.sha256 and construct archives in tmpdir
This change brings some new additions to our archives:
- SHA-256 .sha256 hexdigests
- We construct our archives in a tmpdir now and move them to the
archive destination when all are completed. This removes some
corrupted downloading when archiving is in-process.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-08 13:28:39 -08:00
Leonidas Spyropoulos
acc8885844
docs: revert changes from *.po
These will be done automatically from next transifex pull

Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2022-02-08 18:40:38 +00:00
Leonidas Spyropoulos
d79d7bdd1e
docs: update issues url to gitlab
Signed-off-by: Leonidas Spyropoulos <artafinde@archlinux.org>
2022-02-08 17:57:50 +00:00
Kevin Morris
bf0623d8c7
change(git-cliff): include a header for untagged cliffs
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-08 09:43:07 -08:00
Kevin Morris
310484a8cc
change: display git-cliff's commit scope
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-08 09:39:14 -08:00
Kevin Morris
a21c48afe7
change: format git-cliff commit hashes a bit better
Use a ':' instead of a '-' to separate commit hash from message

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-08 09:32:46 -08:00
Kevin Morris
477e814cd8
change: set git-cliff's output format to asciidoc style
we'll be using git-cliff to produce changelogs for new tags from
now on. we want to include these changelogs within the tag body
without markdown.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-08 09:29:31 -08:00
Kevin Morris
95bbdfc3bb
upgrade: bump to v6.0.8
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-08 09:13:47 -08:00
Kevin Morris
4c14a10b91
fix: support multiple SSHPubKey records per user
There was one blazing issue with the previous implementation regardless
of the multiple records: we were generating fingerprints by storing
the key into a file and reading it with ssh-keygen. This is absolutely
terrible and was not meant to be left around (it was forgotten, my bad).

Took this opportunity to clean up a few things:
- simplify pubkey validation
- centralize things a bit better

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-08 07:50:41 -08:00
Kevin Morris
660d57340a
fix: TUVote inner join TUVoteInfo for "Last Votes by TU" listing
By implicitly joining, sqlalchemy joined on
`TUVote.UsersID = TUVoteInfo.SubmitterID`. This should be joining on
`TUVote.VoteID = TUVoteInfo.ID` instead to include all TUVote instances
found in the database.

Closes #266

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-07 18:16:15 -08:00
Kevin Morris
957803a70b
fix: M/c search with multiple keywords
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-07 16:22:21 -08:00
Kevin Morris
828847cfcd
fix: OutOfDateTS db fetch for pkgbase action display
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-07 12:23:35 -08:00
Kevin Morris
33cddb36ff
fix: restore URL field in mkpkglists meta archives
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-07 01:20:42 -08:00
Kevin Morris
2dfa41c9a5
feat(rpc): support POST method
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-07 00:49:34 -08:00
Kevin Morris
26f0b014f9
fix: /packages search by keywords
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-06 23:29:09 -08:00
Kevin Morris
83f5d9e460
fix: RSS aurlogo.png url
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-06 21:32:48 -08:00
Kevin Morris
750653361f
fix: remove /packages search count limit
...took this opportunity to use the new options.max_search_results
tunable for a PP upper-bound.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-06 16:21:56 -08:00
Kevin Morris
1545eab81d
feat: add timezone to datetime display across the board
- the "Flagged Out-of-date on ..." link in the package action panel does
  not contain a timezone specifier.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-05 18:35:50 -08:00
Kevin Morris
e777b7052e
fix: send out a FlagNotification when a package is flagged
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-05 04:40:25 -08:00
Kevin Morris
2d6c09bec5
fix: handling of user registration HideEmail
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-05 04:28:50 -08:00
Kevin Morris
d5a1c16458
upgrade: bump to v6.0.7
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-05 03:48:58 -08:00
Kevin Morris
39d6f927e6
fix: Maintainer, Co-maintainer /package search
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-05 03:47:16 -08:00
Kevin Morris
7618101b1b
fix: depend on OutOfDateTS for flag state
It was found in the aur.al database that some records have
a non-null flagger, but are not flagged. Using the flagger
relationship, we were false redirecting away from the flag
page.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-05 03:47:16 -08:00
Kevin Morris
a445a40bea
fix: Maintainer's comaintainer annotation display
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-05 03:47:16 -08:00
Kevin Morris
c1420b52fb
fix: rpc doc should not have v=6 information
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-05 03:47:15 -08:00
Kevin Morris
28549b47bb
fix: /packages search by co-maintainer
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-05 03:47:15 -08:00
Kevin Morris
c80a16c254
fix: allow users to login using their email
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-05 03:47:14 -08:00
Kevin Morris
f3360d1249
fix: eradicate spaces from pgp key fingerprint input
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-05 03:47:14 -08:00
Kevin Morris
ac68f74c69
fix: Hide Email Address checkbox markup
also:
- support empty strings in util.strtobool

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-05 02:16:52 -08:00
Kevin Morris
6986d1bb1e
fix: update rpc documentation
- we no longer prefer the use of trailing slashes on the AUR
  website.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-04 21:27:01 -08:00
Kevin Morris
3cb106bc9d
upgrade: bump to v6.0.6
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-04 20:23:28 -08:00
Kevin Morris
b7bf83c5f0
fix: prioritize local db record in pkgname_link
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-04 20:13:24 -08:00
Kevin Morris
c783ce17be
fix: remove erroneous official pkg check
This causes an issue that should have been obvious from the get-go:
if a package request is up in the AUR, but the package has already
been picked up by an official repository, we would end up returning
a 404 here, leading a TU to not be able to perform an action for
a request's target.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-04 19:57:59 -08:00
Kevin Morris
101de8e7b1
temporarily support /rpc.php/?
We'll leave these routes in for one month. On 02/04, they'll be
removed. This gives some time for aur helpers to update their
method.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-04 19:26:36 -08:00
Kevin Morris
0c1bd982ea
fix(rpc): remove trailing slash redirection
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-04 19:08:25 -08:00
Kevin Morris
987f9eab3b
fix: link to user account in last votes by tu listing
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-04 18:36:29 -08:00
Kevin Morris
ab1479925b
fix: tu last votes listing vote id
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-04 18:02:33 -08:00
Kevin Morris
2c08672f15
fix: participation display generation should check voteinfo.ActiveTUs
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-04 16:50:27 -08:00
Kevin Morris
2f8e2288ad
poetry: bump version to 6.0.5
... I need to stop missing this. We need to centralize version
setting into a single file and source it.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-04 16:18:44 -08:00
Kevin Morris
164037da43
upgrade: bump to v6.0.5
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-04 15:55:33 -08:00
Kevin Morris
8310357029
fix: only display registration time when RegistrationTS is valid
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-04 15:49:45 -08:00
Kevin Morris
bfe48a7d76
fix: dashboard's My Packages should not have comaintained packages
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-04 14:24:30 -08:00
Kevin Morris
c39a648bf2
pyproject.toml: set version to current (v6.0.4)
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-03 19:24:57 -08:00
Kevin Morris
9111f645b7
fix: require passreset's target user is unsuspended
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-03 19:05:01 -08:00
Kevin Morris
ef0285bc7c
upgrade: bump to v6.0.4
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-03 17:47:31 -08:00
Kevin Morris
4659b5f941
upgrade: bump to v6.0.3
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-03 17:43:39 -08:00
Kevin Morris
ad1d5a1217
fix: don't check email deliverability when verifying input
For tests, we only care about emails having a valid syntax.
I don't think we should verify this at all, as aurweb.scripts.notify
will timeout if it cant deliver via sendmail/smtp.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-03 17:13:48 -08:00
Kevin Morris
16bdbee520
poetry: lock email-validator to 1.1.3, update the rest
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-02-03 17:12:30 -08:00
Kevin Morris
85012bb100
feat: support language direction switching
changes can be found in 82972d28e2
contributed by Yaron Shahrabani.

- the 'hu' (hebrew) language toggles the `dir="rtl"` attr on
- the 'ar' (arabic) language toggles the `dir="rtl"` attr on
- other languages, including the default, omit the `dir="rtl"` attr

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-26 11:19:35 -08:00
Yaron Shahrabani
82972d28e2 All the RTL related changes 2022-01-26 17:19:39 +02:00
Kevin Morris
ba6ba4c367
Merge branch 'cron-compose-fix' into pu 2022-01-25 14:36:01 -08:00
Kevin Morris
f7c81ce855
Revert "docker: simplify keyring update"
This reverts commit 2f294480a9.
2022-01-24 14:54:11 -08:00
Kevin Morris
2f294480a9
docker: simplify keyring update
- this wasn't using .pkg-cache before; we should, in case we already
  have the updated package downloaded.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-23 15:47:31 -08:00
Kevin Morris
8cca03a3f6
gitlab-ci: add lint job (stage: .pre)
- directories to be linted can be configured in .gitlab-ci.yml's
  `REQUIRES_LINT` variable
- removed linting from the test job

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-23 15:47:26 -08:00
Hunter Wittenborn
34c2692193
Added missing immutable config flag to Compose file 2022-01-20 15:50:22 -06:00
Kevin Morris
01a0c286c9
upgrade: bump to v6.0.2
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-20 11:56:14 -08:00
Kevin Morris
05e3e4bda7
Merge branch 'fix-package-comaintainers' into pu 2022-01-20 11:04:09 -08:00
Kevin Morris
5fade479a3
fix(routers.html): show comaintained packages which have been flagged
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-20 10:57:48 -08:00
Kevin Morris
62388b4161
fix(package/pkgbase view): include comaintainers in Maintainer field
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-20 09:43:14 -08:00
Kevin Morris
2c4f4155d6
fix(templates): Maintainer field does not require auth
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-20 09:14:30 -08:00
Kevin Morris
fee7e41ae4
fix(routers.html): show comaintained packages which have been flagged
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-20 09:04:55 -08:00
Kevin Morris
a2dfb97b6b
fix: add dependency step to docker.md
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-19 08:15:28 -08:00
Kevin Morris
5e52bafb5c
add doc/docker.md & update README.md
Changes to README.md:
- Added Documentation links to various documents in the repository.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-19 08:13:35 -08:00
Kevin Morris
8c665d1651
upgrade: bump to v6.0.1
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-18 11:04:07 -08:00
Kevin Morris
18b18bf667
fix: remove trailing slash from package search form action
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-18 11:01:12 -08:00
Kevin Morris
57bc9b6b73
fix(footer.html): update version git log link to gitlab
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-18 10:55:52 -08:00
Kevin Morris
d7c19ee6ce
upgrade: bump to v6.0.0
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-18 10:42:33 -08:00
Kevin Morris
a467b18474
Merge branch 'pu': pre-v6.0.0
Release v6.0.0 - Python

This documents UX and functional changes for the v6.0.0 aurweb release.
Following this release, we'll be working on a few very nice features
noted at the end of this article in Upcoming Work.

Preface
-------

This v6.0.0 release makes the long-awaited Python port official.

Along with the development of the python port, we have modified a
number of features. There have been some integral changes to how
package requests are dealt with, so _Trusted Users_ should read
the entirety of this document.

Legend
------

There are a few terms which I'd like to define to increase
understanding of these changes as they are listed:

- _self_
    - Refers to a user viewing or doing something regarding their own account
- _/pkgbase/{name}/{action}_
    - Refers to a POST action which can be triggered via the relevent package
      page at `/{pkgbase,packages}/{name}`.

Grouped changes explained in multiple items will always be prefixed with
the same letter surrounded by braces. Example:

- [A] Some feature that does something
- [A] The same feature where another thing has changed

Infrastructure
--------------

- Python packaging is now done with poetry.
- SQLite support has been removed. This was done because even though
  SQLAlchemy is an ORM, SQLite has quite a few SQL-server-like features
  missing both out of the box and integrally which force us to account
  for the different database types. We now only support mysql, and should
  be able to support postgresql without much effort in the future.
  Note: Users wishing to easily spin up a database quickly can use
  `docker-compose up -d mariadb` for a Docker-hosted mariadb service.
- An example systemd service has been included at `examples/aurweb.service`.
- Example wrappers to `aurweb-git-(auth|serve|update)` have been included
  at `examples/aurweb-git-(auth|serve|update).sh` and should be used to
  call these scripts when aurweb is installed into a poetry virtualenv.

HTML
----

- Pagers have all been modified. They still serve the same purpose, but
  they have slightly different display.
- Some markup and methods around the website has been changed for
  post requests, and some forms have been completely reworked.

Package Requests
----------------

- Normal users can now view and close their own requests
- [A] Requests can no longer be accepted through manual closures
- [A] Requests are now closed via their relevent actions
    - Deletion
        - Through `/packages` bulk delete action
        - Through `/pkgbase/{name}/delete`
    - Merge
        - Through `/pkgbase/{name}/merge`
    - Orphan
        - Through `/packages` bulk disown action
        - Through `/pkgbase/{name}/disown`
- Deletion and merge requests (and their closures) are now autogenerated
  if no pre-existing request exists. This was done to increase tracking of
  package modifications performed by those with access to do so (TUs).
- Deletion, merge and orphan request actions now close all (1 or more)
  requests pertaining to the action performed. This comes with the downside
  of multiple notifications sent out about a closure if more than one
  request (or no request) exists for them
- Merge actions now automatically reject other pre-existing merge requests
  with a mismatched `MergeBaseName` column when a merge action is performed
- The last `/requests` page no longer goes nowhere

Package Bulk Actions: /packages
-------------------------------

- The `Merge into` field has been removed. Merges now require being
  performed via the `/pkgbase/{name}/merge` action.

Package View
------------

- Some cached metadata is no longer cached (pkginfo). Previously,
  this was defaulted to a one day cache for some package information.
  If we need to bring this back, we can.

TU Proposals
------------

- A valid username is now required for any addition or removal of a TU.

RPC
---

- `type=get-comment-form` has been removed and is now located at
  `/pkgbase/{name}/comments/{id}/form`.
- Support for versions 1-4 have been removed.
- JSON key ordering is different than PHP's JSON.
- `type=search` performance is overall slightly worse than PHP's. This
  should not heavily affect users, as a 3,000 record query is returned
  in roughly 0.20ms from a local standpoint. We will be working on this
  in aim to push it over PHP.

Archives
--------

- Added metadata archive `packages-meta-v1.json.gz`.
- Added metadata archive `packages-meta-ext-v1.json.gz`.
    - Enable this by passing `--extended` to `aurweb-mkpkglists`.

Performance Changes
-------------------

As is expected from a complete rewrite of the website, performance
has changed across the board. In most places, Python's implementation
now performs better than the pre-existing PHP implementation, with the
exception of a few routes. Notably:

- `/` loads much quicker as it is now persistently cached forcibly
  for five minutes at a time.
- `/packages` search is much quicker.
- `/packages/{name}` view is slightly slower; we are no longer caching
  various pieces of package info for `cache_pkginfo_ttl`, which is
  defaulted to 86400 seconds, or one day.
- Request actions are slower due to the removal of the `via` parameter.
  We now query the database for requests related to the action based on
  the current state of the DB.
- `/rpc?type=info` queries are slightly quicker.
- `/rpc?type=search` queries of low result counts are quicker.
- `/rpc?type=search` queries of large result counts (> 2500) are slower.
    - We are not satisfied with this. We'll be working on pushing this
      over the edge along with the rest of the DB-intensive routes.
      However, the speed degredation is quite negligible for users'
      experience: 0.12ms PHP vs 0.15ms Python on a 3,000 record query
      on my local 4-core 8-thread system.

Upcoming Work
-------------

This release is the first major release of the Python implementation.
We have multiple tasks up for work immediately, which will bring us
a few more minor versions forward as they are completed.

- Update request and tu vote pagers
- Archive differentials
- Archive mimetypes
- (a) Git scripts to ORM conversion
- (a) Sharness removal
- Restriction of number of requests users can submit
2022-01-18 10:39:59 -08:00
Kevin Morris
d3d4424bc5
merge: updated translations
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-18 10:15:47 -08:00
Kevin Morris
621f030977
Merge branch 'html-escape-agenda' into pu 2022-01-18 09:17:09 -08:00
Kevin Morris
8d8f7954e9
fix(routers.trusted_user): html.escape agenda
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-18 09:10:28 -08:00
Kevin Morris
12f74fc40a
fix: docker cron config timing and doc
This wasn't matching up with what's suggested in doc/maintenance.
This patch resolves that inconsistency.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-18 09:05:47 -08:00
Kevin Morris
2feb9b90b2
housekeep: move templates/partials/widgets/* to templates/partials/
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-18 08:33:00 -08:00
Kevin Morris
dbbae97038
housekeep: move templates/packages/widgets/* to templates/packages/
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-18 08:27:19 -08:00
Kevin Morris
e1a87c3407
housekeep: move pkgbase templates to their own dir
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-18 08:15:59 -08:00
Kevin Morris
7f6c23d4cb
housekeep: centralize datetime generation
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-18 07:31:04 -08:00
Kevin Morris
7bcc8d7ce7
feat: support LOG_CONFIG environment variable
This variable allows users to override the logging.conf used
for Python logging configuration. By default, this is set
to logging.conf, which is a production config. LOG_CONFIG
is treated relative to [options] aurwebdir.

This patch allows us to specify the logging config as opposed
to copying over logging.conf in our test docker and gitlab
test scripts, as well as ease-of-testing as a developer.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-18 07:27:36 -08:00
Kevin Morris
7f1de72e08
fix(docker): remove logging setup in run-tests.sh
This was left in when we removed logging.prod.conf in a
previous patch. `test-mysql-entrypoint.sh` takes care of
test logging for us now, so this section is unnecessary.

Closes #261

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-18 05:22:00 -08:00
Kevin Morris
211ca5e49c
housekeep: define filters in their own modules
This patch cleans up aurweb.templates and removes direct
module-level initialization of the environment.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-18 03:06:17 -08:00
Kevin Morris
fca175ed84
update more documentation
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-18 01:55:41 -08:00
Kevin Morris
3102736b13
update documentation
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-18 01:41:14 -08:00
Kevin Morris
ce7c44758e
update INSTALL with Redis caching
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-18 01:25:46 -08:00
Kevin Morris
8a81eae8f4
update test/README.md
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-18 00:35:04 -08:00
Kevin Morris
ebb333565e
update INSTALL: asgi-driven aurweb direction
We heavily attempt to provide easy use of poetry virtualenvs
with aurweb in this revision of the INSTALL file. Added a
section about cron jobs and updated the nginx config example
with a lot more detail and locations for other parts of
the AUR infrastructure.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-17 14:18:28 -08:00
Kevin Morris
e5dfd53b9a
examples: poetry-driven git scripts & aurweb service
This introduces examples of a gunicorn systemd service in
addition to git script wrappers that can be used for poetry
virtualenv-driven installations.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-17 14:05:39 -08:00
Kevin Morris
290ef1a2ed
fix(gitlab-ci): remove logging.prod.conf copy
No longer needed; logging.conf, which is the default config
used, is now setup for production INFO logging.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-17 12:27:06 -08:00
Kevin Morris
bf4662e26f
change(logging): restrict logging.conf & add logging.test.conf
We'll override logging.conf with logging.test.conf for debug logging
needed for tests now, so we can rely on the default logging.conf
for production use.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-17 12:11:08 -08:00
Kevin Morris
cce9385fb1
fix(db): remove debug logging of dbname
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-17 12:11:02 -08:00
Kevin Morris
c07c40bcb6
fix: clean up package action templates (merge, delete, disown)
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-17 11:53:05 -08:00
Kevin Morris
d94e2dc9d7
feat(poetry): add srcinfo
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-16 18:16:42 -08:00
Kevin Morris
eb59cbaa39
change(python): use transaction query in BasicAuthBackend
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-16 02:01:52 -08:00
Kevin Morris
64069b9b5d
change(python): use a transaction query in get_pkg_or_base
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-16 02:00:41 -08:00
Kevin Morris
9441f4f904
change(python): move test_requests tests to their own suite
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-15 21:52:53 -08:00
Kevin Morris
3e3706911c
change(python): move test_pkgbase tests to their own suite
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-15 21:43:23 -08:00
Kevin Morris
42aa12d075
fix(docker): unrestrict --forwarded-allow-ips on (uvi|hyper)corn
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-15 21:04:47 -08:00
Kevin Morris
b092e247fc
fix(docker): update keyring before installing deps
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-15 20:21:40 -08:00
Kevin Morris
34a29df1a8
fix(docker): remove fastapi rewrite rule
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-15 20:19:58 -08:00
Kevin Morris
0f4ead759c
fix(docker): correct proxy configuration
- On non-localhost communication, this whitelists forwarded headers
  on all remote ips
- Add more headers
- Force https X-Forwarded-Proto
- Unset Forwarded header and rely on X-Forwarded-*

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-15 20:11:51 -08:00
Kevin Morris
ec3295ffd4
fix(docker): update archlinux-keyring prior to -Syu
When the Docker image is outdated, we need to fetch updated
archlinux-keyring keys to perform an -Syu without problems.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-15 15:18:23 -08:00
Kevin Morris
88cb1096c0
feat(docker): add more cron scripts
Added the rest:
- aurweb-pkgmaint
- aurweb-usermaint
- aurweb-tuvotereminder
- aurweb-popupdate

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-14 01:02:00 -08:00
Kevin Morris
b4495a49bf
fix(rpc): improve type=search performance
This patch brings in the use of .with_entities on our
RPC search query. This primarily fixes performance issues
we were seeing with large queries.

That being said, we do see a bit of a slowdown on
large record count rpc queries, but it's quite negligible
at this point.

We still do aim to perform better than the older PHP
implementation, so this is not a finishing patch by
any means.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-14 00:50:39 -08:00
Kevin Morris
d31a51742b
fix(gitlab-ci): compile asciidoc
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-13 22:16:17 -08:00
Kevin Morris
c4ea1171cd
fix(docker): compile doc during image build
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-13 22:16:17 -08:00
Kevin Morris
43b7fdb61d
fix(rpc): display rpc doc when no query string is provided
Closes #255

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-13 22:16:13 -08:00
Kevin Morris
60ae676075
fix(util): catch homepage validation exceptions
We were allowing erroneous URLs through, raising exceptions,
from e.g. `http://[localhost:8444/blah`. This patch catches
any ValueErrors raised during the parse process and returns
False, indicating that the validation failed.

This patch also adds testing specifically for `util.valid_homepage`.
We didn't have specific testing for this before; this will allow us
to catch regressions in this area.

Closes #250

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-13 19:47:36 -08:00
Kevin Morris
1ee8d177b4
fix(docker): rewrite trailing slashes to non-trailing in nginx config
Without this rewriting, we've been running into conversing with
HTTP over HTTPS (400 Bad Request).

TODO: Refactor this entire nginx config to something a bit more
simple and clean.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-10 14:49:53 -08:00
Kevin Morris
6d4e8028eb
change(gitlab-ci): explicitly down containers before upping
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-10 00:26:11 -08:00
Kevin Morris
4edae5015a
change(docker): remove ca dependencies on php-fpm/fastapi
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-10 00:18:09 -08:00
Kevin Morris
9f9b1c1732
change(docker): host fastapi over plain http
We don't need the https certificates being dealt with in the fastapi
service; we will define our certificates in any frontend nginx
running on top.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-10 00:12:01 -08:00
Kevin Morris
d675c0dc26
feat(python): catch all exceptions thrown through fastapi route paths
This commit does quite a bit:
- Catches unhandled exceptions raised in the route handler and
  produces a 500 Internal Server Error Arch-themed response.
- Each unhandled exception causes a notification to be sent to new
  `notifications.postmaster` email with a "Traceback ID."
- Traceback ID is logged to the server along with the traceback which
  caused the 500: `docker-compose logs fastapi | grep '<traceback_id>'`
- If `options.traceback` is set to `1`, traceback is displayed in
  the new 500.html template.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-09 23:10:02 -08:00
Kevin Morris
c775e8a692
feat(templates): add version to make_context
Prioritizes COMMIT_HASH environment variable and uses
`aurweb.config.AURWEB_VERSION` as a fallback.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-09 22:39:11 -08:00
Kevin Morris
e6679e4c4e
change(poetry): update fastapi to 0.71.0 release
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-08 13:54:54 -08:00
Kevin Morris
6f6f067597
feat: add aurweb-adduser console script
Originally left at util/adduser.py, this script allows administrators
to simply add a user to the configured aurweb database.

See --help for options.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-08 13:40:38 -08:00
Kevin Morris
9e7ae5904f
feat(python): handle RuntimeErrors raised through routes
This gets raised when a client closes a connection before receiving
a valid response; this is not controllable from our side.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-07 18:21:23 -08:00
Kevin Morris
bf371c447f
change(poetry): move fastapi to tiangolo/fastapi@2b10ca1
After two months, this finally got merged by somebody else.
Still largely considering moving away from FastAPI in the
long run, but this is better than relying on kevr's fork
for starlette 0.17.1 compat.

Other packages have also been upgraded and locked to versions.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-07 02:20:38 -08:00
Kevin Morris
a6faf9bd2e
feat(docker): perform migrations when starting the fastapi service
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-06 22:11:03 -08:00
Kevin Morris
b5ff8581f3
feat(migrations): add upgrade_voteinfo_integers ref
This migration modifies the Yes, No, Abstain and ActiveTUs columns
of the TUVoteInfo table from unsigned TINYINT to unsigned INTEGER.

TINYINT supports a total of 1 byte (up to 255 trusted users). This
is quite limited and we don't spend too much more by storing a
standard 4-byte INTEGER.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-06 20:27:23 -08:00
Kevin Morris
6e27f62e1b
fix(routers.trusted_user): set ActiveTUs on vote creation
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-06 20:26:45 -08:00
Kevin Morris
efd61979f7
fix(models.tu_voteinfo): default vote-count related columns to 0
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-06 20:25:30 -08:00
Kevin Morris
d49886f44f
fix(web/html/addvote): convert quorum to str using strval
Previous conversion was causing a straight up bug when submitting
new proposals. This patch fixes that issue.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-06 15:29:40 -08:00
Kevin Morris
059733cb8c
fix(routers.trusted_user): use creds to determine authorization
Closes #237

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-05 22:09:49 -08:00
Kevin Morris
9d221604b4
fix(routers.trusted_user): fix proposal participation percentage
Closes #238

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-05 20:17:47 -08:00
Kevin Morris
902c4d7a9c
fix(routers.packages): fix repeatead user joins
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-05 19:46:06 -08:00
Kevin Morris
0df57debb8
fix(routers.trusted_user): only display Voters on ended proposals
In addition, we display the Voters partial regardless of them existing
or not; with no voters, an empty Voters list is displayed.

Closes #236

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-05 17:51:57 -08:00
Kevin Morris
ae7621fb54
fix(routers.trusted_user): fix missing submitter link on /tu/{id}
Closes #235

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-05 17:18:19 -08:00
Kevin Morris
0988415931
fix(models.package_relation): add RelTypeID to PKs
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-05 14:37:03 -08:00
Kevin Morris
8ffff6261b
fix(models.package_dependency): add DepTypeID to PKs
This was stopping us from using numerous records for each dep type.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-05 14:36:57 -08:00
Kevin Morris
2cb9de0800
fix(models.package_group): add backref cascade
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-05 14:36:49 -08:00
Kevin Morris
1af61b0c50
fix(routers.packages): fix /packages/{name} relation ordering
Conflicts, Provides and Replaces did not have consistent
ordering with PHP. This patch fixes that issue.

Closes #228

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-03 22:58:48 -08:00
Kevin Morris
b0eea00181
fix(pkgbase.util): filter pending requests
Closes #229

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-03 22:33:31 -08:00
Kevin Morris
71e73ca654
fix(routers.pkgbase): fix next argument for merge redirection
This was redirecting us to the package which we merged, leading
us into a 404. This fixes that issue by instead redirecting us
into the target we merge into.

Closes #231

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-03 22:25:35 -08:00
Kevin Morris
83dc26ccde
fix(packages.request): fix autogenerated merge closure
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-03 21:16:54 -08:00
Kevin Morris
6c6eb2c21b
test: add tests to check various 404 paths and 503
Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-03 18:22:10 -08:00
Steven Guikal
e126d431d7
fix(FastAPI): add custom error templates for certain exceptions
Signed-off-by: Steven Guikal <void@fluix.one>
2022-01-03 18:22:03 -08:00
Kevin Morris
51b60f4210
feat(auth): add requires_{auth,guest} decorators
These new decorators are meant to be used without any arguments
and provide aliases to auth_required:
- `auth_required(True) -> requires_auth`
- `auth_required(False) -> requires_guest`

These decorators should be used without arguments, e.g.:

    @router.get("/")
    @requires_guest
    async def my_route(request: Request):
        return HTMLResponse()

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-02 16:57:42 -08:00
Kevin Morris
3e048e9675
change(python): centralize router inclusion
Now, when we want to add, remove routes, our base routes should
be defined in aurweb.routers.__init__.APP_ROUTES.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-02 01:52:28 -08:00
Kevin Morris
a1f46611e1
change(python): move request & pkgbase request routes
Move package request routes and related routes to their
respective routers. In addition, move some utility used
for requests over from `aurweb.packages`.

Introduced routers:
- `aurweb.routers.requests`

Introduced package:
- `aurweb.requests`

Introduced module:
- `aurweb.requests.util`

Changes:
- Moved `aurweb.packages.validate` to `aurweb.pkgbase.validate`
- Moved requests listing & request closure routes to
  `aurweb.routers.requests`
- Moved pkgbase request creation route to `aurweb.routers.pkgbase`
- Moved `get_pkgreq_by_id` from `aurweb.packages.util` to
  `aurweb.requests.util` and fixed its return type hint.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-02 01:44:36 -08:00
Kevin Morris
a77d44e919
change(python): move comaint routes to pkgbase router
Also brings over comaint utility functions to the pkgbase
package.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-02 00:33:31 -08:00
Kevin Morris
bd2ad9b616
change(python): put pkgbase routes & impl into their own modules
Introduces new router:
- `aurweb.routers.pkgbase`

Introduces new package:
- `aurweb.pkgbase`

Introduces new modules:
- `aurweb.pkgbase.actions`
- `aurweb.pkgbase.util`

Changes:
- `pkgbase_{action}_instance` functions are now located in
  `aurweb.pkgbase.actions`.
- `pkgbase`-wise routes have been moved to
  `aurweb.routers.pkgbase`.
- `make_single_context` was moved to
  `aurweb.pkgbase.util.make_context`.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-01 21:06:17 -08:00
Kevin Morris
c735f9868b
change(routers.packages): delete_package -> pkgbase_delete_instance
`delete_package` was processing package deletions through `Package`
instances. This doesn't make sense; if we delete a package, we want
to target its package base.

This new function vastly simplifies the previous.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2022-01-01 12:29:50 -08:00
Kevin Morris
53fabdfaea
fix(templates): require valid User relationships for <a> usage
Previously, when the relationship was None, an <a> would still
wrap the None value erroneously. This addresses that for all
three user fields.

In addition, this commit adds direct testing for the
`templates/partials/packages/details.html` template.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-31 18:31:48 -08:00
Kevin Morris
278490e103
feat(models.user): add User.__str__ -> User.Username
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-31 18:31:48 -08:00
Kevin Morris
67dd432e86
feat(testing.requests): add Request.__init__
This new constructor is a beginning to making Request a bit more easy
to deal with for standard testing needs. With this commit, users can
now specify a `user` and `authenticated` state while constructing a
Request:

    request = Request(user=some_user, authenticated=True)

By default, the `authenticated` kwarg is set to False and `user` is
set to `aurweb.testing.requests.User()`.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-31 18:31:48 -08:00
Kevin Morris
cab86035e9
feat(poetry): add pyalpm dependency & update some others
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-31 18:26:38 -08:00
Kevin Morris
8f8929f324
fix(routers.packages): handle package source display
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-30 23:10:00 -08:00
Kevin Morris
be7a96076e
fix: handle broken packages which have valid provides
Closes #226

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-30 19:49:49 -08:00
Kevin Morris
6fdaeee026
change(packages.util): handle queried record links via .is_official
This removes an unneeded query from our path.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-30 19:49:41 -08:00
Kevin Morris
fc229d755b
change(python): refactor & centralize comaintainer management
This commit centralizes comaintainer management with a few new
functions and uses them more appropriately within routes:

- aurweb.packages.util.latest_priority
- aurweb.packages.util.remove_comaintainer
- aurweb.packages.util.remove_comaintainers
- aurweb.packages.util.add_comaintainer
- aurweb.packages.util.add_comaintainers
- aurweb.packages.util.rotate_comaintainers

Closes #117

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-29 19:50:17 -08:00
Kevin Morris
9d3e77bab1
fix(packages.util.pkg_required): correct type hints and docstring
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-29 14:48:12 -08:00
Kevin Morris
3a771fc807
fix(packages.requests): disown as maintainer does not need handle_requests
As a maintainer, we don't deal with requests at all.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-28 18:31:21 -08:00
Kevin Morris
34cb8ec268
fix(routers.packages): all authenticated users can see sshd clone uri
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-28 13:13:46 -08:00
Kevin Morris
d55dab93da
revert account type permission changes
While this does make more sense to me personally, there is no need
to change how the AUR treats its users; it has been accepted for
ages and not found to be ridden with flaws. Stay with the tried
and true method.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-27 22:41:18 -08:00
Kevin Morris
80ee7f3d4b
fix(routers.accounts): use User.can_edit_user across the board
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-27 16:05:01 -08:00
Kevin Morris
260b67c49e
change(models.user): can_edit_user should check account type id priority
The credential alone does not completely encapsulate our new
requirements for editing an account.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-27 16:04:57 -08:00
Kevin Morris
b27dab99d8
fix(routers.accounts): correct disable decision for More button
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-26 18:35:38 -08:00
Kevin Morris
2baf061b96
test(routers.packages): fix package view dependency test
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-26 17:03:16 -08:00
Kevin Morris
84a54bb6e6
fix(routers.packages): fix package dependency ordering
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-26 15:33:00 -08:00
Kevin Morris
56bd60559c
fix(packages.search): fix default ordering & improve performance
- Use queries more closely aligned to PHP's implementation; removes
  the need for separate vote/notification queries.
- Default sort by popularity

Closes #214

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-25 11:15:20 -08:00
Kevin Morris
e75aa386ea
Merge branch 'pu-cron-fix' into pu
- Removed user specification from cron config.
- Removed logging to /var/log; this commit brings in `-x proc`,
  which logs out to std(out|err).
2021-12-22 14:41:03 -08:00
Kevin Morris
50eec96dd0
fix(routers.packages): fix related package metadata
Closes #218

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-21 18:02:37 -08:00
Kevin Morris
5142447b7e
fix(models.package_source): fix primary key constraints
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-21 16:00:10 -08:00
Kevin Morris
22093c5c38
fix(routers.packages): restrict /pkgbase/{name}/voters to those with creds
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-19 17:15:47 -08:00
Kevin Morris
0c07c14860
change(poetry): update Markdown to 3.3.6
Previous versions when encountered with an updated `importlib_metadata`
produce a deprecation warning. This update resolves that deprecation.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-19 15:54:31 -08:00
Kevin Morris
36bc9ae29b
fix(notify): gracefully fail notifications
Instead of allowing an exception to propogate through the framework
routes, catch it and log out an error about notifications not being
sent.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-17 18:08:36 -08:00
Kevin Morris
d6d41cdbad
fix(templates): add missing empty package results text
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-16 22:01:14 -08:00
Kevin Morris
94e8d34948
fix(routers.accounts): use target user's account type for autofill
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-16 16:10:01 -08:00
Kevin Morris
e17389485b
test(templates): add pager tests
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-15 17:50:53 -08:00
Kevin Morris
f273cfc87d
change(templates): omit page count in pager partial if pages <= 0
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-15 17:46:15 -08:00
Kevin Morris
e1543f2e91
fix(templates): import aurweb.auth.creds directly
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-15 16:06:37 -08:00
Kevin Morris
c86f71a4b4
fix(time): unquote timezone when producing it
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-15 14:01:20 -08:00
Kevin Morris
703d655a5e
fix(users.validate): fix type hints
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-14 17:11:52 -08:00
Kevin Morris
3b878da59a
fix(templates): a user can set Inactive on themselves
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-14 16:46:33 -08:00
Kevin Morris
f357615bfb
change(users.validate): users can't edit their own account types
This commit also decouples testing regarding this feature
into several test functions.

Signed-off-by: Kevin Morris <kevr@0cost.org>

bump

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-14 16:45:40 -08:00
Hunter Wittenborn
2e12417a6c
Added '-x proc' flag to 'crond' command 2021-12-14 17:02:36 -06:00
Hunter Wittenborn
48973fe036
Fixed incorrect syntax usage and missing environment variables in cron jobs 2021-12-14 16:56:29 -06:00
Kevin Morris
c7751d5d63
fix(util): fix account_url's base url generation
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-14 14:30:34 -08:00
Kevin Morris
02a62532da
fix(python): fix difference parsing of comaintainers
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-13 16:34:44 -08:00
Kevin Morris
918593c3e6
change(poetry): bump dependency versions
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-13 14:16:12 -08:00
Kevin Morris
95a215ec58
change(poetry): dep on python >= 3.9 < 3.11
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-13 14:16:02 -08:00
Kevin Morris
de671e9b9c
fix(time): fall through and prefer AURTZ for timezone
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-09 23:03:26 -08:00
Kevin Morris
c47578f158
fix(auth): refresh the user record on successful auth
This will ensure the state of `request.user` is good to go
for any other users which obtain it after the backend.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-09 23:01:45 -08:00
Kevin Morris
d0e183a738
Revert "fix(gitlab-ci): only run services we need for deployment"
We'll need to update the nginx config to do this; putting
this off into an MR.

This reverts commit 19bd3766d2.
2021-12-09 21:24:26 -08:00
Kevin Morris
19bd3766d2
fix(gitlab-ci): only run services we need for deployment
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-09 21:11:23 -08:00
Kevin Morris
3a43e2b98c
fix(docker): reduce health check interval to 2s
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-09 21:03:37 -08:00
Kevin Morris
1fede8d2a3
change(requests): closures are now handled by pkgbase actions
Workflow has changed and TUs should now depend on actions taken closing
requests which exist for the package base (deletion, merge, disown|orphan).

The `/requests/{id}/close` route is now purely used for rejecting
requests. The deletion, merge and orphan closures have been added
into their related action routes. See the lists below.

Disowning can only be done if an existing orphan request can be found
for the action by TUs. Maintainers can disown their own packages at
any time.

Actions which provide request closures:
--------------------------------------
- `/pkgbase/{name}/delete`: deletion request closure
- `/pkgbase/{name}/merge`: merge request closure
- `/pkgbase/{name}/disown`: orphan request closure

To close a request:
------------------
- `/requests/{id}/close`: close a request with rejected status

For deletion and merge actions, if no request yet exists, one
will be autogenerated and closed.

For orphan requests, a preexisting require is required and an
error is now returned in cases where one cannot be found.

For all closure actions, if the new comments field is left empty,
a closure comment will be autogenerated.

Note: This is a documentation commit summing up UX changes from
recent commits.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-09 20:27:40 -08:00
Kevin Morris
26b1674c9e
fix(requests): rework handling of requests
This commit changes several things about how we were handling
package requests.

Modifications (requests):
-------------
- `/requests/{id}/close` no longer provides an Accepted selection.
  All manual request closures will cause a rejection.
- Relevent `pkgbase` actions now trigger request closures:
  `/pkgbase/{name}/delete` (deletion), `/pkgbase/{name}/merge` (merge)
  and `/pkgbase/{name}/disown` (orphan).
- Comment fields have been added to
  `/pkgbase/{name}/{delete,merge,disown}`, which is used to set the
  `PackageRequest.ClosureComment` on pending requests. If the comment
  field is left blank, a closure comment is autogenerated.
- Autogenerated request notifications are only sent out once
  as a closure notification.
- Some markup has been fixed.

Modifications (disown/orphan):
-----------------------------
- Orphan requests are now handled through the same path as
  deletion/merge.
- We now check for due date when disowning as non-maintainer;
  previously, this was only done for display and not functionally.
  This check applies to Trusted Users' disowning of a package.

This style of notification flow does reduce our visibility, but
accounting can still be done via the close request; it includes
the action, pkgbase name and the user who accepted it.

Closes #204

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-09 19:09:51 -08:00
Kevin Morris
bad57ba502
feat(exceptions): add InvariantError
This exception is to be used when a known invariant is violated.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-09 15:10:06 -08:00
Kevin Morris
85e6ad03db
feat(testing.email): add Email.dump
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-09 15:10:04 -08:00
Kevin Morris
60b098a2f2
fix(git-cliff): define Housekeeping group + match all tags
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-09 14:57:31 -08:00
Kevin Morris
32660881f6
fix(docker): set notifications up in test config
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-09 11:06:29 -08:00
Kevin Morris
c3d962a0d0
fix(templates): add some comments
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-09 11:06:19 -08:00
Kevin Morris
061e828f16
fix(gitlab-ci): use logging.prod.conf for sharness
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-07 15:09:20 -08:00
Kevin Morris
7831503c19
fix(docker): use logging.prod.conf for sharness
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-07 15:09:20 -08:00
Kevin Morris
409229739e
feat(conftest): set default logging.conf to DEBUG
We now maintain a logging.prod.conf, which should contain sane
defaults for a production instance. Our main logging.conf is
a good default for both testing and debugging, but provides
too much logging for production.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-07 15:09:13 -08:00
Kevin Morris
50d6a9b5c8
Merge branch 'fix-unlinked-pkgname' into pu 2021-12-07 13:57:14 -08:00
Kevin Morris
73034c7998
Merge branch 'fix-unneeded-newline' into pu 2021-12-07 13:57:06 -08:00
Kevin Morris
1b203f0d30
fix(requests): show unlinked pkgname when PackageBase has been deleted
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-07 13:51:38 -08:00
Kevin Morris
452f5d160a
fix(python): remove unneeded newline
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-07 13:50:40 -08:00
Kevin Morris
31d82fb1af
fix(templates): correct Closed link display
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-07 13:49:57 -08:00
Kevin Morris
2df54bd7a0
Merge branch 'fix-package-link' into pu 2021-12-07 12:35:21 -08:00
Kevin Morris
de7e3ab607
fix(logging): restore aurweb logger; null out root logger
After actually digging into how the logger does things,
since the root logger is required and we have specific
level-changing loggers for our components, we must no-op
the root logger to avoid it duplicating logs from the others.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-07 07:45:59 -08:00
Kevin Morris
a9a0adaead
fix(python): fix package_link check
This was failing when it matched more than one record.
This fixes that issue by using an EXISTS query.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-07 07:44:56 -08:00
Kevin Morris
8b350066c1
Merge branch 'fix-package-vote' into pu 2021-12-06 23:45:35 -08:00
Kevin Morris
4667993dad
Merge branch 'fix-comaintainer' into pu 2021-12-06 23:45:20 -08:00
Kevin Morris
0447afa2e5
fix(PackageNotification): add missing backref cascade
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-06 23:02:53 -08:00
Kevin Morris
51b4709ea4
fix(PackageVote): include backref cascade definition
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-06 23:02:06 -08:00
Kevin Morris
57df6db609
fix(PackageComaintainer): populate backref cascade properly
Closes #205

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-06 23:01:04 -08:00
Kevin Morris
27f8603dc5
fix(python): fix ordering of fields in partials/account_form.html
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-04 17:56:02 -08:00
Kevin Morris
cf978e23aa
fix(python): use S argument to decide Suspended
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-04 17:56:02 -08:00
Kevin Morris
0ed752277c
Merge branch 'fix-account-show' into pu 2021-12-04 17:54:36 -08:00
Kevin Morris
8501bba0ac
change(python): rework session timing
Previously, we were just relying on the cookie expiration
for sessions to expire. We were not cleaning up Session
records either.

Rework timing to depend on an AURREMEMBER cookie which is
now emitted on login during BasicAuthBackend processing.

If the SID does still have a session but it's expired,
we now delete the session record before returning.

Otherwise, we update the session's LastUpdateTS to
the current time.

In addition, stored the unauthenticated result value
in a variable to reduce redundancy.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-04 02:16:22 -08:00
Kevin Morris
224a0de784
fix(python): add logged in date field to account/show.html
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-04 01:16:14 -08:00
Kevin Morris
2ea4559b60
fix(python): use correct Status field in account/show.html
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-04 00:54:03 -08:00
Kevin Morris
f8bef16d32
Merge branch 'fix-account-links' into pu 2021-12-04 00:25:57 -08:00
Kevin Morris
973dbf0482
fix(python): use creds to determine account links to display
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-04 00:15:34 -08:00
Kevin Morris
d0fc56d53f
fix(python): redirect when the request user can't edit target user
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-04 00:14:55 -08:00
Kevin Morris
bfa916c7b2
fix(fastapi): fix PGP Key Fingerprint display for account/show.html
There's a space between every 4 characters in the fingerprint
in PHP; we were missing it in FastAPI. This commit fixes that
inconsistency.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-03 23:40:16 -08:00
Kevin Morris
522177e813
Merge branch 'fix-clean-auth-docs' into pu 2021-12-03 18:29:47 -08:00
Kevin Morris
aa717a4ef9
change(fastapi): no longer care about ResetKey collisions
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-03 17:59:02 -08:00
Kevin Morris
b0b5e4c9d1
fix(fastapi): use secrets module to generate random strings
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-03 17:58:58 -08:00
Steven Guikal
75ad2fb53d fix(FastAPI): cleanup auth_required decorator
Signed-off-by: Steven Guikal <void@fluix.one>
2021-12-03 14:07:47 -05:00
Kevin Morris
81f8c23265
fix(fastapi): log out IntegrityError from failed SID generation
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-02 23:42:13 -08:00
Kevin Morris
806a19b91a
feat(fastapi): render a 500 html response when unique SID generation fails
We've seen a bug in the past where unique SID generation fails and
still ends up raising an exception.

This commit reworks how we deal with database exceptions internally,
tries for 36 iterations to set a fresh unique SID, and raises a 500
HTTPException if we were unable to.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-02 23:26:42 -08:00
Kevin Morris
abfd41f31e
change(fastapi): centralize HTTPException
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-02 23:23:27 -08:00
Kevin Morris
a747548254
Merge branch 'fix-navbar-spacing' into pu 2021-12-02 17:32:19 -08:00
Kevin Morris
e1bf6dd562
fix(fastapi): restore stripped whitespace in archdev-navbar
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-02 17:09:37 -08:00
Steven Guikal
42701514e7 fix(FastAPI): Use HTTPStatus instead of raw number
Signed-off-by: Steven Guikal <void@fluix.one>
2021-12-01 21:15:49 +00:00
Kevin Morris
0435c56a41
update test/README.md to be more aligned with the current state
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-01 12:27:14 -08:00
Kevin Morris
c09784d58f
fix(auth.auth_required): remove unused keyword arguments
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-01 11:56:44 -08:00
Kevin Morris
112837e0e9
fix(test_auth): cover mismatched referer situation
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-01 11:53:43 -08:00
Kevin Morris
043ac7fe92
fix(test_aurblup): use correct type hint for tmpdir
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-01 00:33:31 -08:00
Kevin Morris
fccd8b63d2
housekeep(fastapi): rewrite test_auth_routes with fixtures
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-01 00:33:30 -08:00
Kevin Morris
7ef3e34386
housekeep(fastapi): rewrite test_accounts_routes with fixtures
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-01 00:33:30 -08:00
Kevin Morris
de0f919077
housekeep(fastapi): rewrite test_ban with fixtures
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-01 00:33:30 -08:00
Kevin Morris
eb396813a8
housekeep(fastapi): rewrite test_package with fixtures
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-01 00:33:29 -08:00
Kevin Morris
5b14ad4065
housekeep(fastapi): rewrite test_user with fixtures
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-01 00:33:28 -08:00
Kevin Morris
140f9b1fb2
housekeep(fastapi): rewrite test_package_dependency with fixtures
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-01 00:30:23 -08:00
Kevin Morris
05bd6e9076
housekeep(fastapi): rewrite test_package_vote with fixtures
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-01 00:30:22 -08:00
Kevin Morris
150c944758
housekeep(fastapi): rewrite test_package_group with fixtures
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-01 00:30:22 -08:00
Kevin Morris
df530d8a73
housekeep(fastapi): rewrite test_package_source with fixtures
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-01 00:30:22 -08:00
Kevin Morris
171b347dad
housekeep(fastapi): rewrite test_package_base with fixtures
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-01 00:30:21 -08:00
Kevin Morris
93bc91cce2
housekeep(fastapi): rewrite test_tu_voteinfo with fixtures
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-01 00:30:21 -08:00
Kevin Morris
ae72817950
housekeep(fastapi): rewrite test_routes with fixtures
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-01 00:30:21 -08:00
Kevin Morris
ca25595022
housekeep(fastapi): rewrite test_sesion with fixtures
Also, added a new test function which tests the IntegrityError
exception.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-01 00:30:20 -08:00
Kevin Morris
a0e1a1641d
fix(fastapi): support UsersID and User columns in the Session model
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-01 00:30:20 -08:00
Kevin Morris
31a093ba06
housekeep(fastapi): rewrite test_package_relation with fixtures
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-01 00:30:20 -08:00
Kevin Morris
14d80d756f
housekeep(fastapi): rewrite test_package_comaintainer with fixtures
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-01 00:30:19 -08:00
Kevin Morris
ff3931e435
housekeep(fastapi): rewrite test_package_notification with fixtures
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-01 00:30:19 -08:00
Kevin Morris
655b98d19e
housekeep(fastapi): rewrite test_package_license with fixtures
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-01 00:30:19 -08:00
Kevin Morris
a082de5244
housekeep(fastapi): rewrite test_package_keyword with fixtures
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-01 00:30:18 -08:00
Kevin Morris
b20ec9925a
housekeep(fastapi): rewrite test_ssh_pub_key with fixtures
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-01 00:30:18 -08:00
Kevin Morris
91f6591141
housekeep(fastapi): rewrite test_accepted_term with fixtures
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-01 00:30:18 -08:00
Kevin Morris
d6cb3b9fac
housekeep(fastapi): rewrite test_auth with fixtures
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-01 00:30:16 -08:00
Kevin Morris
735c5f57cb
housekeep(fastapi): rewrite test_package_blacklist
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-01 00:28:40 -08:00
Kevin Morris
adafa6ebc1
housekeep(fastapi): rewrite test_package_request with fixtures
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-01 00:28:39 -08:00
Kevin Morris
012dd24fd8
housekeep(fastapi): rewrite test_tu_vote with fixtures
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-01 00:28:39 -08:00
Kevin Morris
604df50b88
housekeep(fastapi): rewrite test_package_comment with fixtures
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-01 00:28:39 -08:00
Kevin Morris
2fee6205a6
housekeep(fastapi): rewrite test_rpc with fixtures
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-12-01 00:28:36 -08:00
Kevin Morris
867825491b
Merge branch 'fix-improve-auth' into pu 2021-12-01 00:14:16 -08:00
Steven Guikal
0b30216229 fix(FastAPI): remove unnecessary arguments to auth_required
Signed-off-by: Steven Guikal <void@fluix.one>
2021-12-01 03:11:01 -05:00
Steven Guikal
429d8059e1 fix(FastAPI): remove login and redirect parameters from auth_required
Signed-off-by: Steven Guikal <void@fluix.one>
2021-12-01 02:57:23 -05:00
Steven Guikal
a10f8663fd fix(FastAPI): reorganize credential checkin into dedicated file
Signed-off-by: Steven Guikal <void@fluix.one>
2021-12-01 02:03:02 -05:00
Steven Guikal
125b244f44 fix(FastAPI): use account type vars instead of strings
Signed-off-by: Steven Guikal <void@fluix.one>
2021-11-30 16:33:34 -05:00
Steven Guikal
ecbab8546b fix(FastAPI): access AccountType ID directly
Signed-off-by: Steven Guikal <void@fluix.one>
2021-11-30 16:33:34 -05:00
Kevin Morris
a6ac5f0dbf fix(rpc): fix ordering of related records
They were being ordered by IDs; they should be ordered by Names.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-30 16:33:34 -05:00
Kevin Morris
274682f040
Merge branch 'fix-rpc-ordering' into pu 2021-11-29 19:57:45 -08:00
Kevin Morris
001e86317f
fix(rpc): fix ordering of related records
They were being ordered by IDs; they should be ordered by Names.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-29 19:44:18 -08:00
Kevin Morris
9bfe2b07ba
fix(fastapi): render Logged-in as page on authenticated /login
This was missed during the initial porting of the /login route.

Modifications:
-------------
- A form is now used for the [Logout] link and some css was
  needed to deal with positioning.

Closes #186

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-29 19:40:55 -08:00
Kevin Morris
fd8d23a379
fix(fastapi): fix new Logout nav item css
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-29 19:04:55 -08:00
Kevin Morris
69eb17cb0d
change(fastapi): remove the GET /logout route; replaced with POST
Had to add some additional CSS in to style a form button the same
as <a> links are styled.

Closes #188

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-29 16:52:10 -08:00
Kevin Morris
44f2366675
fix: remove TODO comments and noop tests from test_notify
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-29 16:20:36 -08:00
Kevin Morris
436d742017
fix(fastapi): use CRED_TU_LIST_VOTES for "Trusted User" navigation item
Closes #189

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-29 14:08:00 -08:00
Kevin Morris
4426c639ce
fix(logging): remove test logger definition
Like the `aurweb` logger definiton was previously, the `test`
logger is being redundant with the root logger. Use root for
all aurweb-local logging.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-28 19:55:12 -08:00
Kevin Morris
67a6b8360e
fix(docker): remove update and build steps from poetry
`install` includes dependencies present in poetry.lock
and we must stick to them if we wish to pin dependencies.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-28 19:55:12 -08:00
Kevin Morris
bc1cf8b1f6
fix(rendercomment): markdown.util.etree -> xml.etree.ElementTree
This removes a deprecation warning.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-28 19:55:12 -08:00
Kevin Morris
2d0e09cd63
change(rendercomment): converted to use aurweb.db ORM
- Added aurweb.util.git_search.
    - Decoupled away from rendercomment for easier testability.
- Added aurweb.testing.git.GitRepository.
- Added templates/testing/{PKGBUILD,SRCINFO}.j2.
- Added aurweb.testing.git.GitRepository + `git` pytest fixture

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-28 19:55:11 -08:00
Kevin Morris
4b0cb0721d
fix(conftest): use synchronization locks for setup_database
We were running into data race issues where the `fn.is_file()`
check would occur twice before writing the file in the `else`
clause. For this reason, a new aurweb.lock.Lock class has been
added which doubles as a thread and process lock. We can use
this elsewhere in the future, but we are also able to use it
to solve this kind of data race issue.

That being said, we still need the lock file state to tell us
when the first caller acquired the lock.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-28 19:55:11 -08:00
Kevin Morris
155aa47a1a
feat(poetry): add posix_ipc
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-28 19:55:11 -08:00
Kevin Morris
d8e3ca1abb
change(notify): converted to use aurweb.db ORM
- Removed notify sharness test

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-28 19:55:10 -08:00
Kevin Morris
9fb1fbe32c
feat(testing): add email testing utilities
Changes:
- util/sendmail now populates email files in the 'test-emails' directory.
    - util/sendmail does this in a serialized fashion based off of
      the test suite and name retrieved from PYTEST_CURRENT_TEST
      in the format: `<test_suite>_<test_function>.n.txt` where n
      is increased by one every time sendmail is run.
- pytest conftest fixtures have been added for test email setup;
  it wipes out old emails for the particular test function being run.
- New aurweb.testing.email.Email class allows developers to test
  against emails stored by util/sendmail. Simple pass the serial
  you want to test against, starting at serial = 1; e.g. Email(serial).

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-28 19:55:10 -08:00
Kevin Morris
b72bd38f76
change(pkgmaint): converted to use aurweb.db ORM
- Replaced time.time() usage with datetime.utcnow().timestamp()
- Removed pkgmaint sharness test

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-28 19:55:10 -08:00
Kevin Morris
f4ef02fa5b
fix(fastapi): fix Package's PackageBase backref cascade
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-28 19:55:09 -08:00
Kevin Morris
d097799b34
change(usermaint): converted to use aurweb.db ORM
- Removed usermaint sharness test

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-28 19:55:09 -08:00
Kevin Morris
8d5683d3f1
change(tuvotereminder): converted to use aurweb.db ORM
- Removed tuvotereminder sharness test.
- Added [tuvotereminder] section to config.defaults.
- Added `range_start` option to config.defaults [tuvotereminder].
- Added `range_end` option to config.defaults [tuvotereminder].

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-28 19:55:09 -08:00
Kevin Morris
29c2d0de6b
change(mkpkglists): converted to use aurweb.db ORM
- Improved speed dramatically
- Removed mkpkglists sharness

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-28 19:55:08 -08:00
Kevin Morris
c59acbf6d6
add noop testing utility
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-28 19:55:08 -08:00
Kevin Morris
29989b7fdb
change(aurblup): converted to use aurweb.db ORM
Introduces:
- aurweb.testing.alpm.AlpmDatabase
    - Used to mock up and manage a remote repository.
- templates/testing/alpm_package.j2
    - Used to generate a single ALPM package desc.
- Removed aurblup sharness test

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-28 19:55:08 -08:00
Kevin Morris
3efb9a57b5
change(popupdate): converted to use aurweb.db ORM
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-28 19:55:07 -08:00
Kevin Morris
3a65e33abe
fix(gitlab-ci): prepare conf/config for setup
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-28 19:35:32 -08:00
Kevin Morris
dbeebd3b01
change(fastapi): setup live database in mariadb-init-entrypoint.sh
Centralize database setup there and remove all copying of
config.dev from the entrypoint scripts (the Dockerfile
now does it).

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-27 23:29:49 -08:00
Kevin Morris
343a306bb8
change(docker): setup AUR_CONFIG in Dockerfile
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-27 23:29:44 -08:00
Kevin Morris
84beacd427
fix(docker): supply AUR_CONFIG_IMMUTABLE for docker-compose
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-27 22:49:52 -08:00
Kevin Morris
5b350bc361
change(docker): use aurweb-config to update AUR_CONFIG
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-27 22:49:48 -08:00
Kevin Morris
0726a08677
fix(docker): remove sqlite scripts
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-27 22:46:11 -08:00
Kevin Morris
f3efc18b50
feat(docker): force test db configuration
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-27 22:42:12 -08:00
Kevin Morris
0e938209af
feat(aurweb-config): add unset action and simplify
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-27 22:34:15 -08:00
Kevin Morris
199622c53f
fix(fastapi): refresh records when fetching updated packages
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-27 21:35:48 -08:00
Kevin Morris
232594ae44
Merge branch 'fix-sid-generation' into pu 2021-11-27 21:32:21 -08:00
Kevin Morris
7b0d664bc0
fix(docker): reorg ./data mounts
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-27 21:03:24 -08:00
Kevin Morris
47feb72f48
fix(fastapi): fix SessionID (and ResetKey) generation
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-27 20:19:40 -08:00
Kevin Morris
d658627e99
fix(fastapi): don't redirect to login on authed /login
Closes #184

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-27 19:14:32 -08:00
Kevin Morris
a87973e0c7
Merge branch 'pu-config' into pu 2021-11-27 16:54:19 -08:00
Kevin Morris
759f18ea75
feat: add aurweb-config console script
This can be used to update config values for the entirety
of a config. When config values are set through this tool,
$AUR_CONFIG is overridden with a copy of the config file
with all sections and options found in $AUR_CONFIG
+ $AUR_CONFIG_DEFAULTS.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-27 16:44:56 -08:00
Kevin Morris
b98159d5b9
change(docker): use step-ca for CA + cert generation
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-27 16:43:29 -08:00
Kevin Morris
e558e979ff
fix(fastapi): check ssh key prefixes against configured valid-keytypes
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-24 21:29:28 -08:00
Kevin Morris
1aab960401
fix: use corrent u2f ssh key prefixes
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-24 21:29:15 -08:00
Kevin Morris
6bb002e708
fix: use correct u2f ssh key prefixes
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-24 21:23:01 -08:00
Kevin Morris
47d83244bb
change(gitlab-ci): add 'fast-single-thread' tag to the test stage
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-22 22:26:56 -08:00
Kevin Morris
3b686c475d
fix: default detailed loglevel to DEBUG
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-22 10:22:58 -08:00
Kevin Morris
39fd3b891e
change: set -v for sh tests
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-22 10:22:32 -08:00
Kevin Morris
e891d7c8e8
change(docker): allow run-pytests to collect coverage
Additionally fix up the argument parsing to be a bit less
flexible.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-22 10:22:01 -08:00
Kevin Morris
34747359ba
fix(docker): expose git service's 2222 through 0.0.0.0
Other ports we use are locked to 127.0.0.1. The `git` service,
however, already promotes security in its sshd service and
can't really be abused from an external source. This simplifies
the need to forward to localhost if deploy targets want the sshd
to be available.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-21 23:11:02 -08:00
Kevin Morris
41e0eaaece
fix(docker): force bind ports to localhost only
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-21 21:43:59 -08:00
Kevin Morris
ffb450db71
Merge branch 'logging-fix' into pu 2021-11-21 00:56:29 -08:00
Kevin Morris
bc7bf9866a
docker: bind ./aurweb in cron service by default
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-21 00:55:27 -08:00
Kevin Morris
e8f4c9cf69
fix(fastapi): remove aurweb logger definition
Both the root and aurweb loggers are included in output,
causing repeated log messages. Now, just rely on the
root logger for aurweb logging.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-21 00:54:23 -08:00
Kevin Morris
d4d9f50b8f
change(docker): use ./data instead of ./cache
For the `git` service, ./data is always used to provide an
optional overriding of ssh host keys. In aur-dev production
containers, most services which use the data mount use an
internal Docker `data` volume instead.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-20 20:05:32 -08:00
Kevin Morris
604901fe74
fix(docker): fix nginx .gz match against cgit snapshots
This only deals with .gz files in the root of the request_uri
and now more. That is: /packages.gz goes through the nginx regex,
but now /cgit/.../snapshot/package.tar.gz is served by the cgit
block.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-20 20:00:53 -08:00
Kevin Morris
c7feecd4b8
housekeep(docker): remove configuration regexes in the nginx service
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-20 19:34:33 -08:00
Kevin Morris
a1e547c057
feat(docker): allow configurable SSH_CMDLINE in git service
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-20 19:04:38 -08:00
Kevin Morris
ba3ef742ce
feat(docker): allow user-customizable ssh host keys
There is a new ./data bind mount used here. If ssh_host_* keys are
in ./data when the git service starts, they'll override the
container-generated host keys.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-20 18:40:32 -08:00
Kevin Morris
233d25b1c3
feat: add test_spawn, an aurweb.spawn test
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-20 15:47:25 -08:00
Kevin Morris
19191fa8b5
fix: update nginx config in aurweb.spawn
Host a specific FastAPI nginx frontend as well as a PHP
nginx frontend, configurable by the (PHP|FASTAPI)_NGINX_PORT
environment variables.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-20 15:47:25 -08:00
Kevin Morris
47d0df76e6
feat: support gunicorn in aurweb.spawn
This also comes with a -w|--workers argument that allows
the caller to set the number of gunicorn workers.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-20 15:47:24 -08:00
Kevin Morris
82ca4ad9a0
feat: check php configuration in aurweb.spawn
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-20 15:47:24 -08:00
Kevin Morris
191198ca41
housekeep(fastapi): simplify aurweb.spawn.stop()
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-20 15:47:24 -08:00
Kevin Morris
0b5d088016
fix(fastapi): catch ProgrammingError instead of OperationalError in conftest
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-20 13:20:47 -08:00
Kevin Morris
008a8824ce
housekeep(fastapi): simplify package_base_comaintainers_post
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-20 13:20:46 -08:00
Kevin Morris
f897411ddf
change(fastapi): let conftest bypass create database errors
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-18 21:27:09 -08:00
Kevin Morris
7f981b9ed7
fix(fastapi): utilize auto_{orphan,deletion}_age
Didn't get this in when the initial request port went down;
here it is.

Auto-accept orphan requests when the package has been out of
date for longer than auto_orphan_age.

Auto-accept deletion requests by the package's maintainer
if the package has been uploaded within auto_deletion_age
seconds ago.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-18 21:27:03 -08:00
Kevin Morris
a348cdaac3
housekeep(fastapi): cleanup unneeded jinja set statement
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-18 16:44:13 -08:00
Kevin Morris
7739b2178e
fix(fastapi): fix comment edit image sources
These were using the old comment image sources. Slipped in
due to cache and not checking without cache.

Fixed them to use src="/static/images/...".

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-18 16:43:10 -08:00
Kevin Morris
dbe5cb4a33
fix(fastapi): only include comment-edit.js where needed
Closes: #178

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-18 16:42:26 -08:00
Kevin Morris
672af707ad
remove C901 and E741 per-file-ignores exclusion
We no longer have C901 violations and we're already ignoring
E741 (short variable names) in the overall `ignore` option.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-17 06:00:12 -08:00
Kevin Morris
2df7187514
fix global test_ssh_pub_key E501 flake8 violation
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-17 06:00:12 -08:00
Kevin Morris
2892d21ff1
remove global aurweb.models flake8 F401 ignore
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-17 06:00:12 -08:00
Kevin Morris
303585cdbf
change(fastapi): decouple update logic from account edit
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-17 06:00:07 -08:00
Kevin Morris
94972841d6
change(fastapi): decouple error logic from process_account_form
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-17 05:58:08 -08:00
Kevin Morris
ccf50cbdf5
change: rework test_rpc's TestClient usage into a fixture
This is the first step on our path to reworking the test
suite in general.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-17 02:30:06 -08:00
Kevin Morris
abe8c0630c
fix(rpc): improve type=info performance
Now, we use an equivalent query to PHP's query, yet we grab
every piece of data we need for all packages asked for in one
database query.

At this time, local benchmarks have shown a slight performance
improvement when compared to PHP.

fastapi 262 requests/sec
php 250 requests/sec

Extras:

- Moved RPCError to the aurweb.exceptions module

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-17 02:30:06 -08:00
Kevin Morris
912b7e0c11
fix(docker): fix database user/password for git-entrypoint
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-17 02:29:36 -08:00
Kevin Morris
a5c0c47e5b
change(.gitlab-ci): adapt for new conftest
No longer do we need to create any database in .gitlab-ci.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-17 01:36:19 -08:00
Kevin Morris
fb92fb509b
change(fastapi): use sys.getrecursionlimit() + 1000 as default
Without the increment, we've seen tests failed due to recursion
errors caused by starlette's base middleware. Just make it safe
in case nobody supplies TEST_RECURSION_LIMIT.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-17 01:36:19 -08:00
Kevin Morris
60f63876c4
change(.gitignore): ignore archives
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-17 01:36:18 -08:00
Kevin Morris
a025118344
change(docker): get python-poetry from arch instead of poetry
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-17 01:36:18 -08:00
Kevin Morris
fa26c8078b
fix(docker): modify db configuration for new tests
A user that can create databases is now required for tests,
we use the 'root' user in Docker.

Added docker services:
---------------------
- mariadb_test - host localhost:13307

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-17 01:36:16 -08:00
Kevin Morris
fa43f6bc3e
change(aurweb): add parallel tests and improve aurweb.db
This change utilizes pytest-xdist to perform a multiproc test
run and reworks aurweb.db's code. We no longer use a global
engine, session or Session, but we now use a memo of engines
and sessions as they are requested, based on the PYTEST_CURRENT_TEST
environment variable, which is available during testing.

Additionally, this change strips several SQLite components
out of the Python code-base.

SQLite is still compatible with PHP and sharness tests, but
not with our FastAPI implementation.

More changes:
------------
- Remove use of aurweb.db.session global in other code.
- Use new aurweb.db.name() dynamic db name function in env.py.
- Added 'addopts' to pytest.ini which utilizes multiprocessing.
    - Highly recommended to leave this be or modify `-n auto` to
      `-n {cpu_threads}` where cpu_threads is at least 2.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-17 01:34:59 -08:00
Kevin Morris
07aac768d6
change(fastapi): remove sqlite support
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-17 01:34:59 -08:00
Kevin Morris
0abdf8d468
fix(fastapi): close connection used for initdb
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-17 01:34:58 -08:00
Kevin Morris
40b21203ed
feat(poetry): add filelock
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-17 01:34:58 -08:00
Kevin Morris
cea9104efb
feat(poetry): add pytest-xdist
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-17 01:34:58 -08:00
Kevin Morris
b0b05df193
fix(fastapi): pin markdown to 3.3.4
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-16 21:11:35 -08:00
Kevin Morris
e3fff9e357
Merge branch 'feat-csrf-login-check' into pu 2021-11-15 12:00:07 -08:00
Kevin Morris
91b570ff0d
Merge branch 'db-rework' into pu 2021-11-15 00:02:56 -08:00
Kevin Morris
7f6d9966e5
fix: initialize engine and session in util/adduser.py
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-15 00:01:13 -08:00
Kevin Morris
9424341b55
fix(docker): fix cgit css config
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-14 23:41:42 -08:00
Kevin Morris
12400147fc
fix: initialize engine and session in util/adduser.py
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-14 16:15:56 -08:00
Kevin Morris
4103ab49c9
housekeep(fastapi): rework aurweb.db session API
Changes:
-------
- Add aurweb.db.get_session()
    - Returns aurweb.db's global `session` instance
    - Provides us a way to change the implementation of the session
      instance without interrupting user code.
- Use aurweb.db.get_session() in session API methods
- Add docstrings to session API methods
- Refactor aurweb.db.delete
    - Normalize aurweb.db.delete to an alias of session.delete
- Refresh instances in places we depend on their non-PK columns
  being up to date.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-14 16:15:50 -08:00
Kevin Morris
f8ba2c5342
cleanup(fastapi): simplify aurweb.routers.accounts.accounts_post
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-14 15:32:23 -08:00
Kevin Morris
cee7512e4d cleanup(fastapi): simplify PackageDependency.is_package()
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-12 20:50:08 -08:00
Kevin Morris
bd59adc886
fix(fastapi): use NumVotes for votes field in package details
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-12 17:39:26 -08:00
Kevin Morris
686c032290
feat(fastapi): add id="licenses" to package details licenses
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-11 19:55:04 -08:00
Kevin Morris
7aa959150e
feat(fastapi): add id="conflicts" to package details conflicts
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-11 19:54:30 -08:00
Kevin Morris
e8e9edbb21
change(fastapi): simplify package details database queries
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-11 19:30:21 -08:00
Kevin Morris
a33e9bd571
feat(fastapi): add Replaces field to package details
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-11 19:15:29 -08:00
Kevin Morris
50a9690c2d
feat(fastapi): add Provides field in package details
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-11 19:15:03 -08:00
Kevin Morris
2016b80ea9
fix(fastapi): hide conflicts when there are none
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-11 18:14:50 -08:00
Kevin Morris
2dc6cfec23
fix(fastapi): reorganize licenses display
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-11 18:14:15 -08:00
Kevin Morris
20f5519b99
fix(fastapi): hide keywords when there are none or they can't be edited
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-11 18:13:21 -08:00
Kevin Morris
363afff332
feat(fastapi): add /pkgbase/{name}/keywords (post)
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-11 17:36:08 -08:00
Kevin Morris
5f5fa44d0d
fix(fastapi): fix licenses check
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-11 17:12:13 -08:00
Kevin Morris
cef217388a
add labels to gitlab issue templates
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-11 16:50:16 -08:00
Kevin Morris
0da11f068b
fix(fastapi): check for prometheus info.response
When this is unchecked, exceptions cause the resulting stack
trace to be oblivious to the original exception thrown.

This commit changes that behavior so that metrics are created
only when info.response exists.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-11 16:24:10 -08:00
Kevin Morris
567090547d
add labels to gitlab issue templates
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-11 13:12:23 -08:00
Kevin Morris
66978e40a4
fix(mkpkglists): fix isort order (master)
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-10 13:57:33 -08:00
Kevin Morris
8788f99005
fix(mkpkglists): restore isort order
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-10 13:54:18 -08:00
Kevin Morris
6e344ce9da
fix(mkpkglists): default keys to result[1]
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-10 13:41:53 -08:00
Kevin Morris
52110b7db5
fix(mkpkglists): default keys to result[1]
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-10 13:41:34 -08:00
Kevin Morris
daef98080e
fix(fastapi): fix broken official package query
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-10 13:05:19 -08:00
Kevin Morris
4b2be7fff8
feat(docker): add poetry caching
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-10 13:00:01 -08:00
Kevin Morris
0c57c53da1
fix(sharness): fix AUR_CONFIG generation for mkpkglists test
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-10 07:39:23 -08:00
Kevin Morris
4f7aeafa8d
feat(docker): host gzip archive downloads
- added config option [mkpkglists] archivedir
    - created by mkpkglists

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-10 07:39:23 -08:00
Kevin Morris
abbecf5194
change(mkpkglists): remove header comments
These comments change every time mkpkglists is run; which
would invalidate the ETag headers disbursed by the gzip
host. This commit removes those changing headers.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-10 07:39:17 -08:00
Kevin Morris
107367f958
feat(docker): use mkpkglists --extended flag
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-09 02:29:39 -08:00
Kevin Morris
068b067e14
feat(docker): log cron executions
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-09 02:28:52 -08:00
Kevin Morris
0403b89f53
feat: add packagesmeta[ext]file option to conf/config.dev
Better defaults for Docker here.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-09 02:08:03 -08:00
Kevin Morris
0155f4ea84
fix(mkpkglists): remove caching
We really need caching for this; however, our current caching
method will cause the script to bypass changes to columns
if they have nothing to do with IDs.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-09 02:07:13 -08:00
Kevin Morris
d62af4ceb5
feat(mkpkglists): added metadata archives
Two new archives are available:

- packages-meta-v1.json.gz
    - RPC search formatted data for all packages
    - ~2.1MB at the time of writing.
- packages-meta-ext-v1.json.gz (via --extended)
    - RPC multiinfo formatted data for all packages.
    - ~9.8MB at the time of writing.

New dependencies are required for this update:

- `python-orjson`

All archives served out by aur.archlinux.org distribute the Last-Modified
header and support the If-Modified-Since header, which should be
populated with Last-Modified's value. These should be used by clients
to avoid redownloading the archive when unnecessary.

Additionally, the new meta archives contain a format suitable for
streaming the data as the file is retrieved. It is still in JSON
format, however, users can parse package objects line by line after
the first '[' found in the file, until the last ']'; both contained
on their own lines.

Note: This commit is a documentation change and commit body.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-09 02:07:04 -08:00
Kevin Morris
f3f662c696
fix(mkpkglists): improve package meta archive
The SQL logic in this file for package metadata now exactly
reflects RPC's search logic, without searching for specific
packages.

Two command line arguments are available:

    --extended | Include License, Keywords, Groups, relations
                 and dependencies.

When --extended is passed, the script will create a
packages-meta-ext-v1.json.gz, configured via packagesmetaextfile.

Archive JSON is in the following format: line-separated package objects
enclosed in a list:

    [
    {...},
    {...},
    {...}
    ]

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-09 02:06:50 -08:00
Kristian Klausen
f606140050
feat(PHP): Add packages dump file with more metadata 2021-11-09 02:04:58 -08:00
Kevin Morris
10fcf93991
fix(fastapi): use correct official pkg base url
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-09 01:51:23 -08:00
Kevin Morris
4b8963b7ba
feat(docker): add cron service (aurblup + mkpkglists)
Normally, these scripts are used to update official providers
in the aurweb database along with archives that can be retrieved.

Run both of these scripts in a 5 minute cron job, to both reflect
the live instance database and production load.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-09 00:29:19 -08:00
Kevin Morris
338a44839f
fix: override aurblup's db-path option in config.dev
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-09 00:18:54 -08:00
Kevin Morris
b8d7619dbc
change: add mkpkglists options to config.dev
Here, we default to using root as the storage directory. Primarily
because it makes sense in Docker; config.dev can always be fixed up
by developers to reflect local system changes.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-09 00:17:52 -08:00
Kevin Morris
464540c9a9
fix: use https for aurblup's default mirror instead of ftp
It seems the ftp mirror from kernel.org cannot be used anymore,
but the https mirror can. So, the default config has been updated
to reflect this; otherwise, aurblup bugs out.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-09 00:14:24 -08:00
Kevin Morris
e9cc133005
Merge branch 'starlette-0.17.0' into pu 2021-11-08 19:12:11 -08:00
Kevin Morris
85ebc72e8a
fix(fastapi): only elevated users are allowed to suspend accounts
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-08 18:51:39 -08:00
Kevin Morris
3517862ecd
change(poetry): use kevr@upgrade-starlette-0.17.0 as fastapi source
Starlette 0.16.0 has a pretty bad bug in terms of logging which
has been fixed in the 0.17.0 release. That being said, FastAPI has
not yet merged a request at https://github.com/tiangolo/fastapi/pull/4145
which resolves this dependency resolution so we can use the updated
starlette package.

kevr has forked the pull request in question and we are using it
for now in our poetry dependencies to get ahead of the game.

When FastAPI upstream is updated to support 0.17.0, we'll need
to switch this back to using upstream's source.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-08 18:46:21 -08:00
Kevin Morris
446a082352
change(fastapi): refactor database ORM model definitions
We don't want to depend on the database to load up data
about the models we define. We now leverage the existing
`aurweb.schema` module for table definitions and set
__table_args__["autoload"] to False.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-07 17:31:34 -08:00
Kevin Morris
9f1f399957
fix(mkpkglists): remove caching
We really need caching for this; however, our current caching
method will cause the script to bypass changes to columns
if they have nothing to do with IDs.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-06 17:13:16 -07:00
Kevin Morris
cdca8bd295
feat(mkpkglists): added metadata archives
Two new archives are available:

- packages-meta-v1.json.gz
    - RPC search formatted data for all packages
    - ~2.1MB at the time of writing.
- packages-meta-ext-v1.json.gz (via --extended)
    - RPC multiinfo formatted data for all packages.
    - ~9.8MB at the time of writing.

New dependencies are required for this update:

- `python-orjson`

All archives served out by aur.archlinux.org distribute the Last-Modified
header and support the If-Modified-Since header, which should be
populated with Last-Modified's value. These should be used by clients
to avoid redownloading the archive when unnecessary.

Additionally, the new meta archives contain a format suitable for
streaming the data as the file is retrieved. It is still in JSON
format, however, users can parse package objects line by line after
the first '[' found in the file, until the last ']'; both contained
on their own lines.

Note: This commit is a documentation change and commit body.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-06 16:42:12 -07:00
Kevin Morris
51fb24ab73
fix(mkpkglists): improve package meta archive
The SQL logic in this file for package metadata now exactly
reflects RPC's search logic, without searching for specific
packages.

Two command line arguments are available:

    --extended | Include License, Keywords, Groups, relations
                 and dependencies.

When --extended is passed, the script will create a
packages-meta-ext-v1.json.gz, configured via packagesmetaextfile.

Archive JSON is in the following format: line-separated package objects
enclosed in a list:

    [
    {...},
    {...},
    {...}
    ]

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-06 16:19:34 -07:00
Kristian Klausen
69773a5b58
feat(PHP): Add packages dump file with more metadata 2021-11-06 16:15:55 -07:00
Steven Guikal
020409ef46 fix(FastAPI): prevent CSRF forging login requests
Signed-off-by: Steven Guikal <void@fluix.one>
2021-11-04 14:34:14 -04:00
Kevin Morris
e4a5b7fae9
fix(docker): use 3s intervals for all healthchecks
This'll speed up the docker development and deployment
processes significantly.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-03 05:40:30 -07:00
Kevin Morris
16e6fa2cdd
fix(fastapi): fix prometheus parsing of HTTPStatus
This wasn't actually casting to int. We shouldn't be providing
HTTPStatus.CONSTANTS directly anyway, but, in case we do, we now
just convert the status to an int before converting it to a string.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-01 14:23:15 -07:00
Kevin Morris
9aa8decf40
fix(fastapi): use metrics in cases where PROMETHEUS_MULTIPROC_DIR is defined
Previously, we restricted this to gunicorn to get it working on aur-dev.
This change makes it usable through any backend, and also no-op if
PROMETHEUS_MULTIPROC_DIR is not defined.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-01 14:18:19 -07:00
Kevin Morris
cdb854259a
fix(docker): share FASTAPI_BACKEND with the server
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-01 13:54:58 -07:00
Kevin Morris
dc397f6bd8
fix(fastapi): utilize PROMETHEUS_MULTIPROC_DIR in our own /metrics
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-01 13:23:48 -07:00
Kevin Morris
1be4ac2fde
feat(docker): use PROMETHEUS_MULTIPROC_DIR
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-01 12:27:33 -07:00
Kevin Morris
f21765bfe4
feat(fastapi): add prometheus /metrics
This commit provides custom metrics, so we can group requests
into their route paths and not by the arguments given, e.g.
/pkgbase/some-package -> /pkgbase/{name}. We also count RPC
requests as `http_api_requests_total`, split by the RPC
query "type" argument.

- `http_api_requests_total`
    - Labels: ["type", "status"]
- `http_requests_total`
    - Number of HTTP requests in total.
    - Labels: ["method", "path", "status"]

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-01 11:41:39 -07:00
Kevin Morris
cc45290ec2
feat(poetry): add prometheus-fastapi-instrumentator
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-11-01 11:41:38 -07:00
Kevin Morris
4d214b9cd9
Merge branch 'fix-rpc-search-generation' into pu 2021-10-31 23:19:02 -07:00
Kevin Morris
a82879210c
fix(poetry): add mysql-connector dep
This is not used anymore in our FastAPI code, however, for
back-compatibility with pre-FastAPI scripts, we need it.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-31 19:56:56 -07:00
Kevin Morris
451eec0c28
fix(fastapi): remove info-specific fields from search results
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-31 16:22:24 -07:00
Kevin Morris
f26cd1e994
fix(gitlab-ci): add docker dep to deploy target
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-31 16:13:01 -07:00
Kevin Morris
cef69b6342
fix(gitlab-ci): prune dangling images and build cache
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-31 15:48:11 -07:00
Kevin Morris
b7475a5bd0
fix(rpc): fix performance of suggest[-pkgbase]
We were selecting the entire record; we should just select
the Name column as done in this commit.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-31 04:11:42 -07:00
Kevin Morris
61f3cb938c
feat(rpc): support the If-None-Match request header
If the If-None-Match header is supplied with a previously
obtained ETag from the same query, a 304 Not Modified is
returned with no content.

This allows clients to completely leverage the ETag header.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-31 01:22:54 -07:00
Kevin Morris
2cc44e8f28
fix(rpc): perform regex match against callback name
Since we're in the hot path, a constant re.compiled
JSONP_EXPR is defined for checks against the callback.

Additionally, reorganized `content_type` and `content`
to avoid performing a DB query when we encounter a
regex mismatch.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-31 01:17:16 -07:00
Kevin Morris
12b4269ba8
feat(rpc): support jsonp callbacks
This change introduces alternate rendering of text/javascript
JSONP-compatible callback content. The `examples/jsonp.html`
HTML document can be used to test this functionality against
a running aurweb server.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-31 00:29:19 -07:00
Kevin Morris
05e6cfca62
feat(rpc): add msearch type handler
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-30 22:56:18 -07:00
Kevin Morris
9fef8b0611
fix(rpc): fix search arg check
When by == 'maintainer', we allow an unspecified keyword,
resulting in a search of orphan packages. Fix our search
check so that when no arg is given, it is set to an empty
str().

We already check for valid args when type is not maintainer,
so there's no need to worry about other args falling through.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-30 22:53:30 -07:00
Kevin Morris
af2f3694e7
feat(rpc): add search type handler
This commit introduces a PackageSearch-derivative class: `RPCSearch`.
This derivative modifies callback behavior of PackageSearch to
suit RPC searches, including [make|check|opt]depends `by` types.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-30 19:25:54 -07:00
Kevin Morris
ece25e0499
Merge branch 'pu-rpc-suggest' into pu 2021-10-30 16:57:03 -07:00
Kevin Morris
c28f1695ed
fix(fastapi): support by maintainer search with no keywords
In this case, package search should return orphaned packages.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-30 16:24:53 -07:00
Kevin Morris
9d6dbaf0ec
feat(rpc): add suggest type handler
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-30 00:36:21 -07:00
Kevin Morris
a38e126f49
Merge branch 'fix-commit-hash-check' into pu 2021-10-30 00:06:28 -07:00
Kevin Morris
6d376fed15
feat(rpc): add ETag header with md5 hash content
The ETag header can be used for client-side caching.

https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-29 23:57:33 -07:00
Kevin Morris
0af6a2c32f
fix(docker): fix COMMIT_HASH variable check
The previous method was super bad. Even if a variable was declared,
if it was empty, we would run into a false-positive. Additionally,
the previous method did not allow us to not specify the COMMIT_HASH
variable; which is problematic for development environments.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-29 23:47:47 -07:00
Kevin Morris
b3b31394e8
fix(rpc): simplify json generation complexity
This simply decouples depends and relations population into
their own helper functions.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-29 22:59:40 -07:00
Kevin Morris
9464de108f
feat(fastapi): add /pkgbase/{name}/comments/{id}/edit (get)
This is needed so that users can edit comments when they don't have
Javascript being used in their browser.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-29 21:37:52 -07:00
Kevin Morris
01e27fa347
fix(fastapi): sanitize /requests params
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-29 20:40:00 -07:00
Kevin Morris
7f4c011dc3
fix(fastapi): sanitize PP/O parameters for package search
This definitely leaked through in more areas. We'll need to reuse
this new utility function in a few other routes in upcoming commits.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-29 20:39:55 -07:00
Kevin Morris
8dcdc7ff38
change(fastapi): decouple account comment templates
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-29 18:28:14 -07:00
Kevin Morris
46c39399ff
fix(fastapi): fix /account/{username} links
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-29 17:18:54 -07:00
Kevin Morris
348128fada
fix(fastapi): fix /account/{username} page title
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-29 17:18:53 -07:00
Kevin Morris
691b7b9091
feat(fastapi): add comment actions to /account/{username}/comments
With this change, we've decoupled some partials shared between
`/pkgbase/{name}` and `/account/{username}/comments`. The comment
actions template now resolves its package base via the `comment`
instance instead of requiring `pkgbase`.

We've also modified the existing package comment routes to
support execution from any location using the `next` parameter.
This allows us to reuse code from package comments for
account comments actions.

Moved the majority of comment editing javascript to its own
.js file.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-29 17:18:49 -07:00
Kevin Morris
adb6252f85
feat(fastapi): add /account/{username}/comments
This commit contains a base template of account comments
in sorted order (based on ColumnTS.desc).

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-28 13:21:16 -07:00
Kevin Morris
9fd07c36eb
fix(fastapi): fix account page title
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-28 12:12:13 -07:00
Kevin Morris
a3a5ec678c
fix(gitlab-ci): enable options.disable_http_login on aur-dev
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-28 08:19:20 -07:00
Kevin Morris
7ee32a4ea1
fix(gitlab-ci): set GIT_DATA_DIR=git_data on aur-dev
This uses the internally defined docker volume `git_data`,
but the variable is configurable for changes in the future.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-28 08:19:20 -07:00
Kevin Morris
8239dcdd1b
feat(docker): configure fastapi's commit_hash based on $COMMIT_HASH
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-28 08:19:20 -07:00
Kevin Morris
b49b629395
feat(gitlab-ci): set FASTAPI_WORKERS=5 on aur-dev
In addition, specify FASTAPI_BACKEND=gunicorn for deployment.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-28 08:19:19 -07:00
Kevin Morris
1c0543c07e
feat(docker): fixup and utilize AURWEB_(SSHD|FASTAPI|PHP)_PREFIX
Previously CGIT_CLONE_PREFIX_(PHP|FASTAPI), we found that we could
use the same env var in multiple places, including non-cgit-clone-prefix
areas.

So, they were renamed, and one additional prefix was added.

- CGIT_CLONE_PREFIX_PHP -> AURWEB_PHP_PREFIX
    - Used for cgit's clone prefix and AUR_CONFIG's aur_location for PHP
- CGIT_CLONE_PREFIX_FASTAPI -> AURWEB_FASTAPI_PREFIX
    - Used for cgit's clone prefix and AUR_CONFIG's aur_location for FastAPI
- AURWEB_SSHD_PREFIX
    - Used for aurweb's sshd clone prefix shown on package pages

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-28 08:19:19 -07:00
Kevin Morris
1656f5824d
fix(docker): restore mariadb service
Additionally, for now, no-op usage of the MARIADB_SOCKET_DIR
environment variable. This is particularly useful for a serious
production setup, but we don't currently use that.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-28 08:19:18 -07:00
Kristian Klausen
651c1cd8c6
feat(gitlab-ci): Add logic for deploying aur-dev with docker-compose
The infrastructure changes are here[1].

[1] https://gitlab.archlinux.org/archlinux/infrastructure/-/merge_requests/492
2021-10-28 08:19:16 -07:00
Kevin Morris
1f2347c6b4
fix(fastapi): fix User.login signature typing
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-28 07:35:20 -07:00
Kevin Morris
034288711b
fix(fastapi): rework cookies - do not re-emit generically
This change removes cookie re-emission of AURLANG and AURTZ,
adds the AURREMEMBER cookie (the state of the "Remember Me"
checkbox on login), and re-emits AURSID based on the AURREMEMBER
cookie.

Previously, re-emission of AURSID was forcefully modifying
the expiration of the AURSID cookie. The introduction of
AURREMEMBER allows us to deduct the correct cookie expiration
timing based on configuration variables. With this addition,
we now re-emit the AURSID cookie with an updated expiration
based on the "Remember Me" checkbox on login.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-28 07:35:14 -07:00
Kevin Morris
64ba18e417
add Account Request issue template
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-27 00:54:16 -07:00
Kevin Morris
7418c33a30
add Account Request issue template
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-27 00:53:46 -07:00
Kevin Morris
d7ac95a707
fix(fastapi): limit cookie migration to whitelisted keys
Whitelisted keys: AURSID, AURTZ, AURLANG

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-26 19:09:39 -07:00
Kevin Morris
65be8b8e07
fix(fastapi): support "Account Type:" input for account edit
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-25 22:05:23 -07:00
Kevin Morris
7e7a1ead88
fix(fastapi): unify homepage cache expiry time to five minutes
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-24 19:08:03 -07:00
Kevin Morris
0d734eb07d
feat(fastapi): add configurable commit hash display
Two new options have been added:

- [devel] commit_url
    - URL including an %s format specifier that can be used to link
      to a webpage for the commit.
- [devel] commit_hash
    - HEAD's commit hash (produced via `git rev-parse HEAD`)

If a `[devel] commit_hash` is configured, a link to the commit based on
`[devel] commit_url` will be displayed in the aurweb footer in
the form: `HEAD@<commit_hash>`. If no `[devel] commit_url` is
configured, a non-linked hash will be displayed.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-24 18:12:19 -07:00
Kevin Morris
da55aa6491
fix(fastapi): use more credentials in archdev-navbar.html
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-23 20:57:43 -07:00
Kevin Morris
94d494866f
fix(fastapi): increase recursion limit during tests
The default recursion limit used by Docker's archlinux:base-devel
Python package becomes problematic in some cases when running tests
against our FastAPI application using starlette.testclient.TestClient
(aliased to fastapi.testclient.TestClient). starlette ends up with
test failures because it exceeds the recursion limit, but this only
happens when using the `TestClient`. When the ASGI servers are run,
this is not an issue and so in that case, the recursion limit has
not been touched.

This change uses a `TEST_RECURSION_LIMIT` environment variable to
modify the recursion limit of the FastAPI application. This variable
is, by default, only supplied when running pytests in Docker, but
can be force-supplied by the user.

TEST_RECURSION_LIMIT=10000 has been added to `.env` and `.gitlab-ci.yml`.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-23 20:15:53 -07:00
Kevin Morris
5fb75b9614
feat(fastapi): add /pkgbase/{name}/merge (post)
Changes:

- `via` is not required in FastAPI. We deduce the involved
  requests via their PackageBaseName / MergeBaseName columns
  and set them to Accepted when merged.
- When erroneous input is given, the error is now presented
  on the merge page instead of sending the user to the pkgbase
  page.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-23 19:03:32 -07:00
Kevin Morris
bc9bb045ed
fix(fastapi): PackageRequest's PackageBase relationship should not required
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-23 19:00:03 -07:00
Kevin Morris
6b065956f7
Merge branch 'pu_packages_action_delete' into pu 2021-10-23 18:48:46 -07:00
Kevin Morris
c6c04f4952
fix(docker): add missing version for docker-compose.override.yml
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-23 18:47:59 -07:00
Kevin Morris
60bffa4fb6
feat(FastAPI): add /packages (post) action: 'delete'
Improvements:

- Package deletion now creates a PackageRequest on behalf of
  the deleter if one does not yet exist.
- All package deletions are now logged to keep track of who did what.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-23 18:23:15 -07:00
Kevin Morris
81417ea8b2
change(docker): merge production git repo bind mount
This merge requires production users to specify an host
directory to bind as the git repository within Docker containers.

This means that a repository can be shared between host
and container, so that the repository does not need to be
managed within Docker alone.

New environment variables:

- GIT_DATA_DIR: Path to aur.git repository that is bind mounted

Do note, this variable only takes affect when users run
production Docker services, by supplying:

    $ docker-compose -f docker-compose.yml -f docker-compose.prod.yml ...
2021-10-22 21:34:58 -07:00
Kevin Morris
13b344d238
feat(FastAPI): add /packages (post) action: 'disown'
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-22 19:38:14 -07:00
Kevin Morris
f1ad1b9aed
feat(FastAPI): add /packages (post) action: 'adopt'
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-22 19:01:13 -07:00
Kevin Morris
9b5eeb7652
fix(pytest): ignore asyncio.base_events deprecation warnings
This deprecation warning is not fixed in a tagged release yet.
Ignoring it for now; it has nothing to do with user code.

Upstream bug at https://bugs.python.org/issue45097

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-22 18:52:10 -07:00
Kevin Morris
4ae3fbd5d1
change(docker): depend on provided poetry.lock for dep resolution
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-22 17:43:00 -07:00
Kevin Morris
d4210c53cf
fix: update poetry dependencies
There were some test failures caused by problematic
dependency versioning, most likely to to the seriously
braindead pyproject.toml config for deps that previously
existed.

This commit defines python version >=3.9<3.10 for our working
Python version and provides updated deps (to latest).

I believe the bug was originally caused by the fact that
we had no python dependency defined, allowing poetry to
resolve dependencies incorrectly for what we intended.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-22 17:39:59 -07:00
Kevin Morris
d5520c9ed2
feat(FastAPI): add /packages (post) action: 'unnotify'
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-21 17:38:44 -07:00
Kevin Morris
b277d94e0b
feat(FastAPI): add /packages (post) action: 'notify'
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-21 16:52:04 -07:00
Steven Guikal
e9fc27a33b feat(docker): make git data directory host-configurable
Signed-off-by: Steven Guikal <void@fluix.one>
2021-10-21 14:37:06 -04:00
Kevin Morris
fb85cb60a0
feat(FastAPI): add /packages (post) action: 'unflag'
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-21 11:29:55 -07:00
Kevin Morris
0b1c3ea539
feat(docker): expose cgit-{php,fastapi} on {13000,13001}
This change exposes the uwsgi daemon we use for cgit on:

- PHP: docker-host:13000
- FastAPI: docker-host:13001

These ports can then be used to take advantage of cgit on
a production server that hosts nginx in front of Docker.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-21 11:11:01 -07:00
Kevin Morris
db730ad8cb
fix(docker): fix cgit clone-prefix
Additionally, clone-prefix is now configurable via environment variables:

- CGIT_CLONE_PREFIX_PHP
- CGIT_CLONE_PREFIX_FASTAPI

These vars can be used by production to customize the clone prefix.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-21 11:11:01 -07:00
Kevin Morris
c4163547f6
fix(docker): swap package cgit -> cgit-aurweb
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-21 11:10:55 -07:00
Kevin Morris
65240c8343
feat(rpc): enforce ratelimiting
New configuration options:

- `[ratelimit] cache`
    - A boolean indicating whether we should use configured cache (1)
      or database (0) for ratelimiting.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-21 11:07:36 -07:00
Kevin Morris
6662975005
change(rpc): handle 'version' and 'type' arguments in constructor
Additionally, added RPC.error, which produces an RPC-compatible
error based on the version passed during construction.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-21 11:01:24 -07:00
Kevin Morris
a06f4ec19c
fix(fastapi): centralize logging initialization
With this change, we provide a wrapper to `logging.getLogger`
in the `aurweb.logging` module. Modules wishing to log using
logging.conf should get their module-local loggers by calling
`aurweb.logging.getLogger(__name__)`, similar to `logging.getLogger`,
this way initialization with logging.conf is guaranteed.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-21 10:40:52 -07:00
Kevin Morris
5ae9d09e98
fix: remove unused "Merge into" input from /packages
When using this input on `live` as a TU, the field is not
taken into account. Tried with no action and with the
Delete Packages action, which ended up deleting the packages
but not merging into the given target.

So, this commit removes that input from the page.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-20 23:27:07 -07:00
Kevin Morris
f5e38e9979
Merge branch 'pu_pkgbase_auth_redirects' into pu 2021-10-20 22:30:26 -07:00
Kevin Morris
7c4fb539d8
change(fastapi): rework /rpc (get)
This reworks the base implementation of the RPC to use a
class called RPC for handling of requests. Took a bit of
a different approach than PHP in terms of exposed methods,
but it does end up achieving the same goal, with one additional
error: "Request type '{type}' is not yet implemented."

For FastAPI development, we'll stick with:

- If the supplied 'type' argument has an alias mapping in
  RPC.ALIASES, we convert the type argument over to its alias
  before doing anything. Example: 'info' is aliased to 'multiinfo',
  so when a user requests type=info, it is converted to type=multiinfo.
- If the type does not exist in RPC.EXPOSED_TYPES, the following
  error is produced: "No request type/data specified."
- If the type **does** exist in RPC.EXPOSED_TYPES, but does not
  have an implemented `RPC._handle_{type}_type` function, the
  following error is produced: "Request type '{type}' is not yet
  implemented."

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-20 22:17:05 -07:00
Kevin Morris
30ab45f459
fix(fastapi): add backref cascade to Package{Keyword,License}
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-20 20:59:42 -07:00
Kevin Morris
2b9840149e
feat(fastapi): add /pkgbase/{name}/merge (get)
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-20 20:31:44 -07:00
Kevin Morris
990f4d182b
feat(rpc): add 'suggest-pkgbase' type
This feature of RPC is required to take advantage of
javascript typeahead.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-20 20:31:42 -07:00
Kevin Morris
c8f3ea2eba
fix(fastapi): fix various pkgbase-wise auth redirects
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-20 20:20:59 -07:00
Kevin Morris
fb0f252b39
Merge branch 'fix-tuple-return-style' into pu 2021-10-20 19:41:17 -07:00
Kevin Morris
ddc51dd5eb
Merge branch 'fix-trailing-slashes' into pu 2021-10-20 17:46:12 -07:00
Steven Guikal
fd58e4df04 fix(FastAPI): unify tuple return style
Closes #134

Signed-off-by: Steven Guikal <void@fluix.one>
2021-10-19 19:04:08 -04:00
Kevin Morris
4cb0994fee
fix(fastapi): correct unauthorized request creation redirect
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-19 15:16:29 -07:00
Kevin Morris
beed64e001
fix(fastapi): persist package request form inputs
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-19 15:16:26 -07:00
Kevin Morris
b4092fe77d
fix(fastapi): pass request type's name to Request*Notification
Previously, we passed the straight up request type instance from
SQLAlchemy and had a .title() function that was transparently
treating the instance the same as the instance's Name in terms
of notify.py's use of it.

This commit removes that transparent behavior; it was not actually
intended.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-19 15:06:16 -07:00
Kevin Morris
3b28be1741
Merge branch 'pu_pkgbase_flag_comment' into pu 2021-10-19 14:56:05 -07:00
Kevin Morris
f4cfc7c5ca
Merge branch 'feat-host-maridb-cfg' into pu 2021-10-19 14:33:05 -07:00
Kevin Morris
4f505ca6c1
feat(docker): support for host-mounted mariadb socket
A new configurable env var has been introduced to production Docker:
MARIADB_SOCKET_DIR, which should contain a path to a directory
containing `mysqld.sock` on the Docker host.

Note: The database name, user and password can be configured by
modifying `conf/config.dev` before building the Docker image.

This feature only works in production mode, when specifying:

    $ export MARIADB_SOCKET_DIR=/var/run/mysqld
    $ docker-compose -f docker-compose.yml -f docker-compose.prod.yml ...

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-19 14:31:51 -07:00
Steven Guikal
1cb1ce0d99 feat(docker): allow production docker setup to use dedicated mariadb
Signed-off-by: Steven Guikal <void@fluix.one>
2021-10-19 17:21:03 -04:00
Kevin Morris
37f0c352f6
feat(FastAPI): add /pkgbase/{name}/flag-comment (get)
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-19 13:39:00 -07:00
Kevin Morris
210d92e382
Merge branch 'pu_packages_action_post' into pu 2021-10-19 13:38:02 -07:00
Steven Guikal
4e3cc1dfe2 feat(docker): only use mariadb socket for connections 2021-10-19 15:35:34 -04:00
Bert Peters via aur-dev
36c1ee35a7
Send request notifications to co-maintainers
This is in addition to the current recipients. Co-maintainers should
also be made aware when their package has pending requests.

NOTE: This commit was slightly modified to resolve cherry-pick
conflicts in `pu`.
2021-10-19 12:18:40 -07:00
Bert Peters via aur-dev
be64ca7b0e
Send request notifications to co-maintainers
This is in addition to the current recipients. Co-maintainers should
also be made aware when their package has pending requests.
2021-10-19 11:55:59 -07:00
Kevin Morris
37232f71ee
feat: add git-cliff configuration
git-cliff is a tool which allows us to generate changelog
based off of conventional commits in the repository.

This commit provides an initial cliff.toml configuration
file which formats changelog output with tables and branch
state metadata.

Upstream: https://github.com/orhun/git-cliff

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-19 09:28:14 -07:00
Kevin Morris
d22580fa74
fix(docker): add aurweb-image service
The new `aurweb-image` service does not perform any purpose
other than providing a build definition for 'aurweb:latest'.
With this, `docker-compose build` now just runs once for the
`aurweb-image` service, which builds the image used by all
other services.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-19 07:40:59 -07:00
Kevin Morris
267f2cb2c4
fix(fastapi): remove trailing slashes from fastapi-driven links
With our FastAPI server, trailing slashes causes a 307 redirect
which ends up redirecting users to routes which do not contain
trailing slashes. This removes trailing slashes from our templates
where FastAPI is concerned to avoid unnecessary redirects.

There may still be links or usages around which have unnecessary
usages of a trailing slash; please keep a look out for these and
remove them where possible.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-18 22:42:40 -07:00
Kevin Morris
e05cfc3375
Merge branch 'fix-missing-tags' into pu 2021-10-18 22:17:29 -07:00
Kevin Morris
c588a4e82e
feat(FastAPI): add /packages (post)
The POST /packages route takes an `action`, `merge_into` and `confirm`
form data arguments. It then routes over to `action`'s callback provided
by `PACKAGE_ACTIONS`. This commit does not implement actions, but
mocks out the flow we would expect from the POST route.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-18 17:43:27 -07:00
Steven Guikal
db67e83bb8 fix(FastAPI): use elif statements where appropriate
Signed-off-by: Steven Guikal <void@fluix.one>
2021-10-18 15:07:25 -04:00
Steven Guikal
7f72d78dcc fix(FastAPI): correct HTML tags and indentation
Signed-off-by: Steven Guikal <void@fluix.one>
2021-10-18 14:51:01 -04:00
Kevin Morris
927f5e8567
feat(docker): add gunicorn support & production default
Supply FASTAPI_BACKEND=gunicorn and FASTAPI_WORKERS=<threads_num> to
docker-compose up to use the gunicorn backend.

This is defaulted in production to gunicorn, but FASTAPI_WORKERS
should definitely be configured by any production deployment.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-16 22:53:46 -07:00
Kevin Morris
28c4e9697b
change(fastapi): simplify model imports across code-base
Closes: #133

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-16 19:40:00 -07:00
Kevin Morris
bfdc85d7d6
change(rpc): use simplified models package
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-16 19:35:51 -07:00
Kevin Morris
6d59a97955
feat(fastapi): include all models in aurweb.models package
This gives developers the ability to import models without importing
them directly from their module:

    from aurweb.models import Ban, AccountType

This provides more conciseness:

    from aurweb import models

    def some_func(ban: models.Ban):
        pass

    def some_other_func(user: models.User):
        pass

This more aligns with a Django-style of core model bases.

NOTE: Docker images must be rebuilt with this change, as setup.cfg
has changed. Old Docker images will cause flake8 violation reports.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-16 19:35:20 -07:00
Kevin Morris
c572a97d1c
fix(fastapi): EXPECTATION_FAILED -> BAD_REQUEST
Usage of EXPECTATION_FAILED in these cases is totally wrong.
EXPECTATION_FAILED is a failure in terms of the HTTP protocol,
not user input. Change all usage of EXPECTATION_FAILED to BAD_REQUEST.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-16 17:38:33 -07:00
Kevin Morris
6ddf888b67
cleanup: remove int(...) casts on HTTPStatus usage
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-16 17:33:33 -07:00
Kevin Morris
56eefabc6d
change(fastapi): sanitize cascade backref strings
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-16 16:24:05 -07:00
Kevin Morris
51320ab22a
change(fastapi): unify all model relationship behavior
Now, we allow the direct relationships and their foreign keys to
be set in all of our models. Previously, we constrained this to
direct relationships, and this forced users to perform a query
in most situations to satisfy that requirement. Now, IDs can be
passed directly.

Additionally, this change removes the need for extraneous imports
when users which to use relationships. We now import and use models
directly instead of passing string-references to them.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-16 16:24:00 -07:00
Kevin Morris
0c37216626
change(gitignore): add various exclusions
Three new root directories are ignored by git:

- /personal/
    - Personal tools excluded by git.
- /notes/
    - Personal notes excluded by git.
- /vendor/
    - PHP Composer vendor directory. We don't want to commit this
      to git.

And one specific root file:

- /taskell.md
    - Data file for the `taskell` program, used for task tracking.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-16 15:08:42 -07:00
Kevin Morris
7b7e571e93
change(FastAPI): run test_initdb.py ahead of time in docker
In some cases, when tests fail through Docker, the database
ends up in an invalid state. This causes subsequent runs to
error out with non-sensical DB errors. The `test_initdb.py`
test suite runs tests which setup every modifiable table
in the database, so let's just run it first here to avoid
any invalid test DB state.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-15 20:16:38 -07:00
Kevin Morris
50634d30b3
Merge branch 'pu_package_keywords_fix' into pu 2021-10-15 20:08:31 -07:00
Kevin Morris
27a6563302
fix(FastAPI): use CRED_PKGBASE_SET_KEYWORDS credential properly
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-15 19:11:15 -07:00
Kevin Morris
8040ef5a9c
fix(FastAPI): use pkgbase in package actions
Previously, `result` was being used which was directly set to
`pkgbase` before rendering the actions.html partial. It didn't
make much sense. This commit cleans things up a bit.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-15 19:02:53 -07:00
Kevin Morris
2d46811c45
fix(FastAPI): display VCS note when flagging a VCS package
Closes: #131

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-15 16:16:11 -07:00
Kevin Morris
71b3f781f7
fix(FastAPI): maintainers are allowed to unflag their packages
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-15 15:11:45 -07:00
Kevin Morris
81c9312606
add Bug.md GitLab issue template
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-15 14:08:27 -07:00
Kevin Morris
dd420f8c41
add Feature.md GitLab issue template
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-15 14:08:17 -07:00
Kevin Morris
b631dcb756
Merge branch 'pu_pkgbase_flag_fix' into pu 2021-10-15 13:50:56 -07:00
Kevin Morris
040bb0d7f4
Revert "feat(PHP): add aurweb Prometheus metrics"
This reverts commit 986fa9ee30.
2021-10-15 13:19:07 -07:00
Kevin Morris
5bfc1e9094
Revert "fix(PHP): sanitize and produce metrics at shutdown"
This reverts commit 22b3af61b5.
2021-10-15 13:18:58 -07:00
Kevin Morris
22b3af61b5
fix(PHP): sanitize and produce metrics at shutdown
This change now requires that PHP routes do not return HTTP 404
to be considered for the /metrics population. Additionally,
we make a small sanitization here to avoid trailing '/'
characters, unless we're on the homepage route.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-13 17:20:29 -07:00
Kevin Morris
748faca87d
fix(FastAPI): translate some untranslated strings
Affects: templates/partials/packages/search_actions.html

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-12 18:04:07 -07:00
Kevin Morris
3d971bfc8d
add Bug.md GitLab issue template
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-11 14:48:00 -07:00
Kevin Morris
68383b79e2
add Feature.md GitLab issue template
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-11 14:13:29 -07:00
Kevin Morris
4525a11d92
fix(FastAPI): change a deep copy instead of original
This was updating offsets and causing unintended behavior.
We should be a bit more functional anyway.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-10 01:00:36 -07:00
Kevin Morris
27fbda5e7b
feat(FastAPI): add get_(errors|successes) testing HTML helpers
These functions will allow us to more easily check errors or success
messages when testing routes.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-09 22:04:48 -07:00
Kevin Morris
34c96ed81b
add Feedback.md GitLab issue template
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-09 20:50:41 -07:00
Kevin Morris
d9ab65cb6f
add Feedback.md GitLab issue template
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-09 20:49:11 -07:00
Kevin Morris
5bbc94f2ef
fix(FastAPI): add /pkgbase/{name}/flag (get)
This was missed in the [un]flag (post) commit.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-09 18:41:32 -07:00
Kevin Morris
305d077973
feat(FastAPI): add /pkgbase/{name}/adopt (post)
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-08 15:58:00 -07:00
Kevin Morris
72d6016da4
Merge branch 'pu_popupdate' into pu 2021-10-08 15:42:50 -07:00
Kevin Morris
63498f5edd
fix(FastAPI): use popupdate when [un]voting
The `aurweb.scripts.popupdate` script is used to maintain
the NumVotes and Popularity field. We could do the NumVotes
change more simply; however, since this is already a long-term
implementation, we're going to use it until we move scripts
over to ORM.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-08 15:29:47 -07:00
Kevin Morris
27c5d17fc8
Merge branch 'fix-email-input' into pu 2021-10-07 23:32:27 -07:00
Kevin Morris
4b95ec41ed
Merge branch 'fix-merge-type' into pu 2021-10-07 23:21:28 -07:00
Kevin Morris
01fb42c5d9
fix(scripts.popupdate): use forced-utc timestamp
Additionally, clean up some controversial PEP-8 warnings by
removing the '+' string concatenation.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-07 22:46:07 -07:00
Kevin Morris
d38abd7832
feat(FastAPI): add /pkgbase/{name}/delete (get, post)
In addition, we've had to add cascade arguments to backref so
sqlalchemy treats the relationships as proper cascades.

Furthermore, our pkgbase actions template was not rendering
actions properly based on TU credentials.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-07 20:36:24 -07:00
Kevin Morris
4e7d2295da
fix(FastAPI): add package-related missing backref cascades
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-07 20:25:24 -07:00
Kevin Morris
0ddc969bdc
feat(FastAPI-dev): add package_delete helper
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-07 20:25:23 -07:00
Kevin Morris
ed68fa2b57
feat(FastAPI): add aurweb.db.delete_all(iterable)
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-07 20:25:23 -07:00
Kevin Morris
c8d01cc5e8
feat(FastAPI): add aurweb.util.apply_all(iterable, fn)
A helper which allows us to apply a specific function to each
item in an iterable.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-07 20:25:20 -07:00
Kevin Morris
16d516c221
feat(FastAPI): add /pkgbase/{name}/disown (get, post)
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-07 20:13:40 -07:00
Kevin Morris
0a02df363a
feat(FastAPI): add /pkgbase/{name}/[un]vote (post)
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-07 18:22:04 -07:00
Kevin Morris
0dfff2bcb2
feat(FastAPI): add /pkgbase/{name}/[un]notify (post)
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-07 17:23:14 -07:00
Kevin Morris
8eadb4251d
feat(FastAPI): add /pkgbase/{name}/[un]flag (post)
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-07 16:04:54 -07:00
Kevin Morris
55ebfa0d01
Merge branch 'pu_auth_redirect_vars' into pu 2021-10-07 12:42:53 -07:00
Kevin Morris
2e6f8cb9f4
change(FastAPI): @auth_required login kwarg defaulted to True
We pretty much want @auth_required to send users to login
if we enforce auth requirements but don't otherwise specify
a way to deal with it.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-07 12:38:34 -07:00
Kevin Morris
a756691d08
change(FastAPI): user_developer_or_trusted_user always True
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-07 10:00:46 -07:00
Kevin Morris
dc11a88ed3
change(FastAPI): depend on auth_required redirect for pkgbase actions
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-07 00:40:22 -07:00
Kevin Morris
8bc1fab74d
change(FastAPI): automate request login requirement
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-07 00:27:51 -07:00
Kevin Morris
75c49e4f8a
feat(FastAPI): support {named} fmt in auth_required redirect
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-07 00:27:47 -07:00
Kevin Morris
e5299b5ed4
fix(FastAPI): pkgbase/package tests
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-06 23:17:08 -07:00
Kevin Morris
33b18907eb
feat(FastAPI): add CRED_PKGBASE_MERGE
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-06 22:30:15 -07:00
Kevin Morris
889c5b1e21
fix(FastAPI): pkgbase actions template
Display Delete, Merge and Disown actions based on user credentials.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-06 22:09:58 -07:00
Steven Guikal
a54a09f61d fix(FastAPI): fix padding on email inputs
Signed-off-by: Steven Guikal <void@fluix.one>
2021-10-05 17:50:22 -04:00
Steven Guikal
1bce53bbb7 fix(FastAPI): mark user and passwd as required fields 2021-10-05 14:36:46 -04:00
Steven Guikal
1956be0f46 fix(FastAPI): prefill login fields with entered data 2021-10-05 14:13:48 -04:00
Kevin Morris
82a3349649
Merge branch 'fix-reqname-tr' into pu 2021-10-05 01:48:14 -07:00
Kevin Morris
aac13cd123
Merge branch 'fix-key-case' into pu 2021-10-05 01:38:18 -07:00
Steven Guikal
f392b3607e fix(FastAPI): add missing translation filter for request type
Signed-off-by: Steven Guikal <void@fluix.one>
2021-10-04 17:42:55 -04:00
Steven Guikal
9af76a73a3 fix(FastAPI): include MergeBaseName in merge request type
This was done on the dedicated requests page, but missed on the
dashboard.

Signed-off-by: Steven Guikal <void@fluix.one>
2021-10-04 17:36:10 -04:00
Steven Guikal
5c179dc4d3 fix(FastAPI): use consistent ordering on dashboard and request page
Signed-off-by: Steven Guikal <void@fluix.one>
2021-10-04 17:11:29 -04:00
Steven Guikal
08068e0a5c fix(FastAPI): use configured letter case for SSH fingerprints
Currently, the config parser converts all keys to lowercase which is
inconsistent with the old PHP behavior. This has been fixed and relevant
fingerprint-getting functions have been simplified without changes in
behavior.

Signed-off-by: Steven Guikal <void@fluix.one>
2021-10-04 18:00:50 +00:00
Kevin Morris
7bfc2bf9b4
fix(FastAPI): Improve sqlite testing speed
This commit adds a new Arch dependency: `libeatmydata`, which
provides the `eatmydata` executable that stubs out fsync() operations.
We use `eatmydata` to run our sharness and pytests in Docker now.

With `autocommit=True`, required by SQLAlchemy to keep the
session up to date with external DB modifications, many fsync
calls are used in the SQLite backend; especially because we're wiping
and creating records in every DB-bound test.

**Before:**

- mysql: 1m42s (elapsed during pytest run)
- sqlite: 3m06s (elapsed during pytest run)

**After:**

- mysql: 1m40s (elapsed during pytest run)
- sqlite: 1m50s (elapsed during pytest run)

Shout out to @klausenbusk, who suggested this as a possible fix,
and it was. Thanks, Kristian!

Closes #120

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-03 15:59:52 -07:00
Kevin Morris
b5f8e69b8a
feat(FastAPI): use SQLAlchemy's scoped_session
Closes #113

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-03 10:47:15 -07:00
Kevin Morris
f6141ff177
feat(FastAPI): add /requests/{id}/close (get, post)
Changes from PHP:

- If a user submits a POST request with an invalid reason,
  they are returned back to the closure form with a BAD_REQUEST status.
- Now, users which created a PackageRequest have the ability to close
  their own.
- Form action has been changed to `/requests/{id}/close`.

Closes https://gitlab.archlinux.org/archlinux/aurweb/-/issues/20

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-02 22:47:05 -07:00
Kevin Morris
1c031638c6
feat(FastAPI): add /pkgbase/{name}/request (post)
This change implements the FastAPI version of the
/pkgbase/{name}/request form's action.

Changes from PHP:

- Additional errors are now displayed for the **merge_into** field,
  which are only displayed when the Merge type is selected.
    - If the **merge_into** field is empty, a new error is displayed:
      'The "Merge into" field must not be empty.'
    - If the **merge_into** field is given the name of a package base
      which does not exist, a new error is displayed:
      "The package base you want to merge into does not exist."
    - If the **merge_into** field is given the name of the package
      base that a request is being created for, a new error is
      displayed: "You cannot merge a package base into itself."
- When an error is encountered, users are now brought back to
  the request form which they submitted and an error is displayed
  at the top of the page.
- If an invalid type is provided, users are returned to a BAD_REQUEST
  status rendering of the request form.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-02 22:46:49 -07:00
Kevin Morris
ad8369395e
feat(FastAPI): add /pkgbase/{name}/request (get)
This change brings in the package base request form
for new submissions.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-02 22:46:48 -07:00
Kevin Morris
1cf9420997
feat(FastAPI): allow reporters to cancel their own requests (1/2)
This change required a slight modification of how we handle
the Requests page. It is now available to all users.

This commit provides 1/2 of the implementation which actually
satisfies this feature. 2/2 will contain the actual implementation
of closures of requests, which will also allow users who created
the request to decide to close it.

Issue: https://gitlab.archlinux.org/archlinux/aurweb/-/issues/20

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-02 22:46:38 -07:00
Kevin Morris
99482f9962
feat(FastAPI): added /requests (get) route
Introduces `aurweb.defaults` and `aurweb.filters`.

`aurweb.filters` is a location developers can put their additional
Jinja2 filters and/or functions. We should slowly move all of our
filters over here, where it makes sense.

`aurweb.defaults` is a new module which hosts some default constants
and utility functions, starting with offsets (O) and per page values
(PP).

As far as the new GET /requests is concerned, we match up here to
PHP's implementation, with some minor improvements:

Improvements:

* PP on this page is now configurable: 50 (default), 100, or 250.
    * Example: `https://localhost:8444/requests?PP=250`

Modifications:

* The pagination is a bit different, but serves the exact same purpose.
* "Last" no longer goes to an empty page.
    * Closes: https://gitlab.archlinux.org/archlinux/aurweb/-/issues/14

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-02 22:43:48 -07:00
Kevin Morris
c164abe256
feat(FastAPI): add Requests navigation item
Along with this, created a new test suite at test/test_html.py,
which has the responsibility of testing various HTML things
that are not suitable for another test suite.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-02 22:43:01 -07:00
Kevin Morris
4d191b51f9
feat(FastAPI): add /pkgbase/{name}/comaintainers (get, post)
Changes from PHP:

- Form action now points to `/pkgbase/{name}/comaintainers`.
- When an error occurs, users are sent back to
  `/pkgbase/{name}/comaintainers` with an error at the top of the page.
  (PHP used to send people to /pkgbase/, which ended up at a blank
  search page).

Closes: https://gitlab.archlinux.org/archlinux/aurweb/-/issues/51

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-02 20:19:09 -07:00
Kevin Morris
986fa9ee30
feat(PHP): add aurweb Prometheus metrics
Along with this initial requests metric implementation,
we also now serve the `/metrics` route, which grabs request
metrics out of cache and renders them properly for Prometheus.

**NOTE** Metrics are only enabled when the aurweb system admin
has enabled caching by configuring `options.cache` correctly
in `$AUR_CONFIG`. Otherwise, an error is logged about no cache
being configured.

New dependencies have been added which require the use of
`composer`. See `INSTALL` for the dependency section in regards
to composer dependencies and how to install them properly for
aurweb.

Metrics are in the following forms:

    aurweb_http_requests_count(method="GET",route="/some_route")
    aurweb_api_requests_count(method="GET",route="/rpc",type="search")

This should allow us to search through the requests for specific routes
and queries.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-02 18:51:45 -07:00
Kevin Morris
2efd254974
feat(FastAPI): add /pkgbase/{name}/comments/{id}/unpin (post)
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-02 16:59:46 -07:00
Kevin Morris
0895dd07ee
feat(FastAPI): add /pkgbase/{name}/comments/{id}/pin (post)
In addition, fix up some templates to display pinned comments,
and include the unpin form input for pinned comments, which is
not yet implemented.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-02 16:59:38 -07:00
Kevin Morris
bb45ae7ac3
feat(FastAPI): add /pkgbase/{name}/comments/{id}/undelete (post)
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-02 16:59:30 -07:00
Kevin Morris
40cd1b9029
feat(FastAPI): add /pkgbase/{name}/comments/{id}/delete (post)
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-02 16:59:22 -07:00
Kevin Morris
d3be30744c
add(FeatAPI): comment pytest.fixture
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-02 16:59:22 -07:00
Kevin Morris
6644c42922
fix(FastAPI): AnonymousUser.has_credential also takes kwargs
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-02 16:59:21 -07:00
Kevin Morris
59d04d6e0c
fix(FastAPI): comment.html template rendering
Deleters and edits were not previously taken into account.
This fix addresses that issue using User.has_credential.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-02 16:59:21 -07:00
Kevin Morris
fc28aad245
feat(FastAPI): add pkgbase comments (new, edit)
In PHP, this was implemented using an /rpc type 'get-comment-form'.
With FastAPI, we've decided to reorganize this into a non-RPC route:
`/pkgbase/{name}/comments/{id}/form`, rendered via the new
`templates/partials/packages/comment_form.html` template.

When the comment_form.html template is provided a `comment` object,
it will produce an edit comment form. Otherwise, it will produce a new
comment form.

A few new FastAPI routes have been introduced:

- GET `/pkgbase/{name}/comments/{id}/form`
    - Produces a JSON response based on {"form": "<form_markup>"}.
- POST `/pkgbase/{name}/comments'
    - Creates a new comment.
- POST `/pkgbase/{name}/comments/{id}`
    - Edits an existing comment.

In addition, some Javascript has been modified for our new routes.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-02 16:59:10 -07:00
Kevin Morris
0d8216e8ea
change(FastAPI): decouple rendercomment logic from main
This commit decouples most of the rendercomment.py logic into
a function, `update_comment_render`, which can be used by other
Python modules to perform comment rendering.

In addition, we silence some deprecation warnings from python-markdown
by removing `md_globals` parameters from python-markdown callbacks.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-02 16:59:10 -07:00
Kevin Morris
7961fa932a
feat(FastAPI): add templates.render_raw_template
This function is now used as `render_template`'s underlying
implementation of rendering a template, and uses that render
in its HTMLResponse path.

This separation allows users to directly render a template
without producing a Response object.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-02 16:59:09 -07:00
Kevin Morris
5e95cfbc8a
fix(FastAPI): get_pkgbase -> get_pkg_or_base
`get_pkgbase` has been replaced with `get_pkg_or_base`, which is
quite similar, but it does take a new `cls` keyword argument which
is to be the model class which we search for via its `Name` column.

Additionally, this change fixes a bug in the `/packages/{name}` route
by supplying the Package object in question to the context and modifying
the template to use it instead of a hacky through-base workaround.

Examples:

    pkgbase = get_pkg_or_base("some_pkgbase_name", PackageBase)
    pkg = get_pkg_or_base("some_package_name", Package)

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-02 16:59:09 -07:00
Kevin Morris
f849e8b696
change(FastAPI): allow User.notified to accept a Package OR PackageBase
In addition, shorten the `package_notifications` relationship to
`notifications`.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-02 16:59:09 -07:00
Kevin Morris
4abbf9a917
fix: use @localhost for dev email addresses
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-02 16:34:34 -07:00
Kevin Morris
a3cb81962f
add: added aur_request_ml setting to config.dev
For the dev environment, we use a no-op address. We don't want
to be spamming aur-requests with development notifications.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-02 16:18:18 -07:00
Kevin Morris
438080827a
fix(Docker): add production config overrides
Additionally, `up -d` will no longer run tests unless `--profile dev`
is specified by the Docker user.

People should now be running docker with two files:

    $ docker-compose -f docker-compose.yml -f docker-compose.override.yml up -d nginx
    $ docker-compose -f docker-compose.yml -f docker-compose.dev.yml run test

Contributed by @klausenbusk. Thanks!
2021-10-02 15:14:54 -07:00
Kevin Morris
eaf012963a
Merge branch 'docker-compose' of ssh://gitlab.archlinux.org:222/klausenbusk/aurweb into docker-compose 2021-10-02 15:05:22 -07:00
Kristian Klausen
ef0c2d5a28 magic 2021-10-02 23:54:10 +02:00
Kevin Morris
3b1809e2ea
feat(Docker): allow custom certificates for fastapi/nginx
Now, when a `./cache/production.{cert,key}.pem` pair is found, it is
used in place of any certificates generated by the `ca` service.
This allows users to customize the certificate that the FastAPI
ASGI server uses as well as the front-end nginx certificates.

Optional:

- ./cache/production.cert.pem
- ./cache/production.key.pem

Fallback:

- ./cache/localhost.cert.pem + ./cache/root.ca.pem (chain)
- ./cache/localhost.key.pem

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-02 13:27:19 -07:00
Kevin Morris
ad9997c48f
feat(Docker): build aurweb:latest via docker-compose build
Users can now build the required image by running (in the aurweb root):

    $ docker-compose build

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-02 12:59:49 -07:00
Kevin Morris
fbd91f346a
feat(FastAPI): add /pkgbase/{name}/voters (get)
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-10-02 12:28:32 -07:00
Kevin Morris
836af2d588
Merge branch 'pu_packages' into pu 2021-09-21 13:42:52 -07:00
Kevin Morris
dc5dc233ec
.gitlab-ci.yml: add coverage regex
This was required for the GitLab coverage badge to get the %
of coverage.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-09-21 00:13:29 -07:00
Kevin Morris
7e58986356
feat: add util/adduser.py database tooling script
We'll need to add tests for these things at some point. However,
I'd like to include this script in here immediately for ease of
testing or administration in general.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-09-20 01:30:12 -07:00
Kevin Morris
5cf7062092
feat(FastAPI): add /packages (get) search
In terms of performance, most queries on this page win over
PHP in query times, with the exception of sorting by Voted or
Notify (https://gitlab.archlinux.org/archlinux/aurweb/-/issues/102).
Otherwise, there are a few modifications: described below.

* Pagination
    * The `paginate` Python module has been used in the FastAPI
      project
      here to implement paging on the packages search page. This
      changes how pagination is displayed, however it serves the
      same purpose. We'll take advantage of this module in other
      places as well.
* Form action
    * The form action for actions now use `POST /packages` to
      perform. This is currently implemented and will be
      addressed in a follow-up commit.
* Input names and values
    * Input names and values have been modified to satisfy the
      snake_case naming convention we'd like to use as much as
      possible.
    * Some input names and values were modified to comply with
      FastAPI Forms: (IDs[<id>]) -> (IDs, <id>).

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-09-19 12:44:19 -07:00
Kevin Morris
6298b1228a
feat(FastAPI): add templates/partials/widgets/pager.html
A pager that can be used for paginated result tables.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-09-19 12:44:19 -07:00
Kevin Morris
741cbfaa4e
auth: add several AnonymousUser method stubs
We'll need to use these, so this commit implements them here
with tests for coverage.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-09-19 12:44:18 -07:00
Kevin Morris
c006386079
add User.is_elevated()
This one returns true if the user is either a Trusted User
or a Developer.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-09-19 12:44:18 -07:00
Kevin Morris
b59601a8b7
feat(poetry): add paginate==0.5.6
With upstream at https://github.com/Pylons/paginate, this module
helps us deal with pagination without reinventing the wheel.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-09-19 12:44:16 -07:00
Kevin Morris
aee1390e2c
fix(FastAPI): registration sends WelcomeNotification
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-09-19 11:48:19 -07:00
Kevin Morris
4de18d8134
fix(FastAPI): voted/notified query efficiency
Previously, we were running a single ORM query for every single package
to check for its voted or notified states. Now, we perform a single
ORM query for each of the set of voted or notified packages in
relation with the request user.

This improves performance drastically at the expense of some
manual code additions and set-dependency; i.e. we add a bit
more complexity and roundabout way of getting our data.

Closes: https://gitlab.archlinux.org/archlinux/aurweb/-/issues/102

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-09-19 00:34:08 -07:00
Kevin Morris
fd9b07c429 Merge branch 'pu-rpc-ontop' into pu 2021-09-17 12:48:40 -07:00
Kevin Morris
f7818e26b5
fix(FastAPI): test_rpc.setup() should be a pytest.fixture
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-09-17 12:42:32 -07:00
Kevin Morris
0bbb3cc4d0
fix(FastAPI): rpc - include other fields with errors
PHP does this, we should persist the behavior here for
v=5.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-09-17 12:33:23 -07:00
Kevin Morris
06ec6388b4
fix(FastAPI): fix flake8 violation
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-09-17 12:13:02 -07:00
Kevin Morris
6afcaf665e
fix(FastAPI): Fix aurweb.template warnings
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-09-16 20:49:06 -07:00
Hunter Wittenborn
c56a23d21c Fixed bug with with type in returned JSON introduced in previous commit
Also removed some redundant return statements
2021-09-16 03:47:33 -05:00
Hunter Wittenborn
25aea216c5 Simplified and reduced redundancy in code
- Removed 'if type == "info"' as requested by @kevr

- Checked for valid type against the type dictionary, removing the 
needed to maintain two separate spots for type definitions.
2021-09-16 03:34:52 -05:00
Hunter Wittenborn
a4f5c8bef6 Fixed autopep violations in test/test_rpc.py 2021-09-16 03:20:56 -05:00
Kevin Morris
3ea515d705
fix(Docker): use cert chain for nginx
Additionally, simplify some of the certificate generation
scripts and rename `ca.ext` to `localhost.ext`.

Certificates should be regenerated as of this commit.
Users can run `rm -rf ./cache/*` to clear out any existing
certs, which will cause the `ca` service to regenerate them.

Additionally, since Docker infrastructure has been modified,
a new `aurweb:latest` image will need to be built.

See https://gitlab.archlinux.org/archlinux/aurweb/-/wikis/Docker

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-09-13 14:19:48 -07:00
Kevin Morris
ab8a44cede
fix(FastAPI): only show comments partial if they exist
This was incorrectly displaying a comment section header
when no comments existed.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-09-12 00:47:34 -07:00
Hunter Wittenborn
c2d3dc1daf Added info and multiinfo types for /rpc 2021-09-12 02:21:55 -05:00
Kevin Morris
db2718fcba
fix: util/fix-coverage sys.stderr typo
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-09-09 16:21:11 -07:00
Kevin Morris
ad3016ef4f
fix: /account/{name}/edit Account Type selection
The "Account Type" selection was not properly being rendered
due to an incorrect equality. This has been fixed in
templates/partials/account_form.html.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-09-08 17:36:37 -07:00
Kevin Morris
0fd31b8d36
refactor(docker): New mariadb_init service
Provides a single source of truth for mariadb database
initialization. Previously, php-fpm and fastapi were
racing against each other; while this wasn't an issue,
it was very messy.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-09-08 17:14:55 -07:00
Kevin Morris
2e3f69ab12
fix(docker): Fix git service's update hook
The update hook was incorrectly linked to /usr/local/bin/aurweb-git-update,
which was neglected during the original patch regarding dependency
conversion to `poetry`.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-09-08 17:10:14 -07:00
Hunter Wittenborn
0386e0dbc2 Merge branch 'pu' of ssh://gitlab.archlinux.org:222/hwittenborn/aurweb into pu 2021-09-05 16:14:00 -05:00
Hunter Wittenborn
95357687f9 Added ability to specify fortune file via an environment variable 2021-09-05 16:13:45 -05:00
Kevin Morris
e93b0a9b45
Docker: expose fastapi (18000) and php-fpm (19000)
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-09-05 00:08:47 -07:00
Kevin Morris
fa07f94051
Docker: Fix FastAPI db initialization
PHP was doing this correctly, but FastAPI was doing this
in it's exec script @ docker/scripts/run-fastapi.sh.

Modify the fastapi service so that it does the same thing as
PHP, and the existing "fastapi restart quirk" is no more.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-09-04 19:08:10 -07:00
Kevin Morris
3f034ac128
Docker: Fix incorrect ENV PATH specification
As root, seems that $HOME doesn't work like I expected it to.
Tested this before, but I apparently had some cache still holding
on. Fixing the issue in this commit here.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-09-04 18:59:24 -07:00
Kevin Morris
2f9994807b
use Poetry to deal with deps and package install
As the new-age Python package manager, Poetry brings a lot
of good additions to the table. It allows us to more easily
deal with virtualenvs for the project and resolve dependencies.

As of this commit, `requirements.txt` is replaced by Poetry,
configured at `pyproject.toml`.

In Docker and GitLab, we currently use Poetry in a root fashion.
We should work toward purely using virtualenvs in Docker, but,
for now we'd like to move forward with other things. The project
can still be installed to a virtualenv and used on a user's system
through Poetry; it is just not yet doing so in Docker.

Modifications:

* docker/scripts/install-deps.sh
    * Remove python dependencies.
* conf/config.defaults
    * Script paths have been updated to use '/usr/bin'.
* docker/git-entrypoint.sh
    * Use '/usr/bin/aurweb-git-auth' instead of
      '/usr/local/bin/aurweb-git-auth'.

Additions:

* docker/scripts/install-python-deps.sh
    * A script used purely to install Python dependencies with Poetry.
      This has to be used within the aurweb project directory and
      requires system-wide dependencies are installed beforehand.
    * Also upgrades system-wide pip.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-09-04 15:46:40 -07:00
Kevin Morris
4e5b67f0a6
Revert "Add GPL 2.0 LICENSE file"
This was already in the repository in ./COPYING

This reverts commit 1b452d1264.
2021-09-04 10:03:16 -07:00
Kevin Morris
5e6f0cb8d7
Revert "Add GPL 2.0 LICENSE file"
This was already in the repository in ./COPYING

This reverts commit 1b452d1264.
2021-09-04 10:02:46 -07:00
Kevin Morris
5c7e76ef89
Add GPL 2.0 LICENSE file
This was missing from the project and really needs to be here.

Closes #107

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-09-03 21:08:24 -07:00
Kevin Morris
1b452d1264
Add GPL 2.0 LICENSE file
This was missing from the project and really needs to be here.

Closes #107

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-09-03 21:03:36 -07:00
Kevin Morris
a5943bf2ad
[FastAPI] Refactor db modifications
For SQLAlchemy to automatically understand updates from the
external world, it must use an `autocommit=True` in its session.

This change breaks how we were using commit previously, as
`autocommit=True` causes SQLAlchemy to commit when a
SessionTransaction context hits __exit__.

So, a refactoring was required of our tests: All usage of
any `db.{create,delete}` must be called **within** a
SessionTransaction context, created via new `db.begin()`.

From this point forward, we're going to require:

```
with db.begin():
    db.create(...)
    db.delete(...)
    db.session.delete(object)
```

With this, we now get external DB modifications automatically
without reloading or restarting the FastAPI server, which we
absolutely need for production.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-09-03 15:03:34 -07:00
Kevin Morris
cfa95ef80a
RPC: add deprecation warning for v1-v4 usage
With FastAPI starting to come closer to a close, we've got to advertise
this deprecation so that users have some time to adjust before making
the changes. We have not specified a specific time here, but we'd like
this message to reach users of the RPC API for at least a month before
any modifications are made to the interface.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-08-31 19:39:27 -07:00
Kevin Morris
b52059d437
RPC: add deprecation warning for v1-v4 usage
With FastAPI starting to come closer to a close, we've got to advertise
this deprecation so that users have some time to adjust before making
the changes. We have not specified a specific time here, but we'd like
this message to reach users of the RPC API for at least a month before
any modifications are made to the interface.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-08-31 17:18:10 -07:00
Kevin Morris
210e459ba9
Eradicate the dedupe_qs filter
The new `extend_query` and `urlencode` filters are way cleaner ways
to achieve what we did with `dedupe_qs`.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-08-31 14:44:29 -07:00
Kevin Morris
c9374732c0
add filters for extend_query, to_qs
New jinja2 filters:

* `extend_query` -> `aurweb.util.extend_query`
* `urlencode` -> `aurweb.util.to_qs`

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-08-31 14:25:58 -07:00
Kevin Morris
a114bd3e16
aurweb.util: add extend_query and to_qs helpers
The first addition, extend_query, can be used to take an existing
query parameter dictionary and inject an *additions as replacement
key/value pairs.

The second, to_qs, converts a query parameter dictionary to
a query string in the form "a=b&c=d".

These two functions simplify and make dedupe_qs and quote_plus more
efficient in terms of constructing custom query string overrides.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-08-31 13:29:06 -07:00
Kevin Morris
49cc12f99d
jinja2: rename filter 'urlencode' to 'quote_plus'
urlencode does more than just a quote_plus. Using urlencode
was not sensible, so this commit addresses that.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-08-31 13:28:54 -07:00
Kevin Morris
e15a18e9fb
remove unneeded comment
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-08-30 23:04:55 -07:00
Kevin Morris
718ae1acba
aurweb.templates: loader -> _loader, env -> _env
These are module local globals and we don't want to expose
global functionality to users, so privatize them with a
leading `_` prefix.

These things should **really** not be accessible by users.
2021-08-30 22:47:28 -07:00
Kevin Morris
55c29c4519
partials/packages/details.html: Add package request count
This was missed during the original implementation merge.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-08-30 18:58:03 -07:00
Kevin Morris
45fbf214b4
jinja2: add 'tn' filter, a numerical translation
The possibly plural version of `tr`, `tn` provides a way to translate
strings into singular or plural form based on a given integer
being 1 or not 1.

Example use:

```
{{ 1 | tn("%d package found.", "%d packages found.") | format(1) }}
```

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-08-30 18:49:01 -07:00
Kevin Morris
1c26ce52a5
[FastAPI] include DepArch in dependency list
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-08-30 18:48:53 -07:00
Kevin Morris
a0be018547
Docker: Reorder dependency installation for cache purposes
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-08-28 21:32:20 -07:00
Hunter Wittenborn
eff7d478ab Updated CI tests for pip dependencies; Changed styling in install-deps.sh 2021-08-28 20:12:35 -05:00
Hunter Wittenborn
85b1a05d01 Removed pip dependencies from docker/scripts/install-deps.sh 2021-08-28 19:51:05 -05:00
Hunter Wittenborn
e61050adcf Added env/ to .gitignore
Folder will be used under virtualenv for pip dependencies
2021-08-28 19:31:11 -05:00
Hunter Wittenborn
e69004bc4a Alphabetized .gitignore file so it looks prettier 2021-08-28 19:29:44 -05:00
Hunter Wittenborn
0075ba3c33 Added .python-version from Pyenv 2021-08-28 19:27:36 -05:00
Hunter Wittenborn
b88fa8386a Removed pyalpm and srcinfo from pip requirements; Changed section title
Changed 'Generic' to 'General'
2021-08-28 19:25:51 -05:00
Hunter Wittenborn
fb908189b6 Began port of dependencies to pip
Adds Python dependencies to requirements list to allow installation via 
pip
2021-08-28 17:18:32 -05:00
Kevin Morris
f147ef3476
models.account_type: remove duplicated constants
Clearly made in mistake, removing to keep things organized.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-08-25 17:08:19 -07:00
Kevin Morris
a72ab61902
[FastAPI] fix dashboard template
Some columns should only be shown when a user is authenticated.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-08-25 16:57:19 -07:00
Kevin Morris
6eafb457ec
aurweb.util: fix code style violation
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-08-25 16:36:55 -07:00
Kevin Morris
f086457741
aurweb.redis: Reduce logging
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-08-17 21:59:15 -07:00
Kevin Morris
5a175bd92a
routers.html: add authenticated dashboard to homepage
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-08-17 21:59:13 -07:00
Kevin Morris
af51b5c460
User: add several utility methods
Added:
- User.voted_for(package)
    - Has a user voted for a particular package?
- User.notified(package)
    - Is a user being notified about a particular package?
- User.packages()
    - Entire collection of Package objects related to User.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-08-17 21:59:11 -07:00
Kevin Morris
5bd3a7bbab
RequestType: add name_display() and record constants
Just like some of the other tables, we have some constant
records that we use to denote types of things. This commit
adds constants which correlate with these record constants.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-08-17 21:59:10 -07:00
Kevin Morris
eb8ea53a44
PackageRequest: add status_display()
A helper function which provides a textual string conversion
of a particular Status column.

In a PackageRequest, Status is split up into four different types:
- PENDING  : "Pending", PENDING_ID: 0
- CLOSED   : "Closed", CLOSED_ID: 1
- ACCEPTED : "Accepted", ACCEPTED_ID: 2
- REJECTED : "Rejected", REJECTED_ID: 3

This commit adds constants for the textual strings and the
IDs. It also adds a PackageRequest.status_display() function which
grabs the proper display string for a particular Status ID.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-08-17 21:59:08 -07:00
Kevin Morris
469c141f6b [FastAPI] bugfix: remove use of scalar() in plural context
Anything where we can have more than one of something, scalar()
cannot be used.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-08-17 20:59:38 -07:00
Kevin Morris
d9cdd5faef
[FastAPI] Modularize homepage and add side panel
This puts one more toward completion of the homepage
overall; we'll need to still implement the authenticated
user dashboard after this.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-08-17 19:58:32 -07:00
Kevin Morris
9e73936c4e
add aurweb.cache, a redis caching utility module
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-08-17 19:58:31 -07:00
Kevin Morris
968ed736c1
add python-orjson dependency
python-orjson speeds up a lot of JSON serialization steps,
so we choose to use it over the standard library json module.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-08-17 19:58:29 -07:00
Kevin Morris
91e769f603
FastAPI: add redis integration
This includes the addition of the python-fakeredis package,
used for stubbing python-redis when a user does not have a
configured cache.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-08-17 19:58:28 -07:00
Kevin Morris
96d1af9363
docker-compose: add redis service
Now, the fastapi docker-compose service uses the new redis
service for a cache option.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-08-17 19:58:26 -07:00
Kevin Morris
35851d5533
Docker: add service 'memcached'
Additionally, setup memcached for php-fpm.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-08-16 22:09:09 -07:00
Kevin Morris
ae0f69a5e4
Docker: remove intervals and timeouts
These weren't needed at all and provided false negatives in
general. Removed them to let Docker deal with them.

Additionally. 'exit 0' -> 'echo' for ca's command; 'exit 0'
happens to depend on the shell running Docker (it seems).
echo is quite a bit more agnostic.

Moreso, added mariadb deps to php-fpm and fastapi.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-08-16 20:07:25 -07:00
Kevin Morris
4ade8b0539 routers.packages: Simplify some existence checks
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-08-09 23:42:54 -07:00
Kevin Morris
bace345da4
Docker: support both '%' and 'localhost' in mariadb
This is needed to be able to reach the mysql service from
other hosts or through localhost. Handling both cases here
means that we can support both localhost access and host access.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-08-08 21:46:46 -07:00
Kevin Morris
04d1c81d3d bugfix: fix extra dependency annotations
These were being displayed regardless of the dep type
and state of DepDesc. This is fixed with this commit.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-07-27 22:05:42 -07:00
Kevin Morris
88569b6d09 add /pkgbase/{name} route
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-07-27 20:48:40 -07:00
Kevin Morris
ae3d302c47 implement /packages/{name} as its own route
A few things added with this commit:

- aurweb.packages.util
    - A module providing package and pkgbase helpers.
- aurweb.template.register_filter
    - A decorator that can be used to register a filter:
      @register_filter("some_filter") def f(): pass

Additionally, template partials have been split off a bit
differently. Changes:

- /packages/{name} is defined in packages/show.html.
- partials/packages/package_actions.html is now
  partials/packages/actions.html.
- partials/packages/details.html has been added.
- partials/packages/comments.html has been added.
- partials/packages/comment.html has been added.
- models.dependency_type additions: name and id constants.
- models.relation_type additions: name and id constants.
- models.official_provider additions: base official url constant.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-07-27 20:48:40 -07:00
Kevin Morris
2d3d03e01e templates: Translate pkgbase.html and partials
+ Include the `is_maintainer` context key.
+ Use `is_maintainer` in more locations.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-07-27 20:48:40 -07:00
Leonidas Spyropoulos
1e1c0c3fe5 [FastAPI] Basic pkgbase template
Co-author: Kevin Morris <kevr@0cost.org>

Signed-off-by: Leonidas Spyropoulos <artafinde@gmail.com>
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-07-27 20:48:40 -07:00
Kevin Morris
9197f86a03 .dockerignore: ignore user-produced configs
We don't want these to even enter the Docker environment.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-07-27 20:38:59 -07:00
Kevin Morris
565f62471b sharness: do not use spaces in trash directory
With a recent curl update, it now rejects URLs with spaces in it.
We should probably fix this so that we can sanitize urls with spaces
to be used properly, but for now, just remove spaces in the directory.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-07-27 16:43:16 -07:00
Kevin Morris
53391bec1a sharness: do not use spaces in trash directory
With a recent curl update, it now rejects URLs with spaces in it.
We should probably fix this so that we can sanitize urls with spaces
to be used properly, but for now, just remove spaces in the directory.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-07-27 16:24:34 -07:00
Kevin Morris
4959f62cf5 rendercomment: move Repository init to __init__
This makes rendercomment a slight bit more lazy. Now,
it will only actually initialize a pygit2.Repository
when it needs to produce a Git commit inline.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-07-20 18:48:28 -07:00
Kevin Morris
a6ca345af4 Docker: Fix git clone url in fastapi/php-fpm
Signed-off-by: Kevin Morris <kevr@0cost.org>

Docker: fix php-entrypoint git clone uri

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-07-20 18:47:36 -07:00
Kevin Morris
d57dfd4d36
PackageBase: test Popularity conversion
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-07-20 14:16:32 -07:00
Kevin Morris
c05fafea0e PackageComment: default RenderedComment to str()
With this, `bool(PackageComment.RenderedComment) == False`

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-07-20 12:47:08 -07:00
Kevin Morris
13b4dbf541 PackageRelation: fix primary key relationships
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-07-20 12:46:59 -07:00
Kevin Morris
b4e46450b5 PackageDependency: fix primary key relationships
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-07-20 12:46:34 -07:00
Kevin Morris
ec38d2f5a0 PackageBase: automatically cast Popularity to float
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-07-20 12:46:19 -07:00
turret-dev
8bf6384504
migrate all links from git.archlinux.org -> gitlab.archlinux.org
Signed-off-by: Eli Schwartz <eschwartz@archlinux.org>
2021-07-14 21:46:47 -04:00
Kevin Morris
3f1ea7d31a Merge branch 'pu_rss' into pu 2021-07-13 22:38:26 -07:00
Kevin Morris
ae953ce19b Merge branch 'pu_accounts' into pu 2021-07-13 21:57:06 -07:00
Kevin Morris
77d54b5e1b Merge branch 'pu_tu_addvote' into pu 2021-07-13 21:16:41 -07:00
Eli Schwartz
07e70690e1
tests: disable gpgsign in the git configuration
If the person running the tests has a global configuration to sign git
commits, this breaks the testsuite which looks for a key capable of
committing dummy data under a dummy author

Signed-off-by: Eli Schwartz <eschwartz@archlinux.org>
2021-07-11 12:14:17 -04:00
Kevin Morris
e0ee881b67 Docker: fix mariadb-entrypoint user host
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-07-10 15:05:09 -07:00
Kevin Morris
c54000045c add [pycodestyle] config to setup.cfg
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-07-10 15:03:07 -07:00
Kevin Morris
7542798335 .gitignore: add more user configs and Python build files
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-07-10 14:57:19 -07:00
Kevin Morris
80ce10eb0f add /pyrightconfig.json to .gitignore
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-07-02 11:29:00 -07:00
Kevin Morris
eec09dec3e [FastAPI] add /rss and /rss/modified
There are slight differences in that, with `python-feedgen`,
an empty description field completely omits the description,
but includes the description when there is one.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-07-01 11:09:09 -07:00
Kevin Morris
8d6e782ba1 add python-feedgen dependency
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-07-01 11:09:09 -07:00
Kevin Morris
021a1c8fb6 add /accounts/ (get, post) routes
Slight markup changes, same style overall and same
form parameters as the PHP implementation.

In addition, we've disabled the "left" and "right"
navigation buttons when we're at the border of the
table.

CSS Changes:

- Added similar styling to submit `<buttons>` that submit `<input>` had.
- Added .results tr td[align="{left,right}"] styling to align
  the result table's `More -->` button to the right of the table.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-07-01 11:08:56 -07:00
Kevin Morris
450469e3d6 add /addvote/ (get, post) routes
Another part of the "Trusted User" collection of routes.
This allows a Trusted User to create a proposal.

New Routes:

- get `/addvote/`
- post `/addvote/`

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-07-01 11:08:44 -07:00
Kevin Morris
bdc913d088 Merge branch 'master' into pu 2021-06-29 23:02:39 -07:00
Kevin Morris
96bc86d153 Merge branch 'typeahead' 2021-06-29 23:02:15 -07:00
Kevin Morris
3a74f76ff9 FastAPI: use internal typeahead and remove jquery
Awesome!

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-29 22:56:48 -07:00
Kevin Morris
2835dd89ea Merge branch 'typeahead' into pu_typeahead 2021-06-29 22:31:18 -07:00
Kevin Morris
427a30ef8a Docker: Remove deprecated links
In addition, remove some unneeded dependencies on tests.
Though, in the future we _should_ craft tests that use these.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-29 21:53:23 -07:00
Kevin Morris
3f60f5048e Docker: add scripts/setup-sqlite.sh
This script purely removes any existing sqlite and is
used before tests are run. This causes the test flow
to run `aurweb.initdb` again (if ever).

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-29 21:53:23 -07:00
Kevin Morris
f4406ccf5c Docker: Centralize repo dependencies
Now, we have `docker/scripts/install-deps.sh`, a script used
by both Docker and .gitlab-ci.yml. We can now focus on changing
deps in this script along as well as documentation going forward.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-29 21:53:18 -07:00
Kevin Morris
6c7bb04b93 Docker: Improve mariadb init
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-29 21:49:57 -07:00
Kevin Morris
4442ba6703 bugfix: return null if config key doesn't exist
This was previously causing a PHP warning due to returning
a missing key.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-29 10:48:45 -07:00
Kevin Morris
a120af5a00 Docker: remove asset forward to index.php
This makes logging look a little better for development purposes.
Now, `docker-compose logs php-fpm` will only show details about PHP
accesses, while `docker-compose logs nginx` will show accesses
regarding PHP assets.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-29 10:30:26 -07:00
Kevin Morris
3bacfe6cd9 Docker: increase nginx and php-fpm logging
Log toward stdout/stderr which is accessible via
`docker-compose logs <service>`.

Examples:

- `docker-compose logs nginx`
- `docker-compose logs php-fpm`

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-29 10:29:24 -07:00
Kevin Morris
af96be7d09 Docker: move nginx config to its own file
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-29 10:27:45 -07:00
Kevin Morris
2a47aa09cd Merge branch 'pu_php_extra_2' into pu 2021-06-29 10:15:21 -07:00
Leonidas Spyropoulos
c3a29171cd [php] aurweb.spawn avoid permission denied when running as user
Signed-off-by: Leonidas Spyropoulos <artafinde@gmail.com>
2021-06-29 18:02:20 +01:00
Leonidas Spyropoulos
2f5d9c63c4 [php] Support DB mysql backend with port instead of socket
Signed-off-by: Leonidas Spyropoulos <artafinde@gmail.com>
2021-06-29 17:59:46 +01:00
Kevin Morris
dbbafc15fa bugfix: PackageKeyword should have two PKs
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-28 12:44:55 -07:00
Kevin Morris
719fa82ae5 Merge branch 'pu_package' into pu 2021-06-28 09:56:05 -07:00
Kevin Morris
7d695f0c6a Update .gitignore
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-28 09:53:05 -07:00
Kevin Morris
f8d2d4c82a PackageBase.package -> PackageBase.packages
A PackageBase can have more than one package
associated with it.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-28 08:31:13 -07:00
Kevin Morris
3c6b2203e9 Docker: bugfix: /usr/local/bin instead of /aurweb/app/bin
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-28 05:36:12 -07:00
Kevin Morris
28300ee889 bugfix: populate context on invalid password (account edit)
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-28 04:12:29 -07:00
Kevin Morris
a26e703343 bugfix: use empty string if backup_email is None
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-28 04:04:52 -07:00
Kevin Morris
987f825714 Merge branch 'account_form' into pu 2021-06-27 09:09:42 -07:00
Jelle van der Waa
222d995e95
Use backup_email field for backup email
The context gives backup_email and not backup for the backup email
field.

Fixes: #91
2021-06-27 17:29:44 +02:00
Jelle van der Waa
b2491ddc07
Use type=email for email fields
Setting the input type gives the use a hint that the field should be an
email and also shows an error when a non-email is filled into the email
field.
2021-06-27 17:25:46 +02:00
Kevin Morris
acc100eb52 Docker: Fix installation, remove pip, simplify sshd
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-27 06:37:06 -07:00
Jelle van der Waa
12911a101e
Port homepage intro to fastapi
Port the main home page content to fastapi.
2021-06-27 15:17:04 +02:00
Kevin Morris
0a3aa40f20 Docker: Fix git sshd
This was completely bugged out. This commit fixes git, provides
two separate cgit servers for the different URL bases and also
supplies a smartgit service for $AURWEB_URL/repo.git interaction.

Docker image needs to be rebuilt with this change:

    $ docker build -t aurweb:latest .

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-27 05:16:12 -07:00
Kevin Morris
83f93c8dbb aurweb.routers.accounts: strip host out of ssh pubkeys
We must store the paired key, otherwise aurweb-git-auth
will fail.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-27 03:54:13 -07:00
Kevin Morris
97c1247b57 /tu/{proposal_id}: Do not show voters if there are none
This was different than PHP.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-26 04:43:00 -07:00
Kevin Morris
04ab98907a aurweb.asgi: patch invalid f-string
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-26 04:20:55 -07:00
Kevin Morris
bfffdd4d91 aurweb.asgi: Allow unsafe-inline style-src in CSP
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-26 04:13:28 -07:00
Kevin Morris
85ba4a33a8 add /tu/{proposal_id} (get, post) routes
This commit ports the `/tu/?id={proposal_id}` PHP routes to
FastAPI into two individual GET and POST routes.

With this port of the single proposal view and POST logic,
several things have changed.

- The only parameter used is now `decision`, which
  must contain `Yes`, `No`, or `Abstain` as a string.
  When an invalid value is given, a BAD_REQUEST response
  is returned in plaintext: Invalid 'decision' value.
- The `doVote` parameter has been removed.
- The details section has been rearranged into a set
  of divs with specific classes that can be used for
  testing. CSS has been added to persist the layout with
  the element changes.
- Several errors that can be discovered in the POST path
  now trigger their own non-200 HTTPStatus codes.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-26 01:11:52 -07:00
Kevin Morris
83c038a42a add TUVoteInfo.total_votes()
Returns the sum of TUVoteInfo.Yes, TUVoteInfo.No and
TUVoteInfo.Abstain.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-26 01:11:52 -07:00
Kevin Morris
ac1779b705 add util.number_format -> number_format Jinja2 filter
Implement a `number_format` equivalent to PHP's version.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-26 01:11:52 -07:00
Kevin Morris
dc4cc9b604 add aurweb.asgi.id_redirect_middleware
A new middleware which redirects requests going to '/route?id=some_id'
to '/route/some_id'. In the FastAPI application, we'll prefer using
restful layouts where possible where resource-based ids are
parameters of the request uri: '/route/{resource_id}'.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-26 01:11:52 -07:00
Kevin Morris
e534704a98 [FastAPI] remove unused Requests navbar item
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-26 01:10:20 -07:00
Kevin Morris
d674aaf736 add /tu/ (get) index
This commit implements the '/tu' Trusted User index page.

In addition to this functionality, this commit introduces
the following jinja2 filters:

- dt: util.timestamp_to_datetime
- as_timezone: util.as_timezone
- dedupe_qs: util.dedupe_qs
- urlencode: urllib.parse.quote_plus

There's also a new decorator that can be used to enforce
permissions: `account_type_required`. If a user does not
meet account type requirements, they are redirected to '/'.

```
@auth_required(True)
@account_type_required({"Trusted User"})
async def some_route(request: fastapi.Request):
    return Response("You are a Trusted User!")
```

Routes added:

- `GET /tu`: aurweb.routers.trusted_user.trusted_user

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-26 01:03:27 -07:00
Kevin Morris
a6bba601a9 add util.get_vote -> get_vote Jinja2 filter
This filter gets a vote of a request's user toward a voteinfo.

Example: {% set vote = (voteinfo | get_vote(request)) %}

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-26 01:03:27 -07:00
Kevin Morris
d606ebc0f1 add User.is_trusted_user() and User.is_developer()
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-26 01:03:27 -07:00
Kevin Morris
ef4a7308ee add AccountType constants
New constants (in aurweb.models.account_type):

- USER: "User"
- USER_ID: USER's ID
- TRUSTED_USER: "Trusted User"
- TRUSTED_USER_ID: TRUSTED_USER's ID
- DEVELOPER: "Developer"
- DEVELOPER_ID: DEVELOPER's ID
- TRUSTED_USER_AND_DEV: "TRUSTED_USER_AND_DEV"
- TRUSTED_USER_AND_DEV_ID: TRUSTED_USER_AND_DEV's ID

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-26 01:03:27 -07:00
Kevin Morris
4927a61378 add TUVoteInfo.is_running() method
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-26 01:03:27 -07:00
Kevin Morris
07c4be0afb Docker: add .dockerignore
Currently, this ignores compiled translation files.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-26 00:42:20 -07:00
Kevin Morris
9ee7be4a1c Docker: remove web/locale from volume mounts
This caused a bug where generated locale would not be used.

Also, removed appending to /etc/hosts which was bugging out
on Mac OS X. archlinux:base-devel seems to come with a valid
/etc/hosts.

Additionally, remove AUR_CONFIG from Dockerfile. We don't
set it up; just use the defaults during installation.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-26 00:38:54 -07:00
Kevin Morris
ff3519ae11 [alembic] Log db name being used in a migration
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-25 22:33:30 -07:00
Kevin Morris
cec07c76b6 User: use aurweb.config options.salt_rounds
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-25 21:39:41 -07:00
Kevin Morris
d8556b0d86 config: add options.salt_rounds
During development, the lower this value is (must be >= 4)
equals faster User generation. This is particularly useful
for running tests.

In production, a higher value (like 12 which is used by various
popular frameworks) should be used.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-25 21:30:11 -07:00
Kevin Morris
a702f7bc0b Merge branch 'master' into pu 2021-06-25 19:03:29 -07:00
Kevin Morris
eb56305091 gendummydata: lower record counts
This commit halves MAX_USERS and MAX_PKGS, in addition
to setting OPEN_PROPOSALS to 15 and CLOSE_PROPOSALS to 50.

A few counts are now configurable via environment variable:

- MAX_USERS, default: 38000
- MAX_PKGS, default: 32000
- OPEN_PROPOSALS, default: 15
- CLOSE_PROPOSALS, default: 15

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-25 18:26:40 -07:00
Kevin Morris
201a04ffb9 gendummydata: employ a salted hash for users
As of Python updates, we are no longer considering rows with
empty salts to be legacy hashes. Update gendummydata.py to
generate salts for the legacy passwords it uses with
salt rounds = 4.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-25 17:09:59 -07:00
Kevin Morris
d95e4ec443 Docker: create missing 'aurweb' DB if needed
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-25 17:09:21 -07:00
Jelle van der Waa
c8d88464b1
Update mailing list address
https://lists.archlinux.org/pipermail/arch-dev-public/2021-June/030462.html
2021-06-25 17:25:24 +02:00
Jelle van der Waa
42bd0027b3
Add archweb typeahead implementation
Use a pure vanilla JavaScript typeahead implementation to finally
deprecate the old jQuery version and typeahead library.
2021-06-25 17:08:54 +02:00
Jelle van der Waa
512f8064c1
Fix JavaScript error on packages overview page 2021-06-25 17:08:47 +02:00
Kevin Morris
61c473405f Docker: add ./templates volume mount
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-24 21:42:19 -07:00
Kevin Morris
df161ef38e Docker: add .env configurable FASTAPI_BACKEND
By default we now use uvicorn because it has a much
better developer feedback out of the box. We'll work
on hypercorn logging, but for now, hypercorn is usable
via: `docker-compose --env-file docker/hypercorn.env up nginx`.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-24 21:31:49 -07:00
Kevin Morris
495dd2d821 Docker: add missing git link to pytest-sqlite
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-24 20:35:29 -07:00
Kevin Morris
565b928a59 Docker: mount codebase volumes
Before, docker build was the only way to transfer new code
over to the docker image. This allows users to execute code
in their working directory.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-24 20:33:02 -07:00
Kevin Morris
a36cc0c00a eradicate Term records after testing them
Otherwise, Terms can leak out into other tests causing /tos
redirects unexpectedly.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-24 19:52:17 -07:00
Kevin Morris
adb42882c5 [FastAPI] add /tos routes (get, post)
This clones the end goal behavior of PHP, but it does not
concern itself with the revision form array at all.

Since this page on PHP renders out the entire list of
terms that a user needs to accept, we can treat a
POST request with the "accept" checkbox enabled as a
request to accept all unaccepted (or outdated revision)
terms.

This commit also adds in a new http middleware used to
redirect authenticated users to '/tos' if they have not
yet accepted all terms.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-24 19:12:49 -07:00
Kevin Morris
e624e25c0f Docker: Add colored output to tests
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-24 19:02:35 -07:00
Kevin Morris
2a3df086d3 Docker: add [c]git, nginx, fastapi, php-fpm, ca
Now, we have a full collection of services used to run
aurweb over HTTPS using a self-signed CA.

New Docker services:

- `ca` - Certificate authority services
    - When the `ca` service is run, it will (if needed) generate
      a CA certificate and leaf certificate for localhost AUR
      access. This ca is then shared with things like nginx to
      use the leaf certificate. Users can import
      `./cache/ca.root.pem` into their browser or ca-certificates
      as a root CA who issued aurweb's certificate.
- `git` - Start sshd and set it up for aur git access
- `cgit` - Serve cgit with uwsgi on port 3000
- `fastapi` - Serve our FastAPI app with `hypercorn` on port 8000
- `php-fpm` - Serve our PHP-wise aurweb
- `nginx` - Serve FastAPI, PHP and CGit with an HTTPS certificate.
    - PHP: https://localhost:8443
    - PHP CGit: https://localhost:8443/cgit
    - FastAPI: https://localhost:8444
    - FastAPI CGit: https://localhost:8444/cgit

Short of it: Run the following in a shell to run PHP and FastAPI
servers on port **8443** and **8444**, respectively.

    $ docker-compose up nginx

This will host the PHP, FastAPI, CGit and Git ecosystems.

Git SSH can be knocked at `aur@localhost:2222` as long as you have a
valid public key in the aurweb database.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-24 19:02:35 -07:00
Kevin Morris
5bd46d18a3 Improve Docker ecosystem
Instead of using Dockerfile for everything, we've introduced
a docker-compose.yml file and kept the Dockerfile to producing
a pure base image for the services defined.

docker-compose services:

- `mariadb` - Setup mariadb
- `sharness` - Run sharness suites
- `pytest-mysql` - Run pytest suites with MariaDB
- `pytest-sqlite` - Run pytest suites with SQLite
- `test` - Run all tests and produce a collective coverage report
    - This target mounts a cache volume and copies any successful
      coverage report back to `./cache/.coverage`. Users can run
      `./util/fix-coverage ./cache/.coverage` to rewrite source
      code paths and move coverage into place to view reports
      on your local system.

== Get Started ==

Build `aurweb:latest`.

    $ docker build -t aurweb:latest .

Run all tests via `docker-compose`.

    $ docker-compose up test

You can also purely run `pytest` in SQLite or MariaDB modes.

    $ docker-compose up pytest-sqlite
    $ docker-compose up pytest-mysql

Or `sharness` alone, which only uses SQLite internally.

    $ docker-compose up sharness

After running tests, coverage reports are stored in `./cache/.coverage`.
This database was most likely created in a different path, and so it
needs to be sanitized with `./util/fix-coverage`.

    $ ./util/fix-coverage cache/.coverage
    Copied coverage db to /path/to/aurweb/.coverage.
    $ coverage report
    ...
    $ coverage html
    $ coverage xml
    ...

Defined components:

**Entrypoints**

- mariadb-entrypoint.sh - setup mariadb and run its daemon
- test-mysql-entrypoint.sh - setup mysql configurations
- test-sqlite-entrypoint.sh - setup sqlite configurations
- tests-entrypoint.sh - setup mysql and sqlite configurations

**Scripts**

- run-mariadb.sh - setup databases
- run-pytests.sh - run pytest suites
- run-sharness.sh - run sharness suites
- run-tests.sh - run both pytests and sharness

**Health**

- mariadb.sh - A healthcheck script for the mariadb service
- pytest.sh - A healthcheck script for the pytest-* services
- sharness.sh - A healthcheck script for the sharness service

This Docker configuration is setup for tests, but should be
extendable for web and git servers.

**Changes to Makefile**

- Remove `.coverage` in the `clean` target
- Add a `coverage` target which prints a report and outputs xml

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-24 19:02:35 -07:00
Kevin Morris
3b8e3f3e4b test_db: remove user-configuration dependency
We should have been using a config that's stored in the
repo all along.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-24 19:02:35 -07:00
Kevin Morris
8abb096d7b use aurweb_test for default mysql dev database
This also updates `test/README.md` to be a bit more specific
and precise with our current state of testing.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-24 19:02:35 -07:00
Kevin Morris
55c0637b98 add logging.config.fileConfig
This resolves logging issues with alembic on aurweb.initdb
in addition to adding more logging utilities for aurweb
and tests in general.

Developers should fetch a logger for their specific module
via `logging.getLogger(__name__)`.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-24 18:56:38 -07:00
Kevin Morris
865c414504 aurweb.asgi: add security headers middleware
This commit introduces a middleware function which adds
the following security headers to each response:

- Content-Security-Policy
    - This includes a new `nonce`, which is tied to a user
      via authentication middleware. Both an anonymous user
      and an authenticated user recieve their own random nonces.
- X-Content-Type-Options
- Referrer-Policy
- X-Frame-Options

They are then tested for existence in test/test_routes.py.

Note: The overcomplicated-looking asyncio behavior in the
middleware function is used to avoid a warning about the old
coroutine awaits being deprecated. See
https://docs.python.org/3/library/asyncio-task.html#asyncio.wait
for more detail.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-22 20:33:45 -07:00
Kevin Morris
13456fea1e set AURLANG + AURTZ on login
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-22 20:33:20 -07:00
Kevin Morris
91dc3efc75 add util.add_samesite_fields(response, value)
This function adds f"SameSite={value}" to each cookie's header
stored in response.

This is needed because starlette does not currently support
the `samesite` argument in Response.set_cookie. It is merged,
however, and waiting for next release.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-22 20:33:20 -07:00
Kevin Morris
ec632a7091 use secure=True when options.disable_http_login is enabled
We'll piggyback off of the current existing configuration item,
`disable_http_login`, to decide how we should submit cookies to
an HTTP response.

Previously, in `sso.py`, the http schema was used to make this
decision. There is an issue with that, however: We cannot actually
test properly if we depend on the https schema.

This change allows us to toggle `disable_http_login` to modify
the behavior of cookies sent with an http response to be secure.

We test this behavior in test/test_auth_routes.py#L81:
`test_secure_login(mock)`.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-22 20:33:19 -07:00
Kevin Morris
763b84d0b9 Merge branch 'master' into pu 2021-06-22 19:39:31 -07:00
Kristian Klausen
959e535126 Use the real ml email address instead of alias
All the arch-x@archlinux.org -> arch-x@lists.archlinux.org aliases will
be dropped soon[1].

[1] https://lists.archlinux.org/pipermail/arch-dev-public/2021-June/030462.html
2021-06-23 03:21:06 +02:00
Kevin Morris
af76e660d0 auth_required: allow formattable template tuples
See docstring for updates.

template= has been modified.
status_code= has been added as an optional template status_code.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-21 21:37:10 -07:00
Jelle van der Waa
06fa8ab5f3
Convert comment editing to vanilla JavaScript
Signed-off-by: Eli Schwartz <eschwartz@archlinux.org>
2021-06-21 15:19:22 -04:00
Jelle van der Waa
d7603fa4d3
Port package details page to pure JavaScript
Use a CSS animation for jQuery.Animate and replace the rest with pure
vanilla JavaScript.

Signed-off-by: Eli Schwartz <eschwartz@archlinux.org>
2021-06-21 15:19:22 -04:00
Jelle van der Waa
8b6f92f9e9
Use the clipboard API for copy paste
The Document.execCommand API is deprecated and no longer recommended to
be used. It's replacement is the much simpler navigator.clipboard API
which is supported in all browsers except internet explorer.

Signed-off-by: Eli Schwartz <eschwartz@archlinux.org>
2021-06-21 15:17:42 -04:00
Kevin Morris
d7941e6bed urllib.parse.quote_plus -> urlencode Jinja2 filter
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-19 09:54:19 -07:00
Kevin Morris
d5e650a339 add util.dedupe_qs -> dedupe_qs Jinja2 filter
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-19 09:53:59 -07:00
Kevin Morris
b1baf76998 add util.as_timezone -> as_timezone Jinja2 filter
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-19 09:53:48 -07:00
Kevin Morris
ac67268a28 add util.timezone_to_datetime -> dt Jinja2 filter
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-19 09:53:30 -07:00
Kevin Morris
f89d06d092 setup_test_db: remove mysql-dependent coverage path
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-19 09:09:12 -07:00
Kevin Morris
b7d67bf5fc render_template: convert HTTPStatus objects
This will automate a lot of conversion that happens
around the codebase in terms of status_code.

As of this commit, we should improve usage and remove
int(status_code) casts wherever we can.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-19 09:08:59 -07:00
Kevin Morris
7ae95ac908 bugfix: removed extra space in " My Account" nav link
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-19 09:08:21 -07:00
Kevin Morris
40448ccd34 aurweb.db: add commit(), add() and autocommit arg
With the addition of these two, some code has been swapped
to use these in some of the other db wrappers with an additional
autocommit kwarg in create and delete, to control batch
transactions.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-19 09:08:09 -07:00
Kevin Morris
bd8f528011 add Base.as_dict() and Base.json()
Two utility functions for all of our ORM models that will
allow us to easily convert them to Python structures and
JSON data.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-13 10:48:31 -07:00
Kevin Morris
be3bab2ce0 Merge branch 'master' into pu 2021-06-12 20:11:48 -07:00
Justin Kromlinger
8d9f20939c Add modified packages RSS feed to frontend 2021-06-12 20:09:56 -07:00
Justin Kromlinger
4330fe4f33 Add RSS feed for modified packages 2021-06-12 20:09:48 -07:00
Justin Kromlinger
e7db894eb7 RSS: Add ability to specify isPermaLink="false" for GUID 2021-06-12 20:09:39 -07:00
Justin Kromlinger
537349e124 Add modified packages RSS feed to frontend 2021-06-12 19:14:43 -07:00
Justin Kromlinger
2bb30f9bf5 Add RSS feed for modified packages 2021-06-12 19:14:43 -07:00
Justin Kromlinger
18ec8e3cc8 RSS: Add ability to specify isPermaLink="false" for GUID 2021-06-12 19:14:43 -07:00
Kevin Morris
0c1241f8bb add TUVote SQLAlchemy ORM model
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-11 22:14:38 -07:00
Kevin Morris
8c345a0448 TUVoteInfo: generalize Quorum
SQLite does not support native DECIMAL columns, and for that
reason, we had to switch to using Strings that can hold the data
in the case we are using sqlite.

This commit sets the TUVoteInfo model up in a generic way, that
it always converts to string when setting Quorum (OK for DECIMAL)
and always converts to float when getting Quorum.

This way, we can treat TUVoteInfo.Quorum as the same thing
everywhere.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-11 21:48:39 -07:00
Kevin Morris
541c978ac4 add PackageRequest SQLAlchemy ORM model
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-11 21:21:38 -07:00
Kevin Morris
809939ab03 add TUVoteInfo SQLAlchemy ORM model
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-11 21:00:26 -07:00
Kevin Morris
65ff0e76da aurweb.schema: Fix off-by-one String impls of DECIMAL
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-11 19:57:52 -07:00
Kevin Morris
3bf4b3717a add RequestType SQLAlchemy ORM model
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-11 17:37:51 -07:00
Kevin Morris
511f174c8b add PackageBlacklist SQLAlchemy ORM model
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-11 17:28:08 -07:00
Kevin Morris
163e4d7389 test_package_comaintainer: sanitize newlines
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-11 17:15:18 -07:00
Kevin Morris
5b856c7af2 add PackageNotification SQLAlchemy ORM model
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-11 17:14:28 -07:00
Kevin Morris
229df1adef test_package_vote: remove useless stuff
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-11 16:56:15 -07:00
Kevin Morris
ebd216edfd add PackageComaintainer SQLAlchemy ORM model
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-11 16:52:45 -07:00
Kevin Morris
fc28c1e5fd add PackageComment SQLAlchemy ORM model
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-11 00:35:18 -07:00
Kevin Morris
11c4926502 add PackageSource SQLAlchemy ORM model
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-10 17:46:29 -07:00
Kevin Morris
d18cfad63e use djangos method of wiping sqlite3 tables
Django uses a reference graph to determine the order
in table deletions that occur. Do the same here.

This commit also adds in the `REGEXP` sqlite function,
exactly how Django uses it in its reference graphing.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-10 17:26:34 -07:00
Kevin Morris
5de7ff64df add PackageVote SQLAlchemy ORM model
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-10 13:55:07 -07:00
Kevin Morris
888cf5118a use declarative_base for all ORM models
This rewrites the entire model base as declarative models.
This allows us to more easily customize overlay fields
in tables and is more common.

This effort also brought some DB violations to light which
this commit addresses.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-10 13:54:27 -07:00
Steven Guikal
b32022a176
Add FIDO/U2F ssh keytypes to default config
Signed-off-by: Eli Schwartz <eschwartz@archlinux.org>
2021-06-10 15:32:20 -04:00
Steven Guikal
a625df07e2
Source valid ssh prefixes from config
Signed-off-by: Eli Schwartz <eschwartz@archlinux.org>
2021-06-10 15:32:02 -04:00
Kevin Morris
7f7a975614 remove autoflush from aurweb.db.Session
This causes issues with the declarative API.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-10 03:04:50 -07:00
Kevin Morris
4f09e939ae bugfix: gendummydata.py was producing invalid usernames
As per our regex and policies, usernames should consist of
ascii alphanumeric characters and possibly (-, _ or .).

gendummydata.py was creating unicode versions of some
usernames and adding them into the DB. With our newfound
collations, this becomes a problem as it treats them as
the same.

This should have never been the case here, and so,
gendummydata.py has been patched to normalize all of its
usernames and package names.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-06 21:46:16 -07:00
Kevin Morris
83887b97df Merge branch 'php_fix' into pu 2021-06-06 21:44:11 -07:00
Kevin Morris
25937d9543 Merge branch 'master' into pu 2021-06-06 17:21:57 -07:00
Kevin Morris
f9f41dc99b restore TU_VoteInfo -> utf8mb4_general_ci
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-06 16:30:16 -07:00
Jelle van der Waa
889d358a6d
Add missing ) for addvote.php 2021-06-06 21:49:27 +02:00
Kevin Morris
1874e821f5 add case [in]sensitivity tests + add OfficialProvider model
`ci` in this context means "Case Insensitive".
`cs` in this context means "Case Sensitive".

New models created:
    - OfficialProvider
      This was required to write a test for checking that
      OfficialProviders behaves as we expect, which was the starter
      for the original aurblup bug.

New tests created:
    - test_official_provider

Modified tests:
    - test_package_base: add ci test
    - test_package: add ci test
    - test_session: add cs test
    - test_ssh_pub_key: add cs test

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 23:20:18 -07:00
Kevin Morris
e865a6347f .gitlab-ci.yml: enforce isort and flake8 compliance
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 21:31:57 -07:00
Kevin Morris
5ceeb88bee remove unused imports, rectify isort violations
Files got into the branch that violate both PEP-8 guidelines
and isorts. This fixes them.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 21:27:39 -07:00
Kevin Morris
4d1faca447 test both mysql and sqlite in .gitlab-ci.yml
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:17:48 -07:00
Kevin Morris
62e58b122f fix test_accounts_routes test coverage
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:17:48 -07:00
Kevin Morris
228bc8fe7c fix aurweb.auth test coverage
With mysqlclient, we no longer need to account for a user not existing
when an ssh key is found.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:17:48 -07:00
Kevin Morris
aecb649473 use mysql backend in config.dev
First off: This commit changes the default development database
backend to mysql. sqlite, however, is still completely supported
with the caveat that a user must now modify config.dev to use
the sqlite backend.

While looking into this, it was discovered that our SQLAlchemy
backend for mysql (mysql-connector) completely broke model
attributes when we switched to utf8mb4_bin (binary) -- it does
not correct the correct conversion to and from binary utf8mb4.

The new, replacement dependency mysqlclient does. mysqlclient
is also recommended in SQLAlchemy documentation as the "best"
one available.

The mysqlclient backend uses a different exception flow then
sqlite, and so tests expecting IntegrityError has to be modified
to expect OperationalError from sqlalchemy.exc.

So, for each model that we define, check keys that can't be
NULL and raise sqlalchemy.exc.IntegrityError if we have to.
This way we keep our exceptions uniform.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:17:48 -07:00
Kevin Morris
d7481b9649 modify schema primary keys to be nullable+defaulted
This fixes SQLAlchemy warnings related to primary keys not
having an auto_increment or nullable.

We've done this by making all foreign primary keys nullable.

In ApiRateLimit's case, we can set a default str to act as
a null, which seems a bit more sensible.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
e5df083d45 use String(max_len) for DECIMAL types with sqlite
This solves an issue where DECIMAL is not native
to sqlite by using a string to store values and
converting them to float in user code.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
a65a60604a add ApiRateLimit SQLAlchemy ORM model
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
2b83d2fb6b add PackageRelation SQLAlchemy ORM model
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
a9cfbce11e add RelationType SQLAlchemy ORM model
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
e401b92acb add PackageDependency (PackageDepends) ORM model
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
068c8ba638 add DependencyType SQLAlchemy ORM model
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
4201348dea add PackageGroup SQLAlchemy ORM model
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
a8a9c28783 Jinja bugfix: add xmlns + xml:lang to <html>
This was not brought over during the initial commit involving
partisl/layout.html.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
75cc0be189 add PackageLicense SQLAlchemy ORM model
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
943d97efac add License SQLAlchemy ORM model
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
38dc2bb99d Sanitize and modernize pytests
Some of these tests were written before some of our convenient
tooling existed. Additionally, some of the tests were not
cooperating with PEP-8 guidelines or isorted.

This commit does the following:
    - Replaces all calls to make_(user|session) with
      aurweb.db.create(Model, ...).
    - Replace calls to session.add(...) + session.commit() with
      aurweb.db.create.
    - Removes the majority of calls to (session|aurweb.db).delete(...).
    - Replaces session.query calls with aurweb.db.query.
    - Initializes all mutable globals in pytest fixture setup().
    - Makes mutable global declarations more concise:
      `var1, var2 = None, None` -> `var1 = var2 = None`
    - Defines a warning exclusion for test/test_ssh_pub_key.py.
    - Removes the aurweb.testing.models module.
    - Removes some useless pytest.fixture yielding.

As of this commit, developers should use the following guidelines
when writing tests:
    - Always use aurweb.db.(create|delete|query) for database
      operations, where possible.
    - Always define mutable globals in the style: `var1 = var2 = None`.
    - `yield` the most dependent model in pytest setup fixture **iff**
      you must delete records after test runs to maintain database
      integrity. Example: test/test_account_type.py.

This all makes the test code look and behave much cleaner.
Previously, aurweb.testing.setup_test_db was buggy and leaving
objects around in SQLAlchemy's IdentityMap.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
f2121fb833 simplify test_package_keyword.py
We no longer need to delete records like this; in fact, it causes
errors now. Fix this by removing the deletions and allow
setup_test_db to do it's job.

We'll need to do this for other tests as well.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
15b1332656 add Package SQLAlchemy ORM model
Additionally, add an optional **kwargs passing via make_relationship.
This allows us to use things like `uselist=False`, which was needed
for test/test_package.py.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
621e459dfb aurweb.models.user: Remove session.commit() from construction
We don't want to do this on construction. We only want to do this
when we want to actually add the user to the database (or modify it).

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
f8a6049de2 aurweb.db.session: Use autoflush=True for Sessions
We'd like SQLAlchemy to automatically maintain flushes for us.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
794868b20f aurweb.testing.setup_test_db: Expunge objects
This is needed to avoid redundant objects in SQLAlchemy's
IdentityMap, since we pass a direct .execute to delete
the tables passed in. Additionally, remove our engine.connect()
call in favor of relying on the already-established Session.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
b692b11f62 add Group SQLAlchemy ORM model
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
e1ab02c2bf Fix database initialization in test_term.py
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
718fa48a5c add AcceptedTerm SQLAlchemy ORM model
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
29db2ee513 add Term SQLAlchemy ORM model
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
fb21015811 add PackageKeyword SQLAlchemy ORM model
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
a7e5498197 add PackageBase SQLAlchemy ORM model
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
822905be7d bugfix: relax next verification
AUR renders its own 404 Not Found page when a bad route
is encountered. Introducing the previous verification
caused an error in this case when setting a language
while viewing the Not Found page. So, instead of checking
through routes, just make sure that the next parameter
starts with a '/' character, which removes the possibility
of any cross attacks.

+ Removed aurweb.asgi.routes; no longer needed.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Leonidas Spyropoulos
32abdbafae fastapi: Jinja contextfilter renamed to pass_context
Closes: #23

Signed-off-by: Leonidas Spyropoulos <artafinde@gmail.com>
2021-06-05 20:11:17 -07:00
Kevin Morris
4f928b4577 add account (view) route
+ Added get /account/{username} route.
+ Added account/show.html template which shows a single use

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
4e9ef6fb00 add account edit (settings) routes
* Added account_url filter to jinja2 environment. This produces a path
  to the user's account url (/account/{username}).
* Updated archdev-navbar to link to new edit route.
+ Added migrate_cookies(request, response) to aurweb.util, a function
  that simply migrates the request cookies to response and returns it.
+ Added account_edit tests to test_accounts_routes.py.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
d323c1f95b add python-lxml to dependencies
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
c94793b0b1 add user registration routes
* Added /register get and post routes.
+ Added default attributes to AnonymousUser, including a new
  AnonymousList which behaves like an sqlalchemy relationship
  list.
+ aurweb.util: Added validation functions for various user fields
  used throughout registration.
+ test_accounts_routes: Added get|post register route tests.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
19b4a896f1 add openssh to test dependencies
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
df0a637d2b add aurweb.captcha, a CAPTCHA utility module
This CAPTCHA workflow is the same workflow used by our current
PHP implementation of account registration.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
7a6a38592e add python-email-validator dependency
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
a5be6fc9be aurweb.templates: add make_variable_context
A new make_context wrapper which additionally includes either
query parameters (get) or form data (post) in the context.

Use this to simplify setting context variables for form data
in particular.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
9052688ed2 add aurweb.time module
This module includes timezone-based utilities for a FastAPI request.
This commit introduces use of the AURTZ cookie within get_request_timezone.
This cookie should be set to the user or session's timezone.

* `make_context` has been modified to parse the request's timezone
  and include the "timezone" and "timezones" variables, along with
  a timezone specified "now" date.
+ Added `Timezone` attribute to aurweb.testing.requests.Request.user.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
07d5907ecd aurweb.auth: add user credentials and matcher functions
This clones the behavior already present in the PHP implementation,
but it uses a global dict with credential constant keys to
validation functions to determine if a given user has a credential.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
670f711b59 add SSHPubKey ORM model
Includes `aurweb.models.ssh_pub_key.get_fingerprint(pubkey)` helper.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
9fdbe3f775 add authenticated User LangPreference tracking
+ Use User.LangPreference when there is no set AURSID
  if request.user.is_authenticated is true.
+ Updated post /language to update LangPreference when
  request.user.is_authenticated.
+ Restore language during test where we change it.
+ Added the user attribute to aurweb.testing.requests.Request.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
a33d076d8b add passreset routes
Introduced `get|post` `/passreset` routes. These routes mimic the
behavior of the existing PHP implementation, with the exception of
HTTP status code returns.

Routes added:
    GET /passreset
    POST /passreset

Routers added:
    aurweb.routers.accounts

* On an unknown user or mismatched resetkey (where resetkey must ==
  user.resetkey), return HTTP status NOT_FOUND (404).
* On another error in the request, return HTTP status BAD_REQUEST (400).

Both `get|post` routes requires that the current user is **not**
authenticated, hence `@auth_required(False, redirect="/")`.

+ Added auth_required decorator to aurweb.auth.
+ Added some more utility to aurweb.models.user.User.
+ Added `partials/error.html` template.
+ Added `passreset.html` template.
+ Added aurweb.db.ConnectionExecutor functor for paramstyle logic.
  Decoupling the executor logic from the database connection logic
  is needed for us to easily use the same logic with a fastapi
  database session, when we need to use aurweb.scripts modules.

At this point, notification configuration is now required to complete
tests involved with notifications properly, like passreset.
`conf/config.dev` has been modified to include [notifications] sendmail,
sender and reply-to overrides. Dockerfile and .gitlab-ci.yml have been
updated to setup /etc/hosts and start postfix before running tests.

* setup.cfg: ignore E741, C901 in aurweb.routers.accounts

These two warnings (shown in the commit) are not dangerous and a bi-product
of maintaining compatibility with our current code flow.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
4423326cec add the request parameter to render_template
This allows us to inspect things about the request we're rendering from.

* Use render_template(request, ...) in aurweb.routers.auth

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
5d4a5deddf implement login + logout routes and templates
+ Added route: GET `/login` via `aurweb.routers.auth.login_get`
+ Added route: POST `/login` via `aurweb.routers.auth.login_post`
+ Added route: GET `/logout` via `aurweb.routers.auth.logout`
+ Added route: POST `/logout` via `aurweb.routers.auth.logout_post`
* Modify archdev-navbar.html template to toggle displays on auth state
+ Added login.html template

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
56f2798279 add aurweb.auth and authentication to User
+ Added aurweb.auth.AnonymousUser
    * An instance of this model is returned as the request user
      when the request is not authenticated
+ Added aurweb.auth.BasicAuthBackend
+ Add starlette's AuthenticationMiddleware to app middleware,
  which uses our BasicAuthBackend facility
+ Added User.is_authenticated()
+ Added User.authenticate(password)
+ Added User.login(request, password)
+ Added User.logout(request)
+ Added repr(User(...)) representation
+ Added aurweb.auth.auth_required decorator.

This change uses the same AURSID logic in the PHP implementation.

Additionally, introduce a few helpers for authentication,
one of which being `User.update_password(password, rounds = 12)`
where `rounds` is a configurable number of salt rounds.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
137c050f99 add python-bcrypt dependency
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
1922e5380d add aurweb.models.session.Session ORM database object
+ Added aurweb.util module.
    - Added make_random_string function.
+ Added aurweb.db.make_random_value function.
    - Takes a model and a column and introspects them to figure out the
      proper column length to create a random string for; then creates
      a unique string for that column.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
adc9fccb7d add aurweb.models.ban.Ban ORM mapping
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
a836892cde aurweb.db: add query, create, delete helpers
Takes sqlalchemy kwargs or stanzas:

query(Model, Model.Column == value)
query(Model, and_(Model.Column == value, Model.Column != "BAD!"))

Updated tests to reflect the new utility and a comment about upcoming
function deprecation is added to get_account_type().

From here on, phase out the use of get_account_type().

+ aurweb.db: Added create utility function
+ aurweb.db: Added delete utility function

The `delete` function can be used to delete a record by search
kwargs directly.

Example:
    delete(User, User.ID == 6)

All three functions added in this commit are typically useful to
perform these operations without having to import aurweb.db.session.
Removes a bit of redundancy overall.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Kevin Morris
5185df629e move aurweb.testing to its own package
+ Added aurweb.testing.setup_test_db(*tables)
+ Added aurweb.testing.models.make_user(**kwargs)
+ Added aurweb.testing.models.make_session(**kwargs)
+ Added aurweb.testing.requests.Client
+ Added aurweb.testing.requests.Request
* Updated test_l10n.py to use our new Request

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 20:11:17 -07:00
Leonidas Spyropoulos
64bc93926f Add support for configuring database with port instead of socket
Signed-off-by: Leonidas Spyropoulos <artafinde@gmail.com>
2021-06-05 20:11:17 -07:00
Kristian Klausen
ac31f520ea Add coverage report for "Test Coverage Visualization"[1]
[1] https://docs.gitlab.com/ee/user/project/merge_requests/test_coverage_visualization.html
2021-06-05 20:11:17 -07:00
Leonidas Spyropoulos
7b7c3abbe2 Conditionally apply SSOAccountId migration to support misaligned databases
Closes: #34

Signed-off-by: Leonidas Spyropoulos <artafinde@gmail.com>
2021-06-05 20:11:17 -07:00
Leonidas Spyropoulos
72f755817c Adds Alembic migration for DB/Tables conversion to utf8mb4
MySql defaults to `utf8` and case insensitive collation so migrate these to case sensitive and `utf8mb4`

Closes #21

Signed-off-by: Leonidas Spyropoulos <artafinde@gmail.com>
2021-06-05 20:11:04 -07:00
Kevin Morris
66189c4460 alembic: restore logging, fix pytest conflicts
In this case, when running pytests, we do not allow alembic to
configure loggers.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 19:52:56 -07:00
Kevin Morris
3f1f03e03c aurweb.db: only pass check_same_thread with sqlite
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 19:52:56 -07:00
Kevin Morris
e0eb6b0e76 test_db: remove use of mkdtemp and os.removedirs
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 19:52:56 -07:00
Marcus Andersson
1d5827007f Adding route tests
Removing status code from 404 title

Removing status code from 503 title

Adding id to 503 error box

Indatation fix
2021-06-05 19:52:56 -07:00
Marcus Andersson
f6744d3e39 Adding error 503 catcher 2021-06-05 19:52:56 -07:00
Marcus Andersson
cdf75ced9a Adding error 404 catcher 2021-06-05 19:52:56 -07:00
Kevin Morris
82f3871a83 Support SQLAlchemy 1.4 URL.create recommendation
This fixes a deprecating warning when using SQLAlchemy 1.4.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 19:52:56 -07:00
Kevin Morris
81856f3b64 Fix incorrect construction of MySQL SQLAlchemy URL
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 19:52:49 -07:00
Kevin Morris
02311eab76 add test_initdb.py
IMPORTANT: This test completely wipes out the database it's using.
Make sure you've got AUR_CONFIG set to a test database configuration!

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 19:52:42 -07:00
Kevin Morris
8a47afd2ea add aurweb.models.user.User
+ Added aurweb.models.user.User class. This is the first example
  of an sqlalchemy ORM model. We can search for users via for example:
  `session.query(User).filter(User.ID==1).first()`, where `session` is
  a configured `aurweb.db.session` object.
+ Along with the User class, defined the AccountType class.
  Each User maintains a relationship to its AccountType via User.AccountType.
+ Added AccountType.users backref.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 19:52:42 -07:00
Kevin Morris
e860d828b6 add aurweb.testing, a module with testing utilities
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 19:52:42 -07:00
Kevin Morris
32f2893095 add aurweb.models.account_type.AccountType
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 19:52:42 -07:00
Kevin Morris
4238a9fc68 add aurweb.db.session
+ Added Session class and global session object to aurweb.db,
  these are sessions created by sqlalchemy ORM's sessionmaker
  and will allow us to use declarative/imperative models.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 19:52:40 -07:00
Kevin Morris
7c65604dad move off env.py's active code to __name__ == "__main__"
* Moved migrations/env.py's logging initialization and migration execution
  into a `__name__ == "__main__"` stanza so it doesn't immediately happen
  when imported by another module.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 19:50:51 -07:00
Kevin Morris
2df90ce280 port over base HTML layout from PHP to FastAPI+Jinja2
+ Mounted static files (at web/html) to /static.
+ Added AURWEB_VERSION to aurweb.config (this is used around HTML
  to refer back to aurweb's release on git.archlinux.org), so we
  need it easily accessible in the Python codebase.
+ Implemented basic Jinja2 partials to put together whole aurweb
  pages. This may be missing some things currently and is a WIP
  until this set is ready to be merged.
+ Added config [options] aurwebdir = YOUR_AUR_ROOT; this configuration
  option should specify the root directory of the aurweb project.
  It is used by various parts of the FastAPI codebase to target
  project directories.

Added routes via aurweb.routers.html:
    * POST /language: Set your session language.
    * GET /favicon.ico: Redirect to /static/images/favicon.ico.
        * Some browsers always look for $ROOT/favicon.ico to get an icon
          for the page being loaded, regardless of a specified "shortcut
          icon" given in a <link> directive.
    * GET /: Home page; WIP.

* Updated aurweb.routers.html.language passes query parameters to
  its next redirection.

When calling aurweb.templates.render_template, the context passed should
be formed via the aurweb.templates.make_context. See
aurweb.routers.html.index for an example of this.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 19:50:51 -07:00
Jelle van der Waa
1ff822bb14 Use the clipboard API for copy paste
The Document.execCommand API is deprecated and no longer recommended to
be used. It's replacement is the much simpler navigator.clipboard API
which is supported in all browsers except internet explorer.

Signed-off-by: Eli Schwartz <eschwartz@archlinux.org>
2021-06-05 19:50:51 -07:00
Marcus Andersson
bda9256ab1 Add error color when package is orphaned
Signed-off-by: Eli Schwartz <eschwartz@archlinux.org>
2021-06-05 19:49:55 -07:00
Kevin Morris
c1e29e90ca aurweb: Globalize a Translator instance, add more utility
+ Added SUPPORTED_LANGUAGES, a global constant dictionary of
  language => display pairs for languages we support.
+ Add Translator.get_translator, a function used to retrieve a
  translator after initializing it (if needed). Use `fallback=True`
  while creating languages, in case we setup a language that we
  don't have a translation for, it will noop the translation.
  This is particularly useful for "en," since we do not translate
  it, but doing this will allow us to go through our normal translation
  flow in any case.
+ Added typing.
+ Added get_request_language, a function that grabs the language for
  a request session, defaulting to aurweb.config [options] default_lang.
+ Added get_raw_translator_for_request, a function that retrieves
  the concrete translation object for a given language.
+ Added tr, a jinja2 contextfilter that can be used to inline translate
  strings in jinja2 templates.
+ Added `python-jinja` dep to .gitlab-ci.yml. This needs to be
  included in documentation before this set is merged in.
+ Introduce pytest units (test_l10n.py) in `test` along with
  __init__.py, which marks `test` as a test package.
+ Additionally, fix up notify.py to use the global translator. Also
  reduce its source width to <= 80 by newlining some code.
+ Additionally, prepare locale in .gitlab-ci.yml and add
  aurweb.config [options] localedir to config.dev with YOUR_AUR_ROOT
  like others.

Signed-off-by: Kevin Morris <kevr@0cost.org>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-06-05 19:49:42 -07:00
Leonidas Spyropoulos
21140e28a8 Filter out current username from co-maintainers list.
Closes: #8

Signed-off-by: Leonidas Spyropoulos <artafinde@gmail.com>
Signed-off-by: Eli Schwartz <eschwartz@archlinux.org>
2021-06-05 19:49:42 -07:00
Kevin Morris
cd3e880264 add Dockerfile
This docker file downloads deps, sets up some things beforehand and
finishes with running our entire collection of tests.

Signed-off-by: Kevin Morris <kevr@0cost.org>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-06-05 19:49:19 -07:00
Kevin Morris
52ab056e18 update documentation for FastAPI tests and deps.
Additionally, we now ask for two more favors from contributors:

1. All source modified or added within a patchset **must** maintain
   equivalent or increased coverage by providing tests that use the
   functionality.
2. Please keep your source within an 80 column width.

PS: Sneak a few test Makefile and gitlab fixes.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 19:49:19 -07:00
Kevin Morris
57c11ae13f install aurweb package & init db on GitLab CI
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 19:49:19 -07:00
Kevin Morris
6d08789ac1 add test_popupdate.py
We had no coverage over aurweb.scripts.popupdate. This test covers
all of its functionality.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 19:49:19 -07:00
Kevin Morris
4b7609681d add test_exceptions.py
This helps gain coverage over aurweb.exceptions regardless
of their actual use in the testing base.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 19:49:19 -07:00
Kevin Morris
e800cefe95 Makefile: run pytest units
Important note: Python tests will repeatedly clear out tables
that they test against; for this reason, one should always run
the shell tests first. The __init__.py file is necessary for
coverage to collect data from the tests being run.

At this point in FastAPI development, I'd like to encourage a
few things going forward:

1. Any time you contribute to the FastAPI codebase, you **must**
   maintain equal or increased coverage on the overall source.
   Developers are highly appreciated for adding tests in your
   specific domain of addition or modification that may be missing
   coverage. Our goal is 100% coverage, and all newly added files
   **must** have 100% coverage through tests.
2. All source should be formatted with the autopep8 tool and
   kept within an 80 column width, with the exception of HTML
   templates.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 19:49:19 -07:00
Kevin Morris
4230772e3b add .coveragerc, update .gitignore
Now, .coveragerc enforces a minimum overall 82% coverage, as is
the current standing. Providing less than 100% coverage for added
code should reduce the overall coverage and eventually reach 81%
or below, causing coverage report to fail on execution.

Developers should increase the failure minimum as they increase
coverage across the uncovered code.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 19:49:19 -07:00
Kevin Morris
bac38edd48 [db] fix schema and migration for case insensitivity
Some of the columns that were changed still want to be
case insensitive. Good thing our tables have nice
separation.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-06-05 15:08:18 -07:00
Leonidas Spyropoulos
b1121dc6ca Adds Alembic migration for DB/Tables conversion to utf8mb4
MySql defaults to `utf8` and case insensitive collation so migrate these to case sensitive and `utf8mb4`

Closes #21

Signed-off-by: Leonidas Spyropoulos <artafinde@gmail.com>
2021-05-18 05:42:36 -07:00
Leonidas Spyropoulos
0d68b914bf Conditionally apply SSOAccountId migration to support misaligned databases
Closes: #34

Signed-off-by: Leonidas Spyropoulos <artafinde@gmail.com>
2021-05-18 05:42:36 -07:00
Kevin Morris
82f6d2ce75 alembic: fix ef39fcd6e1cd downgrade
op.drop_constraint requires a valid field to drop the constraint on.
Without this, downgrade cannot occur.

Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-05-18 05:42:36 -07:00
Kevin Morris
b41422450a aurweb.db: only pass check_same_thread with sqlite
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-05-18 05:42:36 -07:00
Kevin Morris
25393dc326 Fix incorrect construction of MySQL SQLAlchemy URL
Signed-off-by: Kevin Morris <kevr@0cost.org>
2021-05-18 05:42:36 -07:00
Marcus Andersson
4fa220850f
Add error color when package is orphaned
Signed-off-by: Eli Schwartz <eschwartz@archlinux.org>
2021-05-13 16:50:51 -04:00
Eli Schwartz
e454a0200c
git update hook: gracefully error on completely broken .SRCINFO
I've seen this happen a bunch of times now. Someone cannot push to the
AUR, and the error report is some traceback with a KeyError which is
difficult to understand without context:

remote: Traceback (most recent call last):
remote:   File "/srv/http/aurweb/aur.git/hooks/update", line 33, in <module>
remote:     sys.exit(load_entry_point('aurweb==5.0.0', 'console_scripts', 'aurweb-git-update')())
remote:   File "/usr/lib/python3.9/site-packages/aurweb-5.0.0-py3.9.egg/aurweb/git/update.py", line 306, in main
remote: KeyError: 'pkgbase'

Eventually it turns out that their .SRCINFO file is... badly corrupted.
Generally, they managed to accidentally commit an *empty* file instead
of a .SRCINFO, and in all cases, the problem was on the very first
lookup for 'pkgbase'.

Point people to the actual failing commit, and have a nicely formatted
message indicating that the .SRCINFO is completely invalid.

Signed-off-by: Eli Schwartz <eschwartz@archlinux.org>
2021-05-10 23:22:00 -04:00
Kevin Morris
19652d6cbe
swap uvicorn out for hypercorn
uvicorn is subjectively nicer to play with for local dev work, but
hypercorn is required in order to do HTTP/2 which is fairly
performance-important.

Signed-off-by: Kevin Morris <kevr@0cost.org>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
Co-authored-by: Eli Schwartz <eschwartz@archlinux.org>
Signed-off-by: Eli Schwartz <eschwartz@archlinux.org>
2021-05-10 23:22:00 -04:00
Kristian Klausen
bab74dd307
Update wiki links to the new short URL
Done with: find -type f -exec sed -Ee ':wiki.archlinux.org: s:(wiki.archlinux.org)/index.php/:\1/title/:g' -i {} \;

Fixes #16

[1] https://gitlab.archlinux.org/archlinux/infrastructure/-/merge_requests/335

Signed-off-by: Eli Schwartz <eschwartz@archlinux.org>
2021-05-09 18:06:32 -04:00
Eli Schwartz
8ec170b3e0
dos2unix a file with Windows linebreaks that editors and human reviewers hate
Signed-off-by: Eli Schwartz <eschwartz@archlinux.org>
2021-05-02 21:46:35 -04:00
Eli Schwartz
c3035a9039
add https://EditorConfig.org setup to ensure consistent style
Mostly here to make sure people continue to use tabbed indents for php
and the TAP tests, since that is what they are currently using.

Signed-off-by: Eli Schwartz <eschwartz@archlinux.org>
2021-04-28 18:06:06 -04:00
Jelle van der Waa
d668ef0bcd Resolve SQL Error when deleting an account
The account deletion code tries to remove user from PackageNotifications
using the wrong column UsersID to identify the user by id. In the
PackagePackageNotifications table the foreign key is called UserID. In
the future ideally this would be unified into UserID for all tables.

Closes: #12
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-03-29 18:39:02 -04:00
Felix Yan
21c457817f Use jsDelivr instead of Google CDN for jquery
jsdelivr is another free CDN service for open source projects.

The main motivation for this change is that it is the only one that works fairly
well across the globe. The Google CDN service is known to be hardly
accessible in mainland China, unfortunately.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:26:32 -05:00
Lukas Fleischer
933d2705f9 Fetch Transifex image from https://www.transifex.com
Fixes GitLab issue #3.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:25:21 -05:00
Lukas Fleischer
62b413f6b7 .gitignore: add test/trash directory*
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:25:21 -05:00
Justin Kromlinger
d5d333005e RSS: Decrease cache time and increase item count
I think after 10-15 years we might want to adjust those values. With a
30min cache and 20 items per creation I would bet some new AUR packages
might be swept under the carpet.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:25:21 -05:00
Justin Kromlinger
eb11943fed RSS: Always provide a GUID
https://validator.w3.org/feed/docs/warning/MissingGuid.html
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:25:21 -05:00
Justin Kromlinger
1d0c6ffe24 RSS: Make sure image title matches channel title
https://validator.w3.org/feed/docs/warning/ImageTitleDoesntMatch.html
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:25:21 -05:00
Justin Kromlinger
78dbbd3dfa RSS: Set proper content type header
https://validator.w3.org/feed/docs/warning/UnexpectedContentType.html
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:25:21 -05:00
Justin Kromlinger
568e0d2fa3 RSS: Add atom self link
https://validator.w3.org/feed/docs/warning/MissingAtomSelfLink.html
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:25:21 -05:00
Jakub Klinkovský
bc972089a1 Fix WHERE clause for keyword search queries with empty keywords
When the keyword parameter is empty, the AND clause has to be omitted,
otherwise we get an SQL syntax error:

... WHERE PackageBases.PackagerUID IS NOT NULL AND () ...

This got broken in commit 9e30013aa4fc6ce3a3c9f6f83a6fe789c1fc2456
Author: Kevin Morris <kevr.gtalk@gmail.com>
Date:   Sun Jul 5 18:19:06 2020 -0700

Support conjunctive keyword search in RPC interface

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:25:21 -05:00
Jakub Klinkovský
3062a78a92 gendummydata.py: optimize iteration for big numbers of pkgs
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:25:21 -05:00
Jakub Klinkovský
51a3535820 gendummydata.py: set MAX_USERS and MAX_PKGS to more realistic values
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:25:21 -05:00
Jakub Klinkovský
879c0622d6 gendummydata.py: set exit code to 1 when there is an error
Of course the default exit code is 0...

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:25:21 -05:00
Jakub Klinkovský
92e315465b gendummydata.py: remove unused database connection variables
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:25:21 -05:00
Filipe Laíns
db75a5528e doc: simplify database setup instructions in TESTING
Signed-off-by: Filipe Laíns <lains@archlinux.org>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:25:21 -05:00
Filipe Laíns
e62d472708 doc: add missing gendummydata.py dependencies in TESTING
Signed-off-by: Filipe Laíns <lains@archlinux.org>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:25:21 -05:00
Filipe Laíns
4e4f5855f1 doc: fix AUR_CONFIG in TESTING
Signed-off-by: Filipe Laíns <lains@archlinux.org>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:25:21 -05:00
Filipe Laíns
83d228d9e8 spawn: expand AUR_CONFIG to the full path
This allows using a relative path for the config. PHP didn't play well
with it.

Signed-off-by: Filipe Laíns <lains@archlinux.org>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:25:21 -05:00
Frédéric Mangano-Tarumi
8c28ba6e7f Redirect to referer after SSO login
Introduce a `redirect` query argument to SSO login endpoints so that
users are redirected to the page they were originally on when they
clicked the Login link.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:25:21 -05:00
Frédéric Mangano-Tarumi
87815d37c0 Remove the per-user session limit
This feature was originally introduced by
f961ffd9c7 as a fix for FS#12898
<https://bugs.archlinux.org/task/12898>.

As of today, it is broken because of the `q.SessionID IS NULL` condition
in the WHERE clause, which can’t be true because SessionID is not
nullable. As a consequence, the session limit was not applied.

The fact the absence of the session limit hasn’t caused any issue so
far, and hadn’t even been noticed, suggests the feature is unneeded.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:25:19 -05:00
Frédéric Mangano-Tarumi
be31675b65 Guard OAuth exceptions to provide better messages
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:24:30 -05:00
Frédéric Mangano-Tarumi
5fb4fc12de HTML error pages for FastAPI
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:24:30 -05:00
Frédéric Mangano-Tarumi
202ffd8923 Update last login information on SSO login
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:24:30 -05:00
Frédéric Mangano-Tarumi
9290eee138 Stop redirecting stderr with proc_open
Error outputs were piped to a temporary buffer that wasn’t read by
anyone, making debugging hard because errors were completely silenced.
By not explicitly redirecting stderr on proc_open, the subprocess
inherits its parent stderr.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:24:30 -05:00
Frédéric Mangano-Tarumi
a1a742b518 aurweb.spawn: Support stdout redirections to non-tty
Only ttys have a terminal size. If we can’t obtain it, we’ll just use 80
as a sane default.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:24:30 -05:00
Kevin Morris
445a991ef1 Exclude suspended Users from being notified
The existing notify.py script was grabbing entries regardless
of user suspension. This has been modified to only send notifications
to unsuspended users.

This change was written as a solution to
https://bugs.archlinux.org/task/65554.

Signed-off-by: Kevin Morris <kevr.gtalk@gmail.com>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:24:30 -05:00
Kevin Morris
efe99dc16f Support conjunctive keyword search in RPC interface
Newly supported API Version 6 modifies `type=search` for _by_ type
`name-desc`: it now behaves the same as `name-desc` search through the
https://aur.archlinux.org/packages/ search page.

Search for packages containing the literal keyword `blah blah` AND `haha`:
https://aur.archlinux.org/rpc/?v=6&type=search&arg="blah blah"%20haha

Search for packages containing the literal keyword `abc 123`:
https://aur.archlinux.org/rpc/?v=6&type=search&arg="abc 123"

The following example searches for packages that contain `blah` AND `abc`:
https://aur.archlinux.org/rpc/?v=6&type=search&arg=blah%20abc

The legacy method still searches for packages that contain `blah abc`:
https://aur.archlinux.org/rpc/?v=5&type=search&arg=blah%20abc
https://aur.archlinux.org/rpc/?v=5&type=search&arg=blah%20abc

API Version 6 is currently only considered during a `search` of `name-desc`.

Note: This change was written as a solution to
https://bugs.archlinux.org/task/49133.

PS: + Some spacing issues fixed in comments.

Signed-off-by: Kevin Morris <kevr.gtalk@gmail.com>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:24:30 -05:00
Frédéric Mangano-Tarumi
239988def7 Build a translation facility for FastAPI
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:24:30 -05:00
Frédéric Mangano-Tarumi
e323156947 SSO: Port account suspension
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:24:30 -05:00
Frédéric Mangano-Tarumi
0e08b151e5 SSO: Port IP ban checking
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:24:30 -05:00
Frédéric Mangano-Tarumi
357dba87b3 Save id_token for the SSO logout
As far as I can see, Keycloak ignores it entirely. I can login in as SSO
user A, then disconnect from the SSO directly and reconnect as user B,
but when I disconnect user A from AUR, Keycloak disconnects B even
though AUR passed it an ID token for A.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:24:30 -05:00
Frédéric Mangano-Tarumi
4d0f2d2279 Implement SSO logout
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:24:30 -05:00
Frédéric Mangano-Tarumi
d12ea08fca SSO: Add an SSO option in the login page
We’ll probably change the whole login page in the future, but this makes
development easier.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:24:30 -05:00
Frédéric Mangano-Tarumi
4bf8228324 SSO: Explain the rationale behind prompt=login
We might reconsider it in the future.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:24:30 -05:00
Frédéric Mangano-Tarumi
8d5244d0c0 Fix typos in CONTRIBUTING.md
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:24:30 -05:00
Frédéric Mangano-Tarumi
42f8f160b6 Open AUR sessions from SSO
Only the core functionality is implemented here. See the TODOs.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:24:30 -05:00
Frédéric Mangano-Tarumi
c77e9d1de0 Integrate SQLAlchemy into FastAPI
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:24:30 -05:00
Frédéric Mangano-Tarumi
a5554c19a9 Add SSO account ID in table Users
This column holds a user ID issed by the single sign-on provider. For
Keycloak, it is an UUID. For more flexibility, we will be using a
standardly-sized VARCHAR field.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:24:30 -05:00
Frédéric Mangano-Tarumi
3f31d149a6 aurweb.l10n: Translate without side effects
The install method in Python’s gettext API aliases the translator’s
gettext method to an application-global _(). We don’t use that anywhere,
and it’s clear from aurweb’s Translator interface that we want to
translate a piece of text without affecting any global namespace.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:24:30 -05:00
Frédéric Mangano-Tarumi
2b439b8199 Guide to setting up Keycloak for the SSO
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:24:30 -05:00
Frédéric Mangano-Tarumi
3b347d3989 Crude OpenID Connect client using Authlib
Developers can go to /sso/login to get redirected to the SSO. On
successful login, the ID token is displayed.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:24:30 -05:00
Frédéric Mangano-Tarumi
b1300117ac aurweb.spawn: Fix isort errors
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:24:30 -05:00
Frédéric Mangano-Tarumi
0e3bd8b596 Remove the FastAPI /hello test route
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:24:30 -05:00
Frédéric Mangano-Tarumi
8c868e088c Introduce conf/config.dev for development
conf/config.dev’s purpose is to provide a lighter configuration template
for developers, and split development-specific options off the default
configuration file.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:24:30 -05:00
Frédéric Mangano-Tarumi
5be07a8a9e aurweb.spawn: Integrate FastAPI and nginx
aurweb.spawn used to launch only PHP’s built-in server. Now it spawns a
dummy FastAPI application too. Since both stacks spawn their own HTTP
server, aurweb.spawn also spawns nginx as a reverse proxy to mount them
under the same base URL, defined by aur_location in the configuration.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:24:30 -05:00
Filipe Laíns
d4abe0b72d Add CONTRIBUTING.md
Signed-off-by: Filipe Laíns <lains@archlinux.org>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:24:30 -05:00
Filipe Laíns
41a8493411 pre-commit: add initial config
Signed-off-by: Filipe Laíns <lains@archlinux.org>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:24:30 -05:00
Filipe Laíns
8f47b8d731 isort: add initial config
Signed-off-by: Filipe Laíns <lains@archlinux.org>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:24:30 -05:00
Filipe Laíns
4cf94816ae flake8: add initial config
Signed-off-by: Filipe Laíns <lains@archlinux.org>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:24:30 -05:00
Filipe Laíns
8d1be7ea8a Refactor code to comply with flake8 and isort
Signed-off-by: Filipe Laíns <lains@archlinux.org>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:24:30 -05:00
Filipe Laíns
48b58b1c2f ci: remove Travis CI
We are are moving to Gitlab CI.

Signed-off-by: Filipe Laíns <lains@archlinux.org>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:24:30 -05:00
Frédéric Mangano-Tarumi
8a13500535 Create aurweb.spawn for spawing the test server
This program makes it easier for developers to spawn the PHP server
since it fetches automatically what it needs from the configuration
file, rather than having the user explicitly pass arguments to the php
executable.

When the setup gets more complicated as we introduce Python,
aurweb.spawn will keep providing the same interface, while under the
hood it is planned to support running multiple sub-processes.

Its Python interface provides an way for the test suite to spawn the
test server when it needs to perform HTTP requests to the test server.

The current implementation is somewhat weak as it doesn’t detect when a
child process dies, but this is not supposed to happen often, and it is
only meant for aurweb developers.

In the long term, aurweb.spawn will eventually become obsolete, and
replaced by Docker or Flask’s tools.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:24:30 -05:00
Filipe Laíns
23f6dd16a7 ci: add cache to gitlab ci
Signed-off-by: Filipe Laíns <lains@archlinux.org>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:24:30 -05:00
Filipe Laíns
db02227cc4 ci: add gitlab ci
Signed-off-by: Filipe Laíns <lains@archlinux.org>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:24:30 -05:00
Eli Schwartz
71740a75a2 rewrite query to support both mysql/sqlite
Signed-off-by: Eli Schwartz <eschwartz@archlinux.org>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:22:11 -05:00
Eli Schwartz
be5197a5fe prevent running mysql-specific query in sqlite
We usually guard such queries and have both mysql and sqlite branches.
But I have not implemented the sqlite branch. Given sqlite is typically
used for local dev setups, the fact that "users with more than the
configured max simultaneous logins" can avoid getting some logins
annulled is probably not a huge risk.

And this always *used* to fail on sqlite, silently. Now, in php 8, it
raises PDOException, which prevents running the test server

Document this as a FIXME for now, until someone reimplements the query
for sqlite.

Signed-off-by: Eli Schwartz <eschwartz@archlinux.org>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:22:11 -05:00
Eli Schwartz
d92dd69aa3 fix broken SQL query that always failed
Due to missing whitespace at the end of strings during joining, we ended
up with the query fragment

"DelTS IS NULLAND NOT PinnedTS"

which should be

"DelTS IS NULL AND NOT PinnedTS"

So the check for pinned comments > 5 likely always failed.

In php 7, a completely broken query that raises exceptions in the
database engine was silently ignored... in php 8, it raises

Uncaught PDOException: SQLSTATE[HY000]: General error: 1 near "PinnedTS": syntax error in <file>

and aborts the page building. End result: users with permission to pin
comments cannot see any comments, or indeed page content below the first
comment header

Signed-off-by: Eli Schwartz <eschwartz@archlinux.org>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2021-02-20 11:22:11 -05:00
Frederik Schwan
d5e308550a Fix requests not being sent to the Cc recipients
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-10-13 20:26:51 -04:00
Morten Linderud
613364b773 pkg_search_page: Limit number of results on package search
The current package search query is quite poorly optimized and becomes a
resource hog when the offsets gets large enough. This DoSes the service.

A quick fix is to just ensure we have some limit to the number of hits
we return. The current hardcoding of 2500 is based on the following:

    * 250 hits per page max
    * 10 pages

We can maybe consider having it lower, but it seems easier to just have
this a multiple of 250 in the first iteration.

Signed-off-by: Morten Linderud <morten@linderud.pw>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-09-05 10:21:16 -04:00
Lukas Fleischer
c4f4ac510b Deliver emails to Cc in smtplib code path
When using the sendmail() function with smtplib.SMTP or
smtplib.SMTP_SSL, the list of actual recipients for the email (to be
translated to RCPT commands) has to be provided as a parameter.

Update the notification script and add all Cc recipients to that
parameter.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-08-27 07:19:57 -04:00
Jelle van der Waa
03a6fa2f7e Call sendmail with to, not recipient
After f7a57c8 (Localize notification emails, 2018-05-17), the
server.sendmail line was not updated to now send the to the email
address but instead sends to (email, 'en') and as sendmail accepts an
iterable an email is also send to 'en'.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-08-26 08:32:32 -04:00
Lukas Fleischer
169607f153 Fix PHP notices in the account form
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-04-05 15:59:56 -04:00
Lukas Fleischer
1369eb87b3 Fix invalid session ID check
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-04-05 15:59:55 -04:00
Lukas Fleischer
853ed9a950 Release 5.0.0
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-03-27 08:51:15 -04:00
Lukas Fleischer
279d8042e3 Add new upgrade instructions
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-03-27 08:49:34 -04:00
Lukas Fleischer
a09c4d8168 Translation updates from Transifex
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-03-27 08:31:46 -04:00
Frédéric Mangano-Tarumi
31a5b40b5c Map BIGINT to INTEGER for SQLite
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-03-22 16:50:10 -04:00
Frédéric Mangano-Tarumi
28ba3f77dc Write test/README.md to help working with tests
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-02-29 14:57:46 +01:00
Frédéric Mangano-Tarumi
bf7c49158c test/Makefile: Run tests with prove when available
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-02-29 14:56:47 +01:00
Frédéric Mangano-Tarumi
90c0a361b5 Support running tests from any directory
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-02-29 14:56:47 +01:00
Frédéric Mangano-Tarumi
e374a91feb Change the extension of TAP test suites to .t
This is the common convention for TAP, and makes harnesses like prove
automatically detect them. Plus, test suites don’t have to be shell
scripts anymore.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-02-27 16:44:36 +01:00
Frédéric Mangano-Tarumi
81d55e70ee Disable Alembic support on test databases
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-02-27 16:44:36 +01:00
Frédéric Mangano-Tarumi
e4cbe264cf Create an initial Alembic migration
This way the database will get stamped, and Git will create the
`versions` directory without which Alembic won’t work.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-02-27 16:44:36 +01:00
Frédéric Mangano-Tarumi
a8a1f74a92 Set up Alembic for database migrations
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-02-27 16:44:36 +01:00
Frédéric Mangano-Tarumi
7188743fc3 Migrate the database schema to SQLAlchemy
The new schema was generated with sqlacodegen and then manually adjusted
to fit schema/aur-schema.sql faithfully, both in the organisation of the
code and in the SQL generated by SQLAlchemy.

Initializing the database now requires the new tool aurweb.initdb.
References to aur-schema.sql have been updated and the old schema
dropped.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-02-27 16:44:36 +01:00
Lukas Fleischer
4b2102ceb2 Properly escape passwords in the account edit form
Addresses FS#65639.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-02-27 16:44:36 +01:00
Lukas Fleischer
cbab9870c1 Fix HTML code in the account search results table
Do not add an opening <tbody> tag for every row. Instead, wrap all rows
in <tbody></tbody>.

While at it, also simplify the code used to color the rows.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-02-26 13:51:18 +01:00
Lukas Fleischer
afe3f5d0e5 README.md: add references to Transifex
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-02-26 13:51:18 +01:00
Yaron Shahrabani
33d8fe035e README.md: fix a small typo
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-02-26 13:51:18 +01:00
Eli Schwartz
050b08081a Fix more PHP 7.4 warnings
The try_login() function documents it returns an array containing an
'error' key, and our only caller *only* consults the 'error' key. Then
the function returns null instead of an array, if the login succeeded!

I question why we bother returning the new SID if we never use it,
surely we could either return the error or return default null. But, for
now, I'm just going to fix it to return what it's actually supposed to,
without changing the API.

Signed-off-by: Eli Schwartz <eschwartz@archlinux.org>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-02-13 09:11:34 +01:00
Eli Schwartz
5ca1e271f9 Fix PHP 7.4 warnings
If a db query returned NULL instead of an array, then accessing $row[0]
now throws a warning. The undocumented behavior of evaluating to NULL
is maintained, and we want to return NULL anyway, so add a check for the
value and fall back on the default function return type.

Signed-off-by: Eli Schwartz <eschwartz@archlinux.org>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-02-13 09:11:28 +01:00
Lukas Fleischer
65c98d1216 Use relative URIs for {source_file,log,commit}_uri
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-02-11 13:21:28 +01:00
Lukas Fleischer
b855ce9452 Make SMTP port and authentication configurable
Add more options to configure the smtplib implementation for sending
notification emails.

The port can be changed using the new smtp-port option.

Encryption can be configured using smtp-use-ssl and smtp-use-starttls.
Keep in mind that you usually also need to change the port when enabling
either of these options.

Authentication can be configured using smtp-user and smtp-password.
Authentication is disabled if either of these values is empty.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-02-11 12:19:56 +01:00
Lukas Fleischer
de549fb2d5 Support smtplib for sending emails
Support mail delivery without a local MTA. Instead, an SMTP server can
now be configured using the smtp-server option in the [notifications]
section. In order to use this option, the value of the sendmail option
must be empty.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-02-11 12:19:56 +01:00
Lukas Fleischer
3f2654e79e Update README and convert to Markdown syntax
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-02-11 12:19:56 +01:00
Lukas Fleischer
d4632aaffa Translation updates from Transifex
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-02-10 11:05:27 +01:00
Frédéric Mangano-Tarumi
e15d5c8180 rendercomment: use python-markdown's new registration API
First, this gets rid of the deprecation warnings Python displayed.

Second, this fixes the case where a link contained a pair of
underscores, which used to be interpreted as an emphasis because the
linkify processor ran after the emphasis processor.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-02-02 20:49:16 +01:00
Frédéric Mangano-Tarumi
81faab9978 rendercomment: test headings lowering
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-02-02 20:49:16 +01:00
Frédéric Mangano-Tarumi
127bb4c84c rendercomment: safer Flyspray task linkification
When an FS#123 is part of a code block, it must not be converted into a
link. FS#123 may also appear inside an URL, in which case regular
linkifaction of URLs must take precedence.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-02-02 20:49:16 +01:00
Frédéric Mangano-Tarumi
199f34e42e rendercomment: safer auto-linkification of URLs
Fixes a few edge cases:

- URLs within code blocks used to get redundant <> added, breaking bash
  code snippets like `curl https://...` into `curl <https://...>`.

- Links written with markdown's <https://...> syntax also used to get an
  extra pair of brackets.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-02-02 12:12:43 +01:00
Frédéric Mangano-Tarumi
0fc69e96bd rendercomment: add a test for Git commit links
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-02-02 12:12:43 +01:00
Frédéric Mangano-Tarumi
c277a3de8f rendercomment: respectful linkification of Git commits
Turn the git-commits markdown processor into an inline processor, which
is smart enough not to convert Git hashes contained in code blocks or
links.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-02-02 12:12:43 +01:00
Lukas Fleischer
8ff21fd39c Update message catalog
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-02-02 12:12:43 +01:00
Lukas Fleischer
aa555f9ae5 Explain syntax/features in the comments section
Addresses FS#64983.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-02-02 12:12:43 +01:00
Lukas Fleischer
e5f8fe5528 Explain the hide email address setting
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-02-02 12:12:43 +01:00
Lukas Fleischer
ee2aa9755f Add support for backup email addresses
Support secondary email addresses that can be used to recover an account
in case access to the primary email address is lost. Reset keys for an
account are always sent to both the primary and the backup email
address.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-02-02 12:12:42 +01:00
Lukas Fleischer
e5a839bf0b Add option to send reset key for a given user name
In addition to supporting email addresses in the reset key form, also
support user names. The reset key is then sent to the email address in
the user's profile.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-02-02 12:12:42 +01:00
Lukas Fleischer
23c0c9c372 Update copyright range in the cgit footer 2020-01-30 14:23:28 +01:00
Lukas Fleischer
def2787b45 Require password when changing account information
Since commits daee20c (Require current password when setting a new one,
2020-01-30) and 8fc8898 (Require password when deleting an account,
2020-01-30), changing a password and deleting an account require the
current password. Extend this to all other profile changes.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-01-30 14:05:24 +01:00
Lukas Fleischer
8fc8898fef Require password when deleting an account
Further reduce the attack surface in case of a stolen session ID.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-01-30 13:25:15 +01:00
Lukas Fleischer
7aa420d24d Verify current password against logged in user
When changing the password of an account, instead of asking for the old
password of the account, ask for the password of the currently logged in
user. This allows privileged users to edit other accounts without
knowing their passwords.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-01-30 13:25:15 +01:00
Lukas Fleischer
f090896fa1 Undo accidental code addition
Rollback an accidental change that sneaked into commit daee20c (Require
current password when setting a new one, 2020-01-30).

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-01-30 13:25:15 +01:00
Lukas Fleischer
d0e5c3db69 t2500: fix test cases
Since commit eeaa1c3 (Separate text from footer in notification emails,
2020-01-04), information about unsubscribing from notifications is added
in a signature block. Fix the test cases accordingly.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-01-30 13:25:15 +01:00
Lukas Fleischer
4ececd6041 Keep signature delimiters intact in notifications
Since commit eeaa1c3 (Separate text from footer in notification emails,
2020-01-04), information about unsubscribing from notifications is added
in a signature block. However, the code to format the email body trimmed
the RFC 3676 signature delimiter, replacing "-- " by "--". Fix this by
adding a special case for signature delimiters.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-01-30 13:25:15 +01:00
Lukas Fleischer
daee20c694 Require current password when setting a new one
Prevent from easily taking over an account by changing the password with
a stolen session ID.

Fixes FS#65325.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-01-30 10:31:26 +01:00
Stephan Springer
eeaa1c3a32 Separate text from footer in notification emails
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2020-01-06 16:37:04 +01:00
Lukas Fleischer
58aa0a9e45 Copy Git repository URL on click
The Git repository URLs are not meant to be visited using a web browser.
Copy the link to the clipboard instead.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2019-12-11 15:40:59 -05:00
Lukas Fleischer
f7f5152be5 .gitignore: add schema/aur-schema-sqlite.sql
The SQLite schema is generated automatically from the main schema and
used in the test suite.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2019-11-24 14:01:28 -05:00
Lukas Fleischer
ee959c9907 t2500: fix test case for orphan request notifications
Since commit a66c7fa (notify.py: Use a/an correctly when sending request
notifications, 2019-08-09), the body of notification emails sent when
filing orphan requests refers to "an orphan request" instead of "a
orphan request".

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2019-11-23 16:23:00 -05:00
Lukas Fleischer
2422fb020b Store timestamp and user ID when closing requests
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2019-11-23 12:30:46 -05:00
Lukas Fleischer
4b97789bab Don't require all Python database modules to be installed
We support multiple database backends. Don't require Python modules for
all backends to be installed.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2019-11-23 12:30:08 -05:00
Lukas Fleischer
882c011e74 Upgrade Sharness to 1.1.0
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2019-11-23 12:25:23 -05:00
Lukas Fleischer
771ced3236 git-serve: check update hook permissions
Verify that the update hook exists and is executable before running Git
to prevent from broken repositories when permissions are broken.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2019-11-23 11:18:16 -05:00
Lukas Fleischer
86e4cd0731 aurjson: use APCu/memcached for rate limiting
There's no need to use permanent storage for rate limiting information;
try to keep it in memory if caching is enabled.

From experiments with our live setup, this reduces the number of
INSERT/DELETE operations per second from 15 to almost 0. Disk writes on
the server hosting the AUR are reduced by 90% (from ~3MB/s to ~300kB/s).

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2019-11-02 18:32:07 -04:00
Lukas Fleischer
a29155ac5b Document maintenance tasks and internals
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2019-11-01 16:45:31 -04:00
Lukas Fleischer
99a3ced73b Display popularity with less decimal points
Limit the display to two decimal points for packages with a popularity
of at least 0.2.

Suggested-by: Allan McRae <allan@archlinux.org>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2019-10-26 21:09:35 -04:00
Lukas Fleischer
c1e5ffb12a Release 4.8.0
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2019-10-19 00:20:40 -04:00
Lukas Fleischer
b922811061 Translation updates from Transifex
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2019-10-19 00:20:09 -04:00
Lukas Fleischer
dd0e090301 Sync CSS with archweb
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2019-10-19 00:19:16 -04:00
Lukas Fleischer
3ec0f6bfbf Cache package requirements and sources
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2019-10-09 15:13:15 -04:00
Lukas Fleischer
734527370d Make package details cache TTL configurable
The TTL for package details can be much longer than for generic values
since they never change. Note that when an update is pushed via Git, all
packages belonging to that package base are deleted and new packages are
created.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2019-10-07 12:21:03 -04:00
Lukas Fleischer
f804ea4abb Cache package licenses, groups and relations
Cache more package details if the global caching mechanism is enabled.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2019-10-07 09:31:18 -04:00
Lukas Fleischer
6493d00db5 aurjson: cache extended fields
Cache the results of the extended fields computation if the global
caching mechanism is enabled.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2019-10-06 16:13:38 -04:00
Lukas Fleischer
1283fe4918 Cache package provider and dependency information
The package provider and dependency queries are quite CPU-intensive and
usually yield rather small result sets. Cache these values if the global
caching mechanism is enabled.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2019-10-06 16:13:38 -04:00
Lukas Fleischer
ef8bad5bbf Make CAPTCHA salt invalidation more robust
With the previous implementation, unlucky users could have their CAPTCHA
be invalidated by a single account creation while filling out their
account registration form.

Make this more robust by allowing up to five account registrations
before rejecting a CAPTCHA salt.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2019-10-05 14:21:53 -04:00
Lukas Fleischer
d6ae970785 Add a simple CAPTCHA to the sign up form
Add a CAPTCHA to protect against automated account creation. The CAPTCHA
changes whenever three new accounts are registered.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2019-10-05 13:44:00 -04:00
Lars Rustand
a66c7fa615 notify.py: Use a/an correctly when sending request notifications
Will no longer send notifications about "a orphan request", but determine
whether to use a/an based on the first character of the request type.

Signed-off-by: Lars Rustand <rustand.lars@gmail.com>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2019-08-19 14:47:07 -04:00
Eli Schwartz
3ac958ac01
Move permission for LIST_COMMENTS to dev/tu block
In commit 3578e77ad4 we implemented
listing of comments from the account details page , but this was
intended to only be available to TUs and Devs. As the comment says:
"display the comment list if they're a TU/dev"

The credential checking code, however, set this credential for all
users, contrary to the intention of the commit.

In order to preserve the ability to list a person's own comments, also
declare the allowed uids based on the profile being viewed.
2019-08-18 13:01:37 -04:00
Johannes Löthberg
7f008b0bc4 pkgreqfuncs: Don't leave out non-default ClosureComment column
Since 09cb61a (schema: Remove invalid default values for TEXT columns,
2017-04-15) the PackageRequests.ClosureComment field no longer has a
default value.

Signed-off-by: Johannes Löthberg <johannes@kyriasis.com>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2019-07-30 13:51:27 -04:00
Michael Straube
23fc96b45b Update copyright year in the cgit footer template
Signed-off-by: Michael Straube <michael.straube@posteo.de>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2019-06-30 08:55:00 -04:00
Lukas Fleischer
fc9c519852 Display warning when flagging VCS packages
VCS packages should not be flagged out-of-date when the package version
does not match the most recent commit.

Implements FS#62733.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2019-05-25 19:01:33 -04:00
Lukas Fleischer
5a66a381fb Sync CSS with archweb
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2019-05-25 12:40:18 -04:00
Lukas Fleischer
952e61a79c Use native language name for Finnish
Addresses FS#61803.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2019-05-24 12:37:08 -04:00
Lukas Fleischer
69deea9f2f Ignore merge target for non-merge requests
Fixes FS#59837.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2019-05-24 12:24:46 -04:00
Lukas Fleischer
dd11321fa3 git-auth: deny login if no password has been set
After creating a new account, users need to verify their email address
and set an initial password. Without setting a password, users cannot
use their account on the web interface. However, when logging in via
SSH, we did not check whether the account is verified.

Fix this by only allowing SSH access once a password is set.

Reported-by: Pat Hogan <pathtofile@gmail.com>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2019-04-28 08:58:29 -04:00
Vladimir Panteleev
e3ca3c96e5 Add "Enable notifications" checkbox in "Add Comment" form
Currently, it is a little to easy to forget to enable notifications
for a package after leaving a comment, thus never being notified of a
reply. Even though the "Enable notifications" link is on the same
page, it is not part of the flow for posting a new comment, and so,
easy to miss.

Most web forums and comment systems include a checkbox to enable
notifications when posting for the first time in a thread. This patch
implements this in aurweb, as well.

Signed-off-by: Eli Schwartz <eschwartz@archlinux.org>
2019-04-28 08:58:29 -04:00
Eli Schwartz
e0d821352f
notify: add X-AUR-Reason header to allow conveniently filtering emails
Because filtering by matching the sender && regular expressions on the
subject is awkward.

Signed-off-by: Eli Schwartz <eschwartz@archlinux.org>
2019-02-08 11:19:16 -05:00
Lukas Fleischer
44af2b430f aurblup: make provider updates more robust
Reverse the order of deletion and addition so that deletion comes first.
This prevents corner cases such as failing unique key constraints when a
provided package changes from lower case to upper case and the old name
is not yet gone.

Helped-by: Eli Schwartz <eschwartz@archlinux.org>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2019-01-21 21:58:00 +01:00
Florian Pritz
042f3f2622
Quote MySql 8.0 reserved keywords
Signed-off-by: Florian Pritz <bluewind@xinu.at>
Signed-off-by: Eli Schwartz <eschwartz@archlinux.org>
2019-01-14 14:45:37 -05:00
Eli Schwartz
f1d109e9b6
Fix notifications emails going to the right people, part #2
Notifications are still going to the wrong people. We tried to fix this
in commit b702e5c0e7, but only fixed it
for the python callers. There's another caller in the php code, which
needs to use the right order of arguments as well.

Fixes FS#60601

Signed-off-by: Eli Schwartz <eschwartz@archlinux.org>
2018-10-26 10:10:00 -04:00
Vladimir Panteleev
f046dd5828
pkg_comments.php: Make comment timestamps link to the comment
As of today, there is no easy way to obtain a link to a specific
comment on a package page.

Many implementations of forums and comment systems today seem to
follow a convention where a comment's timestamp is an unobtrusive link
to the comment itself. Some examples are:

- phpBB (e.g. bbs.archlinux.org)
- GitHub
- Disqus
- Discourse

This patch adopts this convention as well, by making the timestamp a
link to the comment.
2018-10-16 21:45:19 -04:00
Lukas Fleischer
8a2f13f8c2 t2500: add test for disown notifications
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-08-12 13:41:56 +02:00
Lukas Fleischer
0ae1ca15e9 t2500: use unique identifiers
Use disjoint sets of IDs for users, package bases, package comments and
package requests to ensure the notification script expects the
parameters in the same order we pass them.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-08-12 13:40:51 +02:00
Lukas Fleischer
bf5a79da6b Initialize locale directory for tests
Since commit a7865ef (Make the locale directory configurable,
2018-07-22), we need to specify the locale directory in the
configuration file.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-08-12 13:40:51 +02:00
Eli Schwartz
b702e5c0e7 Fix notifications emails going to the right people
In commit f3b4c5c (Refactor the notification script, 2018-05-17), the
parameters of the adopt, disown, comaintainer-add and
comaintainer-remove notification modules were accidentally pushed around
without changing the order in the callers. The notify script now expects
to see the userid followed by additional arguments like the pkgbase id.

As a result, some random userid with the same id as the pkgbase, got
sent a notification regarding some package with the same id as the real
user's id.

Fix this by changing the order in every invocation of the aforementioned
modules.

Signed-off-by: Eli Schwartz <eschwartz@archlinux.org>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-08-12 09:38:19 +02:00
Johannes Löthberg
257115943e Allow paginating package comments
Signed-off-by: Johannes Löthberg <johannes@kyriasis.com>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-08-06 06:03:58 +02:00
Johannes Löthberg
3578e77ad4 Allow listing all comments from a user
Signed-off-by: Johannes Löthberg <johannes@kyriasis.com>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-08-06 06:03:58 +02:00
Lukas Fleischer
a7865ef5aa Make the locale directory configurable
Add a new configuration option to specify the locale directory to use.
This allows the Python scripts to find the translations, even when not
being run from the source code checkout. At the same time, multiple
parallel aurweb setups can still use different sets of translations.

Fixes FS#59278.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-08-06 06:03:52 +02:00
Eli Schwartz
c8d99bac8e Fix regression in translating anything at all
In commit 840ee20 (Rename translation resources from aur to aurweb,
2018-07-07) the translations file was renamed but we never actually
switched to using the renamed translations.

As a result, every single push to the AUR contains the following
traceback:

    remote: Traceback (most recent call last):
    remote:   File "/usr/bin/aurweb-notify", line 11, in <module>
    remote:     load_entry_point('aurweb==4.7.0', 'console_scripts', 'aurweb-notify')()
    remote:   File "/usr/lib/python3.6/site-packages/aurweb-4.7.0-py3.6.egg/aurweb/scripts/notify.py", line 541, in main
    remote:   File "/usr/lib/python3.6/site-packages/aurweb-4.7.0-py3.6.egg/aurweb/scripts/notify.py", line 69, in send
    remote:   File "/usr/lib/python3.6/site-packages/aurweb-4.7.0-py3.6.egg/aurweb/scripts/notify.py", line 56, in get_body_fmt
    remote:   File "/usr/lib/python3.6/site-packages/aurweb-4.7.0-py3.6.egg/aurweb/scripts/notify.py", line 192, in get_body
    remote:   File "/usr/lib/python3.6/site-packages/aurweb-4.7.0-py3.6.egg/aurweb/l10n.py", line 14, in translate
    remote:   File "/usr/lib/python3.6/gettext.py", line 514, in translation
    remote:     raise OSError(ENOENT, 'No translation file found for domain', domain)
    remote: FileNotFoundError: [Errno 2] No translation file found for domain: 'aur'

Signed-off-by: Eli Schwartz <eschwartz@archlinux.org>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-07-09 16:43:31 +02:00
Lukas Fleischer
2c03766841 Release 4.7.0
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-07-07 17:23:29 +02:00
Lukas Fleischer
2aa78d75d3 Translation updates from Transifex
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-07-07 16:08:33 +02:00
Lukas Fleischer
840ee20f7b Rename translation resources from aur to aurweb
* Rename the aur project to aurweb on Transifex.
* Rename aur.pot to aurweb.pot.
* Update documentation and Makefile.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-07-07 16:05:12 +02:00
Lukas Fleischer
41a4189d20 Sync CSS with archweb
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-07-07 15:57:47 +02:00
Lukas Fleischer
b70f048bc3 Add package base name in request close notifications
Mention both the package base name and the request type in the subject
of request closure notification.

Implements FS#41607.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-05-20 16:55:58 +02:00
Eli Schwartz
16795eaf46 git-update: accept any arch in arch-dependent metadata
Currently we hardcode the architectures the official repos historically
supported, which seems both inefficient because of hardcoding, and
simply wrong, because many packages support various ARM platforms too.

If we were to say "only officially supported arches will be supported in
the AUR" we'd have to disable i686, which seems silly and arbitrarily
restrictive. Also there's better places to implement such a blacklist
(via die_commit in the main loop, via a config option to list supported
arches, would make much more sense in terms of logic).

As for the metadata extraction itself, there's no reason to hardcode the
arches to check for at all. We can get this information too, from the
.SRCINFO itself. Detecting this dynamically is not incompatible with a
blacklist, should we ever decide to implement such a thing.

Signed-off-by: Eli Schwartz <eschwartz@archlinux.org>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-05-18 17:40:37 +02:00
Lukas Fleischer
d24737f3f5 Update message catalog
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-05-17 22:58:54 +02:00
Lukas Fleischer
6367dfd245 Use modern format strings in notification messages
User modern Python format() strings with curly braces. Also, convert all
placeholders to named arguments. This allows translators to reorder
messages.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-05-17 22:58:54 +02:00
Lukas Fleischer
f7a57c82bc Localize notification emails
Add support for translating notification emails and send localized
notifications, based on the user's language preferences. Also, update
the translations Makefile to add strings from the notification script
to the message catalog.

Implements FS#31850.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-05-17 22:58:54 +02:00
Lukas Fleischer
f3b4c5c6bc Refactor the notification script
Reimplement most of the notification script logic. Create a separate
class for each notification type. Each class provides methods for
generating the list of recipients, the message subject, the message
body, the references to add at the end of the message and the message
headers. Additionally, a method for sending notification emails is
provided.

One major benefit of the new implementation is that both the generation
of recipients and message contents are much more flexible. For example,
it is now easily possible to make user-specific adjustments to every
single notification of a batch.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-05-17 22:05:52 +02:00
Lukas Fleischer
fec253a65d t2500: Add test cases for all notifications
Check that for all kinds of notifications, the generated messages match
what we expect.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-05-17 22:05:33 +02:00
Lukas Fleischer
7e452fdfb0 notify.py: Do not add stray newlines
Make sure we are consistent with not adding newlines at the end of
notification emails.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-05-16 20:10:03 +02:00
Lukas Fleischer
4b8b2e3eb1 Stop using each()
The each() function has been deprecated as of PHP 7.2.0. Use foreach
loops instead.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-05-12 13:35:11 +02:00
Lukas Fleischer
8838490665 Add newline after accept link for orphan requests
Fixes a regression introduced in 0ffa067 (Use a link to accept orphan
requests, 2018-05-10).

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-05-12 12:37:16 +02:00
Lukas Fleischer
5c48302aaf confparser.inc.php: Add missing dollar sign
Fixes a regression introduced in 97c5bce (config: allow reading both the
defaults file and the modified config, 2018-04-15).

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-05-12 12:37:16 +02:00
Lukas Fleischer
ad9422ca19 confparser.inc.php: Add missing semicolon
Fixes a regression introduced in 97c5bce (config: allow reading both the
defaults file and the modified config, 2018-04-15).

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-05-12 12:25:21 +02:00
Eli Schwartz
0ffa0679d2 Use a link to accept orphan requests
Currently, a form is used instead of a link. This forwards to a
confirmation page, and currently drops the "via" parameter in the
process.

As a result, accepted orphan requests usually show:

    Request #XXXXXX has been accepted automatically by the Arch User
    Repository package request system:

    The user YYYYYYY disowned the package.

This is wrong, and should show (will show, if you manually add it or use
the close button instead of the accept button):

    Request #XXXXXX has been rejected by YYYYYYY [1]:

Fixes FS#56606.

Signed-off-by: Eli Schwartz <eschwartz@archlinux.org>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-05-11 11:23:52 +02:00
Lukas Fleischer
ce93360257 Erase login IP addresses after seven days
Add a script to periodically remove old IP addresses from the users
database.

The login IP addresses are stored for spam protection and to prevent
from abuse. It is quite unlikely that we ever need the IP address of a
user whose last login is more than a week old. It makes sense to remove
such IP addresses to protect our users' privacy.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-05-10 21:38:25 +02:00
Eli Schwartz
4381a0d7c2 Update copyright year in the cgit footer template
Four years just passed in the blink of an eye :)

Signed-off-by: Eli Schwartz <eschwartz@archlinux.org>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-05-10 09:29:03 +02:00
Eli Schwartz
97c5bcec13 config: allow reading both the defaults file and the modified config
In the process, rename config.proto to config.defaults (because that is
what it is now).

Also use dict.get('key', default_value) when querying os.environ, rather
than an if block, as it is more pythonic/readable/concise, and reduces
the number of dict lookups.

This change allows aurweb configuration to be done via either:
- copying config.defaults to config and modifying values
- creating a new config only containing modified values, next to a
  config.defaults containing unmodified values

The motivation for this change is to enable ansible configuration in our
flagship deployment by storing only changed values, and deferring to
config.defaults otherwise.

A side benefit is, it is easier to see what has changed by inspecting
only the site configuration file.

If a config.defaults file does not exist next to $AUR_CONFIG or in
$AUR_CONFIG_DEFAULTS, it is ignored and *all* values are expected to
live in the modified config file.

Signed-off-by: Eli Schwartz <eschwartz@archlinux.org>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-04-22 09:26:10 +02:00
Lukas Fleischer
2b280ea3d8 Allow manual breaks and horizontal lines in comments
When sanitizing rendered comments, keep <hr> tags and <br> tags. The
former are generated when using "---" in Markdown comments, the latter
are used when putting two spaces at the end of a line.

Fixes FS#56649.
2018-04-08 09:33:35 +02:00
nodivbyzero
eccd328d42 Handle empty resultset getting recent 10 packages
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-03-21 07:45:26 +01:00
nodivbyzero
3d90623154 Terminate execution if config file is missing
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-03-20 19:23:02 +01:00
nodivbyzero
bcd795c339 schema/Makefile: Replace MySQL with SQLite in comment
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-03-14 17:19:53 +01:00
nodivbyzero
82ef1d09b9 TESTING: Add two required packages
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-03-14 17:19:39 +01:00
Johannes Löthberg
879db7012c notify: Send vote reminders to TUs that are also devs
Signed-off-by: Johannes Löthberg <johannes@kyriasis.com>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-03-13 21:05:37 +01:00
Jelle van der Waa
ca6332de6e Update cache code to INI style configuration
Change the defines to config_get and add one cache option and one option
to define memcache_servers. Mention the required dependency to get
memcached working in the INSTALL file.

Signed-off-by: Jelle van der Waa <jelle@vdwaa.nl>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-03-10 16:48:14 +01:00
Jelle van der Waa
c3bca45973 Remove unused variable $dbh in pkgbase_display_details
Signed-off-by: Jelle van der Waa <jelle@vdwaa.nl>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-03-10 16:47:11 +01:00
Baptiste Jonglez
1ff409874e RPC: Allow to search packages by "*depends" fields
It is now possible to search for packages that depend on a given package,
for instance:

    /rpc/?v=5&type=search&by=depends&arg=ocaml

It is similarly possible to match on "makedepends", "checkdepends" and
"optdepends".

Signed-off-by: Baptiste Jonglez <git@bitsofnetworks.org>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-02-24 14:57:31 +01:00
Mark Weiman
f15c700ad2 Add capability for co-maintainers to disown packages
Implements FS#53832.

Signed-off-by: Mark Weiman <mark.weiman@markzz.com>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-02-24 14:57:31 +01:00
Florian Pritz
27654afadb Add rate limit support to API
This allows us to prevent users from hammering the API every few seconds
to check if any of their packages were updated. Real world users check
as often as every 5 or 10 seconds.

Signed-off-by: Florian Pritz <bluewind@xinu.at>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-02-24 14:57:31 +01:00
Florian Pritz
f51d4c32cd Remove disjunction in pkg_providers query
For some reason, running the SELECT .. WHERE .. OR .. query takes e.g.
58ms on a randomly generated db for some dependency name. Splitting the
OR into two dedicated queries and UNIONing the result takes only 0.42ms.

On the Arch Linux installation, searching for the providers of e.g.
mongodb takes >=110ms when not cached by the query cache. The new query
takes <1ms even when not cached.

Signed-off-by: Florian Pritz <bluewind@xinu.at>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-02-24 14:57:31 +01:00
Remy Marquis
34a0d39910 Document required PHP extensions in php.ini
To people unfamiliar with the code, it is not obvious that
the pdo_* PHP extensions must be enabled.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-01-26 20:17:04 +01:00
Johannes Löthberg
e5b43760c2 Move AUR_OVERWRITE privilege check from git/auth to git/update
git/auth is run as an AutherizedKeysCommand which does not get the
environment variables passed to it, so AUR_OVERWRITE always got
hard-set to '0' by it.  Instead we need to perform the actual privilege
check in git/update instead.

Signed-off-by: Johannes Löthberg <johannes@kyriasis.com>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2018-01-21 18:09:10 +01:00
Eli Schwartz
ac29097ce8 Fix regression that stopped maintainers from pinning comments
In commit 8c98db0b82 support was added for
package co-maintainers to pin comments in addition to maintainers.

Due to a typo, the SQL query was reset halfway through and only added
the co-maintainer IDs to the list of allowed users.

Fixes FS#56783.

Signed-off-by: Eli Schwartz <eschwartz@archlinux.org>
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2017-12-23 22:51:39 +01:00
Lukas Fleischer
a04fe6a13e Add route for /users.gz
Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2017-12-03 13:59:54 +01:00
Lukas Fleischer
4660892e58 Allow setting an empty home page
Since commit 4efba18 (Only allow valid HTTP(s) URLs as home page,
2017-11-05), the home page field in the account settings must be a valid
URL. However, this new check prevents from leaving the field empty. Keep
the check in place but skip it if the home page field is left empty.

Fixes FS#56550.

Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
2017-12-03 13:52:28 +01:00
571 changed files with 117872 additions and 21243 deletions

9
.coveragerc Normal file
View file

@ -0,0 +1,9 @@
[run]
disable_warnings = already-imported
[report]
include = aurweb/*
fail_under = 95
exclude_lines =
if __name__ == .__main__.:
pragma: no cover

23
.dockerignore Normal file
View file

@ -0,0 +1,23 @@
# Config files
conf/config
conf/config.sqlite
conf/config.sqlite.defaults
conf/docker
conf/docker.defaults
# Compiled translation files
**/*.mo
# Typical virtualenv directories
env/
venv/
.venv/
# Test output
htmlcov/
test-emails/
test/__pycache__
test/test-results
test/trash_directory*
.coverage
.pytest_cache

10
.editorconfig Normal file
View file

@ -0,0 +1,10 @@
# EditorConfig configuration for aurweb
# https://editorconfig.org
# Top-most EditorConfig file
root = true
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8

8
.env Normal file
View file

@ -0,0 +1,8 @@
FASTAPI_BACKEND="uvicorn"
FASTAPI_WORKERS=2
MARIADB_SOCKET_DIR="/var/run/mysqld/"
AURWEB_FASTAPI_PREFIX=https://localhost:8444
AURWEB_SSHD_PREFIX=ssh://aur@localhost:2222
GIT_DATA_DIR="./aur.git/"
TEST_RECURSION_LIMIT=10000
COMMIT_HASH=

2
.git-blame-ignore-revs Normal file
View file

@ -0,0 +1,2 @@
# style: Run pre-commit
9c6c13b78a30cb9d800043410799e29631f803d2

62
.gitignore vendored
View file

@ -1,9 +1,63 @@
/data/
__pycache__/
*.py[cod]
.vim/
.pylintrc
.coverage
.idea
/cache/*
/logs/*
/build/
/dist/
/aurweb.egg-info/
/personal/
/notes/
/vendor/
/pyrightconfig.json
/taskell.md
aur.git/
aurweb.sqlite3
conf/config
conf/config.sqlite
conf/config.sqlite.defaults
conf/docker
conf/docker.defaults
data.sql
dummy-data.sql*
fastapi_aw/
htmlcov/
po/*.mo
po/*.po~
po/POTFILES
web/locale/*/
aur.git/
__pycache__/
*.py[cod]
schema/aur-schema-sqlite.sql
test/test-results/
test/trash_directory*
web/locale/*/
web/html/*.gz
# Do not stage compiled asciidoc: make -C doc
doc/rpc.html
# Ignore any user-configured .envrc files at the root.
/.envrc
# Ignore .python-version file from Pyenv
.python-version
# Ignore coverage report
coverage.xml
# Ignore pytest report
report.xml
# Ignore test emails
test-emails/
# Ignore typical virtualenv directories
env/
venv/
.venv/
# Ignore some terraform files
/ci/tf/.terraform
/ci/tf/terraform.tfstate*

161
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,161 @@
image: archlinux:base-devel
cache:
key: system-v1
paths:
# For some reason Gitlab CI only supports storing cache/artifacts in a path relative to the build directory
- .pkg-cache
- .venv
- .pre-commit
variables:
AUR_CONFIG: conf/config # Default MySQL config setup in before_script.
DB_HOST: localhost
TEST_RECURSION_LIMIT: 10000
CURRENT_DIR: "$(pwd)"
LOG_CONFIG: logging.test.conf
DEV_FQDN: aurweb-$CI_COMMIT_REF_SLUG.sandbox.archlinux.page
INFRASTRUCTURE_REPO: https://gitlab.archlinux.org/archlinux/infrastructure.git
lint:
stage: .pre
before_script:
- pacman -Sy --noconfirm --noprogressbar
archlinux-keyring
- pacman -Syu --noconfirm --noprogressbar
git python python-pre-commit
script:
- export XDG_CACHE_HOME=.pre-commit
- pre-commit run -a
test:
stage: test
before_script:
- export PATH="$HOME/.poetry/bin:${PATH}"
- ./docker/scripts/install-deps.sh
- virtualenv -p python3 .venv
- source .venv/bin/activate # Enable our virtualenv cache
- ./docker/scripts/install-python-deps.sh
- useradd -U -d /aurweb -c 'AUR User' aur
- ./docker/mariadb-entrypoint.sh
- (cd '/usr' && /usr/bin/mysqld_safe --datadir='/var/lib/mysql') &
- 'until : > /dev/tcp/127.0.0.1/3306; do sleep 1s; done'
- cp -v conf/config.dev conf/config
- sed -i "s;YOUR_AUR_ROOT;$(pwd);g" conf/config
- ./docker/test-mysql-entrypoint.sh # Create mysql AUR_CONFIG.
- make -C po all install # Compile translations.
- make -C doc # Compile asciidoc.
- make -C test clean # Cleanup coverage.
script:
# Run sharness.
- make -C test sh
# Run pytest.
- pytest --junitxml="pytest-report.xml"
- make -C test coverage # Produce coverage reports.
coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'
artifacts:
reports:
junit: pytest-report.xml
coverage_report:
coverage_format: cobertura
path: coverage.xml
.init_tf: &init_tf
- pacman -Syu --needed --noconfirm terraform
- export TF_VAR_name="aurweb-${CI_COMMIT_REF_SLUG}"
- TF_ADDRESS="${CI_API_V4_URL}/projects/${TF_STATE_PROJECT}/terraform/state/${CI_COMMIT_REF_SLUG}"
- cd ci/tf
- >
terraform init \
-backend-config="address=${TF_ADDRESS}" \
-backend-config="lock_address=${TF_ADDRESS}/lock" \
-backend-config="unlock_address=${TF_ADDRESS}/lock" \
-backend-config="username=x-access-token" \
-backend-config="password=${TF_STATE_GITLAB_ACCESS_TOKEN}" \
-backend-config="lock_method=POST" \
-backend-config="unlock_method=DELETE" \
-backend-config="retry_wait_min=5"
deploy_review:
stage: deploy
script:
- *init_tf
- terraform apply -auto-approve
environment:
name: review/$CI_COMMIT_REF_NAME
url: https://$DEV_FQDN
on_stop: stop_review
auto_stop_in: 1 week
rules:
- if: $CI_COMMIT_REF_NAME =~ /^renovate\//
when: never
- if: $CI_MERGE_REQUEST_ID && $CI_PROJECT_PATH == "archlinux/aurweb"
when: manual
provision_review:
stage: deploy
needs:
- deploy_review
script:
- *init_tf
- pacman -Syu --noconfirm --needed ansible git openssh jq
# Get ssh key from terraform state file
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- terraform show -json |
jq -r '.values.root_module.resources[] |
select(.address == "tls_private_key.this") |
.values.private_key_openssh' > ~/.ssh/id_ed25519
- chmod 400 ~/.ssh/id_ed25519
# Clone infra repo
- git clone $INFRASTRUCTURE_REPO
- cd infrastructure
# Remove vault files
- rm $(git grep -l 'ANSIBLE_VAULT;1.1;AES256$')
# Remove vault config
- sed -i '/^vault/d' ansible.cfg
# Add host config
- mkdir -p host_vars/$DEV_FQDN
- 'echo "filesystem: btrfs" > host_vars/$DEV_FQDN/misc'
# Add host
- echo "$DEV_FQDN" > hosts
# Add our pubkey and hostkeys
- ssh-keyscan $DEV_FQDN >> ~/.ssh/known_hosts
- ssh-keygen -f ~/.ssh/id_ed25519 -y > pubkeys/aurweb-dev.pub
# Run our ansible playbook
- >
ansible-playbook playbooks/aur-dev.archlinux.org.yml \
-e "aurdev_fqdn=$DEV_FQDN" \
-e "aurweb_repository=$CI_REPOSITORY_URL" \
-e "aurweb_version=$CI_COMMIT_SHA" \
-e "{\"vault_mariadb_users\":{\"root\":\"aur\"}}" \
-e "vault_aurweb_db_password=aur" \
-e "vault_aurweb_gitlab_instance=https://does.not.exist" \
-e "vault_aurweb_error_project=set-me" \
-e "vault_aurweb_error_token=set-me" \
-e "vault_aurweb_secret=aur" \
-e "vault_goaurrpc_metrics_token=aur" \
-e '{"root_additional_keys": ["moson.pub", "aurweb-dev.pub"]}'
environment:
name: review/$CI_COMMIT_REF_NAME
action: access
rules:
- if: $CI_COMMIT_REF_NAME =~ /^renovate\//
when: never
- if: $CI_MERGE_REQUEST_ID && $CI_PROJECT_PATH == "archlinux/aurweb"
stop_review:
stage: deploy
needs:
- deploy_review
script:
- *init_tf
- terraform destroy -auto-approve
- 'curl --silent --show-error --fail --header "Private-Token: ${TF_STATE_GITLAB_ACCESS_TOKEN}" --request DELETE "${CI_API_V4_URL}/projects/${TF_STATE_PROJECT}/terraform/state/${CI_COMMIT_REF_SLUG}"'
environment:
name: review/$CI_COMMIT_REF_NAME
action: stop
rules:
- if: $CI_COMMIT_REF_NAME =~ /^renovate\//
when: never
- if: $CI_MERGE_REQUEST_ID && $CI_PROJECT_PATH == "archlinux/aurweb"
when: manual

View file

@ -0,0 +1,60 @@
<!--
This template is used to report potential bugs with the AURweb website.
NOTE: All comment sections with a MODIFY note need to be edited. All checkboxes
in the "Checklist" section need to be checked by the owner of the issue.
-->
/label ~bug ~unconfirmed
/title [BUG] <!-- MODIFY: add subject -->
<!--
Please do not remove the above quick actions, which automatically label the
issue and assign relevant users.
-->
### Checklist
**NOTE:** This bug template is meant to provide bug issues for code existing in
the aurweb repository.
**This bug template is not meant to handle bugs with user-uploaded packages.**
To report issues you might have found in a user-uploaded package, contact
the package's maintainer in comments.
- [ ] I confirm that this is an issue with aurweb's code and not a
user-uploaded package.
- [ ] I have described the bug in complete detail in the
[Description](#description) section.
- [ ] I have specified steps in the [Reproduction](#reproduction) section.
- [ ] I have included any logs related to the bug in the
[Logs](#logs) section.
- [ ] I have included the versions which are affected in the
[Version(s)](#versions) section.
### Description
Describe the bug in full detail.
### Reproduction
Describe a specific set of actions that can be used to reproduce
this bug.
### Logs
If you have any logs relevant to the bug, include them here in
quoted or code blocks.
### Version(s)
In this section, please include a list of versions you have found
to be affected by this program. This can either come in the form
of `major.minor.patch` (if it affects a release tarball), or a
commit hash if the bug does not directly affect a release version.
All development is done without modifying version displays in
aurweb's HTML render output. If you're testing locally, use the
commit on which you are experiencing the bug. If you have found
a bug which exists on live aur.archlinux.org, include the version
located at the bottom of the webpage.
/label bug unconfirmed

View file

@ -0,0 +1,52 @@
<!--
This template is used to feature request for AURweb website.
NOTE: All comment sections with a MODIFY note need to be edited. All checkboxes
in the "Checklist" section need to be checked by the owner of the issue.
-->
/label ~feature ~unconfirmed
/title [FEATURE] <!-- MODIFY: add subject -->
<!--
Please do not remove the above quick actions, which automatically label the
issue and assign relevant users.
-->
### Checklist
**NOTE:** This bug template is meant to provide bug issues for code existing in
the aurweb repository.
**This bug template is not meant to handle bugs with user-uploaded packages.**
To report issues you might have found in a user-uploaded package, contact
the package's maintainer in comments.
- [ ] I have summed up the feature in concise words in the [Summary](#summary) section.
- [ ] I have completely described the feature in the [Description](#description) section.
- [ ] I have completed the [Blockers](#blockers) section.
### Summary
Fill this section out with a concise wording about the feature being
requested.
Example: _A new `Tyrant` account type for users_.
### Description
Describe your feature in full detail.
Example: _The `Tyrant` account type should be used to allow a user to be
tyrannical. When a user is a `Tyrant`, they should be able to assassinate
users due to not complying with their laws. Laws can be configured by updating
the Tyrant laws page at https://aur.archlinux.org/account/{username}/laws.
More specifics about laws._
### Blockers
Include any blockers in a list. If there are no blockers, this section
should be omitted from the issue.
Example:
- [Feature] Do not allow users to be Tyrants
- \<(issue|merge_request)_link\>

36
.pre-commit-config.yaml Normal file
View file

@ -0,0 +1,36 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: check-added-large-files
- id: check-case-conflict
- id: check-merge-conflict
- id: check-toml
- id: end-of-file-fixer
- id: trailing-whitespace
exclude: ^po/
- id: debug-statements
- repo: https://github.com/myint/autoflake
rev: v2.3.1
hooks:
- id: autoflake
args:
- --in-place
- --remove-all-unused-imports
- --ignore-init-module-imports
- repo: https://github.com/pycqa/isort
rev: 5.13.2
hooks:
- id: isort
- repo: https://github.com/psf/black
rev: 24.4.1
hooks:
- id: black
- repo: https://github.com/PyCQA/flake8
rev: 7.0.0
hooks:
- id: flake8

View file

@ -1,23 +0,0 @@
language: python
python: 3.6
addons:
apt:
packages:
- bsdtar
- libarchive-dev
- libgpgme11-dev
- libprotobuf-dev
install:
- curl https://codeload.github.com/libgit2/libgit2/tar.gz/v0.26.0 | tar -xz
- curl https://sources.archlinux.org/other/pacman/pacman-5.0.2.tar.gz | tar -xz
- curl https://git.archlinux.org/pyalpm.git/snapshot/pyalpm-0.8.1.tar.gz | tar -xz
- ( cd libgit2-0.26.0 && cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr && make && sudo make install )
- ( cd pacman-5.0.2 && ./configure --prefix=/usr && make && sudo make install )
- ( cd pyalpm-0.8.1 && python setup.py build && python setup.py install )
- pip install mysql-connector-python-rf pygit2==0.26 srcinfo
- pip install bleach Markdown
script: make -C test

View file

@ -1,8 +1,7 @@
[main]
host = https://www.transifex.com
host = https://app.transifex.com
[aur.aurpot]
[o:lfleischer:p:aurweb:r:aurwebpot]
file_filter = po/<lang>.po
source_file = po/aur.pot
source_file = po/aurweb.pot
source_lang = en

105
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,105 @@
# Contributing
Patches should be sent to the [aur-dev@lists.archlinux.org][1] mailing list
or included in a merge request on the [aurweb repository][2].
Before sending patches, you are recommended to run `flake8` and `isort`.
You can add a git hook to do this by installing `python-pre-commit` and running
`pre-commit install`.
[1]: https://lists.archlinux.org/mailman3/lists/aur-dev.lists.archlinux.org/
[2]: https://gitlab.archlinux.org/archlinux/aurweb
### Coding Guidelines
DISCLAIMER: We realise the code doesn't necessarily follow all the rules.
This is an attempt to establish a standard coding style for future
development.
1. All source modified or added within a patchset **must** maintain equivalent
or increased coverage by providing tests that use the functionality
2. Please keep your source within an 80 column width
3. Use four space indentation
4. Use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/)
5. DRY: Don't Repeat Yourself
6. All code should be tested for good _and_ bad cases (see [test/README.md][3])
[3]: https://gitlab.archlinux.org/archlinux/aurweb/-/blob/master/test/README.md
Test patches that increase coverage in the codebase are always welcome.
### Coding Style
We use `autoflake`, `isort`, `black` and `flake8` to enforce coding style in a
PEP-8 compliant way. These tools run in GitLab CI using `pre-commit` to verify
that any pushed code changes comply with this.
To enable the `pre-commit` git hook, install the `pre-commit` package either
with `pacman` or `pip` and then run `pre-commit install --install-hooks`. This
will ensure formatting is done before any code is commited to the git
repository.
There are plugins for editors or IDEs which automate this process. Some
example plugins:
- [tenfyzhong/autoflake.vim](https://github.com/tenfyzhong/autoflake.vim)
- [fisadev/vim-isort](https://github.com/fisadev/vim-isort)
- [psf/black](https://github.com/psf/black)
- [nvie/vim-flake8](https://github.com/nvie/vim-flake8)
- [prabirshrestha/vim-lsp](https://github.com/prabirshrestha/vim-lsp)
- [dense-analysis/ale](https://github.com/dense-analysis/ale)
See `setup.cfg`, `pyproject.toml` and `.pre-commit-config.yaml` for tool
specific configurations.
### Development Environment
To get started with local development, an instance of aurweb must be
brought up. This can be done using the following sections:
- [Using Docker](#using-docker)
- [Using INSTALL](#using-install)
There are a number of services aurweb employs to run the application
in its entirety:
- ssh
- cron jobs
- starlette/fastapi asgi server
Project structure:
- `./aurweb`: `aurweb` Python package
- `./templates`: Jinja2 templates
- `./docker`: Docker scripts and configuration files
#### Using Docker
Using Docker, we can run the entire infrastructure in two steps:
# Build the aurweb:latest image
$ docker-compose build
# Start all services in the background
$ docker-compose up -d nginx
`docker-compose` services will generate a locally signed root certificate
at `./data/root_ca.crt`. Users can import this into ca-certificates or their
browser if desired.
Accessible services (on the host):
- https://localhost:8444 (python via nginx)
- localhost:13306 (mariadb)
- localhost:16379 (redis)
Docker services, by default, are setup to be hot reloaded when source code
is changed.
For detailed setup instructions have a look at [TESTING](TESTING)
#### Using INSTALL
The [INSTALL](INSTALL) file describes steps to install the application on
bare-metal systems.

47
Dockerfile Normal file
View file

@ -0,0 +1,47 @@
FROM archlinux:base-devel
VOLUME /root/.cache/pypoetry/cache
VOLUME /root/.cache/pypoetry/artifacts
VOLUME /root/.cache/pre-commit
ENV PATH="/root/.poetry/bin:${PATH}"
ENV PYTHONPATH=/aurweb
ENV AUR_CONFIG=conf/config
ENV COMPOSE=1
# Install system-wide dependencies.
COPY ./docker/scripts/install-deps.sh /install-deps.sh
RUN /install-deps.sh
# Copy Docker scripts
COPY ./docker /docker
COPY ./docker/scripts/* /usr/local/bin/
# Copy over all aurweb files.
COPY . /aurweb
# Working directory is aurweb root @ /aurweb.
WORKDIR /aurweb
# Copy initial config to conf/config.
RUN cp -vf conf/config.dev conf/config
RUN sed -i "s;YOUR_AUR_ROOT;/aurweb;g" conf/config
# Install Python dependencies.
RUN /docker/scripts/install-python-deps.sh compose
# Compile asciidocs.
RUN make -C doc
# Add our aur user.
RUN useradd -U -d /aurweb -c 'AUR User' aur
# Setup some default system stuff.
RUN ln -sf /usr/share/zoneinfo/UTC /etc/localtime
# Install translations.
RUN make -C po all install
# Install pre-commit repositories and run lint check.
RUN pre-commit run -a

167
INSTALL
View file

@ -4,58 +4,129 @@ Setup on Arch Linux
For testing aurweb patches before submission, you can use the instructions in
TESTING for testing the web interface only.
Note that you can only do limited testing using the PHP built-in web server.
In particular, the cgit interface will be unusable as well as the ssh+git
interface. For a detailed description on how to setup a full aurweb server,
For a detailed description on how to setup a full aurweb server,
read the instructions below.
1) Clone the aurweb project:
1) Clone the aurweb project and install it (via `python-poetry`):
$ cd /srv/http/
$ git clone git://git.archlinux.org/aurweb.git
$ cd /srv/http/
$ git clone git://git.archlinux.org/aurweb.git
$ cd aurweb
$ poetry install
2) Setup a web server with PHP and MySQL. Configure the web server to redirect
all URLs to /index.php/foo/bar/. The following block can be used with nginx:
2) Setup a web server with MySQL. The following block can be used with nginx:
server {
listen 80;
# https is preferred and can be done easily with LetsEncrypt
# or self-CA signing. Users can still listen over 80 for plain
# http, for which the [options] disable_http_login used to toggle
# the authentication feature.
listen 443 ssl http2;
server_name aur.local aur;
root /srv/http/aurweb/web/html;
index index.php;
# To enable SSL proxy properly, make sure gunicorn and friends
# are supporting forwarded headers over 127.0.0.1 or any if
# the asgi server is contacted by non-localhost hosts.
ssl_certificate /etc/ssl/certs/aur.cert.pem;
ssl_certificate_key /etc/ssl/private/aur.key.pem;
location ~ ^/[^/]+\.php($|/) {
fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock;
fastcgi_index index.php;
fastcgi_split_path_info ^(/[^/]+\.php)(/.*)$;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
include fastcgi_params;
# smartgit location.
location ~ "^/([a-z0-9][a-z0-9.+_-]*?)(\.git)?/(git-(receive|upload)-pack|HEAD|info/refs|objects/(info/(http-)?alternates|packs)|[0-9a-f]{2}/[0-9a-f]{38}|pack/pack-[0-9a-f]{40}\.(pack|idx))$" {
include uwsgi_params;
uwsgi_pass smartgit;
uwsgi_modifier1 9;
uwsgi_param SCRIPT_FILENAME /usr/lib/git-core/git-http-backend;
uwsgi_param PATH_INFO /aur.git/$3;
uwsgi_param GIT_HTTP_EXPORT_ALL "";
uwsgi_param GIT_NAMESPACE $1;
uwsgi_param GIT_PROJECT_ROOT /srv/http/aurweb;
}
location ~ .* {
rewrite ^/(.*)$ /index.php/$1 last;
# cgitrc.proto should be configured and located somewhere
# of your choosing.
location ~ ^/cgit {
include uwsgi_params;
rewrite ^/cgit/([^?/]+/[^?]*)?(?:\?(.*))?$ /cgit.cgi?url=$1&$2 last;
uwsgi_modifier1 9;
uwsgi_param CGIT_CONFIG /srv/http/aurweb/conf/cgitrc.proto;
uwsgi_pass cgit;
}
# Static archive assets.
location ~ \.gz$ {
# Asset root. This is used to match against gzip archives.
root /srv/http/aurweb/archives;
types { application/gzip text/plain }
default_type text/plain;
add_header Content-Encoding gzip;
expires 5m;
}
# For everything else, proxy the http request to (guni|uvi|hyper)corn.
# The ASGI server application should allow this request's IP to be
# forwarded via the headers used below.
# https://docs.gunicorn.org/en/stable/settings.html#forwarded-allow-ips
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Protocol ssl;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Ssl on;
}
}
3) Copy conf/config.proto to /etc/aurweb/config and adjust the configuration
(pay attention to disable_http_login, enable_maintenance and aur_location).
3) Optionally copy conf/config.defaults to /etc/aurweb/. Create or copy
/etc/aurweb/config (this is expected to contain all configuration settings
if the defaults file does not exist) and adjust the configuration (pay
attention to disable_http_login, enable_maintenance and aur_location).
4) Create a new MySQL database and a user and import the aurweb SQL schema:
4) Install system-wide dependencies:
$ mysql -uaur -p AUR </srv/http/aurweb/schema/aur-schema.sql
# pacman -S git gpgme cgit curl openssh uwsgi uwsgi-plugin-cgi \
python-poetry
5) Install Python modules and dependencies:
# pacman -S python-mysql-connector python-pygit2 python-srcinfo
# pacman -S python-bleach python-markdown
# python3 setup.py install
6) Create a new user:
5) Create a new user:
# useradd -U -d /srv/http/aurweb -c 'AUR user' aur
# su - aur
7) Initialize the Git repository:
6a) Install Python dependencies via poetry:
# Install the package and scripts as the aur user.
$ poetry install
6b) Setup Services
aurweb utilizes the following systemd services:
- mariadb
- redis (optional, requires [options] cache 'redis')
- `examples/aurweb.service`
6c) Setup Cron
Using [cronie](https://archlinux.org/packages/core/x86_64/cronie/):
# su - aur
$ crontab -e
The following crontab file uses every script meant to be run on an
interval:
AUR_CONFIG='/etc/aurweb/config'
*/5 * * * * bash -c 'poetry run aurweb-mkpkglists --extended'
*/2 * * * * bash -c 'poetry run aurweb-aurblup'
*/2 * * * * bash -c 'poetry run aurweb-pkgmaint'
*/2 * * * * bash -c 'poetry run aurweb-usermaint'
*/2 * * * * bash -c 'poetry run aurweb-popupdate'
*/12 * * * * bash -c 'poetry run aurweb-votereminder'
7) Create a new database and a user and import the aurweb SQL schema:
$ poetry run python -m aurweb.initdb
8) Initialize the Git repository:
# mkdir /srv/http/aurweb/aur.git/
# cd /srv/http/aurweb/aur.git/
@ -63,19 +134,26 @@ read the instructions below.
# git config --local transfer.hideRefs '^refs/'
# git config --local --add transfer.hideRefs '!refs/'
# git config --local --add transfer.hideRefs '!HEAD'
# ln -s /usr/local/bin/aurweb-git-update hooks/update
# chown -R aur .
Link to `aurweb-git-update` poetry wrapper provided at
`examples/aurweb-git-update.sh` which should be installed
somewhere as executable.
# ln -s /path/to/aurweb-git-update.sh hooks/update
It is recommended to read doc/git-interface.txt for more information on the
administration of the package Git repository.
8) Configure sshd(8) for the AUR. Add the following lines at the end of your
sshd_config(5) and restart the sshd. Note that OpenSSH 6.9 or newer is
needed!
9) Configure sshd(8) for the AUR. Add the following lines at the end of your
sshd_config(5) and restart the sshd.
If using a virtualenv, copy `examples/aurweb-git-auth.sh` to a location
and call it below:
Match User aur
PasswordAuthentication no
AuthorizedKeysCommand /usr/local/bin/aurweb-git-auth "%t" "%k"
AuthorizedKeysCommand /path/to/aurweb-git-auth.sh "%t" "%k"
AuthorizedKeysCommandUser aur
AcceptEnv AUR_OVERWRITE
@ -93,3 +171,18 @@ read the instructions below.
}
Sample systemd unit files for fcgiwrap can be found under conf/.
10) If you want Redis to cache data.
# pacman -S redis
# systemctl enable --now redis
And edit the configuration file to enabled redis caching
(`[options] cache = redis`).
11) Start `aurweb.service`.
An example systemd unit has been included at `examples/aurweb.service`.
This unit can be used to manage the aurweb asgi backend. By default,
it is configured to use `poetry` as the `aur` user; this should be
configured as needed.

201
LICENSES/starlette_exporter Normal file
View file

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

58
README
View file

@ -1,58 +0,0 @@
aurweb
======
aurweb is a hosting platform for the Arch User Repository (AUR), a collection
of packaging scripts that are created and submitted by the Arch Linux
community. The scripts contained in the repository can be built using `makepkg`
and installed using the Arch Linux package manager `pacman`.
The aurweb project includes
* A web interface to search for packaging scripts and display package details.
* A SSH/Git interface to submit and update packages and package meta data.
* Community features such as comments, votes, package flagging and requests.
* Editing/deletion of packages and accounts by Trusted Users and Developers.
* Area for Trusted Users to post AUR-related proposals and vote on them.
Directory Layout
----------------
aurweb::
aurweb Python modules.
conf::
Configuration and configuration templates.
doc::
Project documentation.
po::
Translation files for strings in the aurweb interface.
schema::
Schema for the SQL database. Script for dummy data generation.
scripts::
Scripts for AUR maintenance.
test::
Test suite and test cases.
upgrading::
Instructions for upgrading setups from one release to another.
web::
Web interface for the AUR.
Links
-----
* The repository is hosted at git://git.archlinux.org/aurweb.git -- see
doc/CodingGuidelines for information on the patch submission process.
* Bugs can (and should) be submitted to the aurweb bug tracker:
https://bugs.archlinux.org/index.php?project=2
* Questions, comments, and patches related to aurweb can be sent to the AUR
development mailing list: aur-dev@archlinux.org -- mailing list archives:
https://mailman.archlinux.org/mailman/listinfo/aur-dev

66
README.md Normal file
View file

@ -0,0 +1,66 @@
aurweb
======
aurweb is a hosting platform for the Arch User Repository (AUR), a collection
of packaging scripts that are created and submitted by the Arch Linux
community. The scripts contained in the repository can be built using `makepkg`
and installed using the Arch Linux package manager `pacman`.
The aurweb project includes
* A web interface to search for packaging scripts and display package details.
* An SSH/Git interface to submit and update packages and package meta data.
* Community features such as comments, votes, package flagging and requests.
* Editing/deletion of packages and accounts by Package Maintainers and Developers.
* Area for Package Maintainers to post AUR-related proposals and vote on them.
Directory Layout
----------------
* `aurweb`: aurweb Python modules, Git interface and maintenance scripts
* `conf`: configuration and configuration templates
* `static`: static resource files
* `templates`: jinja2 template collection
* `doc`: project documentation
* `po`: translation files for strings in the aurweb interface
* `schema`: schema for the SQL database
* `test`: test suite and test cases
* `upgrading`: instructions for upgrading setups from one release to another
Documentation
-------------
| What | Link |
|--------------|--------------------------------------------------|
| Installation | [INSTALL](./INSTALL) |
| Testing | [test/README.md](./test/README.md) |
| Git | [doc/git-interface.txt](./doc/git-interface.txt) |
| Maintenance | [doc/maintenance.txt](./doc/maintenance.txt) |
| RPC | [doc/rpc.txt](./doc/rpc.txt) |
| Docker | [doc/docker.md](./doc/docker.md) |
Links
-----
* The repository is hosted at https://gitlab.archlinux.org/archlinux/aurweb
-- see [CONTRIBUTING.md](./CONTRIBUTING.md) for information on the patch submission process.
* Bugs can (and should) be submitted to the aurweb bug tracker:
https://gitlab.archlinux.org/archlinux/aurweb/-/issues/new?issuable_template=Bug
* Questions, comments, and patches related to aurweb can be sent to the AUR
development mailing list: aur-dev@archlinux.org -- mailing list archives:
https://mailman.archlinux.org/mailman/listinfo/aur-dev
Translations
------------
Translations are welcome via our Transifex project at
https://www.transifex.com/lfleischer/aurweb; see [doc/i18n.md](./doc/i18n.md) for details.
![Transifex](https://www.transifex.com/projects/p/aurweb/chart/image_png)
Testing
-------
See [test/README.md](test/README.md) for details on dependencies and testing.

181
TESTING
View file

@ -1,6 +1,56 @@
Setup Testing Environment
=========================
The quickest way to get you hacking on aurweb is to utilize docker.
In case you prefer to run it bare-metal see instructions further below.
Containerized environment
-------------------------
1) Clone the aurweb project:
$ git clone https://gitlab.archlinux.org/archlinux/aurweb.git
$ cd aurweb
2) Install the necessary packages:
# pacman -S --needed docker docker-compose
3) Build the aurweb:latest image:
# systemctl start docker
# docker compose build
4) Run local Docker development instance:
# docker compose up -d
5) Browse to local aurweb development server.
https://localhost:8444/
6) [Optionally] populate the database with dummy data:
# docker compose exec mariadb /bin/bash
# pacman -S --noconfirm words fortune-mod
# poetry run schema/gendummydata.py dummy_data.sql
# mariadb -uaur -paur aurweb < dummy_data.sql
# exit
Inspect `dummy_data.sql` for test credentials.
Passwords match usernames.
We now have fully set up environment which we can start and stop with:
# docker compose start
# docker compose stop
Proceed with topic "Setup for running tests"
Bare Metal installation
-----------------------
Note that this setup is only to test the web interface. If you need to have a
full aurweb instance with cgit, ssh interface, etc, follow the directions in
INSTALL.
@ -8,25 +58,128 @@ INSTALL.
1) Clone the aurweb project:
$ git clone git://git.archlinux.org/aurweb.git
$ cd aurweb
2) Install php and necessary modules:
2) Install the necessary packages:
# pacman -S php php-sqlite sqlite
# pacman -S --needed python-poetry mariadb words fortune-mod nginx
3) Prepare the testing database:
3) Install the package/dependencies via `poetry`:
$ cd /path/to/aurweb/schema
$ make
$ ./gendummydata.py out.sql
$ sqlite3 ../aurweb.sqlite3 < aur-schema-sqlite.sql
$ sqlite3 ../aurweb.sqlite3 < out.sql
$ poetry install
4) Copy conf/config.proto to conf/config and adjust the configuration
(pay attention to disable_http_login, enable_maintenance and aur_location).
4) Copy conf/config.dev to conf/config and replace YOUR_AUR_ROOT by the absolute
path to the root of your aurweb clone. sed can do both tasks for you:
Be sure to change backend to sqlite and name to the file location of your
created test database.
$ sed -e "s;YOUR_AUR_ROOT;$PWD;g" conf/config.dev > conf/config
5) Run the PHP built-in web server:
Note that when the upstream config.dev is updated, you should compare it to
your conf/config, or regenerate your configuration with the command above.
$ AUR_CONFIG='/path/to/aurweb/conf/config' php -S localhost:8080 -t /path/to/aurweb/web/html
5) Set up mariadb:
# mariadb-install-db --user=mysql --basedir=/usr --datadir=/var/lib/mysql
# systemctl start mariadb
# mariadb -u root
> CREATE USER 'aur'@'localhost' IDENTIFIED BY 'aur';
> GRANT ALL ON *.* TO 'aur'@'localhost' WITH GRANT OPTION;
> CREATE DATABASE aurweb;
> exit
6) Prepare a database and insert dummy data:
$ AUR_CONFIG=conf/config poetry run python -m aurweb.initdb
$ poetry run schema/gendummydata.py dummy_data.sql
$ mariadb -uaur -paur aurweb < dummy_data.sql
7) Run the test server:
## set AUR_CONFIG to our locally created config
$ export AUR_CONFIG=conf/config
## with aurweb.spawn
$ poetry run python -m aurweb.spawn
## with systemd service
$ sudo install -m644 examples/aurweb.service /etc/systemd/system/
# systemctl enable --now aurweb.service
Setup for running tests
-----------------------
If you've set up a docker environment, you can run the full test-suite with:
# docker compose run test
You can collect code-coverage data with:
$ ./util/fix-coverage data/.coverage
See information further below on how to visualize the data.
For running individual tests, we need to perform a couple of additional steps.
In case you did the bare-metal install, steps 2, 3, 4 and 5 should be skipped.
1) Install the necessary packages:
# pacman -S --needed python-poetry mariadb-libs asciidoc openssh
2) Install the package/dependencies via `poetry`:
$ poetry install
3) Copy conf/config.dev to conf/config and replace YOUR_AUR_ROOT by the absolute
path to the root of your aurweb clone. sed can do both tasks for you:
$ sed -e "s;YOUR_AUR_ROOT;$PWD;g" conf/config.dev > conf/config
Note that when the upstream config.dev is updated, you should compare it to
your conf/config, or regenerate your configuration with the command above.
4) Edit the config file conf/config and change the mysql/mariadb portion
We can make use of our mariadb docker container instead of having to install
mariadb. Change the config as follows:
---------------------------------------------------------------------
; MySQL database information. User defaults to root for containerized
; testing with mysqldb. This should be set to a non-root user.
user = root
password = aur
host = 127.0.0.1
port = 13306
;socket = /var/run/mysqld/mysqld.sock
---------------------------------------------------------------------
5) Start our mariadb docker container
# docker compose start mariadb
6) Set environment variables
$ export AUR_CONFIG=conf/config
$ export LOG_CONFIG=logging.test.conf
7) Compile translation & doc files
$ make -C po install
$ make -C doc
Now we can run our python test-suite or individual tests with:
$ poetry run pytest test/
$ poetry run pytest test/test_whatever.py
To run Sharness tests:
$ poetry run make -C test sh
The e-Mails that have been generated can be found at test-emails/
After test runs, code-coverage reports can be created with:
## CLI report
$ coverage report
## HTML version stored at htmlcov/
$ coverage html
More information about tests can be found at test/README.md

86
alembic.ini Normal file
View file

@ -0,0 +1,86 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = migrations
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# timezone to use when rendering the date
# within the migration file as well as the filename.
# string value is passed to dateutil.tz.gettz()
# leave blank for localtime
# timezone =
# max length of characters to apply to the
# "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; this defaults
# to alembic/versions. When using multiple version
# directories, initial revisions must be specified with --version-path
# version_locations = %(here)s/bar %(here)s/bat alembic/versions
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
# the database URL is generated in env.py
# sqlalchemy.url = driver://user:pass@localhost/dbname
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks=black
# black.type=console_scripts
# black.entrypoint=black
# black.options=-l 79
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

View file

@ -0,0 +1 @@
# aurweb.archives

View file

@ -0,0 +1 @@
# aurweb.archives.spec

View file

@ -0,0 +1,77 @@
from pathlib import Path
from typing import Any, Dict, Iterable, List, Set
class GitInfo:
"""Information about a Git repository."""
""" Path to Git repository. """
path: str
""" Local Git repository configuration. """
config: Dict[str, Any]
def __init__(self, path: str, config: Dict[str, Any] = dict()) -> "GitInfo":
self.path = Path(path)
self.config = config
class SpecOutput:
"""Class used for git_archive.py output details."""
""" Filename relative to the Git repository root. """
filename: Path
""" Git repository information. """
git_info: GitInfo
""" Bytes bound for `SpecOutput.filename`. """
data: bytes
def __init__(self, filename: str, git_info: GitInfo, data: bytes) -> "SpecOutput":
self.filename = filename
self.git_info = git_info
self.data = data
class SpecBase:
"""
Base for Spec classes defined in git_archve.py --spec modules.
All supported --spec modules must contain the following classes:
- Spec(SpecBase)
"""
""" A list of SpecOutputs, each of which contain output file data. """
outputs: List[SpecOutput] = list()
""" A set of repositories to commit changes to. """
repos: Set[str] = set()
def generate(self) -> Iterable[SpecOutput]:
"""
"Pure virtual" output generator.
`SpecBase.outputs` and `SpecBase.repos` should be populated within an
overridden version of this function in SpecBase derivatives.
"""
raise NotImplementedError()
def add_output(self, filename: str, git_info: GitInfo, data: bytes) -> None:
"""
Add a SpecOutput instance to the set of outputs.
:param filename: Filename relative to the git repository root
:param git_info: GitInfo instance
:param data: Binary data bound for `filename`
"""
if git_info.path not in self.repos:
self.repos.add(git_info.path)
self.outputs.append(
SpecOutput(
filename,
git_info,
data,
)
)

View file

@ -0,0 +1,85 @@
from typing import Iterable
import orjson
from aurweb import config, db
from aurweb.models import Package, PackageBase, User
from aurweb.rpc import RPC
from .base import GitInfo, SpecBase, SpecOutput
ORJSON_OPTS = orjson.OPT_SORT_KEYS | orjson.OPT_INDENT_2
class Spec(SpecBase):
def __init__(self) -> "Spec":
self.metadata_repo = GitInfo(
config.get("git-archive", "metadata-repo"),
)
def generate(self) -> Iterable[SpecOutput]:
# Base query used by the RPC.
base_query = (
db.query(Package)
.join(PackageBase)
.join(User, PackageBase.MaintainerUID == User.ID, isouter=True)
)
# Create an instance of RPC, use it to get entities from
# our query and perform a metadata subquery for all packages.
rpc = RPC(version=5, type="info")
print("performing package database query")
packages = rpc.entities(base_query).all()
print("performing package database subqueries")
rpc.subquery({pkg.ID for pkg in packages})
pkgbases, pkgnames = dict(), dict()
for package in packages:
# Produce RPC type=info data for `package`
data = rpc.get_info_json_data(package)
pkgbase_name = data.get("PackageBase")
pkgbase_data = {
"ID": data.pop("PackageBaseID"),
"URLPath": data.pop("URLPath"),
"FirstSubmitted": data.pop("FirstSubmitted"),
"LastModified": data.pop("LastModified"),
"OutOfDate": data.pop("OutOfDate"),
"Maintainer": data.pop("Maintainer"),
"Keywords": data.pop("Keywords"),
"NumVotes": data.pop("NumVotes"),
"Popularity": data.pop("Popularity"),
"PopularityUpdated": package.PopularityUpdated.timestamp(),
}
# Store the data in `pkgbases` dict. We do this so we only
# end up processing a single `pkgbase` if repeated after
# this loop
pkgbases[pkgbase_name] = pkgbase_data
# Remove Popularity and NumVotes from package data.
# These fields change quite often which causes git data
# modification to explode.
# data.pop("NumVotes")
# data.pop("Popularity")
# Remove the ID key from package json.
data.pop("ID")
# Add the `package`.Name to the pkgnames set
name = data.get("Name")
pkgnames[name] = data
# Add metadata outputs
self.add_output(
"pkgname.json",
self.metadata_repo,
orjson.dumps(pkgnames, option=ORJSON_OPTS),
)
self.add_output(
"pkgbase.json",
self.metadata_repo,
orjson.dumps(pkgbases, option=ORJSON_OPTS),
)
return self.outputs

View file

@ -0,0 +1,26 @@
from typing import Iterable
import orjson
from aurweb import config, db
from aurweb.models import PackageBase
from .base import GitInfo, SpecBase, SpecOutput
ORJSON_OPTS = orjson.OPT_SORT_KEYS | orjson.OPT_INDENT_2
class Spec(SpecBase):
def __init__(self) -> "Spec":
self.pkgbases_repo = GitInfo(config.get("git-archive", "pkgbases-repo"))
def generate(self) -> Iterable[SpecOutput]:
query = db.query(PackageBase.Name).order_by(PackageBase.Name.asc()).all()
pkgbases = [pkgbase.Name for pkgbase in query]
self.add_output(
"pkgbase.json",
self.pkgbases_repo,
orjson.dumps(pkgbases, option=ORJSON_OPTS),
)
return self.outputs

View file

@ -0,0 +1,31 @@
from typing import Iterable
import orjson
from aurweb import config, db
from aurweb.models import Package, PackageBase
from .base import GitInfo, SpecBase, SpecOutput
ORJSON_OPTS = orjson.OPT_SORT_KEYS | orjson.OPT_INDENT_2
class Spec(SpecBase):
def __init__(self) -> "Spec":
self.pkgnames_repo = GitInfo(config.get("git-archive", "pkgnames-repo"))
def generate(self) -> Iterable[SpecOutput]:
query = (
db.query(Package.Name)
.join(PackageBase, PackageBase.ID == Package.PackageBaseID)
.order_by(Package.Name.asc())
.all()
)
pkgnames = [pkg.Name for pkg in query]
self.add_output(
"pkgname.json",
self.pkgnames_repo,
orjson.dumps(pkgnames, option=ORJSON_OPTS),
)
return self.outputs

View file

@ -0,0 +1,26 @@
from typing import Iterable
import orjson
from aurweb import config, db
from aurweb.models import User
from .base import GitInfo, SpecBase, SpecOutput
ORJSON_OPTS = orjson.OPT_SORT_KEYS | orjson.OPT_INDENT_2
class Spec(SpecBase):
def __init__(self) -> "Spec":
self.users_repo = GitInfo(config.get("git-archive", "users-repo"))
def generate(self) -> Iterable[SpecOutput]:
query = db.query(User.Username).order_by(User.Username.asc()).all()
users = [user.Username for user in query]
self.add_output(
"users.json",
self.users_repo,
orjson.dumps(users, option=ORJSON_OPTS),
)
return self.outputs

339
aurweb/asgi.py Normal file
View file

@ -0,0 +1,339 @@
import hashlib
import http
import io
import os
import re
import sys
import traceback
import typing
from contextlib import asynccontextmanager
from urllib.parse import quote_plus
import requests
from fastapi import FastAPI, HTTPException, Request, Response
from fastapi.responses import RedirectResponse
from fastapi.staticfiles import StaticFiles
from jinja2 import TemplateNotFound
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from sqlalchemy import and_
from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.middleware.authentication import AuthenticationMiddleware
from starlette.middleware.sessions import SessionMiddleware
import aurweb.captcha # noqa: F401
import aurweb.config
import aurweb.filters # noqa: F401
from aurweb import aur_logging, prometheus, util
from aurweb.aur_redis import redis_connection
from aurweb.auth import BasicAuthBackend
from aurweb.db import get_engine, query
from aurweb.models import AcceptedTerm, Term
from aurweb.packages.util import get_pkg_or_base
from aurweb.prometheus import instrumentator
from aurweb.routers import APP_ROUTES
from aurweb.templates import make_context, render_template
logger = aur_logging.get_logger(__name__)
session_secret = aurweb.config.get("fastapi", "session_secret")
@asynccontextmanager
async def lifespan(app: FastAPI):
await app_startup()
yield
# Setup the FastAPI app.
app = FastAPI(lifespan=lifespan)
# Instrument routes with the prometheus-fastapi-instrumentator
# library with custom collectors and expose /metrics.
instrumentator().add(prometheus.http_api_requests_total())
instrumentator().add(prometheus.http_requests_total())
instrumentator().instrument(app)
# Instrument FastAPI for tracing
FastAPIInstrumentor.instrument_app(app)
resource = Resource(attributes={"service.name": "aurweb"})
otlp_endpoint = aurweb.config.get("tracing", "otlp_endpoint")
otlp_exporter = OTLPSpanExporter(endpoint=otlp_endpoint)
span_processor = BatchSpanProcessor(otlp_exporter)
trace.set_tracer_provider(TracerProvider(resource=resource))
trace.get_tracer_provider().add_span_processor(span_processor)
async def app_startup():
# https://stackoverflow.com/questions/67054759/about-the-maximum-recursion-error-in-fastapi
# Test failures have been observed by internal starlette code when
# using starlette.testclient.TestClient. Looking around in regards
# to the recursion error has really not recommended a course of action
# other than increasing the recursion limit. For now, that is how
# we handle the issue: an optional TEST_RECURSION_LIMIT env var
# provided by the user. Docker uses .env's TEST_RECURSION_LIMIT
# when running test suites.
# TODO: Find a proper fix to this issue.
recursion_limit = int(
os.environ.get("TEST_RECURSION_LIMIT", sys.getrecursionlimit() + 1000)
)
sys.setrecursionlimit(recursion_limit)
backend = aurweb.config.get("database", "backend")
if backend not in aurweb.db.DRIVERS:
raise ValueError(
f"The configured database backend ({backend}) is unsupported. "
f"Supported backends: {str(aurweb.db.DRIVERS.keys())}"
)
if not session_secret:
raise Exception("[fastapi] session_secret must not be empty")
if not os.environ.get("PROMETHEUS_MULTIPROC_DIR", None):
logger.warning(
"$PROMETHEUS_MULTIPROC_DIR is not set, the /metrics "
"endpoint is disabled."
)
app.mount("/static", StaticFiles(directory="static"), name="static_files")
# Add application routes.
def add_router(module):
app.include_router(module.router)
util.apply_all(APP_ROUTES, add_router)
# Initialize the database engine and ORM.
get_engine()
async def internal_server_error(request: Request, exc: Exception) -> Response:
"""
Catch all uncaught Exceptions thrown in a route.
:param request: FastAPI Request
:return: Rendered 500.html template with status_code 500
"""
repo = aurweb.config.get("notifications", "gitlab-instance")
project = aurweb.config.get("notifications", "error-project")
token = aurweb.config.get("notifications", "error-token")
context = make_context(request, "Internal Server Error")
# Print out the exception via `traceback` and store the value
# into the `traceback` context variable.
tb_io = io.StringIO()
traceback.print_exc(file=tb_io)
tb = tb_io.getvalue()
context["traceback"] = tb
# Produce a SHA1 hash of the traceback string.
tb_hash = hashlib.sha1(tb.encode()).hexdigest()
tb_id = tb_hash[:7]
redis = redis_connection()
key = f"tb:{tb_hash}"
retval = redis.get(key)
if not retval:
# Expire in one hour; this is just done to make sure we
# don't infinitely store these values, but reduce the number
# of automated reports (notification below). At this time of
# writing, unexpected exceptions are not common, thus this
# will not produce a large memory footprint in redis.
pipe = redis.pipeline()
pipe.set(key, tb)
pipe.expire(key, 86400) # One day.
pipe.execute()
# Send out notification about it.
if "set-me" not in (project, token):
proj = quote_plus(project)
endp = f"{repo}/api/v4/projects/{proj}/issues"
base = f"{request.url.scheme}://{request.url.netloc}"
title = f"Traceback [{tb_id}]: {base}{request.url.path}"
desc = [
"DISCLAIMER",
"----------",
"**This issue is confidential** and should be sanitized "
"before sharing with users or developers. Please ensure "
"you've completed the following tasks:",
"- [ ] I have removed any sensitive data and "
"the description history.",
"",
"Exception Details",
"-----------------",
f"- Route: `{request.url.path}`",
f"- User: `{request.user.Username}`",
f"- Email: `{request.user.Email}`",
]
# Add method-specific information to the description.
if request.method.lower() == "get":
# get
if request.url.query:
desc = desc + [f"- Query: `{request.url.query}`"]
desc += ["", f"```{tb}```"]
else:
# post
form_data = str(dict(request.state.form_data))
desc = desc + [f"- Data: `{form_data}`"] + ["", f"```{tb}```"]
headers = {"Authorization": f"Bearer {token}"}
data = {
"title": title,
"description": "\n".join(desc),
"labels": ["triage"],
"confidential": True,
}
logger.info(endp)
resp = requests.post(endp, json=data, headers=headers)
if resp.status_code != http.HTTPStatus.CREATED:
logger.error(f"Unable to report exception to {repo}: {resp.text}")
else:
logger.warning(
"Unable to report an exception found due to "
"unset notifications.error-{{project,token}}"
)
# Log details about the exception traceback.
logger.error(f"FATAL[{tb_id}]: An unexpected exception has occurred.")
logger.error(tb)
else:
retval = retval.decode()
return render_template(
request,
"errors/500.html",
context,
status_code=http.HTTPStatus.INTERNAL_SERVER_ERROR,
)
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request: Request, exc: HTTPException) -> Response:
"""Handle an HTTPException thrown in a route."""
phrase = http.HTTPStatus(exc.status_code).phrase
context = make_context(request, phrase)
context["exc"] = exc
context["phrase"] = phrase
# Additional context for some exceptions.
if exc.status_code == http.HTTPStatus.NOT_FOUND:
tokens = request.url.path.split("/")
matches = re.match("^([a-z0-9][a-z0-9.+_-]*?)(\\.git)?$", tokens[1])
if matches and len(tokens) == 2:
try:
pkgbase = get_pkg_or_base(matches.group(1))
context["pkgbase"] = pkgbase
context["git_clone_uri_anon"] = aurweb.config.get(
"options", "git_clone_uri_anon"
)
context["git_clone_uri_priv"] = aurweb.config.get(
"options", "git_clone_uri_priv"
)
except HTTPException:
pass
try:
return render_template(
request, f"errors/{exc.status_code}.html", context, exc.status_code
)
except TemplateNotFound:
return render_template(request, "errors/detail.html", context, exc.status_code)
@app.middleware("http")
async def add_security_headers(request: Request, call_next: typing.Callable):
"""This middleware adds the CSP, XCTO, XFO and RP security
headers to the HTTP response associated with request.
CSP: Content-Security-Policy
XCTO: X-Content-Type-Options
RP: Referrer-Policy
XFO: X-Frame-Options
"""
try:
response = await util.error_or_result(call_next, request)
except Exception as exc:
return await internal_server_error(request, exc)
# Add CSP header.
nonce = request.user.nonce
csp = "default-src 'self'; "
# swagger-ui needs access to cdn.jsdelivr.net javascript
script_hosts = ["cdn.jsdelivr.net"]
csp += f"script-src 'self' 'unsafe-inline' 'nonce-{nonce}' " + " ".join(
script_hosts
)
# swagger-ui needs access to cdn.jsdelivr.net css
css_hosts = ["cdn.jsdelivr.net"]
csp += "; style-src 'self' 'unsafe-inline' " + " ".join(css_hosts)
response.headers["Content-Security-Policy"] = csp
# Add XTCO header.
xcto = "nosniff"
response.headers["X-Content-Type-Options"] = xcto
# Add Referrer Policy header.
rp = "same-origin"
response.headers["Referrer-Policy"] = rp
# Add X-Frame-Options header.
xfo = "SAMEORIGIN"
response.headers["X-Frame-Options"] = xfo
return response
@app.middleware("http")
async def check_terms_of_service(request: Request, call_next: typing.Callable):
"""This middleware function redirects authenticated users if they
have any outstanding Terms to agree to."""
if request.user.is_authenticated() and request.url.path != "/tos":
accepted = (
query(Term)
.join(AcceptedTerm)
.filter(
and_(
AcceptedTerm.UsersID == request.user.ID,
AcceptedTerm.TermsID == Term.ID,
AcceptedTerm.Revision >= Term.Revision,
),
)
)
if query(Term).count() - accepted.count() > 0:
return RedirectResponse("/tos", status_code=int(http.HTTPStatus.SEE_OTHER))
return await util.error_or_result(call_next, request)
@app.middleware("http")
async def id_redirect_middleware(request: Request, call_next: typing.Callable):
id = request.query_params.get("id")
if id is not None:
# Preserve query string.
qs = []
for k, v in request.query_params.items():
if k != "id":
qs.append(f"{k}={quote_plus(str(v))}")
qs = str() if not qs else "?" + "&".join(qs)
path = request.url.path.rstrip("/")
return RedirectResponse(f"{path}/{id}{qs}")
return await util.error_or_result(call_next, request)
# Add application middlewares.
app.add_middleware(AuthenticationMiddleware, backend=BasicAuthBackend())
app.add_middleware(SessionMiddleware, secret_key=session_secret)

26
aurweb/aur_logging.py Normal file
View file

@ -0,0 +1,26 @@
import logging
import logging.config
import os
import aurweb.config
# For testing, users should set LOG_CONFIG=logging.test.conf
# We test against various debug log output.
aurwebdir = aurweb.config.get("options", "aurwebdir")
log_config = os.environ.get("LOG_CONFIG", "logging.conf")
config_path = os.path.join(aurwebdir, log_config)
logging.config.fileConfig(config_path, disable_existing_loggers=False)
logging.getLogger("root").addHandler(logging.NullHandler())
def get_logger(name: str) -> logging.Logger:
"""A logging.getLogger wrapper. Importing this function and
using it to get a module-local logger ensures that logging.conf
initialization is performed wherever loggers are used.
:param name: Logger name; typically `__name__`
:returns: name's logging.Logger
"""
return logging.getLogger(name)

58
aurweb/aur_redis.py Normal file
View file

@ -0,0 +1,58 @@
import fakeredis
from opentelemetry.instrumentation.redis import RedisInstrumentor
from redis import ConnectionPool, Redis
import aurweb.config
from aurweb import aur_logging
logger = aur_logging.get_logger(__name__)
pool = None
RedisInstrumentor().instrument()
class FakeConnectionPool:
"""A fake ConnectionPool class which holds an internal reference
to a fakeredis handle.
We normally deal with Redis by keeping its ConnectionPool globally
referenced so we can persist connection state through different calls
to redis_connection(), and since FakeRedis does not offer a ConnectionPool,
we craft one up here to hang onto the same handle instance as long as the
same instance is alive; this allows us to use a similar flow from the
redis_connection() user's perspective.
"""
def __init__(self):
self.handle = fakeredis.FakeStrictRedis()
def disconnect(self):
pass
def redis_connection(): # pragma: no cover
global pool
disabled = aurweb.config.get("options", "cache") != "redis"
# If we haven't initialized redis yet, construct a pool.
if disabled:
if pool is None:
logger.debug("Initializing fake Redis instance.")
pool = FakeConnectionPool()
return pool.handle
else:
if pool is None:
logger.debug("Initializing real Redis instance.")
redis_addr = aurweb.config.get("options", "redis_address")
pool = ConnectionPool.from_url(redis_addr)
# Create a connection to the pool.
return Redis(connection_pool=pool)
def kill_redis():
global pool
if pool:
pool.disconnect()
pool = None

227
aurweb/auth/__init__.py Normal file
View file

@ -0,0 +1,227 @@
import functools
from http import HTTPStatus
from typing import Callable
import fastapi
from fastapi import HTTPException
from fastapi.responses import RedirectResponse
from starlette.authentication import AuthCredentials, AuthenticationBackend
from starlette.requests import HTTPConnection
import aurweb.config
from aurweb import db, filters, l10n, time, util
from aurweb.models import Session, User
from aurweb.models.account_type import ACCOUNT_TYPE_ID
class StubQuery:
"""Acts as a stubbed version of an orm.Query. Typically used
to masquerade fake records for an AnonymousUser."""
def filter(self, *args):
return StubQuery()
def scalar(self):
return 0
class AnonymousUser:
"""A stubbed User class used when an unauthenticated User
makes a request against FastAPI."""
# Stub attributes used to mimic a real user.
ID = 0
Username = "N/A"
Email = "N/A"
class AccountType:
"""A stubbed AccountType static class. In here, we use an ID
and AccountType which do not exist in our constant records.
All records primary keys (AccountType.ID) should be non-zero,
so using a zero here means that we'll never match against a
real AccountType."""
ID = 0
AccountType = "Anonymous"
# AccountTypeID == AccountType.ID; assign a stubbed column.
AccountTypeID = AccountType.ID
LangPreference = aurweb.config.get("options", "default_lang")
Timezone = aurweb.config.get("options", "default_timezone")
Suspended = 0
InactivityTS = 0
# A stub ssh_pub_key relationship.
ssh_pub_key = None
# Add stubbed relationship backrefs.
notifications = StubQuery()
package_votes = StubQuery()
# A nonce attribute, needed for all browser sessions; set in __init__.
nonce = None
def __init__(self):
self.nonce = util.make_nonce()
@staticmethod
def is_authenticated():
return False
@staticmethod
def is_package_maintainer():
return False
@staticmethod
def is_developer():
return False
@staticmethod
def is_elevated():
return False
@staticmethod
def has_credential(credential, **kwargs):
return False
@staticmethod
def voted_for(package):
return False
@staticmethod
def notified(package):
return False
class BasicAuthBackend(AuthenticationBackend):
@db.async_retry_deadlock
async def authenticate(self, conn: HTTPConnection):
unauthenticated = (None, AnonymousUser())
sid = conn.cookies.get("AURSID")
if not sid:
return unauthenticated
timeout = aurweb.config.getint("options", "login_timeout")
remembered = conn.cookies.get("AURREMEMBER") == "True"
if remembered:
timeout = aurweb.config.getint("options", "persistent_cookie_timeout")
# If no session with sid and a LastUpdateTS now or later exists.
now_ts = time.utcnow()
record = db.query(Session).filter(Session.SessionID == sid).first()
if not record:
return unauthenticated
elif record.LastUpdateTS < (now_ts - timeout):
with db.begin():
db.delete_all([record])
return unauthenticated
# At this point, we cannot have an invalid user if the record
# exists, due to ForeignKey constraints in the schema upheld
# by mysqlclient.
user = db.query(User).filter(User.ID == record.UsersID).first()
user.nonce = util.make_nonce()
user.authenticated = True
return AuthCredentials(["authenticated"]), user
def _auth_required(auth_goal: bool = True):
"""
Enforce a user's authentication status, bringing them to the login page
or homepage if their authentication status does not match the goal.
NOTE: This function should not need to be used in downstream code.
See `requires_auth` and `requires_guest` for decorators meant to be
used on routes (they're a bit more implicitly understandable).
:param auth_goal: Whether authentication is required or entirely disallowed
for a user to perform this request.
:return: Return the FastAPI function this decorator wraps.
"""
def decorator(func):
@functools.wraps(func)
async def wrapper(request, *args, **kwargs):
if request.user.is_authenticated() == auth_goal:
return await func(request, *args, **kwargs)
url = "/"
if auth_goal is False:
return RedirectResponse(url, status_code=int(HTTPStatus.SEE_OTHER))
# Use the request path when the user can visit a page directly but
# is not authenticated and use the Referer header if visiting the
# page itself is not directly possible (e.g. submitting a form).
if request.method in ("GET", "HEAD"):
url = request.url.path
elif referer := request.headers.get("Referer"):
aur = aurweb.config.get("options", "aur_location") + "/"
if not referer.startswith(aur):
_ = l10n.get_translator_for_request(request)
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=_("Bad Referer header."),
)
url = referer[len(aur) - 1 :]
url = "/login?" + filters.urlencode({"next": url})
return RedirectResponse(url, status_code=int(HTTPStatus.SEE_OTHER))
return wrapper
return decorator
def requires_auth(func: Callable) -> Callable:
"""Require an authenticated session for a particular route."""
@functools.wraps(func)
async def wrapper(*args, **kwargs):
return await _auth_required(True)(func)(*args, **kwargs)
return wrapper
def requires_guest(func: Callable) -> Callable:
"""Require a guest (unauthenticated) session for a particular route."""
@functools.wraps(func)
async def wrapper(*args, **kwargs):
return await _auth_required(False)(func)(*args, **kwargs)
return wrapper
def account_type_required(one_of: set):
"""A decorator that can be used on FastAPI routes to dictate
that a user belongs to one of the types defined in one_of.
This decorator should be run after an @auth_required(True) is
dictated.
- Example code:
@router.get('/some_route')
@auth_required(True)
@account_type_required({"Package Maintainer", "Package Maintainer & Developer"})
async def some_route(request: fastapi.Request):
return Response()
:param one_of: A set consisting of strings to match against AccountType.
:return: Return the FastAPI function this decorator wraps.
"""
# Convert any account type string constants to their integer IDs.
one_of = {ACCOUNT_TYPE_ID[atype] for atype in one_of if isinstance(atype, str)}
def decorator(func):
@functools.wraps(func)
async def wrapper(request: fastapi.Request, *args, **kwargs):
if request.user.AccountTypeID not in one_of:
return RedirectResponse("/", status_code=int(HTTPStatus.SEE_OTHER))
return await func(request, *args, **kwargs)
return wrapper
return decorator

82
aurweb/auth/creds.py Normal file
View file

@ -0,0 +1,82 @@
from aurweb.models.account_type import (
DEVELOPER_ID,
PACKAGE_MAINTAINER_AND_DEV_ID,
PACKAGE_MAINTAINER_ID,
USER_ID,
)
from aurweb.models.user import User
ACCOUNT_CHANGE_TYPE = 1
ACCOUNT_EDIT = 2
ACCOUNT_EDIT_DEV = 3
ACCOUNT_LAST_LOGIN = 4
ACCOUNT_SEARCH = 5
ACCOUNT_LIST_COMMENTS = 28
COMMENT_DELETE = 6
COMMENT_UNDELETE = 27
COMMENT_VIEW_DELETED = 22
COMMENT_EDIT = 25
COMMENT_PIN = 26
PKGBASE_ADOPT = 7
PKGBASE_SET_KEYWORDS = 8
PKGBASE_DELETE = 9
PKGBASE_DISOWN = 10
PKGBASE_EDIT_COMAINTAINERS = 24
PKGBASE_FLAG = 11
PKGBASE_LIST_VOTERS = 12
PKGBASE_NOTIFY = 13
PKGBASE_UNFLAG = 15
PKGBASE_VOTE = 16
PKGREQ_FILE = 23
PKGREQ_CLOSE = 17
PKGREQ_LIST = 18
PM_ADD_VOTE = 19
PM_LIST_VOTES = 20
PM_VOTE = 21
PKGBASE_MERGE = 29
user_developer_or_package_maintainer = set(
[USER_ID, PACKAGE_MAINTAINER_ID, DEVELOPER_ID, PACKAGE_MAINTAINER_AND_DEV_ID]
)
package_maintainer_or_dev = set(
[PACKAGE_MAINTAINER_ID, DEVELOPER_ID, PACKAGE_MAINTAINER_AND_DEV_ID]
)
developer = set([DEVELOPER_ID, PACKAGE_MAINTAINER_AND_DEV_ID])
package_maintainer = set([PACKAGE_MAINTAINER_ID, PACKAGE_MAINTAINER_AND_DEV_ID])
cred_filters = {
PKGBASE_FLAG: user_developer_or_package_maintainer,
PKGBASE_NOTIFY: user_developer_or_package_maintainer,
PKGBASE_VOTE: user_developer_or_package_maintainer,
PKGREQ_FILE: user_developer_or_package_maintainer,
ACCOUNT_CHANGE_TYPE: package_maintainer_or_dev,
ACCOUNT_EDIT: package_maintainer_or_dev,
ACCOUNT_LAST_LOGIN: package_maintainer_or_dev,
ACCOUNT_LIST_COMMENTS: package_maintainer_or_dev,
ACCOUNT_SEARCH: package_maintainer_or_dev,
COMMENT_DELETE: package_maintainer_or_dev,
COMMENT_UNDELETE: package_maintainer_or_dev,
COMMENT_VIEW_DELETED: package_maintainer_or_dev,
COMMENT_EDIT: package_maintainer_or_dev,
COMMENT_PIN: package_maintainer_or_dev,
PKGBASE_ADOPT: package_maintainer_or_dev,
PKGBASE_SET_KEYWORDS: package_maintainer_or_dev,
PKGBASE_DELETE: package_maintainer_or_dev,
PKGBASE_EDIT_COMAINTAINERS: package_maintainer_or_dev,
PKGBASE_DISOWN: package_maintainer_or_dev,
PKGBASE_LIST_VOTERS: package_maintainer_or_dev,
PKGBASE_UNFLAG: package_maintainer_or_dev,
PKGREQ_CLOSE: package_maintainer_or_dev,
PKGREQ_LIST: package_maintainer_or_dev,
PM_ADD_VOTE: package_maintainer,
PM_LIST_VOTES: package_maintainer_or_dev,
PM_VOTE: package_maintainer,
ACCOUNT_EDIT_DEV: developer,
PKGBASE_MERGE: package_maintainer_or_dev,
}
def has_credential(user: User, credential: int, approved: list = tuple()):
if user in approved:
return True
return user.AccountTypeID in cred_filters[credential]

21
aurweb/benchmark.py Normal file
View file

@ -0,0 +1,21 @@
from datetime import UTC, datetime
class Benchmark:
def __init__(self):
self.start()
def _timestamp(self) -> float:
"""Generate a timestamp."""
return float(datetime.now(UTC).timestamp())
def start(self) -> int:
"""Start a benchmark."""
self.current = self._timestamp()
return self.current
def end(self):
"""Return the diff between now - start()."""
n = self._timestamp() - self.current
self.current = float(0)
return n

64
aurweb/cache.py Normal file
View file

@ -0,0 +1,64 @@
import pickle
from typing import Any, Callable
from sqlalchemy import orm
from aurweb import config
from aurweb.aur_redis import redis_connection
from aurweb.prometheus import SEARCH_REQUESTS
_redis = redis_connection()
def lambda_cache(key: str, value: Callable[[], Any], expire: int = None) -> list:
"""Store and retrieve lambda results via redis cache.
:param key: Redis key
:param value: Lambda callable returning the value
:param expire: Optional expiration in seconds
:return: result of callable or cache
"""
result = _redis.get(key)
if result is not None:
return pickle.loads(result)
_redis.set(key, (pickle.dumps(result := value())), ex=expire)
return result
def db_count_cache(key: str, query: orm.Query, expire: int = None) -> int:
"""Store and retrieve a query.count() via redis cache.
:param key: Redis key
:param query: SQLAlchemy ORM query
:param expire: Optional expiration in seconds
:return: query.count()
"""
result = _redis.get(key)
if result is None:
_redis.set(key, (result := int(query.count())))
if expire:
_redis.expire(key, expire)
return int(result)
def db_query_cache(key: str, query: orm.Query, expire: int = None) -> list:
"""Store and retrieve query results via redis cache.
:param key: Redis key
:param query: SQLAlchemy ORM query
:param expire: Optional expiration in seconds
:return: query.all()
"""
result = _redis.get(key)
if result is None:
SEARCH_REQUESTS.labels(cache="miss").inc()
if _redis.dbsize() > config.getint("cache", "max_search_entries", 50000):
return query.all()
_redis.set(key, (result := pickle.dumps(query.all())))
if expire:
_redis.expire(key, expire)
else:
SEARCH_REQUESTS.labels(cache="hit").inc()
return pickle.loads(result)

62
aurweb/captcha.py Normal file
View file

@ -0,0 +1,62 @@
""" This module consists of aurweb's CAPTCHA utility functions and filters. """
import hashlib
from jinja2 import pass_context
from sqlalchemy import func
from aurweb.db import query
from aurweb.models import User
from aurweb.templates import register_filter
def get_captcha_salts():
"""Produce salts based on the current user count."""
count = query(func.count(User.ID)).scalar()
salts = []
for i in range(0, 6):
salts.append(f"aurweb-{count - i}")
return salts
def get_captcha_token(salt):
"""Produce a token for the CAPTCHA salt."""
return hashlib.md5(salt.encode()).hexdigest()[:3]
def get_captcha_challenge(salt):
"""Get a CAPTCHA challenge string (shell command) for a salt."""
token = get_captcha_token(salt)
return f"LC_ALL=C pacman -V|sed -r 's#[0-9]+#{token}#g'|md5sum|cut -c1-6"
def get_captcha_answer(token):
"""Compute the answer via md5 of the real template text, return the
first six digits of the hexadecimal hash."""
text = r"""
.--. Pacman v%s.%s.%s - libalpm v%s.%s.%s
/ _.-' .-. .-. .-. Copyright (C) %s-%s Pacman Development Team
\ '-. '-' '-' '-' Copyright (C) %s-%s Judd Vinet
'--'
This program may be freely redistributed under
the terms of the GNU General Public License.
""" % tuple(
[token] * 10
)
return hashlib.md5((text + "\n").encode()).hexdigest()[:6]
@register_filter("captcha_salt")
@pass_context
def captcha_salt_filter(context):
"""Returns the most recent CAPTCHA salt in the list of salts."""
salts = get_captcha_salts()
return salts[0]
@register_filter("captcha_cmdline")
@pass_context
def captcha_cmdline_filter(context, salt):
"""Returns a CAPTCHA challenge for a given salt."""
return get_captcha_challenge(salt)

View file

@ -1,5 +1,8 @@
import configparser
import os
from typing import Any
import tomlkit
_parser = None
@ -8,23 +11,69 @@ def _get_parser():
global _parser
if not _parser:
path = os.environ.get("AUR_CONFIG", "/etc/aurweb/config")
defaults = os.environ.get("AUR_CONFIG_DEFAULTS", path + ".defaults")
_parser = configparser.RawConfigParser()
if 'AUR_CONFIG' in os.environ:
path = os.environ.get('AUR_CONFIG')
else:
path = "/etc/aurweb/config"
_parser.optionxform = lambda option: option
if os.path.isfile(defaults):
with open(defaults) as f:
_parser.read_file(f)
_parser.read(path)
return _parser
def rehash():
"""Globally rehash the configuration parser."""
global _parser
_parser = None
_get_parser()
def get_with_fallback(section, option, fallback):
return _get_parser().get(section, option, fallback=fallback)
def get(section, option):
return _get_parser().get(section, option)
def _get_project_meta():
with open(os.path.join(get("options", "aurwebdir"), "pyproject.toml")) as pyproject:
file_contents = pyproject.read()
return tomlkit.parse(file_contents)["tool"]["poetry"]
# Publicly visible version of aurweb. This is used to display
# aurweb versioning in the footer and must be maintained.
AURWEB_VERSION = str(_get_project_meta()["version"])
def getboolean(section, option):
return _get_parser().getboolean(section, option)
def getint(section, option):
return _get_parser().getint(section, option)
def getint(section, option, fallback=None):
return _get_parser().getint(section, option, fallback=fallback)
def get_section(section):
if section in _get_parser().sections():
return _get_parser()[section]
def unset_option(section: str, option: str) -> None:
_get_parser().remove_option(section, option)
def set_option(section: str, option: str, value: Any) -> None:
_get_parser().set(section, option, value)
return value
def save() -> None:
aur_config = os.environ.get("AUR_CONFIG", "/etc/aurweb/config")
with open(aur_config, "w") as fp:
_get_parser().write(fp)

8
aurweb/cookies.py Normal file
View file

@ -0,0 +1,8 @@
def samesite() -> str:
"""Produce cookie SameSite value.
Currently this is hard-coded to return "lax"
:returns "lax"
"""
return "lax"

View file

@ -1,43 +1,379 @@
import mysql.connector
import sqlite3
import aurweb.config
# Supported database drivers.
DRIVERS = {"mysql": "mysql+mysqldb"}
class Connection:
def make_random_value(table: str, column: str, length: int):
"""Generate a unique, random value for a string column in a table.
:return: A unique string that is not in the database
"""
import aurweb.util
string = aurweb.util.make_random_string(length)
while query(table).filter(column == string).first():
string = aurweb.util.make_random_string(length)
return string
def test_name() -> str:
"""
Return the unhashed database name.
The unhashed database name is determined (lower = higher priority) by:
-------------------------------------------
1. {test_suite} portion of PYTEST_CURRENT_TEST
2. aurweb.config.get("database", "name")
During `pytest` runs, the PYTEST_CURRENT_TEST environment variable
is set to the current test in the format `{test_suite}::{test_func}`.
This allows tests to use a suite-specific database for its runs,
which decouples database state from test suites.
:return: Unhashed database name
"""
import os
import aurweb.config
db = os.environ.get("PYTEST_CURRENT_TEST", aurweb.config.get("database", "name"))
return db.split(":")[0]
def name() -> str:
"""
Return sanitized database name that can be used for tests or production.
If test_name() starts with "test/", the database name is SHA-1 hashed,
prefixed with 'db', and returned. Otherwise, test_name() is passed
through and not hashed at all.
:return: SHA1-hashed database name prefixed with 'db'
"""
dbname = test_name()
if not dbname.startswith("test/"):
return dbname
import hashlib
sha1 = hashlib.sha1(dbname.encode()).hexdigest()
return "db" + sha1
# Module-private global memo used to store SQLAlchemy sessions.
_sessions = dict()
def get_session(engine=None):
"""Return aurweb.db's global session."""
dbname = name()
global _sessions
if dbname not in _sessions:
from sqlalchemy.orm import scoped_session, sessionmaker
if not engine: # pragma: no cover
engine = get_engine()
Session = scoped_session(
sessionmaker(autocommit=True, autoflush=False, bind=engine)
)
_sessions[dbname] = Session()
return _sessions.get(dbname)
def pop_session(dbname: str) -> None:
"""
Pop a Session out of the private _sessions memo.
:param dbname: Database name
:raises KeyError: When `dbname` does not exist in the memo
"""
global _sessions
_sessions.pop(dbname)
def refresh(model):
"""
Refresh the session's knowledge of `model`.
:returns: Passed in `model`
"""
get_session().refresh(model)
return model
def query(Model, *args, **kwargs):
"""
Perform an ORM query against the database session.
This method also runs Query.filter on the resulting model
query with *args and **kwargs.
:param Model: Declarative ORM class
"""
return get_session().query(Model).filter(*args, **kwargs)
def create(Model, *args, **kwargs):
"""
Create a record and add() it to the database session.
:param Model: Declarative ORM class
:return: Model instance
"""
instance = Model(*args, **kwargs)
return add(instance)
def delete(model) -> None:
"""
Delete a set of records found by Query.filter(*args, **kwargs).
:param Model: Declarative ORM class
"""
get_session().delete(model)
def delete_all(iterable) -> None:
"""Delete each instance found in `iterable`."""
import aurweb.util
session_ = get_session()
aurweb.util.apply_all(iterable, session_.delete)
def rollback() -> None:
"""Rollback the database session."""
get_session().rollback()
def add(model):
"""Add `model` to the database session."""
get_session().add(model)
return model
def begin():
"""Begin an SQLAlchemy SessionTransaction."""
return get_session().begin()
def retry_deadlock(func):
from sqlalchemy.exc import OperationalError
def wrapper(*args, _i: int = 0, **kwargs):
# Retry 10 times, then raise the exception
# If we fail before the 10th, recurse into `wrapper`
# If we fail on the 10th, continue to throw the exception
limit = 10
try:
return func(*args, **kwargs)
except OperationalError as exc:
if _i < limit and "Deadlock found" in str(exc):
# Retry on deadlock by recursing into `wrapper`
return wrapper(*args, _i=_i + 1, **kwargs)
# Otherwise, just raise the exception
raise exc
return wrapper
def async_retry_deadlock(func):
from sqlalchemy.exc import OperationalError
async def wrapper(*args, _i: int = 0, **kwargs):
# Retry 10 times, then raise the exception
# If we fail before the 10th, recurse into `wrapper`
# If we fail on the 10th, continue to throw the exception
limit = 10
try:
return await func(*args, **kwargs)
except OperationalError as exc:
if _i < limit and "Deadlock found" in str(exc):
# Retry on deadlock by recursing into `wrapper`
return await wrapper(*args, _i=_i + 1, **kwargs)
# Otherwise, just raise the exception
raise exc
return wrapper
def get_sqlalchemy_url():
"""
Build an SQLAlchemy URL for use with create_engine.
:return: sqlalchemy.engine.url.URL
"""
import sqlalchemy
from sqlalchemy.engine.url import URL
import aurweb.config
constructor = URL
parts = sqlalchemy.__version__.split(".")
major = int(parts[0])
minor = int(parts[1])
if major == 1 and minor >= 4: # pragma: no cover
constructor = URL.create
aur_db_backend = aurweb.config.get("database", "backend")
if aur_db_backend == "mysql":
param_query = {}
port = aurweb.config.get_with_fallback("database", "port", None)
if not port:
param_query["unix_socket"] = aurweb.config.get("database", "socket")
return constructor(
DRIVERS.get(aur_db_backend),
username=aurweb.config.get("database", "user"),
password=aurweb.config.get_with_fallback(
"database", "password", fallback=None
),
host=aurweb.config.get("database", "host"),
database=name(),
port=port,
query=param_query,
)
elif aur_db_backend == "sqlite":
return constructor(
"sqlite",
database=aurweb.config.get("database", "name"),
)
else:
raise ValueError("unsupported database backend")
def sqlite_regexp(regex, item) -> bool: # pragma: no cover
"""Method which mimics SQL's REGEXP for SQLite."""
import re
return bool(re.search(regex, str(item)))
def setup_sqlite(engine) -> None: # pragma: no cover
"""Perform setup for an SQLite engine."""
from sqlalchemy import event
@event.listens_for(engine, "connect")
def do_begin(conn, record):
import functools
create_deterministic_function = functools.partial(
conn.create_function, deterministic=True
)
create_deterministic_function("REGEXP", 2, sqlite_regexp)
# Module-private global memo used to store SQLAlchemy engines.
_engines = dict()
def get_engine(dbname: str = None, echo: bool = False):
"""
Return the SQLAlchemy engine for `dbname`.
The engine is created on the first call to get_engine and then stored in the
`engine` global variable for the next calls.
:param dbname: Database name (default: aurweb.db.name())
:param echo: Flag passed through to sqlalchemy.create_engine
:return: SQLAlchemy Engine instance
"""
import aurweb.config
if not dbname:
dbname = name()
global _engines
if dbname not in _engines:
db_backend = aurweb.config.get("database", "backend")
connect_args = dict()
is_sqlite = bool(db_backend == "sqlite")
if is_sqlite: # pragma: no cover
connect_args["check_same_thread"] = False
kwargs = {"echo": echo, "connect_args": connect_args}
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
from sqlalchemy import create_engine
engine = create_engine(get_sqlalchemy_url(), **kwargs)
SQLAlchemyInstrumentor().instrument(engine=engine)
_engines[dbname] = engine
if is_sqlite: # pragma: no cover
setup_sqlite(_engines.get(dbname))
return _engines.get(dbname)
def pop_engine(dbname: str) -> None:
"""
Pop an Engine out of the private _engines memo.
:param dbname: Database name
:raises KeyError: When `dbname` does not exist in the memo
"""
global _engines
_engines.pop(dbname)
def kill_engine() -> None:
"""Close the current session and dispose of the engine."""
dbname = name()
session = get_session()
session.close()
pop_session(dbname)
engine = get_engine()
engine.dispose()
pop_engine(dbname)
def connect():
"""
Return an SQLAlchemy connection. Connections are usually pooled. See
<https://docs.sqlalchemy.org/en/13/core/connections.html>.
Since SQLAlchemy connections are context managers too, you should use it
with Pythons `with` operator, or with FastAPIs dependency injection.
"""
return get_engine().connect()
class ConnectionExecutor:
_conn = None
_paramstyle = None
def __init__(self):
aur_db_backend = aurweb.config.get('database', 'backend')
def __init__(self, conn, backend=None):
import aurweb.config
backend = backend or aurweb.config.get("database", "backend")
self._conn = conn
if backend == "mysql":
self._paramstyle = "format"
elif backend == "sqlite":
import sqlite3
if aur_db_backend == 'mysql':
aur_db_host = aurweb.config.get('database', 'host')
aur_db_name = aurweb.config.get('database', 'name')
aur_db_user = aurweb.config.get('database', 'user')
aur_db_pass = aurweb.config.get('database', 'password')
aur_db_socket = aurweb.config.get('database', 'socket')
self._conn = mysql.connector.connect(host=aur_db_host,
user=aur_db_user,
passwd=aur_db_pass,
db=aur_db_name,
unix_socket=aur_db_socket,
buffered=True)
self._paramstyle = mysql.connector.paramstyle
elif aur_db_backend == 'sqlite':
aur_db_name = aurweb.config.get('database', 'name')
self._conn = sqlite3.connect(aur_db_name)
self._paramstyle = sqlite3.paramstyle
else:
raise ValueError('unsupported database backend')
def execute(self, query, params=()):
if self._paramstyle in ('format', 'pyformat'):
query = query.replace('%', '%%').replace('?', '%s')
elif self._paramstyle == 'qmark':
def paramstyle(self):
return self._paramstyle
def execute(self, query, params=()): # pragma: no cover
# TODO: SQLite support has been removed in FastAPI. It remains
# here to fund its support for the Sharness testsuite.
if self._paramstyle in ("format", "pyformat"):
query = query.replace("%", "%%").replace("?", "%s")
elif self._paramstyle == "qmark":
pass
else:
raise ValueError('unsupported paramstyle')
raise ValueError("unsupported paramstyle")
cur = self._conn.cursor()
cur.execute(query, params)
@ -49,3 +385,51 @@ class Connection:
def close(self):
self._conn.close()
class Connection:
_executor = None
_conn = None
def __init__(self):
import aurweb.config
aur_db_backend = aurweb.config.get("database", "backend")
if aur_db_backend == "mysql":
import MySQLdb
aur_db_host = aurweb.config.get("database", "host")
aur_db_name = name()
aur_db_user = aurweb.config.get("database", "user")
aur_db_pass = aurweb.config.get_with_fallback("database", "password", str())
aur_db_socket = aurweb.config.get("database", "socket")
self._conn = MySQLdb.connect(
host=aur_db_host,
user=aur_db_user,
passwd=aur_db_pass,
db=aur_db_name,
unix_socket=aur_db_socket,
)
elif aur_db_backend == "sqlite": # pragma: no cover
# TODO: SQLite support has been removed in FastAPI. It remains
# here to fund its support for Sharness testsuite.
import math
import sqlite3
aur_db_name = aurweb.config.get("database", "name")
self._conn = sqlite3.connect(aur_db_name)
self._conn.create_function("POWER", 2, math.pow)
else:
raise ValueError("unsupported database backend")
self._conn = ConnectionExecutor(self._conn, aur_db_backend)
def execute(self, query, params=()):
return self._conn.execute(query, params)
def commit(self):
self._conn.commit()
def close(self):
self._conn.close()

24
aurweb/defaults.py Normal file
View file

@ -0,0 +1,24 @@
""" Constant default values centralized in one place. """
# Default [O]ffset
O = 0
# Default [P]er [P]age
PP = 50
# Default Comments Per Page
COMMENTS_PER_PAGE = 10
# A whitelist of valid PP values
PP_WHITELIST = {50, 100, 250}
# Default `by` parameter for RPC search.
RPC_SEARCH_BY = "name-desc"
def fallback_pp(per_page: int) -> int:
"""If `per_page` is a valid value in PP_WHITELIST, return it.
Otherwise, return defaults.PP."""
if per_page not in PP_WHITELIST:
return PP
return per_page

View file

@ -1,3 +1,9 @@
import functools
from typing import Any, Callable
import fastapi
class AurwebException(Exception):
pass
@ -12,58 +18,95 @@ class BannedException(AurwebException):
class PermissionDeniedException(AurwebException):
def __init__(self, user):
msg = 'permission denied: {:s}'.format(user)
msg = "permission denied: {:s}".format(user)
super(PermissionDeniedException, self).__init__(msg)
class BrokenUpdateHookException(AurwebException):
def __init__(self, cmd):
msg = "broken update hook: {:s}".format(cmd)
super(BrokenUpdateHookException, self).__init__(msg)
class InvalidUserException(AurwebException):
def __init__(self, user):
msg = 'unknown user: {:s}'.format(user)
msg = "unknown user: {:s}".format(user)
super(InvalidUserException, self).__init__(msg)
class InvalidPackageBaseException(AurwebException):
def __init__(self, pkgbase):
msg = 'package base not found: {:s}'.format(pkgbase)
msg = "package base not found: {:s}".format(pkgbase)
super(InvalidPackageBaseException, self).__init__(msg)
class InvalidRepositoryNameException(AurwebException):
def __init__(self, pkgbase):
msg = 'invalid repository name: {:s}'.format(pkgbase)
msg = "invalid repository name: {:s}".format(pkgbase)
super(InvalidRepositoryNameException, self).__init__(msg)
class PackageBaseExistsException(AurwebException):
def __init__(self, pkgbase):
msg = 'package base already exists: {:s}'.format(pkgbase)
msg = "package base already exists: {:s}".format(pkgbase)
super(PackageBaseExistsException, self).__init__(msg)
class InvalidReasonException(AurwebException):
def __init__(self, reason):
msg = 'invalid reason: {:s}'.format(reason)
msg = "invalid reason: {:s}".format(reason)
super(InvalidReasonException, self).__init__(msg)
class InvalidCommentException(AurwebException):
def __init__(self, comment):
msg = 'comment is too short: {:s}'.format(comment)
msg = "comment is too short: {:s}".format(comment)
super(InvalidCommentException, self).__init__(msg)
class AlreadyVotedException(AurwebException):
def __init__(self, comment):
msg = 'already voted for package base: {:s}'.format(comment)
msg = "already voted for package base: {:s}".format(comment)
super(AlreadyVotedException, self).__init__(msg)
class NotVotedException(AurwebException):
def __init__(self, comment):
msg = 'missing vote for package base: {:s}'.format(comment)
msg = "missing vote for package base: {:s}".format(comment)
super(NotVotedException, self).__init__(msg)
class InvalidArgumentsException(AurwebException):
def __init__(self, msg):
super(InvalidArgumentsException, self).__init__(msg)
class RPCError(AurwebException):
pass
class ValidationError(AurwebException):
def __init__(self, data: Any, *args, **kwargs):
super().__init__(*args, **kwargs)
self.data = data
class InvariantError(AurwebException):
pass
def handle_form_exceptions(route: Callable) -> fastapi.Response:
"""
A decorator required when fastapi POST routes are defined.
This decorator populates fastapi's `request.state` with a `form_data`
attribute, which is then used to report form data when exceptions
are caught and reported.
"""
@functools.wraps(route)
async def wrapper(request: fastapi.Request, *args, **kwargs):
request.state.form_data = await request.form()
return await route(request, *args, **kwargs)
return wrapper

181
aurweb/filters.py Normal file
View file

@ -0,0 +1,181 @@
import copy
import math
from datetime import UTC, datetime
from typing import Any, Union
from urllib.parse import quote_plus, urlencode
from zoneinfo import ZoneInfo
import fastapi
import paginate
from jinja2 import pass_context
from jinja2.filters import do_format
import aurweb.models
from aurweb import config, l10n
from aurweb.templates import register_filter, register_function
@register_filter("pager_nav")
@pass_context
def pager_nav(context: dict[str, Any], page: int, total: int, prefix: str) -> str:
page = int(page) # Make sure this is an int.
pp = context.get("PP", 50)
# Setup a local query string dict, optionally passed by caller.
q = context.get("q", dict())
search_by = context.get("SeB", None)
if search_by:
q["SeB"] = search_by
sort_by = context.get("SB", None)
if sort_by:
q["SB"] = sort_by
def create_url(page: int):
nonlocal q
offset = max(page * pp - pp, 0)
qs = to_qs(extend_query(q, ["O", offset]))
return f"{prefix}?{qs}"
# Use the paginate module to produce our linkage.
pager = paginate.Page(
[], page=page + 1, items_per_page=pp, item_count=total, url_maker=create_url
)
return pager.pager(
link_attr={"class": "page"},
curpage_attr={"class": "page"},
separator="&nbsp",
format="$link_first $link_previous ~5~ $link_next $link_last",
symbol_first="« First",
symbol_previous=" Previous",
symbol_next="Next ",
symbol_last="Last »",
)
@register_function("config_getint")
def config_getint(section: str, key: str) -> int:
return config.getint(section, key)
@register_function("round")
def do_round(f: float) -> int:
return round(f)
@register_filter("tr")
@pass_context
def tr(context: dict[str, Any], value: str):
"""A translation filter; example: {{ "Hello" | tr("de") }}."""
_ = l10n.get_translator_for_request(context.get("request"))
return _(value)
@register_filter("tn")
@pass_context
def tn(context: dict[str, Any], count: int, singular: str, plural: str) -> str:
"""A singular and plural translation filter.
Example:
{{ some_integer | tn("singular %d", "plural %d") }}
:param context: Response context
:param count: The number used to decide singular or plural state
:param singular: The singular translation
:param plural: The plural translation
:return: Translated string
"""
gettext = l10n.get_raw_translator_for_request(context.get("request"))
return gettext.ngettext(singular, plural, count)
@register_filter("dt")
def timestamp_to_datetime(timestamp: int):
return datetime.fromtimestamp(timestamp, UTC)
@register_filter("as_timezone")
def as_timezone(dt: datetime, timezone: str):
return dt.astimezone(tz=ZoneInfo(timezone))
@register_filter("extend_query")
def extend_query(query: dict[str, Any], *additions) -> dict[str, Any]:
"""Add additional key value pairs to query."""
q = copy.copy(query)
for k, v in list(additions):
q[k] = v
return q
@register_filter("urlencode")
def to_qs(query: dict[str, Any]) -> str:
return urlencode(query, doseq=True)
@register_filter("get_vote")
def get_vote(voteinfo, request: fastapi.Request):
from aurweb.models import Vote
return voteinfo.votes.filter(Vote.User == request.user).first()
@register_filter("number_format")
def number_format(value: float, places: int):
"""A converter function similar to PHP's number_format."""
return f"{value:.{places}f}"
@register_filter("account_url")
@pass_context
def account_url(context: dict[str, Any], user: "aurweb.models.user.User") -> str:
base = aurweb.config.get("options", "aur_location")
return f"{base}/account/{user.Username}"
@register_filter("quote_plus")
def _quote_plus(*args, **kwargs) -> str:
return quote_plus(*args, **kwargs)
@register_filter("ceil")
def ceil(*args, **kwargs) -> int:
return math.ceil(*args, **kwargs)
@register_function("date_strftime")
@pass_context
def date_strftime(context: dict[str, Any], dt: Union[int, datetime], fmt: str) -> str:
if isinstance(dt, int):
dt = timestamp_to_datetime(dt)
tz = context.get("timezone")
return as_timezone(dt, tz).strftime(fmt)
@register_function("date_display")
@pass_context
def date_display(context: dict[str, Any], dt: Union[int, datetime]) -> str:
return date_strftime(context, dt, "%Y-%m-%d (%Z)")
@register_function("datetime_display")
@pass_context
def datetime_display(context: dict[str, Any], dt: Union[int, datetime]) -> str:
return date_strftime(context, dt, "%Y-%m-%d %H:%M (%Z)")
@register_filter("format")
def safe_format(value: str, *args: Any, **kwargs: Any) -> str:
"""Wrapper for jinja2 format function to perform additional checks."""
# If we don't have anything to be formatted, just return the value.
# We have some translations that do not contain placeholders for replacement.
# In these cases the jinja2 function is throwing an error:
# "TypeError: not all arguments converted during string formatting"
if "%" not in value:
return value
return do_format(value, *args, **kwargs)

View file

@ -1,8 +1,7 @@
#!/usr/bin/env python3
import os
import shlex
import re
import shlex
import sys
import aurweb.config
@ -10,12 +9,12 @@ import aurweb.db
def format_command(env_vars, command, ssh_opts, ssh_key):
environment = ''
environment = ""
for key, var in env_vars.items():
environment += '{}={} '.format(key, shlex.quote(var))
environment += "{}={} ".format(key, shlex.quote(var))
command = shlex.quote(command)
command = '{}{}'.format(environment, command)
command = "{}{}".format(environment, command)
# The command is being substituted into an authorized_keys line below,
# so we need to escape the double quotes.
@ -25,10 +24,10 @@ def format_command(env_vars, command, ssh_opts, ssh_key):
def main():
valid_keytypes = aurweb.config.get('auth', 'valid-keytypes').split()
username_regex = aurweb.config.get('auth', 'username-regex')
git_serve_cmd = aurweb.config.get('auth', 'git-serve-cmd')
ssh_opts = aurweb.config.get('auth', 'ssh-options')
valid_keytypes = aurweb.config.get("auth", "valid-keytypes").split()
username_regex = aurweb.config.get("auth", "username-regex")
git_serve_cmd = aurweb.config.get("auth", "git-serve-cmd")
ssh_opts = aurweb.config.get("auth", "ssh-options")
keytype = sys.argv[1]
keytext = sys.argv[2]
@ -37,10 +36,13 @@ def main():
conn = aurweb.db.Connection()
cur = conn.execute("SELECT Users.Username, Users.AccountTypeID FROM Users "
"INNER JOIN SSHPubKeys ON SSHPubKeys.UserID = Users.ID "
"WHERE SSHPubKeys.PubKey = ? AND Users.Suspended = 0",
(keytype + " " + keytext,))
cur = conn.execute(
"SELECT Users.Username, Users.AccountTypeID FROM Users "
"INNER JOIN SSHPubKeys ON SSHPubKeys.UserID = Users.ID "
"WHERE SSHPubKeys.PubKey = ? AND Users.Suspended = 0 "
"AND NOT Users.Passwd = ''",
(keytype + " " + keytext,),
)
row = cur.fetchone()
if not row or cur.fetchone():
@ -51,14 +53,13 @@ def main():
exit(1)
env_vars = {
'AUR_USER': user,
'AUR_PRIVILEGED': '1' if account_type > 1 else '0',
'AUR_OVERWRITE' : os.environ.get('AUR_OVERWRITE', '0') if account_type > 1 else '0',
"AUR_USER": user,
"AUR_PRIVILEGED": "1" if account_type > 1 else "0",
}
key = keytype + ' ' + keytext
key = keytype + " " + keytext
print(format_command(env_vars, git_serve_cmd, ssh_opts, key))
if __name__ == '__main__':
if __name__ == "__main__":
main()

View file

@ -11,16 +11,16 @@ import aurweb.config
import aurweb.db
import aurweb.exceptions
notify_cmd = aurweb.config.get('notifications', 'notify-cmd')
notify_cmd = aurweb.config.get("notifications", "notify-cmd")
repo_path = aurweb.config.get('serve', 'repo-path')
repo_regex = aurweb.config.get('serve', 'repo-regex')
git_shell_cmd = aurweb.config.get('serve', 'git-shell-cmd')
git_update_cmd = aurweb.config.get('serve', 'git-update-cmd')
ssh_cmdline = aurweb.config.get('serve', 'ssh-cmdline')
repo_path = aurweb.config.get("serve", "repo-path")
repo_regex = aurweb.config.get("serve", "repo-regex")
git_shell_cmd = aurweb.config.get("serve", "git-shell-cmd")
git_update_cmd = aurweb.config.get("serve", "git-update-cmd")
ssh_cmdline = aurweb.config.get("serve", "ssh-cmdline")
enable_maintenance = aurweb.config.getboolean('options', 'enable-maintenance')
maintenance_exc = aurweb.config.get('options', 'maintenance-exceptions').split()
enable_maintenance = aurweb.config.getboolean("options", "enable-maintenance")
maintenance_exc = aurweb.config.get("options", "maintenance-exceptions").split()
def pkgbase_from_name(pkgbase):
@ -43,14 +43,16 @@ def list_repos(user):
if userid == 0:
raise aurweb.exceptions.InvalidUserException(user)
cur = conn.execute("SELECT Name, PackagerUID FROM PackageBases " +
"WHERE MaintainerUID = ?", [userid])
cur = conn.execute(
"SELECT Name, PackagerUID FROM PackageBases " + "WHERE MaintainerUID = ?",
[userid],
)
for row in cur:
print((' ' if row[1] else '*') + row[0])
print((" " if row[1] else "*") + row[0])
conn.close()
def create_pkgbase(pkgbase, user):
def validate_pkgbase(pkgbase, user):
if not re.match(repo_regex, pkgbase):
raise aurweb.exceptions.InvalidRepositoryNameException(pkgbase)
if pkgbase_exists(pkgbase):
@ -60,23 +62,12 @@ def create_pkgbase(pkgbase, user):
cur = conn.execute("SELECT ID FROM Users WHERE Username = ?", [user])
userid = cur.fetchone()[0]
conn.close()
if userid == 0:
raise aurweb.exceptions.InvalidUserException(user)
now = int(time.time())
cur = conn.execute("INSERT INTO PackageBases (Name, SubmittedTS, " +
"ModifiedTS, SubmitterUID, MaintainerUID, " +
"FlaggerComment) VALUES (?, ?, ?, ?, ?, '')",
[pkgbase, now, now, userid, userid])
pkgbase_id = cur.lastrowid
cur = conn.execute("INSERT INTO PackageNotifications " +
"(PackageBaseID, UserID) VALUES (?, ?)",
[pkgbase_id, userid])
conn.commit()
conn.close()
def pkgbase_adopt(pkgbase, user, privileged):
pkgbase_id = pkgbase_from_name(pkgbase)
@ -85,8 +76,10 @@ def pkgbase_adopt(pkgbase, user, privileged):
conn = aurweb.db.Connection()
cur = conn.execute("SELECT ID FROM PackageBases WHERE ID = ? AND " +
"MaintainerUID IS NULL", [pkgbase_id])
cur = conn.execute(
"SELECT ID FROM PackageBases WHERE ID = ? AND " + "MaintainerUID IS NULL",
[pkgbase_id],
)
if not privileged and not cur.fetchone():
raise aurweb.exceptions.PermissionDeniedException(user)
@ -95,19 +88,25 @@ def pkgbase_adopt(pkgbase, user, privileged):
if userid == 0:
raise aurweb.exceptions.InvalidUserException(user)
cur = conn.execute("UPDATE PackageBases SET MaintainerUID = ? " +
"WHERE ID = ?", [userid, pkgbase_id])
cur = conn.execute(
"UPDATE PackageBases SET MaintainerUID = ? " + "WHERE ID = ?",
[userid, pkgbase_id],
)
cur = conn.execute("SELECT COUNT(*) FROM PackageNotifications WHERE " +
"PackageBaseID = ? AND UserID = ?",
[pkgbase_id, userid])
cur = conn.execute(
"SELECT COUNT(*) FROM PackageNotifications WHERE "
+ "PackageBaseID = ? AND UserID = ?",
[pkgbase_id, userid],
)
if cur.fetchone()[0] == 0:
cur = conn.execute("INSERT INTO PackageNotifications " +
"(PackageBaseID, UserID) VALUES (?, ?)",
[pkgbase_id, userid])
cur = conn.execute(
"INSERT INTO PackageNotifications "
+ "(PackageBaseID, UserID) VALUES (?, ?)",
[pkgbase_id, userid],
)
conn.commit()
subprocess.Popen((notify_cmd, 'adopt', str(pkgbase_id), str(userid)))
subprocess.Popen((notify_cmd, "adopt", str(userid), str(pkgbase_id)))
conn.close()
@ -115,13 +114,16 @@ def pkgbase_adopt(pkgbase, user, privileged):
def pkgbase_get_comaintainers(pkgbase):
conn = aurweb.db.Connection()
cur = conn.execute("SELECT UserName FROM PackageComaintainers " +
"INNER JOIN Users " +
"ON Users.ID = PackageComaintainers.UsersID " +
"INNER JOIN PackageBases " +
"ON PackageBases.ID = PackageComaintainers.PackageBaseID " +
"WHERE PackageBases.Name = ? " +
"ORDER BY Priority ASC", [pkgbase])
cur = conn.execute(
"SELECT UserName FROM PackageComaintainers "
+ "INNER JOIN Users "
+ "ON Users.ID = PackageComaintainers.UsersID "
+ "INNER JOIN PackageBases "
+ "ON PackageBases.ID = PackageComaintainers.PackageBaseID "
+ "WHERE PackageBases.Name = ? "
+ "ORDER BY Priority ASC",
[pkgbase],
)
return [row[0] for row in cur.fetchall()]
@ -140,8 +142,7 @@ def pkgbase_set_comaintainers(pkgbase, userlist, user, privileged):
uids_old = set()
for olduser in userlist_old:
cur = conn.execute("SELECT ID FROM Users WHERE Username = ?",
[olduser])
cur = conn.execute("SELECT ID FROM Users WHERE Username = ?", [olduser])
userid = cur.fetchone()[0]
if userid == 0:
raise aurweb.exceptions.InvalidUserException(user)
@ -149,8 +150,7 @@ def pkgbase_set_comaintainers(pkgbase, userlist, user, privileged):
uids_new = set()
for newuser in userlist:
cur = conn.execute("SELECT ID FROM Users WHERE Username = ?",
[newuser])
cur = conn.execute("SELECT ID FROM Users WHERE Username = ?", [newuser])
userid = cur.fetchone()[0]
if userid == 0:
raise aurweb.exceptions.InvalidUserException(user)
@ -162,24 +162,33 @@ def pkgbase_set_comaintainers(pkgbase, userlist, user, privileged):
i = 1
for userid in uids_new:
if userid in uids_add:
cur = conn.execute("INSERT INTO PackageComaintainers " +
"(PackageBaseID, UsersID, Priority) " +
"VALUES (?, ?, ?)", [pkgbase_id, userid, i])
subprocess.Popen((notify_cmd, 'comaintainer-add', str(pkgbase_id),
str(userid)))
cur = conn.execute(
"INSERT INTO PackageComaintainers "
+ "(PackageBaseID, UsersID, Priority) "
+ "VALUES (?, ?, ?)",
[pkgbase_id, userid, i],
)
subprocess.Popen(
(notify_cmd, "comaintainer-add", str(userid), str(pkgbase_id))
)
else:
cur = conn.execute("UPDATE PackageComaintainers " +
"SET Priority = ? " +
"WHERE PackageBaseID = ? AND UsersID = ?",
[i, pkgbase_id, userid])
cur = conn.execute(
"UPDATE PackageComaintainers "
+ "SET Priority = ? "
+ "WHERE PackageBaseID = ? AND UsersID = ?",
[i, pkgbase_id, userid],
)
i += 1
for userid in uids_rem:
cur = conn.execute("DELETE FROM PackageComaintainers " +
"WHERE PackageBaseID = ? AND UsersID = ?",
[pkgbase_id, userid])
subprocess.Popen((notify_cmd, 'comaintainer-remove',
str(pkgbase_id), str(userid)))
cur = conn.execute(
"DELETE FROM PackageComaintainers "
+ "WHERE PackageBaseID = ? AND UsersID = ?",
[pkgbase_id, userid],
)
subprocess.Popen(
(notify_cmd, "comaintainer-remove", str(userid), str(pkgbase_id))
)
conn.commit()
conn.close()
@ -188,18 +197,21 @@ def pkgbase_set_comaintainers(pkgbase, userlist, user, privileged):
def pkgreq_by_pkgbase(pkgbase_id, reqtype):
conn = aurweb.db.Connection()
cur = conn.execute("SELECT PackageRequests.ID FROM PackageRequests " +
"INNER JOIN RequestTypes ON " +
"RequestTypes.ID = PackageRequests.ReqTypeID " +
"WHERE PackageRequests.Status = 0 " +
"AND PackageRequests.PackageBaseID = ? " +
"AND RequestTypes.Name = ?", [pkgbase_id, reqtype])
cur = conn.execute(
"SELECT PackageRequests.ID FROM PackageRequests "
+ "INNER JOIN RequestTypes ON "
+ "RequestTypes.ID = PackageRequests.ReqTypeID "
+ "WHERE PackageRequests.Status = 0 "
+ "AND PackageRequests.PackageBaseID = ? "
+ "AND RequestTypes.Name = ?",
[pkgbase_id, reqtype],
)
return [row[0] for row in cur.fetchall()]
def pkgreq_close(reqid, user, reason, comments, autoclose=False):
statusmap = {'accepted': 2, 'rejected': 3}
statusmap = {"accepted": 2, "rejected": 3}
if reason not in statusmap:
raise aurweb.exceptions.InvalidReasonException(reason)
status = statusmap[reason]
@ -207,20 +219,28 @@ def pkgreq_close(reqid, user, reason, comments, autoclose=False):
conn = aurweb.db.Connection()
if autoclose:
userid = 0
userid = None
else:
cur = conn.execute("SELECT ID FROM Users WHERE Username = ?", [user])
userid = cur.fetchone()[0]
if userid == 0:
raise aurweb.exceptions.InvalidUserException(user)
conn.execute("UPDATE PackageRequests SET Status = ?, ClosureComment = ? " +
"WHERE ID = ?", [status, comments, reqid])
now = int(time.time())
conn.execute(
"UPDATE PackageRequests SET Status = ?, ClosedTS = ?, "
+ "ClosedUID = ?, ClosureComment = ? "
+ "WHERE ID = ?",
[status, now, userid, comments, reqid],
)
conn.commit()
conn.close()
subprocess.Popen((notify_cmd, 'request-close', str(userid), str(reqid),
reason)).wait()
if not userid:
userid = 0
subprocess.Popen(
(notify_cmd, "request-close", str(userid), str(reqid), reason)
).wait()
def pkgbase_disown(pkgbase, user, privileged):
@ -235,9 +255,9 @@ def pkgbase_disown(pkgbase, user, privileged):
# TODO: Support disowning package bases via package request.
# Scan through pending orphan requests and close them.
comment = 'The user {:s} disowned the package.'.format(user)
for reqid in pkgreq_by_pkgbase(pkgbase_id, 'orphan'):
pkgreq_close(reqid, user, 'accepted', comment, True)
comment = "The user {:s} disowned the package.".format(user)
for reqid in pkgreq_by_pkgbase(pkgbase_id, "orphan"):
pkgreq_close(reqid, user, "accepted", comment, True)
comaintainers = []
new_maintainer_userid = None
@ -245,28 +265,31 @@ def pkgbase_disown(pkgbase, user, privileged):
conn = aurweb.db.Connection()
# Make the first co-maintainer the new maintainer, unless the action was
# enforced by a Trusted User.
# enforced by a Package Maintainer.
if initialized_by_owner:
comaintainers = pkgbase_get_comaintainers(pkgbase)
if len(comaintainers) > 0:
new_maintainer = comaintainers[0]
cur = conn.execute("SELECT ID FROM Users WHERE Username = ?",
[new_maintainer])
cur = conn.execute(
"SELECT ID FROM Users WHERE Username = ?", [new_maintainer]
)
new_maintainer_userid = cur.fetchone()[0]
comaintainers.remove(new_maintainer)
pkgbase_set_comaintainers(pkgbase, comaintainers, user, privileged)
cur = conn.execute("UPDATE PackageBases SET MaintainerUID = ? " +
"WHERE ID = ?", [new_maintainer_userid, pkgbase_id])
cur = conn.execute(
"UPDATE PackageBases SET MaintainerUID = ? " + "WHERE ID = ?",
[new_maintainer_userid, pkgbase_id],
)
conn.commit()
cur = conn.execute("SELECT ID FROM Users WHERE Username = ?", [user])
userid = cur.fetchone()[0]
if userid == 0:
raise aurweb.exceptions.InvalidUserException(user)
raise aurweb.exceptions.InvalidUserException(user)
subprocess.Popen((notify_cmd, 'disown', str(pkgbase_id), str(userid)))
subprocess.Popen((notify_cmd, "disown", str(userid), str(pkgbase_id)))
conn.close()
@ -286,14 +309,16 @@ def pkgbase_flag(pkgbase, user, comment):
raise aurweb.exceptions.InvalidUserException(user)
now = int(time.time())
conn.execute("UPDATE PackageBases SET " +
"OutOfDateTS = ?, FlaggerUID = ?, FlaggerComment = ? " +
"WHERE ID = ? AND OutOfDateTS IS NULL",
[now, userid, comment, pkgbase_id])
conn.execute(
"UPDATE PackageBases SET "
+ "OutOfDateTS = ?, FlaggerUID = ?, FlaggerComment = ? "
+ "WHERE ID = ? AND OutOfDateTS IS NULL",
[now, userid, comment, pkgbase_id],
)
conn.commit()
subprocess.Popen((notify_cmd, 'flag', str(userid), str(pkgbase_id)))
subprocess.Popen((notify_cmd, "flag", str(userid), str(pkgbase_id)))
def pkgbase_unflag(pkgbase, user):
@ -309,12 +334,15 @@ def pkgbase_unflag(pkgbase, user):
raise aurweb.exceptions.InvalidUserException(user)
if user in pkgbase_get_comaintainers(pkgbase):
conn.execute("UPDATE PackageBases SET OutOfDateTS = NULL " +
"WHERE ID = ?", [pkgbase_id])
conn.execute(
"UPDATE PackageBases SET OutOfDateTS = NULL " + "WHERE ID = ?", [pkgbase_id]
)
else:
conn.execute("UPDATE PackageBases SET OutOfDateTS = NULL " +
"WHERE ID = ? AND (MaintainerUID = ? OR FlaggerUID = ?)",
[pkgbase_id, userid, userid])
conn.execute(
"UPDATE PackageBases SET OutOfDateTS = NULL "
+ "WHERE ID = ? AND (MaintainerUID = ? OR FlaggerUID = ?)",
[pkgbase_id, userid, userid],
)
conn.commit()
@ -331,17 +359,24 @@ def pkgbase_vote(pkgbase, user):
if userid == 0:
raise aurweb.exceptions.InvalidUserException(user)
cur = conn.execute("SELECT COUNT(*) FROM PackageVotes " +
"WHERE UsersID = ? AND PackageBaseID = ?",
[userid, pkgbase_id])
cur = conn.execute(
"SELECT COUNT(*) FROM PackageVotes "
+ "WHERE UsersID = ? AND PackageBaseID = ?",
[userid, pkgbase_id],
)
if cur.fetchone()[0] > 0:
raise aurweb.exceptions.AlreadyVotedException(pkgbase)
now = int(time.time())
conn.execute("INSERT INTO PackageVotes (UsersID, PackageBaseID, VoteTS) " +
"VALUES (?, ?, ?)", [userid, pkgbase_id, now])
conn.execute("UPDATE PackageBases SET NumVotes = NumVotes + 1 " +
"WHERE ID = ?", [pkgbase_id])
conn.execute(
"INSERT INTO PackageVotes (UsersID, PackageBaseID, VoteTS) "
+ "VALUES (?, ?, ?)",
[userid, pkgbase_id, now],
)
conn.execute(
"UPDATE PackageBases SET NumVotes = NumVotes + 1 " + "WHERE ID = ?",
[pkgbase_id],
)
conn.commit()
@ -357,16 +392,22 @@ def pkgbase_unvote(pkgbase, user):
if userid == 0:
raise aurweb.exceptions.InvalidUserException(user)
cur = conn.execute("SELECT COUNT(*) FROM PackageVotes " +
"WHERE UsersID = ? AND PackageBaseID = ?",
[userid, pkgbase_id])
cur = conn.execute(
"SELECT COUNT(*) FROM PackageVotes "
+ "WHERE UsersID = ? AND PackageBaseID = ?",
[userid, pkgbase_id],
)
if cur.fetchone()[0] == 0:
raise aurweb.exceptions.NotVotedException(pkgbase)
conn.execute("DELETE FROM PackageVotes WHERE UsersID = ? AND " +
"PackageBaseID = ?", [userid, pkgbase_id])
conn.execute("UPDATE PackageBases SET NumVotes = NumVotes - 1 " +
"WHERE ID = ?", [pkgbase_id])
conn.execute(
"DELETE FROM PackageVotes WHERE UsersID = ? AND " + "PackageBaseID = ?",
[userid, pkgbase_id],
)
conn.execute(
"UPDATE PackageBases SET NumVotes = NumVotes - 1 " + "WHERE ID = ?",
[pkgbase_id],
)
conn.commit()
@ -377,11 +418,12 @@ def pkgbase_set_keywords(pkgbase, keywords):
conn = aurweb.db.Connection()
conn.execute("DELETE FROM PackageKeywords WHERE PackageBaseID = ?",
[pkgbase_id])
conn.execute("DELETE FROM PackageKeywords WHERE PackageBaseID = ?", [pkgbase_id])
for keyword in keywords:
conn.execute("INSERT INTO PackageKeywords (PackageBaseID, Keyword) " +
"VALUES (?, ?)", [pkgbase_id, keyword])
conn.execute(
"INSERT INTO PackageKeywords (PackageBaseID, Keyword) " + "VALUES (?, ?)",
[pkgbase_id, keyword],
)
conn.commit()
conn.close()
@ -390,24 +432,30 @@ def pkgbase_set_keywords(pkgbase, keywords):
def pkgbase_has_write_access(pkgbase, user):
conn = aurweb.db.Connection()
cur = conn.execute("SELECT COUNT(*) FROM PackageBases " +
"LEFT JOIN PackageComaintainers " +
"ON PackageComaintainers.PackageBaseID = PackageBases.ID " +
"INNER JOIN Users " +
"ON Users.ID = PackageBases.MaintainerUID " +
"OR PackageBases.MaintainerUID IS NULL " +
"OR Users.ID = PackageComaintainers.UsersID " +
"WHERE Name = ? AND Username = ?", [pkgbase, user])
cur = conn.execute(
"SELECT COUNT(*) FROM PackageBases "
+ "LEFT JOIN PackageComaintainers "
+ "ON PackageComaintainers.PackageBaseID = PackageBases.ID "
+ "INNER JOIN Users "
+ "ON Users.ID = PackageBases.MaintainerUID "
+ "OR PackageBases.MaintainerUID IS NULL "
+ "OR Users.ID = PackageComaintainers.UsersID "
+ "WHERE Name = ? AND Username = ?",
[pkgbase, user],
)
return cur.fetchone()[0] > 0
def pkgbase_has_full_access(pkgbase, user):
conn = aurweb.db.Connection()
cur = conn.execute("SELECT COUNT(*) FROM PackageBases " +
"INNER JOIN Users " +
"ON Users.ID = PackageBases.MaintainerUID " +
"WHERE Name = ? AND Username = ?", [pkgbase, user])
cur = conn.execute(
"SELECT COUNT(*) FROM PackageBases "
+ "INNER JOIN Users "
+ "ON Users.ID = PackageBases.MaintainerUID "
+ "WHERE Name = ? AND Username = ?",
[pkgbase, user],
)
return cur.fetchone()[0] > 0
@ -415,9 +463,11 @@ def log_ssh_login(user, remote_addr):
conn = aurweb.db.Connection()
now = int(time.time())
conn.execute("UPDATE Users SET LastSSHLogin = ?, " +
"LastSSHLoginIPAddress = ? WHERE Username = ?",
[now, remote_addr, user])
conn.execute(
"UPDATE Users SET LastSSHLogin = ?, "
+ "LastSSHLoginIPAddress = ? WHERE Username = ?",
[now, remote_addr, user],
)
conn.commit()
conn.close()
@ -426,8 +476,7 @@ def log_ssh_login(user, remote_addr):
def bans_match(remote_addr):
conn = aurweb.db.Connection()
cur = conn.execute("SELECT COUNT(*) FROM Bans WHERE IPAddress = ?",
[remote_addr])
cur = conn.execute("SELECT COUNT(*) FROM Bans WHERE IPAddress = ?", [remote_addr])
return cur.fetchone()[0] > 0
@ -454,13 +503,13 @@ def usage(cmds):
def checkarg_atleast(cmdargv, *argdesc):
if len(cmdargv) - 1 < len(argdesc):
msg = 'missing {:s}'.format(argdesc[len(cmdargv) - 1])
msg = "missing {:s}".format(argdesc[len(cmdargv) - 1])
raise aurweb.exceptions.InvalidArgumentsException(msg)
def checkarg_atmost(cmdargv, *argdesc):
if len(cmdargv) - 1 > len(argdesc):
raise aurweb.exceptions.InvalidArgumentsException('too many arguments')
raise aurweb.exceptions.InvalidArgumentsException("too many arguments")
def checkarg(cmdargv, *argdesc):
@ -468,7 +517,7 @@ def checkarg(cmdargv, *argdesc):
checkarg_atmost(cmdargv, *argdesc)
def serve(action, cmdargv, user, privileged, remote_addr):
def serve(action, cmdargv, user, privileged, remote_addr): # noqa: C901
if enable_maintenance:
if remote_addr not in maintenance_exc:
raise aurweb.exceptions.MaintenanceException
@ -476,89 +525,87 @@ def serve(action, cmdargv, user, privileged, remote_addr):
raise aurweb.exceptions.BannedException
log_ssh_login(user, remote_addr)
if action == 'git' and cmdargv[1] in ('upload-pack', 'receive-pack'):
action = action + '-' + cmdargv[1]
if action == "git" and cmdargv[1] in ("upload-pack", "receive-pack"):
action = action + "-" + cmdargv[1]
del cmdargv[1]
if action == 'git-upload-pack' or action == 'git-receive-pack':
checkarg(cmdargv, 'path')
if action == "git-upload-pack" or action == "git-receive-pack":
checkarg(cmdargv, "path")
path = cmdargv[1].rstrip('/')
if not path.startswith('/'):
path = '/' + path
if not path.endswith('.git'):
path = path + '.git'
path = cmdargv[1].rstrip("/")
if not path.startswith("/"):
path = "/" + path
if not path.endswith(".git"):
path = path + ".git"
pkgbase = path[1:-4]
if not re.match(repo_regex, pkgbase):
raise aurweb.exceptions.InvalidRepositoryNameException(pkgbase)
if action == 'git-receive-pack' and pkgbase_exists(pkgbase):
if action == "git-receive-pack" and pkgbase_exists(pkgbase):
if not privileged and not pkgbase_has_write_access(pkgbase, user):
raise aurweb.exceptions.PermissionDeniedException(user)
if not os.access(git_update_cmd, os.R_OK | os.X_OK):
raise aurweb.exceptions.BrokenUpdateHookException(git_update_cmd)
os.environ["AUR_USER"] = user
os.environ["AUR_PKGBASE"] = pkgbase
os.environ["GIT_NAMESPACE"] = pkgbase
cmd = action + " '" + repo_path + "'"
os.execl(git_shell_cmd, git_shell_cmd, '-c', cmd)
elif action == 'set-keywords':
checkarg_atleast(cmdargv, 'repository name')
os.execl(git_shell_cmd, git_shell_cmd, "-c", cmd)
elif action == "set-keywords":
checkarg_atleast(cmdargv, "repository name")
pkgbase_set_keywords(cmdargv[1], cmdargv[2:])
elif action == 'list-repos':
elif action == "list-repos":
checkarg(cmdargv)
list_repos(user)
elif action == 'setup-repo':
checkarg(cmdargv, 'repository name')
warn('{:s} is deprecated. '
'Use `git push` to create new repositories.'.format(action))
create_pkgbase(cmdargv[1], user)
elif action == 'restore':
checkarg(cmdargv, 'repository name')
elif action == "restore":
checkarg(cmdargv, "repository name")
pkgbase = cmdargv[1]
create_pkgbase(pkgbase, user)
validate_pkgbase(pkgbase, user)
os.environ["AUR_USER"] = user
os.environ["AUR_PKGBASE"] = pkgbase
os.execl(git_update_cmd, git_update_cmd, 'restore')
elif action == 'adopt':
checkarg(cmdargv, 'repository name')
os.execl(git_update_cmd, git_update_cmd, "restore")
elif action == "adopt":
checkarg(cmdargv, "repository name")
pkgbase = cmdargv[1]
pkgbase_adopt(pkgbase, user, privileged)
elif action == 'disown':
checkarg(cmdargv, 'repository name')
elif action == "disown":
checkarg(cmdargv, "repository name")
pkgbase = cmdargv[1]
pkgbase_disown(pkgbase, user, privileged)
elif action == 'flag':
checkarg(cmdargv, 'repository name', 'comment')
elif action == "flag":
checkarg(cmdargv, "repository name", "comment")
pkgbase = cmdargv[1]
comment = cmdargv[2]
pkgbase_flag(pkgbase, user, comment)
elif action == 'unflag':
checkarg(cmdargv, 'repository name')
elif action == "unflag":
checkarg(cmdargv, "repository name")
pkgbase = cmdargv[1]
pkgbase_unflag(pkgbase, user)
elif action == 'vote':
checkarg(cmdargv, 'repository name')
elif action == "vote":
checkarg(cmdargv, "repository name")
pkgbase = cmdargv[1]
pkgbase_vote(pkgbase, user)
elif action == 'unvote':
checkarg(cmdargv, 'repository name')
elif action == "unvote":
checkarg(cmdargv, "repository name")
pkgbase = cmdargv[1]
pkgbase_unvote(pkgbase, user)
elif action == 'set-comaintainers':
checkarg_atleast(cmdargv, 'repository name')
elif action == "set-comaintainers":
checkarg_atleast(cmdargv, "repository name")
pkgbase = cmdargv[1]
userlist = cmdargv[2:]
pkgbase_set_comaintainers(pkgbase, userlist, user, privileged)
elif action == 'help':
elif action == "help":
cmds = {
"adopt <name>": "Adopt a package base.",
"disown <name>": "Disown a package base.",
@ -568,7 +615,6 @@ def serve(action, cmdargv, user, privileged, remote_addr):
"restore <name>": "Restore a deleted package base.",
"set-comaintainers <name> [...]": "Set package base co-maintainers.",
"set-keywords <name> [...]": "Change package base keywords.",
"setup-repo <name>": "Create a repository (deprecated).",
"unflag <name>": "Remove out-of-date flag from a package base.",
"unvote <name>": "Remove vote from a package base.",
"vote <name>": "Vote for a package base.",
@ -577,21 +623,21 @@ def serve(action, cmdargv, user, privileged, remote_addr):
}
usage(cmds)
else:
msg = 'invalid command: {:s}'.format(action)
msg = "invalid command: {:s}".format(action)
raise aurweb.exceptions.InvalidArgumentsException(msg)
def main():
user = os.environ.get('AUR_USER')
privileged = (os.environ.get('AUR_PRIVILEGED', '0') == '1')
ssh_cmd = os.environ.get('SSH_ORIGINAL_COMMAND')
ssh_client = os.environ.get('SSH_CLIENT')
user = os.environ.get("AUR_USER")
privileged = os.environ.get("AUR_PRIVILEGED", "0") == "1"
ssh_cmd = os.environ.get("SSH_ORIGINAL_COMMAND")
ssh_client = os.environ.get("SSH_CLIENT")
if not ssh_cmd:
die_with_help("Interactive shell is disabled.")
die_with_help(f"Welcome to AUR, {user}! Interactive shell is disabled.")
cmdargv = shlex.split(ssh_cmd)
action = cmdargv[0]
remote_addr = ssh_client.split(' ')[0] if ssh_client else None
remote_addr = ssh_client.split(" ")[0] if ssh_client else None
try:
serve(action, cmdargv, user, privileged, remote_addr)
@ -600,10 +646,10 @@ def main():
except aurweb.exceptions.BannedException:
die("The SSH interface is disabled for your IP address.")
except aurweb.exceptions.InvalidArgumentsException as e:
die_with_help('{:s}: {}'.format(action, e))
die_with_help("{:s}: {}".format(action, e))
except aurweb.exceptions.AurwebException as e:
die('{:s}: {}'.format(action, e))
die("{:s}: {}".format(action, e))
if __name__ == '__main__':
if __name__ == "__main__":
main()

View file

@ -1,35 +1,35 @@
#!/usr/bin/env python3
import os
import pygit2
import re
import subprocess
import sys
import time
import pygit2
import srcinfo.parse
import srcinfo.utils
import aurweb.config
import aurweb.db
notify_cmd = aurweb.config.get('notifications', 'notify-cmd')
notify_cmd = aurweb.config.get("notifications", "notify-cmd")
repo_path = aurweb.config.get('serve', 'repo-path')
repo_regex = aurweb.config.get('serve', 'repo-regex')
repo_path = aurweb.config.get("serve", "repo-path")
repo_regex = aurweb.config.get("serve", "repo-regex")
max_blob_size = aurweb.config.getint('update', 'max-blob-size')
max_blob_size = aurweb.config.getint("update", "max-blob-size")
def size_humanize(num):
for unit in ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB']:
for unit in ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB"]:
if abs(num) < 2048.0:
if isinstance(num, int):
return "{}{}".format(num, unit)
else:
return "{:.2f}{}".format(num, unit)
num /= 1024.0
return "{:.2f}{}".format(num, 'YiB')
return "{:.2f}{}".format(num, "YiB")
def extract_arch_fields(pkginfo, field):
@ -39,20 +39,20 @@ def extract_arch_fields(pkginfo, field):
for val in pkginfo[field]:
values.append({"value": val, "arch": None})
for arch in ['i686', 'x86_64']:
if field + '_' + arch in pkginfo:
for val in pkginfo[field + '_' + arch]:
for arch in pkginfo["arch"]:
if field + "_" + arch in pkginfo:
for val in pkginfo[field + "_" + arch]:
values.append({"value": val, "arch": arch})
return values
def parse_dep(depstring):
dep, _, desc = depstring.partition(': ')
depname = re.sub(r'(<|=|>).*', '', dep)
depcond = dep[len(depname):]
dep, _, desc = depstring.partition(": ")
depname = re.sub(r"(<|=|>).*", "", dep)
depcond = dep[len(depname) :]
return (depname, desc, depcond)
return depname, desc, depcond
def create_pkgbase(conn, pkgbase, user):
@ -60,26 +60,30 @@ def create_pkgbase(conn, pkgbase, user):
userid = cur.fetchone()[0]
now = int(time.time())
cur = conn.execute("INSERT INTO PackageBases (Name, SubmittedTS, " +
"ModifiedTS, SubmitterUID, MaintainerUID, " +
"FlaggerComment) VALUES (?, ?, ?, ?, ?, '')",
[pkgbase, now, now, userid, userid])
cur = conn.execute(
"INSERT INTO PackageBases (Name, SubmittedTS, "
+ "ModifiedTS, SubmitterUID, MaintainerUID, "
+ "FlaggerComment) VALUES (?, ?, ?, ?, ?, '')",
[pkgbase, now, now, userid, userid],
)
pkgbase_id = cur.lastrowid
cur = conn.execute("INSERT INTO PackageNotifications " +
"(PackageBaseID, UserID) VALUES (?, ?)",
[pkgbase_id, userid])
cur = conn.execute(
"INSERT INTO PackageNotifications " + "(PackageBaseID, UserID) VALUES (?, ?)",
[pkgbase_id, userid],
)
conn.commit()
return pkgbase_id
def save_metadata(metadata, conn, user):
def save_metadata(metadata, conn, user): # noqa: C901
# Obtain package base ID and previous maintainer.
pkgbase = metadata['pkgbase']
cur = conn.execute("SELECT ID, MaintainerUID FROM PackageBases "
"WHERE Name = ?", [pkgbase])
pkgbase = metadata["pkgbase"]
cur = conn.execute(
"SELECT ID, MaintainerUID FROM PackageBases " "WHERE Name = ?", [pkgbase]
)
(pkgbase_id, maintainer_uid) = cur.fetchone()
was_orphan = not maintainer_uid
@ -89,119 +93,142 @@ def save_metadata(metadata, conn, user):
# Update package base details and delete current packages.
now = int(time.time())
conn.execute("UPDATE PackageBases SET ModifiedTS = ?, " +
"PackagerUID = ?, OutOfDateTS = NULL WHERE ID = ?",
[now, user_id, pkgbase_id])
conn.execute("UPDATE PackageBases SET MaintainerUID = ? " +
"WHERE ID = ? AND MaintainerUID IS NULL",
[user_id, pkgbase_id])
for table in ('Sources', 'Depends', 'Relations', 'Licenses', 'Groups'):
conn.execute("DELETE FROM Package" + table + " WHERE EXISTS (" +
"SELECT * FROM Packages " +
"WHERE Packages.PackageBaseID = ? AND " +
"Package" + table + ".PackageID = Packages.ID)",
[pkgbase_id])
conn.execute(
"UPDATE PackageBases SET ModifiedTS = ?, "
+ "PackagerUID = ?, OutOfDateTS = NULL WHERE ID = ?",
[now, user_id, pkgbase_id],
)
conn.execute(
"UPDATE PackageBases SET MaintainerUID = ? "
+ "WHERE ID = ? AND MaintainerUID IS NULL",
[user_id, pkgbase_id],
)
for table in ("Sources", "Depends", "Relations", "Licenses", "Groups"):
conn.execute(
"DELETE FROM Package"
+ table
+ " WHERE EXISTS ("
+ "SELECT * FROM Packages "
+ "WHERE Packages.PackageBaseID = ? AND "
+ "Package"
+ table
+ ".PackageID = Packages.ID)",
[pkgbase_id],
)
conn.execute("DELETE FROM Packages WHERE PackageBaseID = ?", [pkgbase_id])
for pkgname in srcinfo.utils.get_package_names(metadata):
pkginfo = srcinfo.utils.get_merged_package(pkgname, metadata)
if 'epoch' in pkginfo and int(pkginfo['epoch']) > 0:
ver = '{:d}:{:s}-{:s}'.format(int(pkginfo['epoch']),
pkginfo['pkgver'],
pkginfo['pkgrel'])
if "epoch" in pkginfo and int(pkginfo["epoch"]) > 0:
ver = "{:d}:{:s}-{:s}".format(
int(pkginfo["epoch"]), pkginfo["pkgver"], pkginfo["pkgrel"]
)
else:
ver = '{:s}-{:s}'.format(pkginfo['pkgver'], pkginfo['pkgrel'])
ver = "{:s}-{:s}".format(pkginfo["pkgver"], pkginfo["pkgrel"])
for field in ('pkgdesc', 'url'):
for field in ("pkgdesc", "url"):
if field not in pkginfo:
pkginfo[field] = None
# Create a new package.
cur = conn.execute("INSERT INTO Packages (PackageBaseID, Name, " +
"Version, Description, URL) " +
"VALUES (?, ?, ?, ?, ?)",
[pkgbase_id, pkginfo['pkgname'], ver,
pkginfo['pkgdesc'], pkginfo['url']])
cur = conn.execute(
"INSERT INTO Packages (PackageBaseID, Name, "
+ "Version, Description, URL) "
+ "VALUES (?, ?, ?, ?, ?)",
[pkgbase_id, pkginfo["pkgname"], ver, pkginfo["pkgdesc"], pkginfo["url"]],
)
conn.commit()
pkgid = cur.lastrowid
# Add package sources.
for source_info in extract_arch_fields(pkginfo, 'source'):
conn.execute("INSERT INTO PackageSources (PackageID, Source, " +
"SourceArch) VALUES (?, ?, ?)",
[pkgid, source_info['value'], source_info['arch']])
for source_info in extract_arch_fields(pkginfo, "source"):
conn.execute(
"INSERT INTO PackageSources (PackageID, Source, "
+ "SourceArch) VALUES (?, ?, ?)",
[pkgid, source_info["value"], source_info["arch"]],
)
# Add package dependencies.
for deptype in ('depends', 'makedepends',
'checkdepends', 'optdepends'):
cur = conn.execute("SELECT ID FROM DependencyTypes WHERE Name = ?",
[deptype])
for deptype in ("depends", "makedepends", "checkdepends", "optdepends"):
cur = conn.execute(
"SELECT ID FROM DependencyTypes WHERE Name = ?", [deptype]
)
deptypeid = cur.fetchone()[0]
for dep_info in extract_arch_fields(pkginfo, deptype):
depname, depdesc, depcond = parse_dep(dep_info['value'])
deparch = dep_info['arch']
conn.execute("INSERT INTO PackageDepends (PackageID, " +
"DepTypeID, DepName, DepDesc, DepCondition, " +
"DepArch) VALUES (?, ?, ?, ?, ?, ?)",
[pkgid, deptypeid, depname, depdesc, depcond,
deparch])
depname, depdesc, depcond = parse_dep(dep_info["value"])
deparch = dep_info["arch"]
conn.execute(
"INSERT INTO PackageDepends (PackageID, "
+ "DepTypeID, DepName, DepDesc, DepCondition, "
+ "DepArch) VALUES (?, ?, ?, ?, ?, ?)",
[pkgid, deptypeid, depname, depdesc, depcond, deparch],
)
# Add package relations (conflicts, provides, replaces).
for reltype in ('conflicts', 'provides', 'replaces'):
cur = conn.execute("SELECT ID FROM RelationTypes WHERE Name = ?",
[reltype])
for reltype in ("conflicts", "provides", "replaces"):
cur = conn.execute("SELECT ID FROM RelationTypes WHERE Name = ?", [reltype])
reltypeid = cur.fetchone()[0]
for rel_info in extract_arch_fields(pkginfo, reltype):
relname, _, relcond = parse_dep(rel_info['value'])
relarch = rel_info['arch']
conn.execute("INSERT INTO PackageRelations (PackageID, " +
"RelTypeID, RelName, RelCondition, RelArch) " +
"VALUES (?, ?, ?, ?, ?)",
[pkgid, reltypeid, relname, relcond, relarch])
relname, _, relcond = parse_dep(rel_info["value"])
relarch = rel_info["arch"]
conn.execute(
"INSERT INTO PackageRelations (PackageID, "
+ "RelTypeID, RelName, RelCondition, RelArch) "
+ "VALUES (?, ?, ?, ?, ?)",
[pkgid, reltypeid, relname, relcond, relarch],
)
# Add package licenses.
if 'license' in pkginfo:
for license in pkginfo['license']:
cur = conn.execute("SELECT ID FROM Licenses WHERE Name = ?",
[license])
if "license" in pkginfo:
for license in pkginfo["license"]:
cur = conn.execute("SELECT ID FROM Licenses WHERE Name = ?", [license])
row = cur.fetchone()
if row:
licenseid = row[0]
else:
cur = conn.execute("INSERT INTO Licenses (Name) " +
"VALUES (?)", [license])
cur = conn.execute(
"INSERT INTO Licenses (Name) " + "VALUES (?)", [license]
)
conn.commit()
licenseid = cur.lastrowid
conn.execute("INSERT INTO PackageLicenses (PackageID, " +
"LicenseID) VALUES (?, ?)",
[pkgid, licenseid])
conn.execute(
"INSERT INTO PackageLicenses (PackageID, "
+ "LicenseID) VALUES (?, ?)",
[pkgid, licenseid],
)
# Add package groups.
if 'groups' in pkginfo:
for group in pkginfo['groups']:
cur = conn.execute("SELECT ID FROM Groups WHERE Name = ?",
[group])
if "groups" in pkginfo:
for group in pkginfo["groups"]:
cur = conn.execute("SELECT ID FROM `Groups` WHERE Name = ?", [group])
row = cur.fetchone()
if row:
groupid = row[0]
else:
cur = conn.execute("INSERT INTO Groups (Name) VALUES (?)",
[group])
cur = conn.execute(
"INSERT INTO `Groups` (Name) VALUES (?)", [group]
)
conn.commit()
groupid = cur.lastrowid
conn.execute("INSERT INTO PackageGroups (PackageID, "
"GroupID) VALUES (?, ?)", [pkgid, groupid])
conn.execute(
"INSERT INTO PackageGroups (PackageID, " "GroupID) VALUES (?, ?)",
[pkgid, groupid],
)
# Add user to notification list on adoption.
if was_orphan:
cur = conn.execute("SELECT COUNT(*) FROM PackageNotifications WHERE " +
"PackageBaseID = ? AND UserID = ?",
[pkgbase_id, user_id])
cur = conn.execute(
"SELECT COUNT(*) FROM PackageNotifications WHERE "
+ "PackageBaseID = ? AND UserID = ?",
[pkgbase_id, user_id],
)
if cur.fetchone()[0] == 0:
conn.execute("INSERT INTO PackageNotifications " +
"(PackageBaseID, UserID) VALUES (?, ?)",
[pkgbase_id, user_id])
conn.execute(
"INSERT INTO PackageNotifications "
+ "(PackageBaseID, UserID) VALUES (?, ?)",
[pkgbase_id, user_id],
)
conn.commit()
@ -212,7 +239,7 @@ def update_notify(conn, user, pkgbase_id):
user_id = int(cur.fetchone()[0])
# Execute the notification script.
subprocess.Popen((notify_cmd, 'update', str(user_id), str(pkgbase_id)))
subprocess.Popen((notify_cmd, "update", str(user_id), str(pkgbase_id)))
def die(msg):
@ -225,28 +252,91 @@ def warn(msg):
def die_commit(msg, commit):
sys.stderr.write("error: The following error " +
"occurred when parsing commit\n")
sys.stderr.write("error: The following error " + "occurred when parsing commit\n")
sys.stderr.write("error: {:s}:\n".format(commit))
sys.stderr.write("error: {:s}\n".format(msg))
exit(1)
def main():
def validate_metadata(metadata, commit): # noqa: C901
try:
metadata_pkgbase = metadata["pkgbase"]
except KeyError:
die_commit(
"invalid .SRCINFO, does not contain a pkgbase (is the file empty?)",
str(commit.id),
)
if not re.match(repo_regex, metadata_pkgbase):
die_commit("invalid pkgbase: {:s}".format(metadata_pkgbase), str(commit.id))
if not metadata["packages"]:
die_commit("missing pkgname entry", str(commit.id))
for pkgname in set(metadata["packages"].keys()):
pkginfo = srcinfo.utils.get_merged_package(pkgname, metadata)
for field in ("pkgver", "pkgrel", "pkgname"):
if field not in pkginfo:
die_commit(
"missing mandatory field: {:s}".format(field), str(commit.id)
)
if "epoch" in pkginfo and not pkginfo["epoch"].isdigit():
die_commit("invalid epoch: {:s}".format(pkginfo["epoch"]), str(commit.id))
if not re.match(r"[a-z0-9][a-z0-9\.+_-]*$", pkginfo["pkgname"]):
die_commit(
"invalid package name: {:s}".format(pkginfo["pkgname"]),
str(commit.id),
)
max_len = {"pkgname": 255, "pkgdesc": 255, "url": 8000}
for field in max_len.keys():
if field in pkginfo and len(pkginfo[field]) > max_len[field]:
die_commit(
"{:s} field too long: {:s}".format(field, pkginfo[field]),
str(commit.id),
)
for field in ("install", "changelog"):
if field in pkginfo and not pkginfo[field] in commit.tree:
die_commit(
"missing {:s} file: {:s}".format(field, pkginfo[field]),
str(commit.id),
)
for field in extract_arch_fields(pkginfo, "source"):
fname = field["value"]
if len(fname) > 8000:
die_commit("source entry too long: {:s}".format(fname), str(commit.id))
if "://" in fname or "lp:" in fname:
continue
if fname not in commit.tree:
die_commit("missing source file: {:s}".format(fname), str(commit.id))
def validate_blob_size(blob: pygit2.Object, commit: pygit2.Commit):
if isinstance(blob, pygit2.Blob) and blob.size > max_blob_size:
die_commit(
"maximum blob size ({:s}) exceeded".format(size_humanize(max_blob_size)),
str(commit.id),
)
def main(): # noqa: C901
repo = pygit2.Repository(repo_path)
user = os.environ.get("AUR_USER")
pkgbase = os.environ.get("AUR_PKGBASE")
privileged = (os.environ.get("AUR_PRIVILEGED", '0') == '1')
allow_overwrite = (os.environ.get("AUR_OVERWRITE", '0') == '1')
privileged = os.environ.get("AUR_PRIVILEGED", "0") == "1"
allow_overwrite = (os.environ.get("AUR_OVERWRITE", "0") == "1") and privileged
warn_or_die = warn if privileged else die
if len(sys.argv) == 2 and sys.argv[1] == "restore":
if 'refs/heads/' + pkgbase not in repo.listall_references():
die('{:s}: repository not found: {:s}'.format(sys.argv[1],
pkgbase))
if "refs/heads/" + pkgbase not in repo.listall_references():
die("{:s}: repository not found: {:s}".format(sys.argv[1], pkgbase))
refname = "refs/heads/master"
branchref = 'refs/heads/' + pkgbase
branchref = "refs/heads/" + pkgbase
sha1_old = sha1_new = repo.lookup_reference(branchref).target
elif len(sys.argv) == 4:
refname, sha1_old, sha1_new = sys.argv[1:4]
@ -266,133 +356,115 @@ def main():
die("denying non-fast-forward (you should pull first)")
# Prepare the walker that validates new commits.
walker = repo.walk(sha1_new, pygit2.GIT_SORT_TOPOLOGICAL)
walker = repo.walk(sha1_new, pygit2.GIT_SORT_REVERSE)
if sha1_old != "0" * 40:
walker.hide(sha1_old)
head_commit = repo[sha1_new]
if ".SRCINFO" not in head_commit.tree:
die_commit("missing .SRCINFO", str(head_commit.id))
# Read .SRCINFO from the HEAD commit.
metadata_raw = repo[head_commit.tree[".SRCINFO"].id].data.decode()
(metadata, errors) = srcinfo.parse.parse_srcinfo(metadata_raw)
if errors:
sys.stderr.write(
"error: The following errors occurred " "when parsing .SRCINFO in commit\n"
)
sys.stderr.write("error: {:s}:\n".format(str(head_commit.id)))
for error in errors:
for err in error["error"]:
sys.stderr.write("error: line {:d}: {:s}\n".format(error["line"], err))
exit(1)
# check if there is a correct .SRCINFO file in the latest revision
validate_metadata(metadata, head_commit)
# Validate all new commits.
for commit in walker:
for fname in ('.SRCINFO', 'PKGBUILD'):
if fname not in commit.tree:
die_commit("missing {:s}".format(fname), str(commit.id))
if "PKGBUILD" not in commit.tree:
die_commit("missing PKGBUILD", str(commit.id))
# Iterate over files in root dir
for treeobj in commit.tree:
blob = repo[treeobj.id]
# Don't allow any subdirs besides "keys/"
if isinstance(treeobj, pygit2.Tree) and treeobj.name != "keys":
die_commit(
"the repository must not contain subdirectories",
str(commit.id),
)
if isinstance(blob, pygit2.Tree):
die_commit("the repository must not contain subdirectories",
str(commit.id))
# Check size of files in root dir
validate_blob_size(treeobj, commit)
if not isinstance(blob, pygit2.Blob):
die_commit("not a blob object: {:s}".format(treeobj),
str(commit.id))
if blob.size > max_blob_size:
die_commit("maximum blob size ({:s}) exceeded".format(
size_humanize(max_blob_size)), str(commit.id))
metadata_raw = repo[commit.tree['.SRCINFO'].id].data.decode()
(metadata, errors) = srcinfo.parse.parse_srcinfo(metadata_raw)
if errors:
sys.stderr.write("error: The following errors occurred "
"when parsing .SRCINFO in commit\n")
sys.stderr.write("error: {:s}:\n".format(str(commit.id)))
for error in errors:
for err in error['error']:
sys.stderr.write("error: line {:d}: {:s}\n".format(
error['line'], err))
exit(1)
metadata_pkgbase = metadata['pkgbase']
if not re.match(repo_regex, metadata_pkgbase):
die_commit('invalid pkgbase: {:s}'.format(metadata_pkgbase),
str(commit.id))
if not metadata['packages']:
die_commit('missing pkgname entry', str(commit.id))
for pkgname in set(metadata['packages'].keys()):
pkginfo = srcinfo.utils.get_merged_package(pkgname, metadata)
for field in ('pkgver', 'pkgrel', 'pkgname'):
if field not in pkginfo:
die_commit('missing mandatory field: {:s}'.format(field),
str(commit.id))
if 'epoch' in pkginfo and not pkginfo['epoch'].isdigit():
die_commit('invalid epoch: {:s}'.format(pkginfo['epoch']),
str(commit.id))
if not re.match(r'[a-z0-9][a-z0-9\.+_-]*$', pkginfo['pkgname']):
die_commit('invalid package name: {:s}'.format(
pkginfo['pkgname']), str(commit.id))
max_len = {'pkgname': 255, 'pkgdesc': 255, 'url': 8000}
for field in max_len.keys():
if field in pkginfo and len(pkginfo[field]) > max_len[field]:
die_commit('{:s} field too long: {:s}'.format(field,
pkginfo[field]), str(commit.id))
for field in ('install', 'changelog'):
if field in pkginfo and not pkginfo[field] in commit.tree:
die_commit('missing {:s} file: {:s}'.format(field,
pkginfo[field]), str(commit.id))
for field in extract_arch_fields(pkginfo, 'source'):
fname = field['value']
if len(fname) > 8000:
die_commit('source entry too long: {:s}'.format(fname),
str(commit.id))
if "://" in fname or "lp:" in fname:
continue
if fname not in commit.tree:
die_commit('missing source file: {:s}'.format(fname),
str(commit.id))
# If we got a subdir keys/,
# make sure it only contains a pgp/ subdir with key files
if "keys" in commit.tree:
# Check for forbidden files/dirs in keys/
for keyobj in commit.tree["keys"]:
if not isinstance(keyobj, pygit2.Tree) or keyobj.name != "pgp":
die_commit(
"the keys/ subdir may only contain a pgp/ directory",
str(commit.id),
)
# Check for forbidden files in keys/pgp/
if "keys/pgp" in commit.tree:
for pgpobj in commit.tree["keys/pgp"]:
if not isinstance(pgpobj, pygit2.Blob) or not pgpobj.name.endswith(
".asc"
):
die_commit(
"the subdir may only contain .asc (PGP pub key) files",
str(commit.id),
)
# Check file size for pgp key files
validate_blob_size(pgpobj, commit)
# Display a warning if .SRCINFO is unchanged.
if sha1_old not in ("0000000000000000000000000000000000000000", sha1_new):
srcinfo_id_old = repo[sha1_old].tree['.SRCINFO'].id
srcinfo_id_new = repo[sha1_new].tree['.SRCINFO'].id
srcinfo_id_old = repo[sha1_old].tree[".SRCINFO"].id
srcinfo_id_new = repo[sha1_new].tree[".SRCINFO"].id
if srcinfo_id_old == srcinfo_id_new:
warn(".SRCINFO unchanged. "
"The package database will not be updated!")
# Read .SRCINFO from the HEAD commit.
metadata_raw = repo[repo[sha1_new].tree['.SRCINFO'].id].data.decode()
(metadata, errors) = srcinfo.parse.parse_srcinfo(metadata_raw)
warn(".SRCINFO unchanged. " "The package database will not be updated!")
# Ensure that the package base name matches the repository name.
metadata_pkgbase = metadata['pkgbase']
metadata_pkgbase = metadata["pkgbase"]
if metadata_pkgbase != pkgbase:
die('invalid pkgbase: {:s}, expected {:s}'.format(metadata_pkgbase,
pkgbase))
die("invalid pkgbase: {:s}, expected {:s}".format(metadata_pkgbase, pkgbase))
# Ensure that packages are neither blacklisted nor overwritten.
pkgbase = metadata['pkgbase']
pkgbase = metadata["pkgbase"]
cur = conn.execute("SELECT ID FROM PackageBases WHERE Name = ?", [pkgbase])
row = cur.fetchone()
pkgbase_id = row[0] if row else 0
cur = conn.execute("SELECT Name FROM PackageBlacklist")
blacklist = [row[0] for row in cur.fetchall()]
if pkgbase in blacklist:
warn_or_die("pkgbase is blacklisted: {:s}".format(pkgbase))
cur = conn.execute("SELECT Name, Repo FROM OfficialProviders")
providers = dict(cur.fetchall())
for pkgname in srcinfo.utils.get_package_names(metadata):
pkginfo = srcinfo.utils.get_merged_package(pkgname, metadata)
pkgname = pkginfo['pkgname']
pkgname = pkginfo["pkgname"]
if pkgname in blacklist:
warn_or_die('package is blacklisted: {:s}'.format(pkgname))
warn_or_die("package is blacklisted: {:s}".format(pkgname))
if pkgname in providers:
warn_or_die('package already provided by [{:s}]: {:s}'.format(
providers[pkgname], pkgname))
warn_or_die(
"package already provided by [{:s}]: {:s}".format(
providers[pkgname], pkgname
)
)
cur = conn.execute("SELECT COUNT(*) FROM Packages WHERE Name = ? " +
"AND PackageBaseID <> ?", [pkgname, pkgbase_id])
cur = conn.execute(
"SELECT COUNT(*) FROM Packages WHERE Name = ? " + "AND PackageBaseID <> ?",
[pkgname, pkgbase_id],
)
if cur.fetchone()[0] > 0:
die('cannot overwrite package: {:s}'.format(pkgname))
die("cannot overwrite package: {:s}".format(pkgname))
# Create a new package base if it does not exist yet.
if pkgbase_id == 0:
@ -403,7 +475,7 @@ def main():
# Create (or update) a branch with the name of the package base for better
# accessibility.
branchref = 'refs/heads/' + pkgbase
branchref = "refs/heads/" + pkgbase
repo.create_reference(branchref, sha1_new, True)
# Work around a Git bug: The HEAD ref is not updated when using
@ -411,7 +483,7 @@ def main():
# mainline. See
# http://git.661346.n2.nabble.com/PATCH-receive-pack-Create-a-HEAD-ref-for-ref-namespace-td7632149.html
# for details.
headref = 'refs/namespaces/' + pkgbase + '/HEAD'
headref = "refs/namespaces/" + pkgbase + "/HEAD"
repo.create_reference(headref, sha1_new, True)
# Send package update notifications.
@ -422,5 +494,5 @@ def main():
conn.close()
if __name__ == '__main__':
if __name__ == "__main__":
main()

83
aurweb/initdb.py Normal file
View file

@ -0,0 +1,83 @@
import argparse
import alembic.command
import alembic.config
import aurweb.aur_logging
import aurweb.db
import aurweb.schema
def feed_initial_data(conn):
conn.execute(
aurweb.schema.AccountTypes.insert(),
[
{"ID": 1, "AccountType": "User"},
{"ID": 2, "AccountType": "Package Maintainer"},
{"ID": 3, "AccountType": "Developer"},
{"ID": 4, "AccountType": "Package Maintainer & Developer"},
],
)
conn.execute(
aurweb.schema.DependencyTypes.insert(),
[
{"ID": 1, "Name": "depends"},
{"ID": 2, "Name": "makedepends"},
{"ID": 3, "Name": "checkdepends"},
{"ID": 4, "Name": "optdepends"},
],
)
conn.execute(
aurweb.schema.RelationTypes.insert(),
[
{"ID": 1, "Name": "conflicts"},
{"ID": 2, "Name": "provides"},
{"ID": 3, "Name": "replaces"},
],
)
conn.execute(
aurweb.schema.RequestTypes.insert(),
[
{"ID": 1, "Name": "deletion"},
{"ID": 2, "Name": "orphan"},
{"ID": 3, "Name": "merge"},
],
)
def run(args):
aurweb.config.rehash()
# Ensure Alembic is fine before we do the real work, in order not to fail at
# the last step and leave the database in an inconsistent state. The
# configuration is loaded lazily, so we query it to force its loading.
if args.use_alembic:
alembic_config = alembic.config.Config("alembic.ini")
alembic_config.get_main_option("script_location")
alembic_config.attributes["configure_logger"] = False
engine = aurweb.db.get_engine(echo=(args.verbose >= 1))
aurweb.schema.metadata.create_all(engine)
conn = engine.connect()
feed_initial_data(conn)
conn.close()
if args.use_alembic:
alembic.command.stamp(alembic_config, "head")
if __name__ == "__main__":
parser = argparse.ArgumentParser(
prog="python -m aurweb.initdb", description="Initialize the aurweb database."
)
parser.add_argument(
"-v", "--verbose", action="count", default=0, help="increase verbosity"
)
parser.add_argument(
"--no-alembic",
help="disable Alembic migrations support",
dest="use_alembic",
action="store_false",
)
args = parser.parse_args()
run(args)

102
aurweb/l10n.py Normal file
View file

@ -0,0 +1,102 @@
import gettext
from collections import OrderedDict
from fastapi import Request
import aurweb.config
SUPPORTED_LANGUAGES = OrderedDict(
{
"ar": "العربية",
"ast": "Asturianu",
"ca": "Català",
"cs": "Český",
"da": "Dansk",
"de": "Deutsch",
"el": "Ελληνικά",
"en": "English",
"es": "Español",
"es_419": "Español (Latinoamérica)",
"fi": "Suomi",
"fr": "Français",
"he": "עברית",
"hr": "Hrvatski",
"hu": "Magyar",
"it": "Italiano",
"ja": "日本語",
"nb": "Norsk",
"nl": "Nederlands",
"pl": "Polski",
"pt_BR": "Português (Brasil)",
"pt_PT": "Português (Portugal)",
"ro": "Română",
"ru": "Русский",
"sk": "Slovenčina",
"sr": "Srpski",
"tr": "Türkçe",
"uk": "Українська",
"zh_CN": "简体中文",
"zh_TW": "正體中文",
}
)
RIGHT_TO_LEFT_LANGUAGES = ("he", "ar")
class Translator:
def __init__(self):
self._localedir = aurweb.config.get("options", "localedir")
self._translator = {}
def get_translator(self, lang: str):
if lang not in self._translator:
self._translator[lang] = gettext.translation(
"aurweb", self._localedir, languages=[lang], fallback=True
)
return self._translator.get(lang)
def translate(self, s: str, lang: str):
return self.get_translator(lang).gettext(s)
# Global translator object.
translator = Translator()
def get_request_language(request: Request) -> str:
"""Get a request's language from either query param, user setting or
cookie. We use the configuration's [options] default_lang otherwise.
@param request FastAPI request
"""
request_lang = request.query_params.get("language")
cookie_lang = request.cookies.get("AURLANG")
if request_lang and request_lang in SUPPORTED_LANGUAGES:
return request_lang
elif (
request.user.is_authenticated()
and request.user.LangPreference in SUPPORTED_LANGUAGES
):
return request.user.LangPreference
elif cookie_lang and cookie_lang in SUPPORTED_LANGUAGES:
return cookie_lang
return aurweb.config.get_with_fallback("options", "default_lang", "en")
def get_raw_translator_for_request(request: Request):
lang = get_request_language(request)
return translator.get_translator(lang)
def get_translator_for_request(request: Request):
"""
Determine the preferred language from a FastAPI request object and build a
translator function for it.
"""
lang = get_request_language(request)
def translate(message):
return translator.translate(message, lang)
return translate

32
aurweb/models/__init__.py Normal file
View file

@ -0,0 +1,32 @@
""" Collection of all aurweb SQLAlchemy declarative models. """
from .accepted_term import AcceptedTerm # noqa: F401
from .account_type import AccountType # noqa: F401
from .api_rate_limit import ApiRateLimit # noqa: F401
from .ban import Ban # noqa: F401
from .dependency_type import DependencyType # noqa: F401
from .group import Group # noqa: F401
from .license import License # noqa: F401
from .official_provider import OfficialProvider # noqa: F401
from .package import Package # noqa: F401
from .package_base import PackageBase # noqa: F401
from .package_blacklist import PackageBlacklist # noqa: F401
from .package_comaintainer import PackageComaintainer # noqa: F401
from .package_comment import PackageComment # noqa: F401
from .package_dependency import PackageDependency # noqa: F401
from .package_group import PackageGroup # noqa: F401
from .package_keyword import PackageKeyword # noqa: F401
from .package_license import PackageLicense # noqa: F401
from .package_notification import PackageNotification # noqa: F401
from .package_relation import PackageRelation # noqa: F401
from .package_request import PackageRequest # noqa: F401
from .package_source import PackageSource # noqa: F401
from .package_vote import PackageVote # noqa: F401
from .relation_type import RelationType # noqa: F401
from .request_type import RequestType # noqa: F401
from .session import Session # noqa: F401
from .ssh_pub_key import SSHPubKey # noqa: F401
from .term import Term # noqa: F401
from .user import User # noqa: F401
from .vote import Vote # noqa: F401
from .voteinfo import VoteInfo # noqa: F401

View file

@ -0,0 +1,42 @@
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import backref, relationship
from aurweb import schema
from aurweb.models.declarative import Base
from aurweb.models.term import Term as _Term
from aurweb.models.user import User as _User
class AcceptedTerm(Base):
__table__ = schema.AcceptedTerms
__tablename__ = __table__.name
__mapper_args__ = {"primary_key": [__table__.c.TermsID]}
User = relationship(
_User,
backref=backref("accepted_terms", lazy="dynamic"),
foreign_keys=[__table__.c.UsersID],
)
Term = relationship(
_Term,
backref=backref("accepted_terms", lazy="dynamic"),
foreign_keys=[__table__.c.TermsID],
)
def __init__(self, **kwargs):
super().__init__(**kwargs)
if not self.User and not self.UsersID:
raise IntegrityError(
statement="Foreign key UsersID cannot be null.",
orig="AcceptedTerms.UserID",
params=("NULL"),
)
if not self.Term and not self.TermsID:
raise IntegrityError(
statement="Foreign key TermID cannot be null.",
orig="AcceptedTerms.TermID",
params=("NULL"),
)

View file

@ -0,0 +1,40 @@
from aurweb import schema
from aurweb.models.declarative import Base
USER = "User"
PACKAGE_MAINTAINER = "Package Maintainer"
DEVELOPER = "Developer"
PACKAGE_MAINTAINER_AND_DEV = "Package Maintainer & Developer"
USER_ID = 1
PACKAGE_MAINTAINER_ID = 2
DEVELOPER_ID = 3
PACKAGE_MAINTAINER_AND_DEV_ID = 4
# Map string constants to integer constants.
ACCOUNT_TYPE_ID = {
USER: USER_ID,
PACKAGE_MAINTAINER: PACKAGE_MAINTAINER_ID,
DEVELOPER: DEVELOPER_ID,
PACKAGE_MAINTAINER_AND_DEV: PACKAGE_MAINTAINER_AND_DEV_ID,
}
# Reversed ACCOUNT_TYPE_ID mapping.
ACCOUNT_TYPE_NAME = {v: k for k, v in ACCOUNT_TYPE_ID.items()}
class AccountType(Base):
"""An ORM model of a single AccountTypes record."""
__table__ = schema.AccountTypes
__tablename__ = __table__.name
__mapper_args__ = {"primary_key": [__table__.c.ID]}
def __init__(self, **kwargs):
self.AccountType = kwargs.pop("AccountType")
def __str__(self):
return str(self.AccountType)
def __repr__(self):
return "<AccountType(ID='%s', AccountType='%s')>" % (self.ID, str(self))

View file

@ -0,0 +1,27 @@
from sqlalchemy.exc import IntegrityError
from aurweb import schema
from aurweb.models.declarative import Base
class ApiRateLimit(Base):
__table__ = schema.ApiRateLimit
__tablename__ = __table__.name
__mapper_args__ = {"primary_key": [__table__.c.IP]}
def __init__(self, **kwargs):
super().__init__(**kwargs)
if self.Requests is None:
raise IntegrityError(
statement="Column Requests cannot be null.",
orig="ApiRateLimit.Requests",
params=("NULL"),
)
if self.WindowStart is None:
raise IntegrityError(
statement="Column WindowStart cannot be null.",
orig="ApiRateLimit.WindowStart",
params=("NULL"),
)

20
aurweb/models/ban.py Normal file
View file

@ -0,0 +1,20 @@
from fastapi import Request
from aurweb import db, schema
from aurweb.models.declarative import Base
from aurweb.util import get_client_ip
class Ban(Base):
__table__ = schema.Bans
__tablename__ = __table__.name
__mapper_args__ = {"primary_key": [__table__.c.IPAddress]}
def __init__(self, **kwargs):
super().__init__(**kwargs)
def is_banned(request: Request):
ip = get_client_ip(request)
exists = db.query(Ban).filter(Ban.IPAddress == ip).exists()
return db.query(exists).scalar()

View file

@ -0,0 +1,29 @@
import json
from sqlalchemy.ext.declarative import declarative_base
from aurweb import util
def to_dict(model):
return {c.name: getattr(model, c.name) for c in model.__table__.columns}
def to_json(model, indent: int = None):
return json.dumps(
{k: util.jsonify(v) for k, v in to_dict(model).items()}, indent=indent
)
Base = declarative_base()
# Setup __table_args__ applicable to every table.
Base.__table_args__ = {"autoload": False, "extend_existing": True}
# Setup Base.as_dict and Base.json.
#
# With this, declarative models can use .as_dict() or .json()
# at any time to produce a dict and json out of table columns.
#
Base.as_dict = to_dict
Base.json = to_json

View file

@ -0,0 +1,21 @@
from aurweb import schema
from aurweb.models.declarative import Base
DEPENDS = "depends"
MAKEDEPENDS = "makedepends"
CHECKDEPENDS = "checkdepends"
OPTDEPENDS = "optdepends"
DEPENDS_ID = 1
MAKEDEPENDS_ID = 2
CHECKDEPENDS_ID = 3
OPTDEPENDS_ID = 4
class DependencyType(Base):
__table__ = schema.DependencyTypes
__tablename__ = __table__.name
__mapper_args__ = {"primary_key": [__table__.c.ID]}
def __init__(self, Name: str = None):
self.Name = Name

19
aurweb/models/group.py Normal file
View file

@ -0,0 +1,19 @@
from sqlalchemy.exc import IntegrityError
from aurweb import schema
from aurweb.models.declarative import Base
class Group(Base):
__table__ = schema.Groups
__tablename__ = __table__.name
__mapper_args__ = {"primary_key": [__table__.c.ID]}
def __init__(self, **kwargs):
super().__init__(**kwargs)
if self.Name is None:
raise IntegrityError(
statement="Column Name cannot be null.",
orig="Groups.Name",
params=("NULL"),
)

20
aurweb/models/license.py Normal file
View file

@ -0,0 +1,20 @@
from sqlalchemy.exc import IntegrityError
from aurweb import schema
from aurweb.models.declarative import Base
class License(Base):
__table__ = schema.Licenses
__tablename__ = __table__.name
__mapper_args__ = {"primary_key": [__table__.c.ID]}
def __init__(self, **kwargs):
super().__init__(**kwargs)
if not self.Name:
raise IntegrityError(
statement="Column Name cannot be null.",
orig="Licenses.Name",
params=("NULL"),
)

View file

@ -0,0 +1,39 @@
from sqlalchemy.exc import IntegrityError
from aurweb import schema
from aurweb.models.declarative import Base
OFFICIAL_BASE = "https://archlinux.org"
class OfficialProvider(Base):
__table__ = schema.OfficialProviders
__tablename__ = __table__.name
__mapper_args__ = {"primary_key": [__table__.c.ID]}
# OfficialProvider instances are official packages.
is_official = True
def __init__(self, **kwargs):
super().__init__(**kwargs)
if not self.Name:
raise IntegrityError(
statement="Column Name cannot be null.",
orig="OfficialProviders.Name",
params=("NULL"),
)
if not self.Repo:
raise IntegrityError(
statement="Column Repo cannot be null.",
orig="OfficialProviders.Repo",
params=("NULL"),
)
if not self.Provides:
raise IntegrityError(
statement="Column Provides cannot be null.",
orig="OfficialProviders.Provides",
params=("NULL"),
)

38
aurweb/models/package.py Normal file
View file

@ -0,0 +1,38 @@
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import backref, relationship
from aurweb import schema
from aurweb.models.declarative import Base
from aurweb.models.package_base import PackageBase as _PackageBase
class Package(Base):
__table__ = schema.Packages
__tablename__ = __table__.name
__mapper_args__ = {"primary_key": [__table__.c.ID]}
PackageBase = relationship(
_PackageBase,
backref=backref("packages", lazy="dynamic", cascade="all, delete"),
foreign_keys=[__table__.c.PackageBaseID],
)
# No Package instances are official packages.
is_official = False
def __init__(self, **kwargs):
super().__init__(**kwargs)
if not self.PackageBase and not self.PackageBaseID:
raise IntegrityError(
statement="Foreign key PackageBaseID cannot be null.",
orig="Packages.PackageBaseID",
params=("NULL"),
)
if self.Name is None:
raise IntegrityError(
statement="Column Name cannot be null.",
orig="Packages.Name",
params=("NULL"),
)

View file

@ -0,0 +1,76 @@
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import backref, relationship
from aurweb import schema, time
from aurweb.models.declarative import Base
from aurweb.models.user import User as _User
class PackageBase(Base):
__table__ = schema.PackageBases
__tablename__ = __table__.name
__mapper_args__ = {"primary_key": [__table__.c.ID]}
Flagger = relationship(
_User,
backref=backref("flagged_bases", lazy="dynamic"),
foreign_keys=[__table__.c.FlaggerUID],
)
Submitter = relationship(
_User,
backref=backref("submitted_bases", lazy="dynamic"),
foreign_keys=[__table__.c.SubmitterUID],
)
Maintainer = relationship(
_User,
backref=backref("maintained_bases", lazy="dynamic"),
foreign_keys=[__table__.c.MaintainerUID],
)
Packager = relationship(
_User,
backref=backref("package_bases", lazy="dynamic"),
foreign_keys=[__table__.c.PackagerUID],
)
# A set used to check for floatable values.
TO_FLOAT = {"Popularity"}
def __init__(self, **kwargs):
super().__init__(**kwargs)
if self.Name is None:
raise IntegrityError(
statement="Column Name cannot be null.",
orig="PackageBases.Name",
params=("NULL"),
)
# If no SubmittedTS/ModifiedTS is provided on creation, set them
# here to the current utc timestamp.
now = time.utcnow()
if not self.SubmittedTS:
self.SubmittedTS = now
if not self.ModifiedTS:
self.ModifiedTS = now
if not self.FlaggerComment:
self.FlaggerComment = str()
def __getattribute__(self, key: str):
attr = super().__getattribute__(key)
if key in PackageBase.TO_FLOAT and not isinstance(attr, float):
return float(attr)
return attr
def popularity_decay(pkgbase: PackageBase, utcnow: int):
"""Return the delta between now and the last time popularity was updated, in days"""
return int((utcnow - pkgbase.PopularityUpdated.timestamp()) / 86400)
def popularity(pkgbase: PackageBase, utcnow: int):
"""Return up-to-date popularity"""
return float(pkgbase.Popularity) * (0.98 ** popularity_decay(pkgbase, utcnow))

View file

@ -0,0 +1,20 @@
from sqlalchemy.exc import IntegrityError
from aurweb import schema
from aurweb.models.declarative import Base
class PackageBlacklist(Base):
__table__ = schema.PackageBlacklist
__tablename__ = __table__.name
__mapper_args__ = {"primary_key": [__table__.c.ID]}
def __init__(self, **kwargs):
super().__init__(**kwargs)
if not self.Name:
raise IntegrityError(
statement="Column Name cannot be null.",
orig="PackageBlacklist.Name",
params=("NULL"),
)

View file

@ -0,0 +1,49 @@
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import backref, relationship
from aurweb import schema
from aurweb.models.declarative import Base
from aurweb.models.package_base import PackageBase as _PackageBase
from aurweb.models.user import User as _User
class PackageComaintainer(Base):
__table__ = schema.PackageComaintainers
__tablename__ = __table__.name
__mapper_args__ = {"primary_key": [__table__.c.UsersID, __table__.c.PackageBaseID]}
User = relationship(
_User,
backref=backref("comaintained", lazy="dynamic", cascade="all, delete"),
foreign_keys=[__table__.c.UsersID],
)
PackageBase = relationship(
_PackageBase,
backref=backref("comaintainers", lazy="dynamic", cascade="all, delete"),
foreign_keys=[__table__.c.PackageBaseID],
)
def __init__(self, **kwargs):
super().__init__(**kwargs)
if not self.User and not self.UsersID:
raise IntegrityError(
statement="Foreign key UsersID cannot be null.",
orig="PackageComaintainers.UsersID",
params=("NULL"),
)
if not self.PackageBase and not self.PackageBaseID:
raise IntegrityError(
statement="Foreign key PackageBaseID cannot be null.",
orig="PackageComaintainers.PackageBaseID",
params=("NULL"),
)
if not self.Priority:
raise IntegrityError(
statement="Column Priority cannot be null.",
orig="PackageComaintainers.Priority",
params=("NULL"),
)

View file

@ -0,0 +1,73 @@
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import backref, relationship
from aurweb import schema
from aurweb.models.declarative import Base
from aurweb.models.package_base import PackageBase as _PackageBase
from aurweb.models.user import User as _User
class PackageComment(Base):
__table__ = schema.PackageComments
__tablename__ = __table__.name
__mapper_args__ = {"primary_key": [__table__.c.ID]}
PackageBase = relationship(
_PackageBase,
backref=backref("comments", lazy="dynamic", cascade="all, delete"),
foreign_keys=[__table__.c.PackageBaseID],
)
User = relationship(
_User,
backref=backref("package_comments", lazy="dynamic"),
foreign_keys=[__table__.c.UsersID],
)
Editor = relationship(
_User,
backref=backref("edited_comments", lazy="dynamic"),
foreign_keys=[__table__.c.EditedUsersID],
)
Deleter = relationship(
_User,
backref=backref("deleted_comments", lazy="dynamic"),
foreign_keys=[__table__.c.DelUsersID],
)
def __init__(self, **kwargs):
super().__init__(**kwargs)
if not self.PackageBase and not self.PackageBaseID:
raise IntegrityError(
statement="Foreign key PackageBaseID cannot be null.",
orig="PackageComments.PackageBaseID",
params=("NULL"),
)
if not self.User and not self.UsersID:
raise IntegrityError(
statement="Foreign key UsersID cannot be null.",
orig="PackageComments.UsersID",
params=("NULL"),
)
if self.Comments is None:
raise IntegrityError(
statement="Column Comments cannot be null.",
orig="PackageComments.Comments",
params=("NULL"),
)
if self.RenderedComment is None:
self.RenderedComment = str()
def maintainers(self):
return list(
filter(
lambda e: e is not None,
[self.PackageBase.Maintainer]
+ [c.User for c in self.PackageBase.comaintainers],
)
)

View file

@ -0,0 +1,100 @@
from sqlalchemy import and_, literal
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import backref, relationship
from aurweb import db, schema
from aurweb.models.declarative import Base
from aurweb.models.dependency_type import DependencyType as _DependencyType
from aurweb.models.official_provider import OfficialProvider as _OfficialProvider
from aurweb.models.package import Package as _Package
from aurweb.models.package_relation import PackageRelation
class PackageDependency(Base):
__table__ = schema.PackageDepends
__tablename__ = __table__.name
__mapper_args__ = {
"primary_key": [
__table__.c.PackageID,
__table__.c.DepTypeID,
__table__.c.DepName,
]
}
Package = relationship(
_Package,
backref=backref("package_dependencies", lazy="dynamic", cascade="all, delete"),
foreign_keys=[__table__.c.PackageID],
)
DependencyType = relationship(
_DependencyType,
backref=backref("package_dependencies", lazy="dynamic"),
foreign_keys=[__table__.c.DepTypeID],
)
def __init__(self, **kwargs):
super().__init__(**kwargs)
if not self.Package and not self.PackageID:
raise IntegrityError(
statement="Foreign key PackageID cannot be null.",
orig="PackageDependencies.PackageID",
params=("NULL"),
)
if not self.DependencyType and not self.DepTypeID:
raise IntegrityError(
statement="Foreign key DepTypeID cannot be null.",
orig="PackageDependencies.DepTypeID",
params=("NULL"),
)
if self.DepName is None:
raise IntegrityError(
statement="Column DepName cannot be null.",
orig="PackageDependencies.DepName",
params=("NULL"),
)
def is_aur_package(self) -> bool:
pkg = db.query(_Package).filter(_Package.Name == self.DepName).exists()
return db.query(pkg).scalar()
def is_package(self) -> bool:
official = (
db.query(_OfficialProvider)
.filter(_OfficialProvider.Name == self.DepName)
.exists()
)
return self.is_aur_package() or db.query(official).scalar()
def provides(self) -> list[PackageRelation]:
from aurweb.models.relation_type import PROVIDES_ID
rels = (
db.query(PackageRelation)
.join(_Package)
.filter(
and_(
PackageRelation.RelTypeID == PROVIDES_ID,
PackageRelation.RelName == self.DepName,
)
)
.with_entities(_Package.Name, literal(False).label("is_official"))
.order_by(_Package.Name.asc())
)
official_rels = (
db.query(_OfficialProvider)
.filter(
and_(
_OfficialProvider.Provides == self.DepName,
_OfficialProvider.Name != self.DepName,
)
)
.with_entities(_OfficialProvider.Name, literal(True).label("is_official"))
.order_by(_OfficialProvider.Name.asc())
)
return rels.union(official_rels).all()

View file

@ -0,0 +1,42 @@
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import backref, relationship
from aurweb import schema
from aurweb.models.declarative import Base
from aurweb.models.group import Group as _Group
from aurweb.models.package import Package as _Package
class PackageGroup(Base):
__table__ = schema.PackageGroups
__tablename__ = __table__.name
__mapper_args__ = {"primary_key": [__table__.c.PackageID, __table__.c.GroupID]}
Package = relationship(
_Package,
backref=backref("package_groups", lazy="dynamic", cascade="all, delete"),
foreign_keys=[__table__.c.PackageID],
)
Group = relationship(
_Group,
backref=backref("package_groups", lazy="dynamic", cascade="all, delete"),
foreign_keys=[__table__.c.GroupID],
)
def __init__(self, **kwargs):
super().__init__(**kwargs)
if not self.Package and not self.PackageID:
raise IntegrityError(
statement="Primary key PackageID cannot be null.",
orig="PackageGroups.PackageID",
params=("NULL"),
)
if not self.Group and not self.GroupID:
raise IntegrityError(
statement="Primary key GroupID cannot be null.",
orig="PackageGroups.GroupID",
params=("NULL"),
)

View file

@ -0,0 +1,28 @@
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import backref, relationship
from aurweb import schema
from aurweb.models.declarative import Base
from aurweb.models.package_base import PackageBase as _PackageBase
class PackageKeyword(Base):
__table__ = schema.PackageKeywords
__tablename__ = __table__.name
__mapper_args__ = {"primary_key": [__table__.c.PackageBaseID, __table__.c.Keyword]}
PackageBase = relationship(
_PackageBase,
backref=backref("keywords", lazy="dynamic", cascade="all, delete"),
foreign_keys=[__table__.c.PackageBaseID],
)
def __init__(self, **kwargs):
super().__init__(**kwargs)
if not self.PackageBase and not self.PackageBaseID:
raise IntegrityError(
statement="Primary key PackageBaseID cannot be null.",
orig="PackageKeywords.PackageBaseID",
params=("NULL"),
)

View file

@ -0,0 +1,42 @@
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import backref, relationship
from aurweb import schema
from aurweb.models.declarative import Base
from aurweb.models.license import License as _License
from aurweb.models.package import Package as _Package
class PackageLicense(Base):
__table__ = schema.PackageLicenses
__tablename__ = __table__.name
__mapper_args__ = {"primary_key": [__table__.c.PackageID, __table__.c.LicenseID]}
Package = relationship(
_Package,
backref=backref("package_licenses", lazy="dynamic", cascade="all, delete"),
foreign_keys=[__table__.c.PackageID],
)
License = relationship(
_License,
backref=backref("package_licenses", lazy="dynamic", cascade="all, delete"),
foreign_keys=[__table__.c.LicenseID],
)
def __init__(self, **kwargs):
super().__init__(**kwargs)
if not self.Package and not self.PackageID:
raise IntegrityError(
statement="Primary key PackageID cannot be null.",
orig="PackageLicenses.PackageID",
params=("NULL"),
)
if not self.License and not self.LicenseID:
raise IntegrityError(
statement="Primary key LicenseID cannot be null.",
orig="PackageLicenses.LicenseID",
params=("NULL"),
)

View file

@ -0,0 +1,42 @@
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import backref, relationship
from aurweb import schema
from aurweb.models.declarative import Base
from aurweb.models.package_base import PackageBase as _PackageBase
from aurweb.models.user import User as _User
class PackageNotification(Base):
__table__ = schema.PackageNotifications
__tablename__ = __table__.name
__mapper_args__ = {"primary_key": [__table__.c.UserID, __table__.c.PackageBaseID]}
User = relationship(
_User,
backref=backref("notifications", lazy="dynamic", cascade="all, delete"),
foreign_keys=[__table__.c.UserID],
)
PackageBase = relationship(
_PackageBase,
backref=backref("notifications", lazy="dynamic", cascade="all, delete"),
foreign_keys=[__table__.c.PackageBaseID],
)
def __init__(self, **kwargs):
super().__init__(**kwargs)
if not self.User and not self.UserID:
raise IntegrityError(
statement="Foreign key UserID cannot be null.",
orig="PackageNotifications.UserID",
params=("NULL"),
)
if not self.PackageBase and not self.PackageBaseID:
raise IntegrityError(
statement="Foreign key PackageBaseID cannot be null.",
orig="PackageNotifications.PackageBaseID",
params=("NULL"),
)

View file

@ -0,0 +1,55 @@
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import backref, relationship
from aurweb import schema
from aurweb.models.declarative import Base
from aurweb.models.package import Package as _Package
from aurweb.models.relation_type import RelationType as _RelationType
class PackageRelation(Base):
__table__ = schema.PackageRelations
__tablename__ = __table__.name
__mapper_args__ = {
"primary_key": [
__table__.c.PackageID,
__table__.c.RelTypeID,
__table__.c.RelName,
]
}
Package = relationship(
_Package,
backref=backref("package_relations", lazy="dynamic", cascade="all, delete"),
foreign_keys=[__table__.c.PackageID],
)
RelationType = relationship(
_RelationType,
backref=backref("package_relations", lazy="dynamic"),
foreign_keys=[__table__.c.RelTypeID],
)
def __init__(self, **kwargs):
super().__init__(**kwargs)
if not self.Package and not self.PackageID:
raise IntegrityError(
statement="Foreign key PackageID cannot be null.",
orig="PackageRelations.PackageID",
params=("NULL"),
)
if not self.RelationType and not self.RelTypeID:
raise IntegrityError(
statement="Foreign key RelTypeID cannot be null.",
orig="PackageRelations.RelTypeID",
params=("NULL"),
)
if not self.RelName:
raise IntegrityError(
statement="Column RelName cannot be null.",
orig="PackageRelations.RelName",
params=("NULL"),
)

View file

@ -0,0 +1,121 @@
import base64
import hashlib
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import backref, relationship
from aurweb import config, schema
from aurweb.models.declarative import Base
from aurweb.models.package_base import PackageBase as _PackageBase
from aurweb.models.request_type import RequestType as _RequestType
from aurweb.models.user import User as _User
PENDING = "Pending"
CLOSED = "Closed"
ACCEPTED = "Accepted"
REJECTED = "Rejected"
# Integer values used for the Status column of PackageRequest.
PENDING_ID = 0
CLOSED_ID = 1
ACCEPTED_ID = 2
REJECTED_ID = 3
class PackageRequest(Base):
__table__ = schema.PackageRequests
__tablename__ = __table__.name
__mapper_args__ = {"primary_key": [__table__.c.ID]}
RequestType = relationship(
_RequestType,
backref=backref("package_requests", lazy="dynamic"),
foreign_keys=[__table__.c.ReqTypeID],
)
User = relationship(
_User,
backref=backref("package_requests", lazy="dynamic"),
foreign_keys=[__table__.c.UsersID],
)
PackageBase = relationship(
_PackageBase,
backref=backref("requests", lazy="dynamic"),
foreign_keys=[__table__.c.PackageBaseID],
)
Closer = relationship(
_User,
backref=backref("closed_requests", lazy="dynamic"),
foreign_keys=[__table__.c.ClosedUID],
)
STATUS_DISPLAY = {
PENDING_ID: PENDING,
CLOSED_ID: CLOSED,
ACCEPTED_ID: ACCEPTED,
REJECTED_ID: REJECTED,
}
def __init__(self, **kwargs):
super().__init__(**kwargs)
if not self.RequestType and not self.ReqTypeID:
raise IntegrityError(
statement="Foreign key ReqTypeID cannot be null.",
orig="PackageRequests.ReqTypeID",
params=("NULL"),
)
if not self.PackageBase and not self.PackageBaseID:
raise IntegrityError(
statement="Foreign key PackageBaseID cannot be null.",
orig="PackageRequests.PackageBaseID",
params=("NULL"),
)
if not self.PackageBaseName:
raise IntegrityError(
statement="Column PackageBaseName cannot be null.",
orig="PackageRequests.PackageBaseName",
params=("NULL"),
)
if not self.User and not self.UsersID:
raise IntegrityError(
statement="Foreign key UsersID cannot be null.",
orig="PackageRequests.UsersID",
params=("NULL"),
)
if self.Comments is None:
raise IntegrityError(
statement="Column Comments cannot be null.",
orig="PackageRequests.Comments",
params=("NULL"),
)
if self.ClosureComment is None:
raise IntegrityError(
statement="Column ClosureComment cannot be null.",
orig="PackageRequests.ClosureComment",
params=("NULL"),
)
def status_display(self) -> str:
"""Return a display string for the Status column."""
return self.STATUS_DISPLAY[self.Status]
def ml_message_id_hash(self) -> str:
"""Return the X-Message-ID-Hash that is used in the mailing list archive."""
# X-Message-ID-Hash is a base32 encoded SHA1 hash
msgid = f"pkg-request-{str(self.ID)}@aur.archlinux.org"
sha1 = hashlib.sha1(msgid.encode()).digest()
return base64.b32encode(sha1).decode()
def ml_message_url(self) -> str:
"""Return the mailing list URL for the request."""
url = config.get("options", "ml_thread_url") % (self.ml_message_id_hash())
return url

View file

@ -0,0 +1,31 @@
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import backref, relationship
from aurweb import schema
from aurweb.models.declarative import Base
from aurweb.models.package import Package as _Package
class PackageSource(Base):
__table__ = schema.PackageSources
__tablename__ = __table__.name
__mapper_args__ = {"primary_key": [__table__.c.PackageID, __table__.c.Source]}
Package = relationship(
_Package,
backref=backref("package_sources", lazy="dynamic", cascade="all, delete"),
foreign_keys=[__table__.c.PackageID],
)
def __init__(self, **kwargs):
super().__init__(**kwargs)
if not self.Package and not self.PackageID:
raise IntegrityError(
statement="Foreign key PackageID cannot be null.",
orig="PackageSources.PackageID",
params=("NULL"),
)
if not self.Source:
self.Source = "/dev/null"

View file

@ -0,0 +1,49 @@
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import backref, relationship
from aurweb import schema
from aurweb.models.declarative import Base
from aurweb.models.package_base import PackageBase as _PackageBase
from aurweb.models.user import User as _User
class PackageVote(Base):
__table__ = schema.PackageVotes
__tablename__ = __table__.name
__mapper_args__ = {"primary_key": [__table__.c.UsersID, __table__.c.PackageBaseID]}
User = relationship(
_User,
backref=backref("package_votes", lazy="dynamic", cascade="all, delete"),
foreign_keys=[__table__.c.UsersID],
)
PackageBase = relationship(
_PackageBase,
backref=backref("package_votes", lazy="dynamic", cascade="all, delete"),
foreign_keys=[__table__.c.PackageBaseID],
)
def __init__(self, **kwargs):
super().__init__(**kwargs)
if not self.User and not self.UsersID:
raise IntegrityError(
statement="Foreign key UsersID cannot be null.",
orig="PackageVotes.UsersID",
params=("NULL"),
)
if not self.PackageBase and not self.PackageBaseID:
raise IntegrityError(
statement="Foreign key PackageBaseID cannot be null.",
orig="PackageVotes.PackageBaseID",
params=("NULL"),
)
if not self.VoteTS:
raise IntegrityError(
statement="Column VoteTS cannot be null.",
orig="PackageVotes.VoteTS",
params=("NULL"),
)

View file

@ -0,0 +1,19 @@
from aurweb import schema
from aurweb.models.declarative import Base
CONFLICTS = "conflicts"
PROVIDES = "provides"
REPLACES = "replaces"
CONFLICTS_ID = 1
PROVIDES_ID = 2
REPLACES_ID = 3
class RelationType(Base):
__table__ = schema.RelationTypes
__tablename__ = __table__.name
__mapper_args__ = {"primary_key": [__table__.c.ID]}
def __init__(self, Name: str = None):
self.Name = Name

View file

@ -0,0 +1,20 @@
from aurweb import schema
from aurweb.models.declarative import Base
DELETION = "deletion"
ORPHAN = "orphan"
MERGE = "merge"
DELETION_ID = 1
ORPHAN_ID = 2
MERGE_ID = 3
class RequestType(Base):
__table__ = schema.RequestTypes
__tablename__ = __table__.name
__mapper_args__ = {"primary_key": [__table__.c.ID]}
def name_display(self) -> str:
"""Return the Name column with its first char capitalized."""
return self.Name.title()

44
aurweb/models/session.py Normal file
View file

@ -0,0 +1,44 @@
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import backref, relationship
from aurweb import db, schema
from aurweb.models.declarative import Base
from aurweb.models.user import User as _User
class Session(Base):
__table__ = schema.Sessions
__tablename__ = __table__.name
__mapper_args__ = {"primary_key": [__table__.c.UsersID]}
User = relationship(
_User,
backref=backref("session", cascade="all, delete", uselist=False),
foreign_keys=[__table__.c.UsersID],
)
def __init__(self, **kwargs):
super().__init__(**kwargs)
# We'll try to either use UsersID or User.ID if we can.
# If neither exist, an AttributeError is raised, in which case
# we set the uid to 0, which triggers IntegrityError below.
try:
uid = self.UsersID or self.User.ID
except AttributeError:
uid = 0
user_exists = db.query(_User).filter(_User.ID == uid).exists()
if not db.query(user_exists).scalar():
raise IntegrityError(
statement=(
"Foreign key UsersID cannot be null and "
"must be a valid user's ID."
),
orig="Sessions.UsersID",
params=("NULL"),
)
def generate_unique_sid():
return db.make_random_value(Session, Session.SessionID, 32)

View file

@ -0,0 +1,29 @@
from subprocess import PIPE, Popen
from sqlalchemy.orm import backref, relationship
from aurweb import schema
from aurweb.models.declarative import Base
class SSHPubKey(Base):
__table__ = schema.SSHPubKeys
__tablename__ = __table__.name
__mapper_args__ = {"primary_key": [__table__.c.Fingerprint]}
User = relationship(
"User",
backref=backref("ssh_pub_keys", lazy="dynamic", cascade="all, delete"),
foreign_keys=[__table__.c.UserID],
)
def __init__(self, **kwargs):
super().__init__(**kwargs)
def get_fingerprint(pubkey: str) -> str:
proc = Popen(["ssh-keygen", "-l", "-f", "-"], stdin=PIPE, stdout=PIPE, stderr=PIPE)
out, _ = proc.communicate(pubkey.encode())
if proc.returncode:
raise ValueError("The SSH public key is invalid.")
return out.decode().split()[1].split(":", 1)[1]

27
aurweb/models/term.py Normal file
View file

@ -0,0 +1,27 @@
from sqlalchemy.exc import IntegrityError
from aurweb import schema
from aurweb.models.declarative import Base
class Term(Base):
__table__ = schema.Terms
__tablename__ = __table__.name
__mapper_args__ = {"primary_key": [__table__.c.ID]}
def __init__(self, **kwargs):
super().__init__(**kwargs)
if not self.Description:
raise IntegrityError(
statement="Column Description cannot be null.",
orig="Terms.Description",
params=("NULL"),
)
if not self.URL:
raise IntegrityError(
statement="Column URL cannot be null.",
orig="Terms.URL",
params=("NULL"),
)

272
aurweb/models/user.py Normal file
View file

@ -0,0 +1,272 @@
import hashlib
from typing import Set
import bcrypt
from fastapi import Request
from sqlalchemy import or_
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import backref, relationship
import aurweb.config
import aurweb.models.account_type
import aurweb.schema
from aurweb import aur_logging, db, schema, time, util
from aurweb.models.account_type import AccountType as _AccountType
from aurweb.models.ban import is_banned
from aurweb.models.declarative import Base
logger = aur_logging.get_logger(__name__)
SALT_ROUNDS_DEFAULT = 12
class User(Base):
"""An ORM model of a single Users record."""
__table__ = schema.Users
__tablename__ = __table__.name
__mapper_args__ = {"primary_key": [__table__.c.ID]}
AccountType = relationship(
_AccountType,
backref=backref("users", lazy="dynamic"),
foreign_keys=[__table__.c.AccountTypeID],
uselist=False,
)
# High-level variables used to track authentication (not in DB).
authenticated = False
nonce = None
# Make this static to the class just in case SQLAlchemy ever
# does something to bypass our constructor.
salt_rounds = aurweb.config.getint("options", "salt_rounds", SALT_ROUNDS_DEFAULT)
def __init__(self, Passwd: str = str(), **kwargs):
super().__init__(**kwargs, Passwd=str())
# Run this again in the constructor in case we rehashed config.
self.salt_rounds = aurweb.config.getint(
"options", "salt_rounds", SALT_ROUNDS_DEFAULT
)
if Passwd:
self.update_password(Passwd)
def update_password(self, password):
self.Passwd = bcrypt.hashpw(
password.encode(), bcrypt.gensalt(rounds=self.salt_rounds)
).decode()
@staticmethod
def minimum_passwd_length():
return aurweb.config.getint("options", "passwd_min_len")
def is_authenticated(self):
"""Return internal authenticated state."""
return self.authenticated
def valid_password(self, password: str):
"""Check authentication against a given password."""
if password is None:
return False
password_is_valid = False
try:
password_is_valid = bcrypt.checkpw(password.encode(), self.Passwd.encode())
except ValueError:
pass
# If our Salt column is not empty, we're using a legacy password.
if not password_is_valid and self.Salt != str():
# Try to login with legacy method.
password_is_valid = (
hashlib.md5(f"{self.Salt}{password}".encode()).hexdigest()
== self.Passwd
)
# We got here, we passed the legacy authentication.
# Update the password to our modern hash style.
if password_is_valid:
self.update_password(password)
return password_is_valid
def _login_approved(self, request: Request):
return not is_banned(request) and not self.Suspended
def login(self, request: Request, password: str) -> str:
"""Login and authenticate a request."""
from aurweb import db
from aurweb.models.session import Session, generate_unique_sid
if not self._login_approved(request):
return None
self.authenticated = self.valid_password(password)
if not self.authenticated:
return None
# Maximum number of iterations where we attempt to generate
# a unique SID. In cases where the Session table has
# exhausted all possible values, this will catch exceptions
# instead of raising them and include details about failing
# generation in an HTTPException.
tries = 36
exc = None
for i in range(tries):
exc = None
now_ts = time.utcnow()
try:
with db.begin():
self.LastLogin = now_ts
self.LastLoginIPAddress = util.get_client_ip(request)
if not self.session:
sid = generate_unique_sid()
self.session = db.create(
Session, User=self, SessionID=sid, LastUpdateTS=now_ts
)
else:
last_updated = self.session.LastUpdateTS
if last_updated and last_updated < now_ts:
self.session.SessionID = generate_unique_sid()
self.session.LastUpdateTS = now_ts
# Unset InactivityTS, we've logged in!
self.InactivityTS = 0
break
except IntegrityError as exc_:
exc = exc_
if exc:
raise exc
return self.session.SessionID
def has_credential(self, credential: Set[int], approved: list["User"] = list()):
from aurweb.auth.creds import has_credential
return has_credential(self, credential, approved)
def logout(self, request: Request) -> None:
self.authenticated = False
if self.session:
with db.begin():
db.delete(self.session)
def is_package_maintainer(self):
return self.AccountType.ID in {
aurweb.models.account_type.PACKAGE_MAINTAINER_ID,
aurweb.models.account_type.PACKAGE_MAINTAINER_AND_DEV_ID,
}
def is_developer(self):
return self.AccountType.ID in {
aurweb.models.account_type.DEVELOPER_ID,
aurweb.models.account_type.PACKAGE_MAINTAINER_AND_DEV_ID,
}
def is_elevated(self):
"""A User is 'elevated' when they have either a
Package Maintainer or Developer AccountType."""
return self.AccountType.ID in {
aurweb.models.account_type.PACKAGE_MAINTAINER_ID,
aurweb.models.account_type.DEVELOPER_ID,
aurweb.models.account_type.PACKAGE_MAINTAINER_AND_DEV_ID,
}
def can_edit_user(self, target: "User") -> bool:
"""
Whether this User instance can edit `target`.
This User can edit user `target` if we both: have credentials and
self.AccountTypeID is greater or equal to `target`.AccountTypeID.
In short, a user must at least have credentials and be at least
the same account type as the target.
User < Package Maintainer < Developer < Package Maintainer & Developer
:param target: Target User to be edited
:return: Boolean indicating whether `self` can edit `target`
"""
from aurweb.auth import creds
has_cred = self.has_credential(creds.ACCOUNT_EDIT, approved=[target])
return has_cred and self.AccountTypeID >= target.AccountTypeID
def voted_for(self, package) -> bool:
"""Has this User voted for package?"""
from aurweb.models.package_vote import PackageVote
return bool(
package.PackageBase.package_votes.filter(
PackageVote.UsersID == self.ID
).scalar()
)
def notified(self, package) -> bool:
"""Is this User being notified about package (or package base)?
:param package: Package or PackageBase instance
:return: Boolean indicating state of package notification
in relation to this User
"""
from aurweb.models.package import Package
from aurweb.models.package_base import PackageBase
from aurweb.models.package_notification import PackageNotification
query = None
if isinstance(package, Package):
query = package.PackageBase.notifications
elif isinstance(package, PackageBase):
query = package.notifications
# Run an exists() query where a pkgbase-related
# PackageNotification exists for self (a user).
return bool(
db.query(
query.filter(PackageNotification.UserID == self.ID).exists()
).scalar()
)
def packages(self):
"""Returns an ORM query to Package objects owned by this user.
This should really be replaced with an internal ORM join
configured for the User model. This has not been done yet
due to issues I've been encountering in the process, so
sticking with this function until we can properly implement it.
:return: ORM query of User-packaged or maintained Package objects
"""
from aurweb.models.package import Package
from aurweb.models.package_base import PackageBase
return (
db.query(Package)
.join(PackageBase)
.filter(
or_(
PackageBase.PackagerUID == self.ID,
PackageBase.MaintainerUID == self.ID,
)
)
)
def __repr__(self):
return "<User(ID='%s', AccountType='%s', Username='%s')>" % (
self.ID,
str(self.AccountType),
self.Username,
)
def __str__(self) -> str:
return self.Username
def generate_resetkey():
return util.make_random_string(32)

42
aurweb/models/vote.py Normal file
View file

@ -0,0 +1,42 @@
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import backref, relationship
from aurweb import schema
from aurweb.models.declarative import Base
from aurweb.models.user import User as _User
from aurweb.models.voteinfo import VoteInfo as _VoteInfo
class Vote(Base):
__table__ = schema.Votes
__tablename__ = __table__.name
__mapper_args__ = {"primary_key": [__table__.c.VoteID, __table__.c.UserID]}
VoteInfo = relationship(
_VoteInfo,
backref=backref("votes", lazy="dynamic"),
foreign_keys=[__table__.c.VoteID],
)
User = relationship(
_User,
backref=backref("votes", lazy="dynamic"),
foreign_keys=[__table__.c.UserID],
)
def __init__(self, **kwargs):
super().__init__(**kwargs)
if not self.VoteInfo and not self.VoteID:
raise IntegrityError(
statement="Foreign key VoteID cannot be null.",
orig="Votes.VoteID",
params=("NULL"),
)
if not self.User and not self.UserID:
raise IntegrityError(
statement="Foreign key UserID cannot be null.",
orig="Votes.UserID",
params=("NULL"),
)

82
aurweb/models/voteinfo.py Normal file
View file

@ -0,0 +1,82 @@
import typing
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import backref, relationship
from aurweb import schema, time
from aurweb.models.declarative import Base
from aurweb.models.user import User as _User
class VoteInfo(Base):
__table__ = schema.VoteInfo
__tablename__ = __table__.name
__mapper_args__ = {"primary_key": [__table__.c.ID]}
Submitter = relationship(
_User,
backref=backref("voteinfo_set", lazy="dynamic"),
foreign_keys=[__table__.c.SubmitterID],
)
def __init__(self, **kwargs):
# Default Quorum, Yes, No and Abstain columns to 0.
for col in ("Quorum", "Yes", "No", "Abstain"):
if col not in kwargs:
kwargs.update({col: 0})
super().__init__(**kwargs)
if self.Agenda is None:
raise IntegrityError(
statement="Column Agenda cannot be null.",
orig="VoteInfo.Agenda",
params=("NULL"),
)
if self.User is None:
raise IntegrityError(
statement="Column User cannot be null.",
orig="VoteInfo.User",
params=("NULL"),
)
if self.Submitted is None:
raise IntegrityError(
statement="Column Submitted cannot be null.",
orig="VoteInfo.Submitted",
params=("NULL"),
)
if self.End is None:
raise IntegrityError(
statement="Column End cannot be null.",
orig="VoteInfo.End",
params=("NULL"),
)
if not self.Submitter:
raise IntegrityError(
statement="Foreign key SubmitterID cannot be null.",
orig="VoteInfo.SubmitterID",
params=("NULL"),
)
def __setattr__(self, key: str, value: typing.Any):
"""Customize setattr to stringify any Quorum keys given."""
if key == "Quorum":
value = str(value)
return super().__setattr__(key, value)
def __getattribute__(self, key: str):
"""Customize getattr to floatify any fetched Quorum values."""
attr = super().__getattribute__(key)
if key == "Quorum":
return float(attr)
return attr
def is_running(self):
return self.End > time.utcnow()
def total_votes(self):
return self.Yes + self.No + self.Abstain

View file

269
aurweb/packages/requests.py Normal file
View file

@ -0,0 +1,269 @@
from typing import Optional, Set
from fastapi import Request
from sqlalchemy import and_, orm
from aurweb import config, db, l10n, time, util
from aurweb.exceptions import InvariantError
from aurweb.models import PackageBase, PackageRequest, User
from aurweb.models.package_request import ACCEPTED_ID, PENDING_ID, REJECTED_ID
from aurweb.models.request_type import (
DELETION,
DELETION_ID,
MERGE,
MERGE_ID,
ORPHAN,
ORPHAN_ID,
)
from aurweb.scripts import notify
class ClosureFactory:
"""A factory class used to autogenerate closure comments."""
REQTYPE_NAMES = {DELETION_ID: DELETION, MERGE_ID: MERGE, ORPHAN_ID: ORPHAN}
def _deletion_closure(
self, requester: User, pkgbase: PackageBase, target: PackageBase = None
):
return f"[Autogenerated] Accepted deletion for {pkgbase.Name}."
def _merge_closure(
self, requester: User, pkgbase: PackageBase, target: PackageBase = None
):
return (
f"[Autogenerated] Accepted merge for {pkgbase.Name} " f"into {target.Name}."
)
def _orphan_closure(
self, requester: User, pkgbase: PackageBase, target: PackageBase = None
):
return f"[Autogenerated] Accepted orphan for {pkgbase.Name}."
def _rejected_merge_closure(
self, requester: User, pkgbase: PackageBase, target: PackageBase = None
):
return (
f"[Autogenerated] Another request to merge {pkgbase.Name} "
f"into {target.Name} has rendered this request invalid."
)
def get_closure(
self,
reqtype_id: int,
requester: User,
pkgbase: PackageBase,
target: PackageBase = None,
status: int = ACCEPTED_ID,
) -> str:
"""
Return a closure comment handled by this class.
:param reqtype_id: RequestType.ID
:param requester: User who is closing a request
:param pkgbase: PackageBase instance related to the request
:param target: Merge request target PackageBase instance
:param status: PackageRequest.Status
"""
reqtype = ClosureFactory.REQTYPE_NAMES.get(reqtype_id)
partial = str()
if status == REJECTED_ID:
partial = "_rejected"
try:
handler = getattr(self, f"{partial}_{reqtype}_closure")
except AttributeError:
raise NotImplementedError("Unsupported 'reqtype_id' value.")
return handler(requester, pkgbase, target)
def update_closure_comment(
pkgbase: PackageBase, reqtype_id: int, comments: str, target: PackageBase = None
) -> None:
"""
Update all pending requests related to `pkgbase` with a closure comment.
In order to persist closure comments through `handle_request`'s
algorithm, we must set `PackageRequest.ClosureComment` before calling
it. This function can be used to update the closure comment of all
package requests related to `pkgbase` and `reqtype_id`.
If an empty `comments` string is provided, we no-op out of this.
:param pkgbase: PackageBase instance
:param reqtype_id: RequestType.ID
:param comments: PackageRequest.ClosureComment to update to
:param target: Merge request target PackageBase instance
"""
if not comments:
return
query = pkgbase.requests.filter(
and_(
PackageRequest.ReqTypeID == reqtype_id, PackageRequest.Status == PENDING_ID
)
)
if reqtype_id == MERGE_ID:
query = query.filter(PackageRequest.MergeBaseName == target.Name)
for pkgreq in query:
pkgreq.ClosureComment = comments
def verify_orphan_request(user: User, pkgbase: PackageBase):
"""Verify that an undue orphan request exists in `requests`."""
requests = pkgbase.requests.filter(PackageRequest.ReqTypeID == ORPHAN_ID)
for pkgreq in requests:
idle_time = config.getint("options", "request_idle_time")
time_delta = time.utcnow() - pkgreq.RequestTS
is_due = pkgreq.Status == PENDING_ID and time_delta > idle_time
if is_due:
# If the requester is the pkgbase maintainer or the
# request is already due, we're good to go: return True.
return True
return False
def close_pkgreq(
pkgreq: PackageRequest,
closer: User,
pkgbase: PackageBase,
target: Optional[PackageBase],
status: int,
) -> None:
"""
Close a package request with `pkgreq`.Status == `status`.
:param pkgreq: PackageRequest instance
:param closer: `pkgreq`.Closer User instance to update to
:param pkgbase: PackageBase instance which `pkgreq` is about
:param target: Optional PackageBase instance to merge into
:param status: `pkgreq`.Status value to update to
"""
now = time.utcnow()
pkgreq.Status = status
pkgreq.Closer = closer
pkgreq.ClosureComment = pkgreq.ClosureComment or ClosureFactory().get_closure(
pkgreq.ReqTypeID, closer, pkgbase, target, status
)
pkgreq.ClosedTS = now
@db.retry_deadlock
def handle_request(
request: Request,
reqtype_id: int,
pkgbase: PackageBase,
target: PackageBase = None,
comments: str = str(),
) -> list[notify.Notification]:
"""
Handle package requests before performing an action.
The actions we're interested in are disown (orphan), delete and
merge. There is now an automated request generation and closure
notification when a privileged user performs one of these actions
without a pre-existing request. They all commit changes to the
database, and thus before calling, state should be verified to
avoid leaked database records regarding these requests.
Otherwise, we accept and reject requests based on their state
and send out the relevent notifications.
:param requester: User who needs this a `pkgbase` request handled
:param reqtype_id: RequestType.ID
:param pkgbase: PackageBase which the request is about
:param target: Optional target to merge into
"""
notifs: list[notify.Notification] = []
# If it's an orphan request, perform further verification
# regarding existing requests.
if reqtype_id == ORPHAN_ID:
if not verify_orphan_request(request.user, pkgbase):
_ = l10n.get_translator_for_request(request)
raise InvariantError(
_("No due existing orphan requests to accept for %s.") % pkgbase.Name
)
# Produce a base query for requests related to `pkgbase`, based
# on ReqTypeID matching `reqtype_id`, pending status and a correct
# PackagBaseName column.
query: orm.Query = pkgbase.requests.filter(
and_(
PackageRequest.ReqTypeID == reqtype_id,
PackageRequest.Status == PENDING_ID,
PackageRequest.PackageBaseName == pkgbase.Name,
)
)
# Build a query for records we should accept. For merge requests,
# this is specific to a matching MergeBaseName. For others, this
# just ends up becoming `query`.
accept_query: orm.Query = query
if target:
# If a `target` was supplied, filter by MergeBaseName
accept_query = query.filter(PackageRequest.MergeBaseName == target.Name)
# Build an accept list out of `accept_query`.
to_accept: list[PackageRequest] = accept_query.all()
accepted_ids: Set[int] = set(p.ID for p in to_accept)
# Build a reject list out of `query` filtered by IDs not found
# in `to_accept`. That is, unmatched records of the same base
# query properties.
to_reject: list[PackageRequest] = query.filter(
~PackageRequest.ID.in_(accepted_ids)
).all()
# If we have no requests to accept, create a new one.
# This is done to increase tracking of actions occurring
# through the website.
if not to_accept:
utcnow = time.utcnow()
with db.begin():
pkgreq = db.create(
PackageRequest,
ReqTypeID=reqtype_id,
RequestTS=utcnow,
User=request.user,
PackageBase=pkgbase,
PackageBaseName=pkgbase.Name,
Comments="Autogenerated by aurweb.",
ClosureComment=comments,
)
# If it's a merge request, set MergeBaseName to `target`.Name.
if pkgreq.ReqTypeID == MERGE_ID:
pkgreq.MergeBaseName = target.Name
# Add the new request to `to_accept` and allow standard
# flow to continue afterward.
to_accept.append(pkgreq)
# Update requests with their new status and closures.
@db.retry_deadlock
def retry_closures():
with db.begin():
util.apply_all(
to_accept,
lambda p: close_pkgreq(p, request.user, pkgbase, target, ACCEPTED_ID),
)
util.apply_all(
to_reject,
lambda p: close_pkgreq(p, request.user, pkgbase, target, REJECTED_ID),
)
retry_closures()
# Create RequestCloseNotifications for all requests involved.
for pkgreq in to_accept + to_reject:
notif = notify.RequestCloseNotification(
request.user.ID, pkgreq.ID, pkgreq.status_display()
)
notifs.append(notif)
# Return notifications to the caller for sending.
return notifs

403
aurweb/packages/search.py Normal file
View file

@ -0,0 +1,403 @@
from typing import Set
from sqlalchemy import and_, case, or_, orm
from aurweb import db, models
from aurweb.models import Group, Package, PackageBase, User
from aurweb.models.dependency_type import (
CHECKDEPENDS_ID,
DEPENDS_ID,
MAKEDEPENDS_ID,
OPTDEPENDS_ID,
)
from aurweb.models.package_comaintainer import PackageComaintainer
from aurweb.models.package_group import PackageGroup
from aurweb.models.package_keyword import PackageKeyword
from aurweb.models.package_notification import PackageNotification
from aurweb.models.package_vote import PackageVote
from aurweb.models.relation_type import CONFLICTS_ID, PROVIDES_ID, REPLACES_ID
class PackageSearch:
"""A Package search query builder."""
# A constant mapping of short to full name sort orderings.
FULL_SORT_ORDER = {"d": "desc", "a": "asc"}
def __init__(self, user: models.User = None):
self.query = db.query(Package).join(PackageBase)
self.user = user
if self.user:
self.query = self.query.join(
PackageVote,
and_(
PackageVote.PackageBaseID == PackageBase.ID,
PackageVote.UsersID == self.user.ID,
),
isouter=True,
).join(
PackageNotification,
and_(
PackageNotification.PackageBaseID == PackageBase.ID,
PackageNotification.UserID == self.user.ID,
),
isouter=True,
)
self.ordering = "d"
# Setup SeB (Search By) callbacks.
self.search_by_cb = {
"nd": self._search_by_namedesc,
"n": self._search_by_name,
"b": self._search_by_pkgbase,
"N": self._search_by_exact_name,
"B": self._search_by_exact_pkgbase,
"k": self._search_by_keywords,
"m": self._search_by_maintainer,
"c": self._search_by_comaintainer,
"M": self._search_by_co_or_maintainer,
"s": self._search_by_submitter,
}
# Setup SB (Sort By) callbacks.
self.sort_by_cb = {
"n": self._sort_by_name,
"v": self._sort_by_votes,
"p": self._sort_by_popularity,
"w": self._sort_by_voted,
"o": self._sort_by_notify,
"m": self._sort_by_maintainer,
"l": self._sort_by_last_modified,
}
self._joined_user = False
self._joined_keywords = False
self._joined_comaint = False
def _join_user(self, outer: bool = True) -> orm.Query:
"""Centralized joining of a package base's maintainer."""
if not self._joined_user:
self.query = self.query.join(
User, User.ID == PackageBase.MaintainerUID, isouter=outer
)
self._joined_user = True
return self.query
def _join_keywords(self) -> orm.Query:
if not self._joined_keywords:
self.query = self.query.join(PackageKeyword)
self._joined_keywords = True
return self.query
def _join_comaint(self, isouter: bool = False) -> orm.Query:
if not self._joined_comaint:
self.query = self.query.join(
PackageComaintainer,
PackageComaintainer.PackageBaseID == PackageBase.ID,
isouter=isouter,
)
self._joined_comaint = True
return self.query
def _search_by_namedesc(self, keywords: str) -> orm.Query:
self._join_user()
self.query = self.query.filter(
or_(
Package.Name.like(f"%{keywords}%"),
Package.Description.like(f"%{keywords}%"),
)
)
return self
def _search_by_name(self, keywords: str) -> orm.Query:
self._join_user()
self.query = self.query.filter(Package.Name.like(f"%{keywords}%"))
return self
def _search_by_exact_name(self, keywords: str) -> orm.Query:
self._join_user()
self.query = self.query.filter(Package.Name == keywords)
return self
def _search_by_pkgbase(self, keywords: str) -> orm.Query:
self._join_user()
self.query = self.query.filter(PackageBase.Name.like(f"%{keywords}%"))
return self
def _search_by_exact_pkgbase(self, keywords: str) -> orm.Query:
self._join_user()
self.query = self.query.filter(PackageBase.Name == keywords)
return self
def _search_by_keywords(self, keywords: Set[str]) -> orm.Query:
self._join_user()
self._join_keywords()
keywords = set(k.lower() for k in keywords)
self.query = self.query.filter(PackageKeyword.Keyword.in_(keywords)).group_by(
models.Package.Name
)
return self
def _search_by_maintainer(self, keywords: str) -> orm.Query:
self._join_user()
if keywords:
self.query = self.query.filter(
and_(User.Username == keywords, User.ID == PackageBase.MaintainerUID)
)
else:
self.query = self.query.filter(PackageBase.MaintainerUID.is_(None))
return self
def _search_by_comaintainer(self, keywords: str) -> orm.Query:
self._join_user()
self._join_comaint()
user = db.query(User).filter(User.Username == keywords).first()
uid = 0 if not user else user.ID
self.query = self.query.filter(PackageComaintainer.UsersID == uid)
return self
def _search_by_co_or_maintainer(self, keywords: str) -> orm.Query:
self._join_user()
self._join_comaint(True)
user = db.query(User).filter(User.Username == keywords).first()
uid = 0 if not user else user.ID
self.query = self.query.filter(
or_(PackageComaintainer.UsersID == uid, User.ID == uid)
)
return self
def _search_by_submitter(self, keywords: str) -> orm.Query:
self._join_user()
uid = 0
user = db.query(User).filter(User.Username == keywords).first()
if user:
uid = user.ID
self.query = self.query.filter(PackageBase.SubmitterUID == uid)
return self
def search_by(self, search_by: str, keywords: str) -> orm.Query:
if search_by not in self.search_by_cb:
search_by = "nd" # Default: Name, Description
callback = self.search_by_cb.get(search_by)
result = callback(keywords)
return result
def _sort_by_name(self, order: str):
column = getattr(models.Package.Name, order)
self.query = self.query.order_by(column())
return self
def _sort_by_votes(self, order: str):
column = getattr(models.PackageBase.NumVotes, order)
name = getattr(models.PackageBase.Name, order)
self.query = self.query.order_by(column(), name())
return self
def _sort_by_popularity(self, order: str):
column = getattr(models.PackageBase.Popularity, order)
name = getattr(models.PackageBase.Name, order)
self.query = self.query.order_by(column(), name())
return self
def _sort_by_voted(self, order: str):
# FIXME: Currently, PHP is destroying this implementation
# in terms of performance. We should improve this; there's no
# reason it should take _longer_.
column = getattr(
case([(models.PackageVote.UsersID == self.user.ID, 1)], else_=0), order
)
name = getattr(models.Package.Name, order)
self.query = self.query.order_by(column(), name())
return self
def _sort_by_notify(self, order: str):
# FIXME: Currently, PHP is destroying this implementation
# in terms of performance. We should improve this; there's no
# reason it should take _longer_.
column = getattr(
case([(models.PackageNotification.UserID == self.user.ID, 1)], else_=0),
order,
)
name = getattr(models.Package.Name, order)
self.query = self.query.order_by(column(), name())
return self
def _sort_by_maintainer(self, order: str):
column = getattr(models.User.Username, order)
name = getattr(models.Package.Name, order)
self.query = self.query.order_by(column(), name())
return self
def _sort_by_last_modified(self, order: str):
column = getattr(models.PackageBase.ModifiedTS, order)
name = getattr(models.PackageBase.Name, order)
self.query = self.query.order_by(column(), name())
return self
def sort_by(self, sort_by: str, ordering: str = "d") -> orm.Query:
if sort_by not in self.sort_by_cb:
sort_by = "p" # Default: Popularity
callback = self.sort_by_cb.get(sort_by)
if ordering not in self.FULL_SORT_ORDER:
ordering = "d" # Default: Descending
ordering = self.FULL_SORT_ORDER.get(ordering)
return callback(ordering)
def count(self) -> int:
"""Return internal query's count."""
return self.query.count()
def results(self) -> orm.Query:
"""Return internal query."""
return self.query
class RPCSearch(PackageSearch):
"""A PackageSearch-derived RPC package search query builder.
With RPC search, we need a subset of PackageSearch's handlers,
with a few additional handlers added. So, within the RPCSearch
constructor, we pop unneeded keys out of inherited self.search_by_cb
and add a few more keys to it, namely: depends, makedepends,
optdepends and checkdepends.
Additionally, some logic within the inherited PackageSearch.search_by
method is not needed, so it is overridden in this class without
sanitization done for the PackageSearch `by` argument.
"""
keys_removed = ("b", "N", "B", "M")
def __init__(self) -> "RPCSearch":
super().__init__()
# Fix-up inherited search_by_cb to reflect RPC-specific by params.
# We keep: "nd", "n" and "m". We also overlay four new by params
# on top: "depends", "makedepends", "optdepends" and "checkdepends".
self.search_by_cb = {
k: v
for k, v in self.search_by_cb.items()
if k not in RPCSearch.keys_removed
}
self.search_by_cb.update(
{
"depends": self._search_by_depends,
"makedepends": self._search_by_makedepends,
"optdepends": self._search_by_optdepends,
"checkdepends": self._search_by_checkdepends,
"provides": self._search_by_provides,
"conflicts": self._search_by_conflicts,
"replaces": self._search_by_replaces,
"groups": self._search_by_groups,
}
)
# We always want an optional Maintainer in the RPC.
self._join_user()
def _join_depends(self, dep_type_id: int) -> orm.Query:
"""Join Package with PackageDependency and filter results
based on `dep_type_id`.
:param dep_type_id: DependencyType ID
:returns: PackageDependency-joined orm.Query
"""
self.query = self.query.join(models.PackageDependency).filter(
models.PackageDependency.DepTypeID == dep_type_id
)
return self.query
def _join_relations(self, rel_type_id: int) -> orm.Query:
"""Join Package with PackageRelation and filter results
based on `rel_type_id`.
:param rel_type_id: RelationType ID
:returns: PackageRelation-joined orm.Query
"""
self.query = self.query.join(models.PackageRelation).filter(
models.PackageRelation.RelTypeID == rel_type_id
)
return self.query
def _join_groups(self) -> orm.Query:
"""Join Package with PackageGroup and Group.
:returns: PackageGroup/Group-joined orm.Query
"""
self.query = self.query.join(PackageGroup).join(Group)
return self.query
def _search_by_depends(self, keywords: str) -> "RPCSearch":
self.query = self._join_depends(DEPENDS_ID).filter(
models.PackageDependency.DepName == keywords
)
return self
def _search_by_makedepends(self, keywords: str) -> "RPCSearch":
self.query = self._join_depends(MAKEDEPENDS_ID).filter(
models.PackageDependency.DepName == keywords
)
return self
def _search_by_optdepends(self, keywords: str) -> "RPCSearch":
self.query = self._join_depends(OPTDEPENDS_ID).filter(
models.PackageDependency.DepName == keywords
)
return self
def _search_by_checkdepends(self, keywords: str) -> "RPCSearch":
self.query = self._join_depends(CHECKDEPENDS_ID).filter(
models.PackageDependency.DepName == keywords
)
return self
def _search_by_provides(self, keywords: str) -> "RPCSearch":
self.query = self._join_relations(PROVIDES_ID).filter(
models.PackageRelation.RelName == keywords
)
return self
def _search_by_conflicts(self, keywords: str) -> "RPCSearch":
self.query = self._join_relations(CONFLICTS_ID).filter(
models.PackageRelation.RelName == keywords
)
return self
def _search_by_replaces(self, keywords: str) -> "RPCSearch":
self.query = self._join_relations(REPLACES_ID).filter(
models.PackageRelation.RelName == keywords
)
return self
def _search_by_groups(self, keywords: str) -> "RPCSearch":
self._join_groups()
self.query = self.query.filter(Group.Name == keywords)
return self
def _search_by_keywords(self, keywords: str) -> "RPCSearch":
self._join_keywords()
self.query = self.query.filter(PackageKeyword.Keyword == keywords)
return self
def search_by(self, by: str, keywords: str) -> "RPCSearch":
"""Override inherited search_by. In this override, we reduce the
scope of what we handle within this function. We do not set `by`
to a default of "nd" in the RPC, as the RPC returns an error when
incorrect `by` fields are specified.
:param by: RPC `by` argument
:param keywords: RPC `arg` argument
:returns: self
"""
callback = self.search_by_cb.get(by)
result = callback(keywords)
return result
def results(self) -> orm.Query:
return self.query

253
aurweb/packages/util.py Normal file
View file

@ -0,0 +1,253 @@
from collections import defaultdict
from http import HTTPStatus
from typing import Tuple, Union
from urllib.parse import quote_plus
import orjson
from fastapi import HTTPException
from sqlalchemy import orm
from aurweb import config, db, models
from aurweb.aur_redis import redis_connection
from aurweb.models import Package
from aurweb.models.official_provider import OFFICIAL_BASE, OfficialProvider
from aurweb.models.package_dependency import PackageDependency
from aurweb.models.package_relation import PackageRelation
from aurweb.templates import register_filter
Providers = list[Union[PackageRelation, OfficialProvider]]
def dep_extra_with_arch(dep: models.PackageDependency, annotation: str) -> str:
output = [annotation]
if dep.DepArch:
output.append(dep.DepArch)
return f"({', '.join(output)})"
def dep_depends_extra(dep: models.PackageDependency) -> str:
return str()
def dep_makedepends_extra(dep: models.PackageDependency) -> str:
return dep_extra_with_arch(dep, "make")
def dep_checkdepends_extra(dep: models.PackageDependency) -> str:
return dep_extra_with_arch(dep, "check")
def dep_optdepends_extra(dep: models.PackageDependency) -> str:
return dep_extra_with_arch(dep, "optional")
@register_filter("dep_extra")
def dep_extra(dep: models.PackageDependency) -> str:
"""Some dependency types have extra text added to their
display. This function provides that output. However, it
**assumes** that the dep passed is bound to a valid one
of: depends, makedepends, checkdepends or optdepends."""
f = globals().get(f"dep_{dep.DependencyType.Name}_extra")
return f(dep)
@register_filter("dep_extra_desc")
def dep_extra_desc(dep: models.PackageDependency) -> str:
extra = dep_extra(dep)
if not dep.DepDesc:
return extra
return extra + f" {dep.DepDesc}"
@register_filter("pkgname_link")
def pkgname_link(pkgname: str) -> str:
record = db.query(Package).filter(Package.Name == pkgname).exists()
if db.query(record).scalar():
return f"/packages/{pkgname}"
official = (
db.query(OfficialProvider).filter(OfficialProvider.Name == pkgname).exists()
)
if db.query(official).scalar():
base = "/".join([OFFICIAL_BASE, "packages"])
return f"{base}/?q={pkgname}"
@register_filter("package_link")
def package_link(package: Union[Package, OfficialProvider]) -> str:
if package.is_official:
base = "/".join([OFFICIAL_BASE, "packages"])
return f"{base}/?q={package.Name}"
return f"/packages/{package.Name}"
@register_filter("provides_markup")
def provides_markup(provides: Providers) -> str:
links = []
for pkg in provides:
aur = "<sup><small>AUR</small></sup>" if not pkg.is_official else ""
links.append(f'<a href="{package_link(pkg)}">{pkg.Name}</a>{aur}')
return ", ".join(links)
def get_pkg_or_base(
name: str, cls: Union[models.Package, models.PackageBase] = models.PackageBase
) -> Union[models.Package, models.PackageBase]:
"""Get a PackageBase instance by its name or raise a 404 if
it can't be found in the database.
:param name: {Package,PackageBase}.Name
:param exception: Whether to raise an HTTPException or simply return None if
the package can't be found.
:raises HTTPException: With status code 404 if record doesn't exist
:return: {Package,PackageBase} instance
"""
instance = db.query(cls).filter(cls.Name == name).first()
if not instance:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND)
return instance
def get_pkgbase_comment(pkgbase: models.PackageBase, id: int) -> models.PackageComment:
comment = pkgbase.comments.filter(models.PackageComment.ID == id).first()
if not comment:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND)
return db.refresh(comment)
@register_filter("out_of_date")
def out_of_date(packages: orm.Query) -> orm.Query:
return packages.filter(models.PackageBase.OutOfDateTS.isnot(None))
def updated_packages(limit: int = 0, cache_ttl: int = 600) -> list[models.Package]:
"""Return a list of valid Package objects ordered by their
ModifiedTS column in descending order from cache, after setting
the cache when no key yet exists.
:param limit: Optional record limit
:param cache_ttl: Cache expiration time (in seconds)
:return: A list of Packages
"""
redis = redis_connection()
packages = redis.get("package_updates")
if packages:
# If we already have a cache, deserialize it and return.
return orjson.loads(packages)
query = (
db.query(models.Package)
.join(models.PackageBase)
.order_by(models.PackageBase.ModifiedTS.desc())
)
if limit:
query = query.limit(limit)
packages = []
for pkg in query:
# For each Package returned by the query, append a dict
# containing Package columns we're interested in.
packages.append(
{
"Name": pkg.Name,
"Version": pkg.Version,
"PackageBase": {"ModifiedTS": pkg.PackageBase.ModifiedTS},
}
)
# Store the JSON serialization of the package_updates key into Redis.
redis.set("package_updates", orjson.dumps(packages))
redis.expire("package_updates", cache_ttl)
# Return the deserialized list of packages.
return packages
def query_voted(query: list[models.Package], user: models.User) -> dict[int, bool]:
"""Produce a dictionary of package base ID keys to boolean values,
which indicate whether or not the package base has a vote record
related to user.
:param query: A collection of Package models
:param user: The user that is being notified or not
:return: Vote state dict (PackageBase.ID: int -> bool)
"""
output = defaultdict(bool)
query_set = {pkg.PackageBaseID for pkg in query}
voted = (
db.query(models.PackageVote)
.join(models.PackageBase, models.PackageBase.ID.in_(query_set))
.filter(models.PackageVote.UsersID == user.ID)
)
for vote in voted:
output[vote.PackageBase.ID] = True
return output
def query_notified(query: list[models.Package], user: models.User) -> dict[int, bool]:
"""Produce a dictionary of package base ID keys to boolean values,
which indicate whether or not the package base has a notification
record related to user.
:param query: A collection of Package models
:param user: The user that is being notified or not
:return: Notification state dict (PackageBase.ID: int -> bool)
"""
output = defaultdict(bool)
query_set = {pkg.PackageBaseID for pkg in query}
notified = (
db.query(models.PackageNotification)
.join(models.PackageBase, models.PackageBase.ID.in_(query_set))
.filter(models.PackageNotification.UserID == user.ID)
)
for notif in notified:
output[notif.PackageBase.ID] = True
return output
def pkg_required(pkgname: str, provides: list[str]) -> list[PackageDependency]:
"""
Get dependencies that match a string in `[pkgname] + provides`.
:param pkgname: Package.Name
:param provides: List of PackageRelation.Name
:param limit: Maximum number of dependencies to query
:return: List of PackageDependency instances
"""
targets = set([pkgname] + provides)
query = (
db.query(PackageDependency)
.join(Package)
.options(orm.contains_eager(PackageDependency.Package))
.filter(PackageDependency.DepName.in_(targets))
.order_by(Package.Name.asc())
)
return query
@register_filter("source_uri")
def source_uri(pkgsrc: models.PackageSource) -> Tuple[str, str]:
"""
Produce a (text, uri) tuple out of `pkgsrc`.
In this filter, we cover various cases:
1. If "::" is anywhere in the Source column, split the string,
which should produce a (text, uri), where text is before "::"
and uri is after "::".
2. Otherwise, if "://" is anywhere in the Source column, it's just
some sort of URI, which we'll return varbatim as both text and uri.
3. Otherwise, we'll return a path to the source file in a uri produced
out of options.source_file_uri formatted with the source file and
the package base name.
:param pkgsrc: PackageSource instance
:return text, uri)tuple
"""
if "::" in pkgsrc.Source:
return pkgsrc.Source.split("::", 1)
elif "://" in pkgsrc.Source:
return pkgsrc.Source, pkgsrc.Source
path = config.get("options", "source_file_uri")
pkgbasename = quote_plus(pkgsrc.Package.PackageBase.Name)
return pkgsrc.Source, path % (pkgsrc.Source, pkgbasename)

View file

195
aurweb/pkgbase/actions.py Normal file
View file

@ -0,0 +1,195 @@
from fastapi import Request
from aurweb import aur_logging, db, util
from aurweb.auth import creds
from aurweb.models import PackageBase, User
from aurweb.models.package_comaintainer import PackageComaintainer
from aurweb.models.package_notification import PackageNotification
from aurweb.models.request_type import DELETION_ID, MERGE_ID, ORPHAN_ID
from aurweb.packages.requests import handle_request, update_closure_comment
from aurweb.pkgbase import util as pkgbaseutil
from aurweb.scripts import notify, popupdate
logger = aur_logging.get_logger(__name__)
@db.retry_deadlock
def _retry_notify(user: User, pkgbase: PackageBase) -> None:
with db.begin():
db.create(PackageNotification, PackageBase=pkgbase, User=user)
def pkgbase_notify_instance(request: Request, pkgbase: PackageBase) -> None:
notif = db.query(
pkgbase.notifications.filter(
PackageNotification.UserID == request.user.ID
).exists()
).scalar()
has_cred = request.user.has_credential(creds.PKGBASE_NOTIFY)
if has_cred and not notif:
_retry_notify(request.user, pkgbase)
@db.retry_deadlock
def _retry_unnotify(notif: PackageNotification, pkgbase: PackageBase) -> None:
with db.begin():
db.delete(notif)
def pkgbase_unnotify_instance(request: Request, pkgbase: PackageBase) -> None:
notif = pkgbase.notifications.filter(
PackageNotification.UserID == request.user.ID
).first()
has_cred = request.user.has_credential(creds.PKGBASE_NOTIFY)
if has_cred and notif:
_retry_unnotify(notif, pkgbase)
@db.retry_deadlock
def _retry_unflag(pkgbase: PackageBase) -> None:
with db.begin():
pkgbase.OutOfDateTS = None
pkgbase.Flagger = None
pkgbase.FlaggerComment = str()
def pkgbase_unflag_instance(request: Request, pkgbase: PackageBase) -> None:
has_cred = request.user.has_credential(
creds.PKGBASE_UNFLAG,
approved=[pkgbase.Flagger, pkgbase.Maintainer]
+ [c.User for c in pkgbase.comaintainers],
)
if has_cred:
_retry_unflag(pkgbase)
@db.retry_deadlock
def _retry_disown(request: Request, pkgbase: PackageBase):
notifs: list[notify.Notification] = []
is_maint = request.user == pkgbase.Maintainer
comaint = pkgbase.comaintainers.filter(
PackageComaintainer.User == request.user
).one_or_none()
is_comaint = comaint is not None
if is_maint:
with db.begin():
# Comaintainer with the lowest Priority value; next-in-line.
prio_comaint = pkgbase.comaintainers.order_by(
PackageComaintainer.Priority.asc()
).first()
if prio_comaint:
# If there is such a comaintainer, promote them to maint.
pkgbase.Maintainer = prio_comaint.User
notifs.append(pkgbaseutil.remove_comaintainer(prio_comaint))
else:
# Otherwise, just orphan the package completely.
pkgbase.Maintainer = None
elif is_comaint:
# This disown request is from a Comaintainer
with db.begin():
notif = pkgbaseutil.remove_comaintainer(comaint)
notifs.append(notif)
elif request.user.has_credential(creds.PKGBASE_DISOWN):
# Otherwise, the request user performing this disownage is a
# Package Maintainer and we treat it like a standard orphan request.
notifs += handle_request(request, ORPHAN_ID, pkgbase)
with db.begin():
pkgbase.Maintainer = None
db.delete_all(pkgbase.comaintainers)
return notifs
def pkgbase_disown_instance(request: Request, pkgbase: PackageBase) -> None:
disowner = request.user
notifs = [notify.DisownNotification(disowner.ID, pkgbase.ID)]
notifs += _retry_disown(request, pkgbase)
util.apply_all(notifs, lambda n: n.send())
@db.retry_deadlock
def _retry_adopt(request: Request, pkgbase: PackageBase) -> None:
with db.begin():
pkgbase.Maintainer = request.user
def pkgbase_adopt_instance(request: Request, pkgbase: PackageBase) -> None:
_retry_adopt(request, pkgbase)
notif = notify.AdoptNotification(request.user.ID, pkgbase.ID)
notif.send()
@db.retry_deadlock
def _retry_delete(pkgbase: PackageBase, comments: str) -> None:
with db.begin():
update_closure_comment(pkgbase, DELETION_ID, comments)
db.delete(pkgbase)
def pkgbase_delete_instance(
request: Request, pkgbase: PackageBase, comments: str = str()
) -> list[notify.Notification]:
notif = notify.DeleteNotification(request.user.ID, pkgbase.ID)
notifs = handle_request(request, DELETION_ID, pkgbase, comments=comments) + [notif]
_retry_delete(pkgbase, comments)
return notifs
@db.retry_deadlock
def _retry_merge(pkgbase: PackageBase, target: PackageBase) -> None:
# Target votes and notifications sets of user IDs that are
# looking to be migrated.
target_votes = set(v.UsersID for v in target.package_votes)
target_notifs = set(n.UserID for n in target.notifications)
with db.begin():
# Merge pkgbase's comments.
for comment in pkgbase.comments:
comment.PackageBase = target
# Merge notifications that don't yet exist in the target.
for notif in pkgbase.notifications:
if notif.UserID not in target_notifs:
notif.PackageBase = target
# Merge votes that don't yet exist in the target.
for vote in pkgbase.package_votes:
if vote.UsersID not in target_votes:
vote.PackageBase = target
# Run popupdate.
popupdate.run_single(target)
with db.begin():
# Delete pkgbase and its packages now that everything's merged.
for pkg in pkgbase.packages:
db.delete(pkg)
db.delete(pkgbase)
def pkgbase_merge_instance(
request: Request,
pkgbase: PackageBase,
target: PackageBase,
comments: str = str(),
) -> None:
pkgbasename = str(pkgbase.Name)
# Create notifications.
notifs = handle_request(request, MERGE_ID, pkgbase, target, comments)
_retry_merge(pkgbase, target)
# Log this out for accountability purposes.
logger.info(
f"Package Maintainer '{request.user.Username}' merged "
f"'{pkgbasename}' into '{target.Name}'."
)
# Send notifications.
util.apply_all(notifs, lambda n: n.send())

246
aurweb/pkgbase/util.py Normal file
View file

@ -0,0 +1,246 @@
from typing import Any
from fastapi import Request
from sqlalchemy import and_
from sqlalchemy.orm import joinedload
from aurweb import config, db, defaults, l10n, time, util
from aurweb.models import PackageBase, User
from aurweb.models.package_base import popularity
from aurweb.models.package_comaintainer import PackageComaintainer
from aurweb.models.package_comment import PackageComment
from aurweb.models.package_request import PENDING_ID, PackageRequest
from aurweb.models.package_vote import PackageVote
from aurweb.scripts import notify
from aurweb.templates import make_context as _make_context
def make_context(
request: Request, pkgbase: PackageBase, context: dict[str, Any] = None
) -> dict[str, Any]:
"""Make a basic context for package or pkgbase.
:param request: FastAPI request
:param pkgbase: PackageBase instance
:return: A pkgbase context without specific differences
"""
if not context:
context = _make_context(request, pkgbase.Name)
is_authenticated = request.user.is_authenticated()
# Per page and offset.
offset, per_page = util.sanitize_params(
request.query_params.get("O", defaults.O),
request.query_params.get("PP", defaults.COMMENTS_PER_PAGE),
)
context["O"] = offset
context["PP"] = per_page
context["git_clone_uri_anon"] = config.get("options", "git_clone_uri_anon")
context["git_clone_uri_priv"] = config.get("options", "git_clone_uri_priv")
context["pkgbase"] = pkgbase
context["comaintainers"] = [
c.User
for c in pkgbase.comaintainers.options(joinedload(PackageComaintainer.User))
.order_by(PackageComaintainer.Priority.asc())
.all()
]
if is_authenticated:
context["unflaggers"] = context["comaintainers"].copy()
context["unflaggers"].extend([pkgbase.Maintainer, pkgbase.Flagger])
else:
context["unflaggers"] = []
context["packages_count"] = pkgbase.packages.count()
context["keywords"] = pkgbase.keywords
context["comments_total"] = pkgbase.comments.order_by(
PackageComment.CommentTS.desc()
).count()
context["comments"] = (
pkgbase.comments.order_by(PackageComment.CommentTS.desc())
.limit(per_page)
.offset(offset)
)
context["pinned_comments"] = pkgbase.comments.filter(
PackageComment.PinnedTS != 0
).order_by(PackageComment.CommentTS.desc())
context["is_maintainer"] = bool(request.user == pkgbase.Maintainer)
if is_authenticated:
context["notified"] = request.user.notified(pkgbase)
else:
context["notified"] = False
context["out_of_date"] = bool(pkgbase.OutOfDateTS)
if is_authenticated:
context["voted"] = db.query(
request.user.package_votes.filter(
PackageVote.PackageBaseID == pkgbase.ID
).exists()
).scalar()
else:
context["voted"] = False
if is_authenticated:
context["requests"] = pkgbase.requests.filter(
and_(PackageRequest.Status == PENDING_ID, PackageRequest.ClosedTS.is_(None))
).count()
else:
context["requests"] = []
context["popularity"] = popularity(pkgbase, time.utcnow())
return context
def remove_comaintainer(
comaint: PackageComaintainer,
) -> notify.ComaintainerRemoveNotification:
"""
Remove a PackageComaintainer.
This function does *not* begin any database transaction and
must be used **within** a database transaction, e.g.:
with db.begin():
remove_comaintainer(comaint)
:param comaint: Target PackageComaintainer to be deleted
:return: ComaintainerRemoveNotification
"""
pkgbase = comaint.PackageBase
notif = notify.ComaintainerRemoveNotification(comaint.User.ID, pkgbase.ID)
db.delete(comaint)
rotate_comaintainers(pkgbase)
return notif
@db.retry_deadlock
def remove_comaintainers(pkgbase: PackageBase, usernames: list[str]) -> None:
"""
Remove comaintainers from `pkgbase`.
:param pkgbase: PackageBase instance
:param usernames: Iterable of username strings
"""
notifications = []
with db.begin():
comaintainers = (
pkgbase.comaintainers.join(User).filter(User.Username.in_(usernames)).all()
)
notifications = [
notify.ComaintainerRemoveNotification(co.User.ID, pkgbase.ID)
for co in comaintainers
]
db.delete_all(comaintainers)
# Rotate comaintainer priority values.
with db.begin():
rotate_comaintainers(pkgbase)
# Send out notifications.
util.apply_all(notifications, lambda n: n.send())
def latest_priority(pkgbase: PackageBase) -> int:
"""
Return the highest Priority column related to `pkgbase`.
:param pkgbase: PackageBase instance
:return: Highest Priority found or 0 if no records exist
"""
# Order comaintainers related to pkgbase by Priority DESC.
record = pkgbase.comaintainers.order_by(PackageComaintainer.Priority.desc()).first()
# Use Priority column if record exists, otherwise 0.
return record.Priority if record else 0
class NoopComaintainerNotification:
"""A noop notification stub used as an error-state return value."""
def send(self) -> None:
"""noop"""
return
@db.retry_deadlock
def add_comaintainer(
pkgbase: PackageBase, comaintainer: User
) -> notify.ComaintainerAddNotification:
"""
Add a new comaintainer to `pkgbase`.
:param pkgbase: PackageBase instance
:param comaintainer: User instance used for new comaintainer record
:return: ComaintainerAddNotification
"""
# Skip given `comaintainers` who are already maintainer.
if pkgbase.Maintainer == comaintainer:
return NoopComaintainerNotification()
# Priority for the new comaintainer is +1 more than the highest.
new_prio = latest_priority(pkgbase) + 1
with db.begin():
db.create(
PackageComaintainer,
PackageBase=pkgbase,
User=comaintainer,
Priority=new_prio,
)
return notify.ComaintainerAddNotification(comaintainer.ID, pkgbase.ID)
def add_comaintainers(
request: Request, pkgbase: PackageBase, usernames: list[str]
) -> None:
"""
Add comaintainers to `pkgbase`.
:param request: FastAPI request
:param pkgbase: PackageBase instance
:param usernames: Iterable of username strings
:return: Error string on failure else None
"""
# For each username in usernames, perform validation of the username
# and append the User record to `users` if no errors occur.
users = []
for username in usernames:
user = db.query(User).filter(User.Username == username).first()
if not user:
_ = l10n.get_translator_for_request(request)
return _("Invalid user name: %s") % username
users.append(user)
notifications = []
def add_comaint(user: User):
nonlocal notifications
# Populate `notifications` with add_comaintainer's return value,
# which is a ComaintainerAddNotification.
notifications.append(add_comaintainer(pkgbase, user))
# Move along: add all `users` as new `pkgbase` comaintainers.
util.apply_all(users, add_comaint)
# Send out notifications.
util.apply_all(notifications, lambda n: n.send())
def rotate_comaintainers(pkgbase: PackageBase) -> None:
"""
Rotate `pkgbase` comaintainers.
This function resets the Priority column of all PackageComaintainer
instances related to `pkgbase` to seqential 1 .. n values with
persisted order.
:param pkgbase: PackageBase instance
"""
comaintainers = pkgbase.comaintainers.order_by(PackageComaintainer.Priority.asc())
for i, comaint in enumerate(comaintainers):
comaint.Priority = i + 1

View file

@ -0,0 +1,55 @@
from http import HTTPStatus
from typing import Any
from fastapi import HTTPException
from aurweb import config, db
from aurweb.exceptions import ValidationError
from aurweb.models import PackageBase
def request(
pkgbase: PackageBase,
type: str,
comments: str,
merge_into: str,
context: dict[str, Any],
) -> None:
# validate comment
comment(comments)
if type == "merge":
# Perform merge-related checks.
if not merge_into:
# TODO: This error needs to be translated.
raise ValidationError(['The "Merge into" field must not be empty.'])
target = db.query(PackageBase).filter(PackageBase.Name == merge_into).first()
if not target:
# TODO: This error needs to be translated.
raise ValidationError(
["The package base you want to merge into does not exist."]
)
db.refresh(target)
if target.ID == pkgbase.ID:
# TODO: This error needs to be translated.
raise ValidationError(["You cannot merge a package base into itself."])
def comment(comment: str):
if not comment:
raise ValidationError(["The comment field must not be empty."])
if len(comment) > config.getint("options", "max_chars_comment", 5000):
raise ValidationError(["Maximum number of characters for comment exceeded."])
def comment_raise_http_ex(comments: str):
try:
comment(comments)
except ValidationError as err:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=err.data[0],
)

143
aurweb/prometheus.py Normal file
View file

@ -0,0 +1,143 @@
from typing import Any, Callable, Optional
from prometheus_client import Counter, Gauge
from prometheus_fastapi_instrumentator import Instrumentator
from prometheus_fastapi_instrumentator.metrics import Info
from starlette.routing import Match, Route
from aurweb import aur_logging
logger = aur_logging.get_logger(__name__)
_instrumentator = Instrumentator()
# Custom metrics
SEARCH_REQUESTS = Counter(
"aur_search_requests", "Number of search requests by cache hit/miss", ["cache"]
)
USERS = Gauge(
"aur_users", "Number of AUR users by type", ["type"], multiprocess_mode="livemax"
)
PACKAGES = Gauge(
"aur_packages",
"Number of AUR packages by state",
["state"],
multiprocess_mode="livemax",
)
REQUESTS = Gauge(
"aur_requests",
"Number of AUR requests by type and status",
["type", "status"],
multiprocess_mode="livemax",
)
def instrumentator():
return _instrumentator
# FastAPI metrics
# Taken from https://github.com/stephenhillier/starlette_exporter
# Their license is included in LICENSES/starlette_exporter.
# The code has been modified to remove child route checks
# (since we don't have any) and to stay within an 80-width limit.
def get_matching_route_path(
scope: dict[Any, Any], routes: list[Route], route_name: Optional[str] = None
) -> str:
"""
Find a matching route and return its original path string
Will attempt to enter mounted routes and subrouters.
Credit to https://github.com/elastic/apm-agent-python
"""
for route in routes:
match, child_scope = route.matches(scope)
if match == Match.FULL:
route_name = route.path
"""
# This path exists in the original function's code, but we
# don't need it (currently), so it's been removed to avoid
# useless test coverage.
child_scope = {**scope, **child_scope}
if isinstance(route, Mount) and route.routes:
child_route_name = get_matching_route_path(child_scope,
route.routes,
route_name)
if child_route_name is None:
route_name = None
else:
route_name += child_route_name
"""
return route_name
elif match == Match.PARTIAL and route_name is None:
route_name = route.path
def http_requests_total() -> Callable[[Info], None]:
metric = Counter(
"http_requests_total",
"Number of HTTP requests.",
labelnames=("method", "path", "status"),
)
def instrumentation(info: Info) -> None:
if info.request.method.lower() in ("head", "options"): # pragma: no cover
return
scope = info.request.scope
# Taken from https://github.com/stephenhillier/starlette_exporter
# Their license is included at LICENSES/starlette_exporter.
# The code has been slightly modified: we no longer catch
# exceptions; we expect this collector to always succeed.
# Failures in this collector shall cause test failures.
if not (scope.get("endpoint", None) and scope.get("router", None)):
return None
root_path = scope.get("root_path", str())
app = scope.get("app", dict())
if hasattr(app, "root_path"):
app_root_path = getattr(app, "root_path")
if root_path.startswith(app_root_path):
root_path = root_path[len(app_root_path) :]
base_scope = {
"type": scope.get("type"),
"path": root_path + scope.get("path"),
"path_params": scope.get("path_params", {}),
"method": scope.get("method"),
}
method = scope.get("method")
path = get_matching_route_path(base_scope, scope.get("router").routes)
if info.response:
status = str(int(info.response.status_code))[:1] + "xx"
metric.labels(method=method, path=path, status=status).inc()
return instrumentation
def http_api_requests_total() -> Callable[[Info], None]:
metric = Counter(
"http_api_requests",
"Number of times an RPC API type has been requested.",
labelnames=("type", "status"),
)
def instrumentation(info: Info) -> None:
if info.request.method.lower() in ("head", "options"): # pragma: no cover
return
if info.request.url.path.rstrip("/") == "/rpc":
type = info.request.query_params.get("type", "None")
if info.response:
status = str(info.response.status_code)[:1] + "xx"
metric.labels(type=type, status=status).inc()
return instrumentation

117
aurweb/ratelimit.py Normal file
View file

@ -0,0 +1,117 @@
from fastapi import Request
from redis.client import Pipeline
from aurweb import aur_logging, config, db, time
from aurweb.aur_redis import redis_connection
from aurweb.models import ApiRateLimit
from aurweb.util import get_client_ip
logger = aur_logging.get_logger(__name__)
def _update_ratelimit_redis(request: Request, pipeline: Pipeline):
window_length = config.getint("ratelimit", "window_length")
now = time.utcnow()
time_to_delete = now - window_length
host = get_client_ip(request)
window_key = f"ratelimit-ws:{host}"
requests_key = f"ratelimit:{host}"
pipeline.get(window_key)
window = pipeline.execute()[0]
if not window or int(window.decode()) < time_to_delete:
pipeline.set(window_key, now)
pipeline.expire(window_key, window_length)
pipeline.set(requests_key, 1)
pipeline.expire(requests_key, window_length)
pipeline.execute()
else:
pipeline.incr(requests_key)
pipeline.execute()
def _update_ratelimit_db(request: Request):
window_length = config.getint("ratelimit", "window_length")
now = time.utcnow()
time_to_delete = now - window_length
@db.retry_deadlock
def retry_delete(records: list[ApiRateLimit]) -> None:
with db.begin():
db.delete_all(records)
records = db.query(ApiRateLimit).filter(ApiRateLimit.WindowStart < time_to_delete)
retry_delete(records)
@db.retry_deadlock
def retry_create(record: ApiRateLimit, now: int, host: str) -> ApiRateLimit:
with db.begin():
if not record:
record = db.create(ApiRateLimit, WindowStart=now, IP=host, Requests=1)
else:
record.Requests += 1
return record
host = get_client_ip(request)
record = db.query(ApiRateLimit, ApiRateLimit.IP == host).first()
record = retry_create(record, now, host)
logger.debug(record.Requests)
return record
def update_ratelimit(request: Request, pipeline: Pipeline):
"""Update the ratelimit stored in Redis or the database depending
on AUR_CONFIG's [options] cache setting.
This Redis-capable function is slightly different than most. If Redis
is not configured to use a real server, this function instead uses
the database to persist tracking of a particular host.
:param request: FastAPI request
:param pipeline: redis.client.Pipeline
:returns: ApiRateLimit record when Redis cache is not configured, else None
"""
if config.getboolean("ratelimit", "cache"):
return _update_ratelimit_redis(request, pipeline)
return _update_ratelimit_db(request)
def check_ratelimit(request: Request):
"""Increment and check to see if request has exceeded their rate limit.
:param request: FastAPI request
:returns: True if the request host has exceeded the rate limit else False
"""
redis = redis_connection()
pipeline = redis.pipeline()
record = update_ratelimit(request, pipeline)
# Get cache value, else None.
host = get_client_ip(request)
pipeline.get(f"ratelimit:{host}")
requests = pipeline.execute()[0]
# Take into account the split paths. When Redis is used, a
# valid cache value will be returned which must be converted
# to an int. Otherwise, use the database record returned
# by update_ratelimit.
if not config.getboolean("ratelimit", "cache") or requests is None:
# If we got nothing from pipeline.get, we did not use
# the Redis path of logic: use the DB record's count.
requests = record.Requests
else:
# Otherwise, just case Redis results over to an int.
requests = int(requests.decode())
limit = config.getint("ratelimit", "request_limit")
exceeded_ratelimit = requests > limit
if exceeded_ratelimit:
logger.debug(f"{host} has exceeded the ratelimit.")
return exceeded_ratelimit

View file

13
aurweb/requests/util.py Normal file
View file

@ -0,0 +1,13 @@
from http import HTTPStatus
from fastapi import HTTPException
from aurweb import db
from aurweb.models import PackageRequest
def get_pkgreq_by_id(id: int) -> PackageRequest:
pkgreq = db.query(PackageRequest).filter(PackageRequest.ID == id).first()
if not pkgreq:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND)
return db.refresh(pkgreq)

View file

@ -0,0 +1,36 @@
"""
API routers for FastAPI.
See https://fastapi.tiangolo.com/tutorial/bigger-applications/
"""
from . import (
accounts,
auth,
html,
package_maintainer,
packages,
pkgbase,
requests,
rpc,
rss,
sso,
)
"""
aurweb application routes. This constant can be any iterable
and each element must have a .router attribute which points
to a fastapi.APIRouter.
"""
APP_ROUTES = [
accounts,
auth,
html,
packages,
pkgbase,
requests,
package_maintainer,
rss,
rpc,
sso,
]

776
aurweb/routers/accounts.py Normal file
View file

@ -0,0 +1,776 @@
import copy
import typing
from http import HTTPStatus
from typing import Any
from fastapi import APIRouter, Form, HTTPException, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from sqlalchemy import and_, or_
import aurweb.config
from aurweb import aur_logging, db, l10n, models, util
from aurweb.auth import account_type_required, creds, requires_auth, requires_guest
from aurweb.captcha import get_captcha_salts
from aurweb.exceptions import ValidationError, handle_form_exceptions
from aurweb.l10n import get_translator_for_request
from aurweb.models import account_type as at
from aurweb.models.ssh_pub_key import get_fingerprint
from aurweb.models.user import generate_resetkey
from aurweb.scripts.notify import ResetKeyNotification, WelcomeNotification
from aurweb.templates import make_context, make_variable_context, render_template
from aurweb.users import update, validate
from aurweb.users.util import get_user_by_name
router = APIRouter()
logger = aur_logging.get_logger(__name__)
@router.get("/passreset", response_class=HTMLResponse)
@requires_guest
async def passreset(request: Request):
context = await make_variable_context(request, "Password Reset")
return render_template(request, "passreset.html", context)
@db.async_retry_deadlock
@router.post("/passreset", response_class=HTMLResponse)
@handle_form_exceptions
@requires_guest
async def passreset_post(
request: Request,
user: str = Form(...),
resetkey: str = Form(default=None),
password: str = Form(default=None),
confirm: str = Form(default=None),
):
context = await make_variable_context(request, "Password Reset")
# The user parameter being required, we can match against
criteria = or_(models.User.Username == user, models.User.Email == user)
db_user = db.query(models.User, and_(criteria, models.User.Suspended == 0)).first()
if db_user is None:
context["errors"] = ["Invalid e-mail."]
return render_template(
request, "passreset.html", context, status_code=HTTPStatus.NOT_FOUND
)
db.refresh(db_user)
if resetkey:
context["resetkey"] = resetkey
if not db_user.ResetKey or resetkey != db_user.ResetKey:
context["errors"] = ["Invalid e-mail."]
return render_template(
request, "passreset.html", context, status_code=HTTPStatus.NOT_FOUND
)
if not user or not password:
context["errors"] = ["Missing a required field."]
return render_template(
request, "passreset.html", context, status_code=HTTPStatus.BAD_REQUEST
)
if password != confirm:
# If the provided password does not match the provided confirm.
context["errors"] = ["Password fields do not match."]
return render_template(
request, "passreset.html", context, status_code=HTTPStatus.BAD_REQUEST
)
if len(password) < models.User.minimum_passwd_length():
# Translate the error here, which simplifies error output
# in the jinja2 template.
_ = get_translator_for_request(request)
context["errors"] = [
_("Your password must be at least %s characters.")
% (str(models.User.minimum_passwd_length()))
]
return render_template(
request, "passreset.html", context, status_code=HTTPStatus.BAD_REQUEST
)
# We got to this point; everything matched up. Update the password
# and remove the ResetKey.
with db.begin():
db_user.ResetKey = str()
if db_user.session:
db.delete(db_user.session)
db_user.update_password(password)
# Render ?step=complete.
return RedirectResponse(
url="/passreset?step=complete", status_code=HTTPStatus.SEE_OTHER
)
# If we got here, we continue with issuing a resetkey for the user.
resetkey = generate_resetkey()
with db.begin():
db_user.ResetKey = resetkey
ResetKeyNotification(db_user.ID).send()
# Render ?step=confirm.
return RedirectResponse(
url="/passreset?step=confirm", status_code=HTTPStatus.SEE_OTHER
)
def process_account_form(request: Request, user: models.User, args: dict[str, Any]):
"""Process an account form. All fields are optional and only checks
requirements in the case they are present.
```
context = await make_variable_context(request, "Accounts")
ok, errors = process_account_form(request, user, **kwargs)
if not ok:
context["errors"] = errors
return render_template(request, "some_account_template.html", context)
```
:param request: An incoming FastAPI request
:param user: The user model of the account being processed
:param args: A dictionary of arguments generated via request.form()
:return: A (passed processing boolean, list of errors) tuple
"""
# Get a local translator.
_ = get_translator_for_request(request)
checks = [
validate.is_banned,
validate.invalid_user_password,
validate.invalid_fields,
validate.invalid_suspend_permission,
validate.invalid_username,
validate.invalid_password,
validate.invalid_email,
validate.invalid_backup_email,
validate.invalid_homepage,
validate.invalid_pgp_key,
validate.invalid_ssh_pubkey,
validate.invalid_language,
validate.invalid_timezone,
validate.username_in_use,
validate.email_in_use,
validate.invalid_account_type,
validate.invalid_captcha,
]
try:
for check in checks:
check(**args, request=request, user=user, _=_)
except ValidationError as exc:
return False, exc.data
return True, []
def make_account_form_context(
context: dict, request: Request, user: models.User, args: dict
):
"""Modify a FastAPI context and add attributes for the account form.
:param context: FastAPI context
:param request: FastAPI request
:param user: Target user
:param args: Persistent arguments: request.form()
:return: FastAPI context adjusted for account form
"""
# Do not modify the original context.
context = copy.copy(context)
context["account_types"] = list(
filter(
lambda e: request.user.AccountTypeID >= e[0],
[
(at.USER_ID, f"Normal {at.USER}"),
(at.PACKAGE_MAINTAINER_ID, at.PACKAGE_MAINTAINER),
(at.DEVELOPER_ID, at.DEVELOPER),
(at.PACKAGE_MAINTAINER_AND_DEV_ID, at.PACKAGE_MAINTAINER_AND_DEV),
],
)
)
if request.user.is_authenticated():
context["username"] = args.get("U", user.Username)
context["account_type"] = args.get("T", user.AccountType.ID)
context["suspended"] = args.get("S", user.Suspended)
context["email"] = args.get("E", user.Email)
context["hide_email"] = args.get("H", user.HideEmail)
context["backup_email"] = args.get("BE", user.BackupEmail)
context["realname"] = args.get("R", user.RealName)
context["homepage"] = args.get("HP", user.Homepage or str())
context["ircnick"] = args.get("I", user.IRCNick)
context["pgp"] = args.get("K", user.PGPKey or str())
context["lang"] = args.get("L", user.LangPreference)
context["tz"] = args.get("TZ", user.Timezone)
ssh_pks = [pk.PubKey for pk in user.ssh_pub_keys]
context["ssh_pks"] = args.get("PK", ssh_pks)
context["cn"] = args.get("CN", user.CommentNotify)
context["un"] = args.get("UN", user.UpdateNotify)
context["on"] = args.get("ON", user.OwnershipNotify)
context["hdc"] = args.get("HDC", user.HideDeletedComments)
context["inactive"] = args.get("J", user.InactivityTS != 0)
else:
context["username"] = args.get("U", str())
context["account_type"] = args.get("T", at.USER_ID)
context["suspended"] = args.get("S", False)
context["email"] = args.get("E", str())
context["hide_email"] = args.get("H", False)
context["backup_email"] = args.get("BE", str())
context["realname"] = args.get("R", str())
context["homepage"] = args.get("HP", str())
context["ircnick"] = args.get("I", str())
context["pgp"] = args.get("K", str())
context["lang"] = args.get("L", context.get("language"))
context["tz"] = args.get("TZ", context.get("timezone"))
context["ssh_pks"] = args.get("PK", str())
context["cn"] = args.get("CN", True)
context["un"] = args.get("UN", False)
context["on"] = args.get("ON", True)
context["hdc"] = args.get("HDC", False)
context["inactive"] = args.get("J", False)
context["password"] = args.get("P", str())
context["confirm"] = args.get("C", str())
return context
@router.get("/register", response_class=HTMLResponse)
@requires_guest
async def account_register(
request: Request,
U: str = Form(default=str()), # Username
E: str = Form(default=str()), # Email
H: str = Form(default=False), # Hide Email
BE: str = Form(default=None), # Backup Email
R: str = Form(default=None), # Real Name
HP: str = Form(default=None), # Homepage
I: str = Form(default=None), # IRC Nick
K: str = Form(default=None), # PGP Key FP
L: str = Form(default=aurweb.config.get("options", "default_lang")),
TZ: str = Form(default=aurweb.config.get("options", "default_timezone")),
PK: str = Form(default=None),
CN: bool = Form(default=False), # Comment Notify
CU: bool = Form(default=False), # Update Notify
CO: bool = Form(default=False), # Owner Notify
HDC: bool = Form(default=False), # Hide Deleted Comments
captcha: str = Form(default=str()),
):
context = await make_variable_context(request, "Register")
context["captcha_salt"] = get_captcha_salts()[0]
context = make_account_form_context(context, request, None, dict())
return render_template(request, "register.html", context)
@db.async_retry_deadlock
@router.post("/register", response_class=HTMLResponse)
@handle_form_exceptions
@requires_guest
async def account_register_post(
request: Request,
U: str = Form(default=str()), # Username
E: str = Form(default=str()), # Email
H: str = Form(default=False), # Hide Email
BE: str = Form(default=None), # Backup Email
R: str = Form(default=""), # Real Name
HP: str = Form(default=None), # Homepage
I: str = Form(default=None), # IRC Nick
K: str = Form(default=None), # PGP Key
L: str = Form(default=aurweb.config.get("options", "default_lang")),
TZ: str = Form(default=aurweb.config.get("options", "default_timezone")),
PK: str = Form(default=str()), # SSH PubKey
CN: bool = Form(default=False),
UN: bool = Form(default=False),
ON: bool = Form(default=False),
HDC: bool = Form(default=False),
captcha: str = Form(default=None),
captcha_salt: str = Form(...),
):
context = await make_variable_context(request, "Register")
args = dict(await request.form())
args["K"] = args.get("K", str()).replace(" ", "")
K = args.get("K")
# Force "H" into a boolean.
args["H"] = H = args.get("H", str()) == "on"
context = make_account_form_context(context, request, None, args)
ok, errors = process_account_form(request, request.user, args)
if not ok:
# If the field values given do not meet the requirements,
# return HTTP 400 with an error.
context["errors"] = errors
return render_template(
request, "register.html", context, status_code=HTTPStatus.BAD_REQUEST
)
if not captcha:
context["errors"] = ["The CAPTCHA is missing."]
return render_template(
request, "register.html", context, status_code=HTTPStatus.BAD_REQUEST
)
# Create a user with no password with a resetkey, then send
# an email off about it.
resetkey = generate_resetkey()
# By default, we grab the User account type to associate with.
atype = db.query(
models.AccountType, models.AccountType.AccountType == "User"
).first()
# Create a user given all parameters available.
with db.begin():
user = db.create(
models.User,
Username=U,
Email=E,
HideEmail=H,
BackupEmail=BE,
RealName=R,
Homepage=HP,
IRCNick=I,
PGPKey=K,
LangPreference=L,
Timezone=TZ,
CommentNotify=CN,
UpdateNotify=UN,
OwnershipNotify=ON,
HideDeletedComments=HDC,
ResetKey=resetkey,
AccountType=atype,
)
# If a PK was given and either one does not exist or the given
# PK mismatches the existing user's SSHPubKey.PubKey.
if PK:
# Get the second element in the PK, which is the actual key.
keys = util.parse_ssh_keys(PK.strip())
for k in keys:
pk = " ".join(k)
fprint = get_fingerprint(pk)
db.create(models.SSHPubKey, User=user, PubKey=pk, Fingerprint=fprint)
# Send a reset key notification to the new user.
WelcomeNotification(user.ID).send()
context["complete"] = True
context["user"] = user
return render_template(request, "register.html", context)
def cannot_edit(
request: Request, user: models.User
) -> typing.Optional[RedirectResponse]:
"""
Decide if `request.user` cannot edit `user`.
If the request user can edit the target user, None is returned.
Otherwise, a redirect is returned to /account/{user.Username}.
:param request: FastAPI request
:param user: Target user to be edited
:return: RedirectResponse if approval != granted else None
"""
# raise 404 if user does not exist
if not user:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND)
approved = request.user.can_edit_user(user)
if not approved and (to := "/"):
if user:
to = f"/account/{user.Username}"
return RedirectResponse(to, status_code=HTTPStatus.SEE_OTHER)
return None
@router.get("/account/{username}/edit", response_class=HTMLResponse)
@requires_auth
async def account_edit(request: Request, username: str):
user = db.query(models.User, models.User.Username == username).first()
response = cannot_edit(request, user)
if response:
return response
context = await make_variable_context(request, "Accounts")
context["user"] = db.refresh(user)
context = make_account_form_context(context, request, user, dict())
return render_template(request, "account/edit.html", context)
@router.post("/account/{username}/edit", response_class=HTMLResponse)
@handle_form_exceptions
@requires_auth
async def account_edit_post(
request: Request,
username: str,
U: str = Form(default=str()), # Username
J: bool = Form(default=False),
E: str = Form(default=str()), # Email
H: str = Form(default=False), # Hide Email
BE: str = Form(default=None), # Backup Email
R: str = Form(default=None), # Real Name
HP: str = Form(default=None), # Homepage
I: str = Form(default=None), # IRC Nick
K: str = Form(default=None), # PGP Key
L: str = Form(aurweb.config.get("options", "default_lang")),
TZ: str = Form(aurweb.config.get("options", "default_timezone")),
P: str = Form(default=str()), # New Password
C: str = Form(default=None), # Password Confirm
S: bool = Form(default=False), # Suspended
PK: str = Form(default=None), # PubKey
CN: bool = Form(default=False), # Comment Notify
UN: bool = Form(default=False), # Update Notify
ON: bool = Form(default=False), # Owner Notify
HDC: bool = Form(default=False), # Hide Deleted Comments
T: int = Form(default=None),
passwd: str = Form(default=str()),
):
user = db.query(models.User).filter(models.User.Username == username).first()
response = cannot_edit(request, user)
if response:
return response
context = await make_variable_context(request, "Accounts")
context["user"] = db.refresh(user)
args = dict(await request.form())
args["K"] = args.get("K", str()).replace(" ", "")
context = make_account_form_context(context, request, user, args)
ok, errors = process_account_form(request, user, args)
if PK:
context["ssh_pks"] = [PK]
if not passwd:
context["errors"] = ["Invalid password."]
return render_template(
request, "account/edit.html", context, status_code=HTTPStatus.BAD_REQUEST
)
if not ok:
context["errors"] = errors
return render_template(
request, "account/edit.html", context, status_code=HTTPStatus.BAD_REQUEST
)
updates = [
update.simple,
update.language,
update.timezone,
update.ssh_pubkey,
update.account_type,
update.password,
update.suspend,
]
# These update functions are all guarded by retry_deadlock;
# there's no need to guard this route itself.
for f in updates:
f(**args, request=request, user=user, context=context)
if not errors:
context["complete"] = True
return render_template(request, "account/edit.html", context)
@router.get("/account/{username}")
async def account(request: Request, username: str):
_ = l10n.get_translator_for_request(request)
context = await make_variable_context(request, _("Account") + " " + username)
if not request.user.is_authenticated():
return render_template(
request, "account/show.html", context, status_code=HTTPStatus.UNAUTHORIZED
)
# Get related User record, if possible.
user = get_user_by_name(username)
context["user"] = user
# Format PGPKey for display with a space between each 4 characters.
k = user.PGPKey or str()
context["pgp_key"] = " ".join([k[i : i + 4] for i in range(0, len(k), 4)])
login_ts = None
session = db.query(models.Session).filter(models.Session.UsersID == user.ID).first()
if session:
login_ts = user.session.LastUpdateTS
context["login_ts"] = login_ts
# Render the template.
return render_template(request, "account/show.html", context)
@router.get("/account/{username}/comments")
@requires_auth
async def account_comments(request: Request, username: str):
user = get_user_by_name(username)
context = make_context(request, "Accounts")
context["username"] = username
context["comments"] = user.package_comments.order_by(
models.PackageComment.CommentTS.desc()
)
return render_template(request, "account/comments.html", context)
@router.get("/accounts")
@requires_auth
@account_type_required(
{at.PACKAGE_MAINTAINER, at.DEVELOPER, at.PACKAGE_MAINTAINER_AND_DEV}
)
async def accounts(request: Request):
context = make_context(request, "Accounts")
return render_template(request, "account/search.html", context)
@router.post("/accounts")
@handle_form_exceptions
@requires_auth
@account_type_required(
{at.PACKAGE_MAINTAINER, at.DEVELOPER, at.PACKAGE_MAINTAINER_AND_DEV}
)
async def accounts_post(
request: Request,
O: int = Form(default=0), # Offset
SB: str = Form(default=str()), # Sort By
U: str = Form(default=str()), # Username
T: str = Form(default=str()), # Account Type
S: bool = Form(default=False), # Suspended
E: str = Form(default=str()), # Email
R: str = Form(default=str()), # Real Name
I: str = Form(default=str()), # IRC Nick
K: str = Form(default=str()),
): # PGP Key
context = await make_variable_context(request, "Accounts")
context["pp"] = pp = 50 # Hits per page.
offset = max(O, 0) # Minimize offset at 0.
context["offset"] = offset # Offset.
context["params"] = dict(await request.form())
if "O" in context["params"]:
context["params"].pop("O")
# Setup order by criteria based on SB.
order_by_columns = {
"t": (models.AccountType.ID.asc(), models.User.Username.asc()),
"r": (models.User.RealName.asc(), models.AccountType.ID.asc()),
"i": (models.User.IRCNick.asc(), models.AccountType.ID.asc()),
}
default_order = (models.User.Username.asc(), models.AccountType.ID.asc())
order_by = order_by_columns.get(SB, default_order)
# Convert parameter T to an AccountType ID.
account_types = {
"u": at.USER_ID,
"t": at.PACKAGE_MAINTAINER_ID,
"d": at.DEVELOPER_ID,
"td": at.PACKAGE_MAINTAINER_AND_DEV_ID,
}
account_type_id = account_types.get(T, None)
# Get a query handle to users, populate the total user
# count into a jinja2 context variable.
query = db.query(models.User).join(models.AccountType)
# Populate this list with any additional statements to
# be ANDed together.
statements = [
v
for k, v in [
(account_type_id is not None, models.AccountType.ID == account_type_id),
(bool(U), models.User.Username.like(f"%{U}%")),
(bool(S), models.User.Suspended == S),
(bool(E), models.User.Email.like(f"%{E}%")),
(bool(R), models.User.RealName.like(f"%{R}%")),
(bool(I), models.User.IRCNick.like(f"%{I}%")),
(bool(K), models.User.PGPKey.like(f"%{K}%")),
]
if k
]
# Filter the query by coe-mbining all statements added above into
# an AND statement, unless there's just one statement, which
# we pass on to filter() as args.
if statements:
query = query.filter(and_(*statements))
context["total_users"] = query.count()
# Finally, order and truncate our users for the current page.
users = query.order_by(*order_by).limit(pp).offset(offset).all()
context["users"] = util.apply_all(users, db.refresh)
return render_template(request, "account/index.html", context)
@router.get("/account/{name}/delete")
@requires_auth
async def account_delete(request: Request, name: str):
user = db.query(models.User).filter(models.User.Username == name).first()
if not user:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND)
has_cred = request.user.has_credential(creds.ACCOUNT_EDIT, approved=[user])
if not has_cred:
_ = l10n.get_translator_for_request(request)
raise HTTPException(
detail=_("You do not have permission to edit this account."),
status_code=HTTPStatus.UNAUTHORIZED,
)
context = make_context(request, "Accounts")
context["name"] = name
return render_template(request, "account/delete.html", context)
@db.async_retry_deadlock
@router.post("/account/{name}/delete")
@handle_form_exceptions
@requires_auth
async def account_delete_post(
request: Request,
name: str,
passwd: str = Form(default=str()),
confirm: bool = Form(default=False),
):
user = db.query(models.User).filter(models.User.Username == name).first()
if not user:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND)
has_cred = request.user.has_credential(creds.ACCOUNT_EDIT, approved=[user])
if not has_cred:
_ = l10n.get_translator_for_request(request)
raise HTTPException(
detail=_("You do not have permission to edit this account."),
status_code=HTTPStatus.UNAUTHORIZED,
)
context = make_context(request, "Accounts")
context["name"] = name
confirm = util.strtobool(confirm)
if not confirm:
context["errors"] = [
"The account has not been deleted, check the confirmation checkbox."
]
return render_template(
request,
"account/delete.html",
context,
status_code=HTTPStatus.BAD_REQUEST,
)
if not request.user.valid_password(passwd):
context["errors"] = ["Invalid password."]
return render_template(
request,
"account/delete.html",
context,
status_code=HTTPStatus.BAD_REQUEST,
)
with db.begin():
db.delete(user)
return RedirectResponse("/", status_code=HTTPStatus.SEE_OTHER)
def render_terms_of_service(request: Request, context: dict, terms: typing.Iterable):
if not terms:
return RedirectResponse("/", status_code=HTTPStatus.SEE_OTHER)
context["unaccepted_terms"] = terms
return render_template(request, "tos/index.html", context)
@router.get("/tos")
@requires_auth
async def terms_of_service(request: Request):
# Query the database for terms that were previously accepted,
# but now have a bumped Revision that needs to be accepted.
diffs = (
db.query(models.Term)
.join(models.AcceptedTerm)
.filter(models.AcceptedTerm.Revision < models.Term.Revision)
.all()
)
# Query the database for any terms that have not yet been accepted.
unaccepted = (
db.query(models.Term)
.filter(~models.Term.ID.in_(db.query(models.AcceptedTerm.TermsID)))
.all()
)
for record in diffs + unaccepted:
db.refresh(record)
# Translate the 'Terms of Service' part of our page title.
_ = l10n.get_translator_for_request(request)
title = f"AUR {_('Terms of Service')}"
context = await make_variable_context(request, title)
accept_needed = sorted(unaccepted + diffs)
return render_terms_of_service(request, context, accept_needed)
@db.async_retry_deadlock
@router.post("/tos")
@handle_form_exceptions
@requires_auth
async def terms_of_service_post(request: Request, accept: bool = Form(default=False)):
# Query the database for terms that were previously accepted,
# but now have a bumped Revision that needs to be accepted.
diffs = (
db.query(models.Term)
.join(models.AcceptedTerm)
.filter(models.AcceptedTerm.Revision < models.Term.Revision)
.all()
)
# Query the database for any terms that have not yet been accepted.
unaccepted = (
db.query(models.Term)
.filter(~models.Term.ID.in_(db.query(models.AcceptedTerm.TermsID)))
.all()
)
if not accept:
# Translate the 'Terms of Service' part of our page title.
_ = l10n.get_translator_for_request(request)
title = f"AUR {_('Terms of Service')}"
context = await make_variable_context(request, title)
# We already did the database filters here, so let's just use
# them instead of reiterating the process in terms_of_service.
accept_needed = sorted(unaccepted + diffs)
return render_terms_of_service(
request, context, util.apply_all(accept_needed, db.refresh)
)
with db.begin():
# For each term we found, query for the matching accepted term
# and update its Revision to the term's current Revision.
for term in diffs:
db.refresh(term)
accepted_term = request.user.accepted_terms.filter(
models.AcceptedTerm.TermsID == term.ID
).first()
accepted_term.Revision = term.Revision
# For each term that was never accepted, accept it!
for term in unaccepted:
db.refresh(term)
db.create(
models.AcceptedTerm,
User=request.user,
Term=term,
Revision=term.Revision,
)
return RedirectResponse("/", status_code=HTTPStatus.SEE_OTHER)

122
aurweb/routers/auth.py Normal file
View file

@ -0,0 +1,122 @@
from http import HTTPStatus
from fastapi import APIRouter, Form, HTTPException, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from sqlalchemy import or_
import aurweb.config
from aurweb import cookies, db
from aurweb.auth import requires_auth, requires_guest
from aurweb.exceptions import handle_form_exceptions
from aurweb.l10n import get_translator_for_request
from aurweb.models import User
from aurweb.templates import make_variable_context, render_template
router = APIRouter()
async def login_template(request: Request, next: str, errors: list = None):
"""Provide login-specific template context to render_template."""
context = await make_variable_context(request, "Login", next)
context["errors"] = errors
context["url_base"] = f"{request.url.scheme}://{request.url.netloc}"
return render_template(request, "login.html", context)
@router.get("/login", response_class=HTMLResponse)
async def login_get(request: Request, next: str = "/"):
return await login_template(request, next)
@db.retry_deadlock
def _retry_login(request: Request, user: User, passwd: str) -> str:
return user.login(request, passwd)
@router.post("/login", response_class=HTMLResponse)
@handle_form_exceptions
@requires_guest
async def login_post(
request: Request,
next: str = Form(...),
user: str = Form(default=str()),
passwd: str = Form(default=str()),
remember_me: bool = Form(default=False),
):
# TODO: Once the Origin header gets broader adoption, this code can be
# slightly simplified to use it.
login_path = aurweb.config.get("options", "aur_location") + "/login"
referer = request.headers.get("Referer")
if not referer or not referer.startswith(login_path):
_ = get_translator_for_request(request)
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail=_("Bad Referer header.")
)
user = (
db.query(User)
.filter(
or_(
User.Username == user,
User.Email == user,
)
)
.first()
)
if not user:
return await login_template(request, next, errors=["Bad username or password."])
if user.Suspended:
return await login_template(request, next, errors=["Account Suspended"])
# If "remember me" was not ticked, we set a session cookie for AURSID,
# otherwise we make it a persistent cookie
cookie_timeout = None
if remember_me:
cookie_timeout = aurweb.config.getint("options", "persistent_cookie_timeout")
perma_timeout = aurweb.config.getint("options", "permanent_cookie_timeout")
sid = _retry_login(request, user, passwd)
if not sid:
return await login_template(request, next, errors=["Bad username or password."])
response = RedirectResponse(url=next, status_code=HTTPStatus.SEE_OTHER)
secure = aurweb.config.getboolean("options", "disable_http_login")
response.set_cookie(
"AURSID",
sid,
max_age=cookie_timeout,
secure=secure,
httponly=secure,
samesite=cookies.samesite(),
)
response.set_cookie(
"AURREMEMBER",
remember_me,
max_age=perma_timeout,
secure=secure,
httponly=secure,
samesite=cookies.samesite(),
)
return response
@db.retry_deadlock
def _retry_logout(request: Request) -> None:
request.user.logout(request)
@router.post("/logout")
@handle_form_exceptions
@requires_auth
async def logout(request: Request, next: str = Form(default="/")):
if request.user.is_authenticated():
_retry_logout(request)
# Use 303 since we may be handling a post request, that'll get it
# to redirect to a get request.
response = RedirectResponse(url=next, status_code=HTTPStatus.SEE_OTHER)
response.delete_cookie("AURSID")
response.delete_cookie("AURREMEMBER")
return response

227
aurweb/routers/html.py Normal file
View file

@ -0,0 +1,227 @@
""" AURWeb's primary routing module. Define all routes via @app.app.{get,post}
decorators in some way; more complex routes should be defined in their
own modules and imported here. """
import os
from http import HTTPStatus
from fastapi import APIRouter, Form, HTTPException, Request, Response
from fastapi.responses import HTMLResponse, RedirectResponse
from prometheus_client import (
CONTENT_TYPE_LATEST,
CollectorRegistry,
generate_latest,
multiprocess,
)
from sqlalchemy import case, or_
import aurweb.config
import aurweb.models.package_request
from aurweb import aur_logging, cookies, db, models, statistics, time, util
from aurweb.exceptions import handle_form_exceptions
from aurweb.models.package_request import PENDING_ID
from aurweb.packages.util import query_notified, query_voted, updated_packages
from aurweb.templates import make_context, render_template
logger = aur_logging.get_logger(__name__)
router = APIRouter()
@router.get("/favicon.ico")
async def favicon(request: Request):
"""Some browsers attempt to find a website's favicon via root uri at
/favicon.ico, so provide a redirection here to our static icon."""
return RedirectResponse("/static/images/favicon.ico")
@db.async_retry_deadlock
@router.post("/language", response_class=RedirectResponse)
@handle_form_exceptions
async def language(
request: Request,
set_lang: str = Form(...),
next: str = Form(...),
q: str = Form(default=None),
):
"""
A POST route used to set a session's language.
Return a 303 See Other redirect to {next}?next={next}. If we are
setting the language on any page, we want to preserve query
parameters across the redirect.
"""
if next[0] != "/":
return HTMLResponse(b"Invalid 'next' parameter.", status_code=400)
query_string = "?" + q if q else str()
response = RedirectResponse(
url=f"{next}{query_string}", status_code=HTTPStatus.SEE_OTHER
)
# If the user is authenticated, update the user's LangPreference.
# Otherwise set an AURLANG cookie
if request.user.is_authenticated():
with db.begin():
request.user.LangPreference = set_lang
else:
secure = aurweb.config.getboolean("options", "disable_http_login")
perma_timeout = aurweb.config.getint("options", "permanent_cookie_timeout")
response.set_cookie(
"AURLANG",
set_lang,
secure=secure,
httponly=secure,
max_age=perma_timeout,
samesite=cookies.samesite(),
)
return response
@router.get("/", response_class=HTMLResponse)
async def index(request: Request):
"""Homepage route."""
context = make_context(request, "Home")
context["ssh_fingerprints"] = util.get_ssh_fingerprints()
cache_expire = aurweb.config.getint("cache", "expiry_time_statistics", 300)
# Package statistics.
counts = statistics.get_homepage_counts()
for k in counts:
context[k] = counts[k]
# Get the 15 most recently updated packages.
context["package_updates"] = updated_packages(15, cache_expire)
if request.user.is_authenticated():
# Authenticated users get a few extra pieces of data for
# the dashboard display.
packages = db.query(models.Package).join(models.PackageBase)
maintained = (
packages.join(
models.PackageComaintainer,
models.PackageComaintainer.PackageBaseID == models.PackageBase.ID,
isouter=True,
)
.join(
models.User,
or_(
models.PackageBase.MaintainerUID == models.User.ID,
models.PackageComaintainer.UsersID == models.User.ID,
),
)
.filter(models.User.ID == request.user.ID)
)
# Packages maintained by the user that have been flagged.
context["flagged_packages"] = (
maintained.filter(models.PackageBase.OutOfDateTS.isnot(None))
.order_by(models.PackageBase.ModifiedTS.desc(), models.Package.Name.asc())
.limit(50)
.all()
)
# Flagged packages that request.user has voted for.
context["flagged_packages_voted"] = query_voted(
context.get("flagged_packages"), request.user
)
# Flagged packages that request.user is being notified about.
context["flagged_packages_notified"] = query_notified(
context.get("flagged_packages"), request.user
)
archive_time = aurweb.config.getint("options", "request_archive_time")
start = time.utcnow() - archive_time
# Package requests created by request.user.
context["package_requests"] = (
request.user.package_requests.filter(
models.PackageRequest.RequestTS >= start
)
.order_by(
# Order primarily by the Status column being PENDING_ID,
# and secondarily by RequestTS; both in descending order.
case([(models.PackageRequest.Status == PENDING_ID, 1)], else_=0).desc(),
models.PackageRequest.RequestTS.desc(),
)
.limit(50)
.all()
)
# Packages that the request user maintains or comaintains.
context["packages"] = (
maintained.filter(models.User.ID == models.PackageBase.MaintainerUID)
.order_by(models.PackageBase.ModifiedTS.desc(), models.Package.Name.desc())
.limit(50)
.all()
)
# Packages that request.user has voted for.
context["packages_voted"] = query_voted(context.get("packages"), request.user)
# Packages that request.user is being notified about.
context["packages_notified"] = query_notified(
context.get("packages"), request.user
)
# Any packages that the request user comaintains.
context["comaintained"] = (
packages.join(models.PackageComaintainer)
.filter(models.PackageComaintainer.UsersID == request.user.ID)
.order_by(models.PackageBase.ModifiedTS.desc(), models.Package.Name.desc())
.limit(50)
.all()
)
# Comaintained packages that request.user has voted for.
context["comaintained_voted"] = query_voted(
context.get("comaintained"), request.user
)
# Comaintained packages that request.user is being notified about.
context["comaintained_notified"] = query_notified(
context.get("comaintained"), request.user
)
return render_template(request, "index.html", context)
@router.get("/{archive}.sha256")
async def archive_sha256(request: Request, archive: str):
archivedir = aurweb.config.get("mkpkglists", "archivedir")
hashfile = os.path.join(archivedir, f"{archive}.sha256")
if not os.path.exists(hashfile):
raise HTTPException(status_code=HTTPStatus.NOT_FOUND)
with open(hashfile) as f:
hash_value = f.read()
headers = {"Content-Type": "text/plain"}
return Response(hash_value, headers=headers)
@router.get("/metrics")
async def metrics(request: Request):
if not os.environ.get("PROMETHEUS_MULTIPROC_DIR", None):
return Response(
"Prometheus metrics are not enabled.",
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
)
# update prometheus gauges for packages and users
statistics.update_prometheus_metrics()
registry = CollectorRegistry()
multiprocess.MultiProcessCollector(registry)
data = generate_latest(registry)
headers = {"Content-Type": CONTENT_TYPE_LATEST, "Content-Length": str(len(data))}
return Response(data, headers=headers)
@router.get("/raisefivethree", response_class=HTMLResponse)
async def raise_service_unavailable(request: Request):
raise HTTPException(status_code=HTTPStatus.SERVICE_UNAVAILABLE)

View file

@ -0,0 +1,394 @@
import html
import typing
from http import HTTPStatus
from typing import Any
from fastapi import APIRouter, Form, HTTPException, Request
from fastapi.responses import RedirectResponse, Response
from sqlalchemy import and_, func, or_
from aurweb import aur_logging, db, l10n, models, time
from aurweb.auth import creds, requires_auth
from aurweb.exceptions import handle_form_exceptions
from aurweb.models import User
from aurweb.models.account_type import (
PACKAGE_MAINTAINER_AND_DEV_ID,
PACKAGE_MAINTAINER_ID,
)
from aurweb.templates import make_context, make_variable_context, render_template
router = APIRouter()
logger = aur_logging.get_logger(__name__)
# Some PM route specific constants.
ITEMS_PER_PAGE = 10 # Paged table size.
MAX_AGENDA_LENGTH = 75 # Agenda table column length.
ADDVOTE_SPECIFICS = {
# This dict stores a vote duration and quorum for a proposal.
# When a proposal is added, duration is added to the current
# timestamp.
# "addvote_type": (duration, quorum)
"add_pm": (7 * 24 * 60 * 60, 0.66),
"remove_pm": (7 * 24 * 60 * 60, 0.75),
"remove_inactive_pm": (5 * 24 * 60 * 60, 0.66),
"bylaws": (7 * 24 * 60 * 60, 0.75),
}
def populate_package_maintainer_counts(context: dict[str, Any]) -> None:
pm_query = db.query(User).filter(
or_(
User.AccountTypeID == PACKAGE_MAINTAINER_ID,
User.AccountTypeID == PACKAGE_MAINTAINER_AND_DEV_ID,
)
)
context["package_maintainer_count"] = pm_query.count()
# In case any records have a None InactivityTS.
active_pm_query = pm_query.filter(
or_(User.InactivityTS.is_(None), User.InactivityTS == 0)
)
context["active_package_maintainer_count"] = active_pm_query.count()
@router.get("/package-maintainer")
@requires_auth
async def package_maintainer(
request: Request,
coff: int = 0, # current offset
cby: str = "desc", # current by
poff: int = 0, # past offset
pby: str = "desc",
): # past by
"""Proposal listings."""
if not request.user.has_credential(creds.PM_LIST_VOTES):
return RedirectResponse("/", status_code=HTTPStatus.SEE_OTHER)
context = make_context(request, "Package Maintainer")
current_by, past_by = cby, pby
current_off, past_off = coff, poff
context["pp"] = pp = ITEMS_PER_PAGE
context["prev_len"] = MAX_AGENDA_LENGTH
ts = time.utcnow()
if current_by not in {"asc", "desc"}:
# If a malicious by was given, default to desc.
current_by = "desc"
context["current_by"] = current_by
if past_by not in {"asc", "desc"}:
# If a malicious by was given, default to desc.
past_by = "desc"
context["past_by"] = past_by
current_votes = (
db.query(models.VoteInfo)
.filter(models.VoteInfo.End > ts)
.order_by(models.VoteInfo.Submitted.desc())
)
context["current_votes_count"] = current_votes.count()
current_votes = current_votes.limit(pp).offset(current_off)
context["current_votes"] = (
reversed(current_votes.all()) if current_by == "asc" else current_votes.all()
)
context["current_off"] = current_off
past_votes = (
db.query(models.VoteInfo)
.filter(models.VoteInfo.End <= ts)
.order_by(models.VoteInfo.Submitted.desc())
)
context["past_votes_count"] = past_votes.count()
past_votes = past_votes.limit(pp).offset(past_off)
context["past_votes"] = (
reversed(past_votes.all()) if past_by == "asc" else past_votes.all()
)
context["past_off"] = past_off
last_vote = func.max(models.Vote.VoteID).label("LastVote")
last_votes_by_pm = (
db.query(models.Vote)
.join(models.User)
.join(models.VoteInfo, models.VoteInfo.ID == models.Vote.VoteID)
.filter(
and_(
models.Vote.VoteID == models.VoteInfo.ID,
models.User.ID == models.Vote.UserID,
models.VoteInfo.End < ts,
or_(models.User.AccountTypeID == 2, models.User.AccountTypeID == 4),
)
)
.with_entities(models.Vote.UserID, last_vote, models.User.Username)
.group_by(models.Vote.UserID)
.order_by(last_vote.desc(), models.User.Username.asc())
)
context["last_votes_by_pm"] = last_votes_by_pm.all()
context["current_by_next"] = "asc" if current_by == "desc" else "desc"
context["past_by_next"] = "asc" if past_by == "desc" else "desc"
populate_package_maintainer_counts(context)
context["q"] = {
"coff": current_off,
"cby": current_by,
"poff": past_off,
"pby": past_by,
}
return render_template(request, "package-maintainer/index.html", context)
def render_proposal(
request: Request,
context: dict,
proposal: int,
voteinfo: models.VoteInfo,
voters: typing.Iterable[models.User],
vote: models.Vote,
status_code: HTTPStatus = HTTPStatus.OK,
):
"""Render a single PM proposal."""
context["proposal"] = proposal
context["voteinfo"] = voteinfo
context["voters"] = voters.all()
total = voteinfo.total_votes()
participation = (total / voteinfo.ActiveUsers) if voteinfo.ActiveUsers else 0
context["participation"] = participation
accepted = (voteinfo.Yes > voteinfo.ActiveUsers / 2) or (
participation > voteinfo.Quorum and voteinfo.Yes > voteinfo.No
)
context["accepted"] = accepted
can_vote = voters.filter(models.Vote.User == request.user).first() is None
context["can_vote"] = can_vote
if not voteinfo.is_running():
context["error"] = "Voting is closed for this proposal."
context["vote"] = vote
context["has_voted"] = vote is not None
return render_template(
request, "package-maintainer/show.html", context, status_code=status_code
)
@router.get("/package-maintainer/{proposal}")
@requires_auth
async def package_maintainer_proposal(request: Request, proposal: int):
if not request.user.has_credential(creds.PM_LIST_VOTES):
return RedirectResponse("/package-maintainer", status_code=HTTPStatus.SEE_OTHER)
context = await make_variable_context(request, "Package Maintainer")
proposal = int(proposal)
voteinfo = db.query(models.VoteInfo).filter(models.VoteInfo.ID == proposal).first()
if not voteinfo:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND)
voters = (
db.query(models.User)
.join(models.Vote)
.filter(models.Vote.VoteID == voteinfo.ID)
)
vote = (
db.query(models.Vote)
.filter(
and_(
models.Vote.UserID == request.user.ID,
models.Vote.VoteID == voteinfo.ID,
)
)
.first()
)
if not request.user.has_credential(creds.PM_VOTE):
context["error"] = "Only Package Maintainers are allowed to vote."
if voteinfo.User == request.user.Username:
context["error"] = "You cannot vote in an proposal about you."
elif vote is not None:
context["error"] = "You've already voted for this proposal."
context["vote"] = vote
return render_proposal(request, context, proposal, voteinfo, voters, vote)
@db.async_retry_deadlock
@router.post("/package-maintainer/{proposal}")
@handle_form_exceptions
@requires_auth
async def package_maintainer_proposal_post(
request: Request, proposal: int, decision: str = Form(...)
):
if not request.user.has_credential(creds.PM_LIST_VOTES):
return RedirectResponse("/package-maintainer", status_code=HTTPStatus.SEE_OTHER)
context = await make_variable_context(request, "Package Maintainer")
proposal = int(proposal) # Make sure it's an int.
voteinfo = db.query(models.VoteInfo).filter(models.VoteInfo.ID == proposal).first()
if not voteinfo:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND)
voters = (
db.query(models.User)
.join(models.Vote)
.filter(models.Vote.VoteID == voteinfo.ID)
)
vote = (
db.query(models.Vote)
.filter(
and_(
models.Vote.UserID == request.user.ID,
models.Vote.VoteID == voteinfo.ID,
)
)
.first()
)
status_code = HTTPStatus.OK
if not request.user.has_credential(creds.PM_VOTE):
context["error"] = "Only Package Maintainers are allowed to vote."
status_code = HTTPStatus.UNAUTHORIZED
elif voteinfo.User == request.user.Username:
context["error"] = "You cannot vote in an proposal about you."
status_code = HTTPStatus.BAD_REQUEST
elif vote is not None:
context["error"] = "You've already voted for this proposal."
status_code = HTTPStatus.BAD_REQUEST
if status_code != HTTPStatus.OK:
return render_proposal(
request, context, proposal, voteinfo, voters, vote, status_code=status_code
)
with db.begin():
if decision in {"Yes", "No", "Abstain"}:
# Increment whichever decision was given to us.
setattr(voteinfo, decision, getattr(voteinfo, decision) + 1)
else:
return Response(
"Invalid 'decision' value.", status_code=HTTPStatus.BAD_REQUEST
)
vote = db.create(models.Vote, User=request.user, VoteInfo=voteinfo)
context["error"] = "You've already voted for this proposal."
return render_proposal(request, context, proposal, voteinfo, voters, vote)
@router.get("/addvote")
@requires_auth
async def package_maintainer_addvote(
request: Request, user: str = str(), type: str = "add_pm", agenda: str = str()
):
if not request.user.has_credential(creds.PM_ADD_VOTE):
return RedirectResponse("/package-maintainer", status_code=HTTPStatus.SEE_OTHER)
context = await make_variable_context(request, "Add Proposal")
if type not in ADDVOTE_SPECIFICS:
context["error"] = "Invalid type."
type = "add_pm" # Default it.
context["user"] = user
context["type"] = type
context["agenda"] = agenda
return render_template(request, "addvote.html", context)
@db.async_retry_deadlock
@router.post("/addvote")
@handle_form_exceptions
@requires_auth
async def package_maintainer_addvote_post(
request: Request,
user: str = Form(default=str()),
type: str = Form(default=str()),
agenda: str = Form(default=str()),
):
if not request.user.has_credential(creds.PM_ADD_VOTE):
return RedirectResponse("/package-maintainer", status_code=HTTPStatus.SEE_OTHER)
# Build a context.
context = await make_variable_context(request, "Add Proposal")
context["type"] = type
context["user"] = user
context["agenda"] = agenda
def render_addvote(context, status_code):
"""Simplify render_template a bit for this test."""
return render_template(request, "addvote.html", context, status_code)
# Alright, get some database records, if we can.
if type != "bylaws":
user_record = db.query(models.User).filter(models.User.Username == user).first()
if user_record is None:
context["error"] = "Username does not exist."
return render_addvote(context, HTTPStatus.NOT_FOUND)
utcnow = time.utcnow()
voteinfo = (
db.query(models.VoteInfo)
.filter(and_(models.VoteInfo.User == user, models.VoteInfo.End > utcnow))
.count()
)
if voteinfo:
_ = l10n.get_translator_for_request(request)
context["error"] = _("%s already has proposal running for them.") % (
html.escape(user),
)
return render_addvote(context, HTTPStatus.BAD_REQUEST)
if type not in ADDVOTE_SPECIFICS:
context["error"] = "Invalid type."
context["type"] = type = "add_pm" # Default for rendering.
return render_addvote(context, HTTPStatus.BAD_REQUEST)
if not agenda:
context["error"] = "Proposal cannot be empty."
return render_addvote(context, HTTPStatus.BAD_REQUEST)
# Gather some mapped constants and the current timestamp.
duration, quorum = ADDVOTE_SPECIFICS.get(type)
timestamp = time.utcnow()
# Active PM types we filter for.
types = {PACKAGE_MAINTAINER_ID, PACKAGE_MAINTAINER_AND_DEV_ID}
# Create a new VoteInfo (proposal)!
with db.begin():
active_pms = (
db.query(User)
.filter(
and_(
User.Suspended == 0,
User.InactivityTS.isnot(None),
User.AccountTypeID.in_(types),
)
)
.count()
)
voteinfo = db.create(
models.VoteInfo,
User=user,
Agenda=html.escape(agenda),
Submitted=timestamp,
End=(timestamp + duration),
Quorum=quorum,
ActiveUsers=active_pms,
Submitter=request.user,
)
# Redirect to the new proposal.
endpoint = f"/package-maintainer/{voteinfo.ID}"
return RedirectResponse(endpoint, status_code=HTTPStatus.SEE_OTHER)

518
aurweb/routers/packages.py Normal file
View file

@ -0,0 +1,518 @@
from collections import defaultdict
from http import HTTPStatus
from typing import Any
from fastapi import APIRouter, Form, Query, Request, Response
import aurweb.filters # noqa: F401
from aurweb import aur_logging, config, db, defaults, models, util
from aurweb.auth import creds, requires_auth
from aurweb.cache import db_count_cache, db_query_cache
from aurweb.exceptions import InvariantError, handle_form_exceptions
from aurweb.models.relation_type import CONFLICTS_ID, PROVIDES_ID, REPLACES_ID
from aurweb.packages import util as pkgutil
from aurweb.packages.search import PackageSearch
from aurweb.packages.util import get_pkg_or_base
from aurweb.pkgbase import actions as pkgbase_actions, util as pkgbaseutil
from aurweb.templates import make_context, make_variable_context, render_template
from aurweb.util import hash_query
logger = aur_logging.get_logger(__name__)
router = APIRouter()
async def packages_get(
request: Request, context: dict[str, Any], status_code: HTTPStatus = HTTPStatus.OK
):
# Query parameters used in this request.
context["q"] = dict(request.query_params)
# Per page and offset.
offset, per_page = util.sanitize_params(
request.query_params.get("O", defaults.O),
request.query_params.get("PP", defaults.PP),
)
context["O"] = offset
# Limit PP to options.max_search_results
max_search_results = config.getint("options", "max_search_results")
context["PP"] = per_page = min(per_page, max_search_results)
# Query search by.
search_by = context["SeB"] = request.query_params.get("SeB", "nd")
# Query sort by.
sort_by = request.query_params.get("SB", None)
# Query sort order.
sort_order = request.query_params.get("SO", None)
# Apply ordering, limit and offset.
search = PackageSearch(request.user)
# For each keyword found in K, apply a search_by filter.
# This means that for any sentences separated by spaces,
# they are used as if they were ANDed.
keywords = context["K"] = request.query_params.get("K", str())
keywords = keywords.split(" ")
if search_by == "k":
# If we're searchin by keywords, supply a set of keywords.
search.search_by(search_by, set(keywords))
else:
for keyword in keywords:
search.search_by(search_by, keyword)
flagged = request.query_params.get("outdated", None)
if flagged:
# If outdated was given, set it up in the context.
context["outdated"] = flagged
# When outdated is set to "on," we filter records which do have
# an OutOfDateTS. When it's set to "off," we filter out any which
# do **not** have OutOfDateTS.
criteria = None
if flagged == "on":
criteria = models.PackageBase.OutOfDateTS.isnot
else:
criteria = models.PackageBase.OutOfDateTS.is_
# Apply the flag criteria to our PackageSearch.query.
search.query = search.query.filter(criteria(None))
submit = request.query_params.get("submit", "Go")
if submit == "Orphans":
# If the user clicked the "Orphans" button, we only want
# orphaned packages.
search.query = search.query.filter(models.PackageBase.MaintainerUID.is_(None))
# Collect search result count here; we've applied our keywords.
# Including more query operations below, like ordering, will
# increase the amount of time required to collect a count.
# we use redis for caching the results of the query
cache_expire = config.getint("cache", "expiry_time_search", 600)
num_packages = db_count_cache(hash_query(search.query), search.query, cache_expire)
# Apply user-specified sort column and ordering.
search.sort_by(sort_by, sort_order)
# Insert search results into the context.
results = search.results().with_entities(
models.Package.ID,
models.Package.Name,
models.Package.PackageBaseID,
models.Package.Version,
models.Package.Description,
models.PackageBase.Popularity,
models.PackageBase.NumVotes,
models.PackageBase.OutOfDateTS,
models.PackageBase.ModifiedTS,
models.User.Username.label("Maintainer"),
models.PackageVote.PackageBaseID.label("Voted"),
models.PackageNotification.PackageBaseID.label("Notify"),
)
# paging
results = results.limit(per_page).offset(offset)
# we use redis for caching the results of the query
packages = db_query_cache(hash_query(results), results, cache_expire)
context["packages"] = packages
context["packages_count"] = num_packages
return render_template(
request, "packages/index.html", context, status_code=status_code
)
@router.get("/packages")
async def packages(request: Request) -> Response:
context = await make_variable_context(request, "Packages")
return await packages_get(request, context)
@router.get("/packages/{name}")
async def package(
request: Request,
name: str,
all_deps: bool = Query(default=False),
all_reqs: bool = Query(default=False),
) -> Response:
"""
Get a package by name.
By default, we limit the number of depends and requires results
to 20. To bypass this and load all of them, which should be triggered
via a "Show more" link near the limited listing.
:param name: Package.Name
:param all_deps: Boolean indicating whether we should load all depends
:param all_reqs: Boolean indicating whether we should load all requires
:return: FastAPI Response
"""
# Get the Package.
pkg = get_pkg_or_base(name, models.Package)
pkgbase = pkg.PackageBase
rels = pkg.package_relations.order_by(models.PackageRelation.RelName.asc())
rels_data = defaultdict(list)
for rel in rels:
if rel.RelTypeID == CONFLICTS_ID:
rels_data["c"].append(rel)
elif rel.RelTypeID == PROVIDES_ID:
rels_data["p"].append(rel)
elif rel.RelTypeID == REPLACES_ID:
rels_data["r"].append(rel)
# Add our base information.
context = pkgbaseutil.make_context(request, pkgbase)
context["q"] = dict(request.query_params)
context.update({"all_deps": all_deps, "all_reqs": all_reqs})
context["package"] = pkg
# Package sources.
context["sources"] = pkg.package_sources.order_by(
models.PackageSource.Source.asc()
).all()
# Listing metadata.
context["max_listing"] = max_listing = 20
# Package dependencies.
deps = pkg.package_dependencies.order_by(
models.PackageDependency.DepTypeID.asc(), models.PackageDependency.DepName.asc()
)
context["depends_count"] = deps.count()
if not all_deps:
deps = deps.limit(max_listing)
context["dependencies"] = deps.all()
# Existing dependencies to avoid multiple lookups
context["dependencies_names_from_aur"] = [
item.Name
for item in db.query(models.Package)
.filter(
models.Package.Name.in_(
pkg.package_dependencies.with_entities(models.PackageDependency.DepName)
)
)
.all()
]
# Package requirements (other packages depend on this one).
reqs = pkgutil.pkg_required(pkg.Name, [p.RelName for p in rels_data.get("p", [])])
context["reqs_count"] = reqs.count()
if not all_reqs:
reqs = reqs.limit(max_listing)
context["required_by"] = reqs.all()
context["licenses"] = pkg.package_licenses
context["groups"] = pkg.package_groups
conflicts = pkg.package_relations.filter(
models.PackageRelation.RelTypeID == CONFLICTS_ID
).order_by(models.PackageRelation.RelName.asc())
context["conflicts"] = conflicts
provides = pkg.package_relations.filter(
models.PackageRelation.RelTypeID == PROVIDES_ID
).order_by(models.PackageRelation.RelName.asc())
context["provides"] = provides
replaces = pkg.package_relations.filter(
models.PackageRelation.RelTypeID == REPLACES_ID
).order_by(models.PackageRelation.RelName.asc())
context["replaces"] = replaces
return render_template(request, "packages/show.html", context)
async def packages_unflag(request: Request, package_ids: list[int] = [], **kwargs):
if not package_ids:
return False, ["You did not select any packages to unflag."]
# Holds the set of package bases we're looking to unflag.
# Constructed below via looping through the packages query.
bases = set()
package_ids = set(package_ids) # Convert this to a set for O(1).
packages = db.query(models.Package).filter(models.Package.ID.in_(package_ids)).all()
for pkg in packages:
has_cred = request.user.has_credential(
creds.PKGBASE_UNFLAG, approved=[pkg.PackageBase.Flagger]
)
if not has_cred:
return False, ["You did not select any packages to unflag."]
if pkg.PackageBase not in bases:
bases.update({pkg.PackageBase})
for pkgbase in bases:
pkgbase_actions.pkgbase_unflag_instance(request, pkgbase)
return True, ["The selected packages have been unflagged."]
async def packages_notify(request: Request, package_ids: list[int] = [], **kwargs):
# In cases where we encounter errors with the request, we'll
# use this error tuple as a return value.
# TODO: This error does not yet have a translation.
error_tuple = (False, ["You did not select any packages to be notified about."])
if not package_ids:
return error_tuple
bases = set()
package_ids = set(package_ids)
packages = db.query(models.Package).filter(models.Package.ID.in_(package_ids)).all()
for pkg in packages:
if pkg.PackageBase not in bases:
bases.update({pkg.PackageBase})
# Perform some checks on what the user selected for notify.
for pkgbase in bases:
notif = db.query(
pkgbase.notifications.filter(
models.PackageNotification.UserID == request.user.ID
).exists()
).scalar()
has_cred = request.user.has_credential(creds.PKGBASE_NOTIFY)
# If the request user either does not have credentials
# or the notification already exists:
if not (has_cred and not notif):
return error_tuple
# If we get here, user input is good.
for pkgbase in bases:
pkgbase_actions.pkgbase_notify_instance(request, pkgbase)
# TODO: This message does not yet have a translation.
return True, ["The selected packages' notifications have been enabled."]
async def packages_unnotify(request: Request, package_ids: list[int] = [], **kwargs):
if not package_ids:
# TODO: This error does not yet have a translation.
return False, ["You did not select any packages for notification removal."]
# TODO: This error does not yet have a translation.
error_tuple = (
False,
["A package you selected does not have notifications enabled."],
)
bases = set()
package_ids = set(package_ids)
packages = db.query(models.Package).filter(models.Package.ID.in_(package_ids)).all()
for pkg in packages:
if pkg.PackageBase not in bases:
bases.update({pkg.PackageBase})
# Perform some checks on what the user selected for notify.
for pkgbase in bases:
notif = db.query(
pkgbase.notifications.filter(
models.PackageNotification.UserID == request.user.ID
).exists()
).scalar()
if not notif:
return error_tuple
for pkgbase in bases:
pkgbase_actions.pkgbase_unnotify_instance(request, pkgbase)
# TODO: This message does not yet have a translation.
return True, ["The selected packages' notifications have been removed."]
async def packages_adopt(
request: Request, package_ids: list[int] = [], confirm: bool = False, **kwargs
):
if not package_ids:
return False, ["You did not select any packages to adopt."]
if not confirm:
return (
False,
[
"The selected packages have not been adopted, "
"check the confirmation checkbox."
],
)
bases = set()
package_ids = set(package_ids)
packages = db.query(models.Package).filter(models.Package.ID.in_(package_ids)).all()
for pkg in packages:
if pkg.PackageBase not in bases:
bases.update({pkg.PackageBase})
# Check that the user has credentials for every package they selected.
for pkgbase in bases:
has_cred = request.user.has_credential(creds.PKGBASE_ADOPT)
if not (has_cred or not pkgbase.Maintainer):
# TODO: This error needs to be translated.
return (
False,
["You are not allowed to adopt one of the " "packages you selected."],
)
# Now, really adopt the bases.
for pkgbase in bases:
pkgbase_actions.pkgbase_adopt_instance(request, pkgbase)
return True, ["The selected packages have been adopted."]
def disown_all(request: Request, pkgbases: list[models.PackageBase]) -> list[str]:
errors = []
for pkgbase in pkgbases:
try:
pkgbase_actions.pkgbase_disown_instance(request, pkgbase)
except InvariantError as exc:
errors.append(str(exc))
return errors
async def packages_disown(
request: Request, package_ids: list[int] = [], confirm: bool = False, **kwargs
):
if not package_ids:
return False, ["You did not select any packages to disown."]
if not confirm:
return (
False,
[
"The selected packages have not been disowned, "
"check the confirmation checkbox."
],
)
bases = set()
package_ids = set(package_ids)
packages = db.query(models.Package).filter(models.Package.ID.in_(package_ids)).all()
for pkg in packages:
if pkg.PackageBase not in bases:
bases.update({pkg.PackageBase})
# Check that the user has credentials for every package they selected.
for pkgbase in bases:
has_cred = request.user.has_credential(
creds.PKGBASE_DISOWN, approved=[pkgbase.Maintainer]
)
if not has_cred:
# TODO: This error needs to be translated.
return (
False,
["You are not allowed to disown one " "of the packages you selected."],
)
# Now, disown all the bases if we can.
if errors := disown_all(request, bases):
return False, errors
return True, ["The selected packages have been disowned."]
async def packages_delete(
request: Request,
package_ids: list[int] = [],
confirm: bool = False,
merge_into: str = str(),
**kwargs,
):
if not package_ids:
return False, ["You did not select any packages to delete."]
if not confirm:
return (
False,
[
"The selected packages have not been deleted, "
"check the confirmation checkbox."
],
)
if not request.user.has_credential(creds.PKGBASE_DELETE):
return False, ["You do not have permission to delete packages."]
# set-ify package_ids and query the database for related records.
package_ids = set(package_ids)
packages = db.query(models.Package).filter(models.Package.ID.in_(package_ids)).all()
if len(packages) != len(package_ids):
# Let the user know there was an issue with their input: they have
# provided at least one package_id which does not exist in the DB.
# TODO: This error has not yet been translated.
return False, ["One of the packages you selected does not exist."]
# Make a set out of all package bases related to `packages`.
bases = {pkg.PackageBase for pkg in packages}
deleted_bases, notifs = [], []
for pkgbase in bases:
deleted_bases.append(pkgbase.Name)
notifs += pkgbase_actions.pkgbase_delete_instance(request, pkgbase)
# Log out the fact that this happened for accountability.
logger.info(
f"Privileged user '{request.user.Username}' deleted the "
f"following package bases: {str(deleted_bases)}."
)
util.apply_all(notifs, lambda n: n.send())
return True, ["The selected packages have been deleted."]
# A mapping of action string -> callback functions used within the
# `packages_post` route below. We expect any action callback to
# return a tuple in the format: (succeeded: bool, message: list[str]).
PACKAGE_ACTIONS = {
"unflag": packages_unflag,
"notify": packages_notify,
"unnotify": packages_unnotify,
"adopt": packages_adopt,
"disown": packages_disown,
"delete": packages_delete,
}
@router.post("/packages")
@handle_form_exceptions
@requires_auth
async def packages_post(
request: Request,
IDs: list[int] = Form(default=[]),
action: str = Form(default=str()),
confirm: bool = Form(default=False),
):
# If an invalid action is specified, just render GET /packages
# with an BAD_REQUEST status_code.
if action not in PACKAGE_ACTIONS:
context = make_context(request, "Packages")
return await packages_get(request, context, HTTPStatus.BAD_REQUEST)
context = make_context(request, "Packages")
# We deal with `IDs`, `merge_into` and `confirm` arguments
# within action callbacks.
callback = PACKAGE_ACTIONS.get(action)
retval = await callback(request, package_ids=IDs, confirm=confirm)
if retval: # If *anything* was returned:
success, messages = retval
if not success:
# If the first element was False:
context["errors"] = messages
return await packages_get(request, context, HTTPStatus.BAD_REQUEST)
else:
# Otherwise:
context["success"] = messages
return await packages_get(request, context)

987
aurweb/routers/pkgbase.py Normal file
View file

@ -0,0 +1,987 @@
from http import HTTPStatus
from fastapi import APIRouter, Form, HTTPException, Query, Request, Response
from fastapi.responses import JSONResponse, RedirectResponse
from sqlalchemy import and_
from aurweb import aur_logging, config, db, l10n, templates, time, util
from aurweb.auth import creds, requires_auth
from aurweb.exceptions import InvariantError, ValidationError, handle_form_exceptions
from aurweb.models import PackageBase
from aurweb.models.package_comment import PackageComment
from aurweb.models.package_keyword import PackageKeyword
from aurweb.models.package_notification import PackageNotification
from aurweb.models.package_request import ACCEPTED_ID, PENDING_ID, PackageRequest
from aurweb.models.package_vote import PackageVote
from aurweb.models.request_type import DELETION_ID, MERGE_ID, ORPHAN_ID
from aurweb.packages.requests import update_closure_comment
from aurweb.packages.util import get_pkg_or_base, get_pkgbase_comment
from aurweb.pkgbase import actions, util as pkgbaseutil, validate
from aurweb.scripts import notify, popupdate
from aurweb.scripts.rendercomment import update_comment_render_fastapi
from aurweb.templates import make_variable_context, render_template
logger = aur_logging.get_logger(__name__)
router = APIRouter()
@router.get("/pkgbase/{name}")
async def pkgbase(request: Request, name: str) -> Response:
"""
Single package base view.
:param request: FastAPI Request
:param name: PackageBase.Name
:return: HTMLResponse
"""
# Get the PackageBase.
pkgbase = get_pkg_or_base(name, PackageBase)
# Redirect to /packages if there's only one related Package
# and its name matches its PackageBase.
packages = pkgbase.packages.all()
pkg = packages[0]
if len(packages) == 1 and pkg.Name == pkgbase.Name:
return RedirectResponse(
f"/packages/{pkg.Name}", status_code=int(HTTPStatus.SEE_OTHER)
)
# Add our base information.
context = pkgbaseutil.make_context(request, pkgbase)
context["packages"] = packages
return render_template(request, "pkgbase/index.html", context)
@router.get("/pkgbase/{name}/voters")
async def pkgbase_voters(request: Request, name: str) -> Response:
"""
View of package base voters.
Requires `request.user` has creds.PKGBASE_LIST_VOTERS credential.
:param request: FastAPI Request
:param name: PackageBase.Name
:return: HTMLResponse
"""
# Get the PackageBase.
pkgbase = get_pkg_or_base(name, PackageBase)
if not request.user.has_credential(creds.PKGBASE_LIST_VOTERS):
return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER)
context = templates.make_context(request, "Voters")
context["pkgbase"] = pkgbase
return render_template(request, "pkgbase/voters.html", context)
@router.get("/pkgbase/{name}/flag-comment")
async def pkgbase_flag_comment(request: Request, name: str):
pkgbase = get_pkg_or_base(name, PackageBase)
if pkgbase.OutOfDateTS is None:
return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER)
context = templates.make_context(request, "Flag Comment")
context["pkgbase"] = pkgbase
return render_template(request, "pkgbase/flag-comment.html", context)
@db.async_retry_deadlock
@router.post("/pkgbase/{name}/keywords")
@handle_form_exceptions
async def pkgbase_keywords(
request: Request, name: str, keywords: str = Form(default=str())
):
pkgbase = get_pkg_or_base(name, PackageBase)
approved = [pkgbase.Maintainer] + [c.User for c in pkgbase.comaintainers]
has_cred = creds.has_credential(
request.user, creds.PKGBASE_SET_KEYWORDS, approved=approved
)
if not has_cred:
return Response(status_code=HTTPStatus.UNAUTHORIZED)
# Lowercase all keywords. Our database table is case insensitive,
# and providing CI duplicates of keywords is erroneous.
keywords = set(k.lower() for k in keywords.split())
# Delete all keywords which are not supplied by the user.
with db.begin():
other_keywords = pkgbase.keywords.filter(~PackageKeyword.Keyword.in_(keywords))
other_keyword_strings = set(kwd.Keyword.lower() for kwd in other_keywords)
existing_keywords = set(
kwd.Keyword.lower()
for kwd in pkgbase.keywords.filter(
~PackageKeyword.Keyword.in_(other_keyword_strings)
)
)
db.delete_all(other_keywords)
new_keywords = keywords.difference(existing_keywords)
for keyword in new_keywords:
db.create(PackageKeyword, PackageBase=pkgbase, Keyword=keyword)
return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER)
@router.get("/pkgbase/{name}/flag")
@requires_auth
async def pkgbase_flag_get(request: Request, name: str):
pkgbase = get_pkg_or_base(name, PackageBase)
has_cred = request.user.has_credential(creds.PKGBASE_FLAG)
if not has_cred or pkgbase.OutOfDateTS is not None:
return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER)
context = templates.make_context(request, "Flag Package Out-Of-Date")
context["pkgbase"] = pkgbase
return render_template(request, "pkgbase/flag.html", context)
@db.async_retry_deadlock
@router.post("/pkgbase/{name}/flag")
@handle_form_exceptions
@requires_auth
async def pkgbase_flag_post(
request: Request, name: str, comments: str = Form(default=str())
):
pkgbase = get_pkg_or_base(name, PackageBase)
if not comments:
context = templates.make_context(request, "Flag Package Out-Of-Date")
context["pkgbase"] = pkgbase
context["errors"] = [
"The selected packages have not been flagged, " "please enter a comment."
]
return render_template(
request, "pkgbase/flag.html", context, status_code=HTTPStatus.BAD_REQUEST
)
validate.comment_raise_http_ex(comments)
has_cred = request.user.has_credential(creds.PKGBASE_FLAG)
if has_cred and not pkgbase.OutOfDateTS:
now = time.utcnow()
with db.begin():
pkgbase.OutOfDateTS = now
pkgbase.Flagger = request.user
pkgbase.FlaggerComment = comments
notify.FlagNotification(request.user.ID, pkgbase.ID).send()
return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER)
@db.async_retry_deadlock
@router.post("/pkgbase/{name}/comments")
@handle_form_exceptions
@requires_auth
async def pkgbase_comments_post(
request: Request,
name: str,
comment: str = Form(default=str()),
enable_notifications: bool = Form(default=False),
):
"""Add a new comment via POST request."""
pkgbase = get_pkg_or_base(name, PackageBase)
validate.comment_raise_http_ex(comment)
# If the provided comment is different than the record's version,
# update the db record.
now = time.utcnow()
with db.begin():
comment = db.create(
PackageComment,
User=request.user,
PackageBase=pkgbase,
Comments=comment,
RenderedComment=str(),
CommentTS=now,
)
if enable_notifications and not request.user.notified(pkgbase):
db.create(PackageNotification, User=request.user, PackageBase=pkgbase)
update_comment_render_fastapi(comment)
notif = notify.CommentNotification(request.user.ID, pkgbase.ID, comment.ID)
notif.send()
# Redirect to the pkgbase page.
return RedirectResponse(
f"/pkgbase/{pkgbase.Name}#comment-{comment.ID}",
status_code=HTTPStatus.SEE_OTHER,
)
@router.get("/pkgbase/{name}/comments/{id}/form")
@requires_auth
async def pkgbase_comment_form(
request: Request, name: str, id: int, next: str = Query(default=None)
):
"""
Produce a comment form for comment {id}.
This route is used as a partial HTML endpoint when editing
package comments via Javascript. This endpoint used to be
part of the RPC as type=get-comment-form and has been
relocated here because the form returned cannot be used
externally and requires a POST request by the user.
:param request: FastAPI Request
:param name: PackageBase.Name
:param id: PackageComment.ID
:param next: Optional `next` value used for the comment form
:return: JSONResponse
"""
pkgbase = get_pkg_or_base(name, PackageBase)
comment = pkgbase.comments.filter(PackageComment.ID == id).first()
if not comment:
return JSONResponse({}, status_code=HTTPStatus.NOT_FOUND)
if not request.user.is_elevated() and request.user != comment.User:
return JSONResponse({}, status_code=HTTPStatus.UNAUTHORIZED)
context = pkgbaseutil.make_context(request, pkgbase)
context["comment"] = comment
if not next:
next = f"/pkgbase/{name}"
context["next"] = next
form = templates.render_raw_template(
request, "partials/packages/comment_form.html", context
)
return JSONResponse({"form": form})
@router.get("/pkgbase/{name}/comments/{id}/edit")
@requires_auth
async def pkgbase_comment_edit(
request: Request, name: str, id: int, next: str = Form(default=None)
):
"""
Render the non-javascript edit form.
:param request: FastAPI Request
:param name: PackageBase.Name
:param id: PackageComment.ID
:param next: Optional `next` parameter used in the POST request
:return: HTMLResponse
"""
pkgbase = get_pkg_or_base(name, PackageBase)
comment = get_pkgbase_comment(pkgbase, id)
if not next:
next = f"/pkgbase/{name}"
context = await make_variable_context(request, "Edit comment", next=next)
context["comment"] = comment
return render_template(request, "pkgbase/comments/edit.html", context)
@db.async_retry_deadlock
@router.post("/pkgbase/{name}/comments/{id}")
@handle_form_exceptions
@requires_auth
async def pkgbase_comment_post(
request: Request,
name: str,
id: int,
comment: str = Form(default=str()),
enable_notifications: bool = Form(default=False),
next: str = Form(default=None),
cancel: bool = Form(default=False),
):
"""Edit an existing comment."""
if cancel:
return RedirectResponse(
f"/pkgbase/{name}#comment-{id}", status_code=HTTPStatus.SEE_OTHER
)
pkgbase = get_pkg_or_base(name, PackageBase)
db_comment = get_pkgbase_comment(pkgbase, id)
validate.comment_raise_http_ex(comment)
if request.user.ID != db_comment.UsersID:
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED)
# If the provided comment is different than the record's version,
# update the db record.
now = time.utcnow()
if db_comment.Comments != comment:
with db.begin():
db_comment.Comments = comment
db_comment.Editor = request.user
db_comment.EditedTS = now
if enable_notifications:
with db.begin():
db_notif = request.user.notifications.filter(
PackageNotification.PackageBaseID == pkgbase.ID
).first()
if not db_notif:
db.create(PackageNotification, User=request.user, PackageBase=pkgbase)
update_comment_render_fastapi(db_comment)
if not next:
next = f"/pkgbase/{pkgbase.Name}"
# Redirect to the pkgbase page anchored to the updated comment.
return RedirectResponse(
f"{next}#comment-{db_comment.ID}", status_code=HTTPStatus.SEE_OTHER
)
@db.async_retry_deadlock
@router.post("/pkgbase/{name}/comments/{id}/pin")
@handle_form_exceptions
@requires_auth
async def pkgbase_comment_pin(
request: Request, name: str, id: int, next: str = Form(default=None)
):
"""
Pin a comment.
:param request: FastAPI Request
:param name: PackageBase.Name
:param id: PackageComment.ID
:param next: Optional `next` parameter used in the POST request
:return: RedirectResponse to `next`
"""
pkgbase = get_pkg_or_base(name, PackageBase)
comment = get_pkgbase_comment(pkgbase, id)
has_cred = request.user.has_credential(
creds.COMMENT_PIN, approved=comment.maintainers()
)
if not has_cred:
_ = l10n.get_translator_for_request(request)
raise HTTPException(
status_code=HTTPStatus.UNAUTHORIZED,
detail=_("You are not allowed to pin this comment."),
)
now = time.utcnow()
with db.begin():
comment.PinnedTS = now
if not next:
next = f"/pkgbase/{name}"
return RedirectResponse(next, status_code=HTTPStatus.SEE_OTHER)
@db.async_retry_deadlock
@router.post("/pkgbase/{name}/comments/{id}/unpin")
@handle_form_exceptions
@requires_auth
async def pkgbase_comment_unpin(
request: Request, name: str, id: int, next: str = Form(default=None)
):
"""
Unpin a comment.
:param request: FastAPI Request
:param name: PackageBase.Name
:param id: PackageComment.ID
:param next: Optional `next` parameter used in the POST request
:return: RedirectResponse to `next`
"""
pkgbase = get_pkg_or_base(name, PackageBase)
comment = get_pkgbase_comment(pkgbase, id)
has_cred = request.user.has_credential(
creds.COMMENT_PIN, approved=comment.maintainers()
)
if not has_cred:
_ = l10n.get_translator_for_request(request)
raise HTTPException(
status_code=HTTPStatus.UNAUTHORIZED,
detail=_("You are not allowed to unpin this comment."),
)
with db.begin():
comment.PinnedTS = 0
if not next:
next = f"/pkgbase/{name}"
return RedirectResponse(next, status_code=HTTPStatus.SEE_OTHER)
@db.async_retry_deadlock
@router.post("/pkgbase/{name}/comments/{id}/delete")
@handle_form_exceptions
@requires_auth
async def pkgbase_comment_delete(
request: Request, name: str, id: int, next: str = Form(default=None)
):
"""
Delete a comment.
This action does **not** delete the comment from the database, but
sets PackageBase.DelTS and PackageBase.DeleterUID, which is used to
decide who gets to view the comment and what utilities it gets.
:param request: FastAPI Request
:param name: PackageBase.Name
:param id: PackageComment.ID
:param next: Optional `next` parameter used in the POST request
:return: RedirectResposne to `next`
"""
pkgbase = get_pkg_or_base(name, PackageBase)
comment = get_pkgbase_comment(pkgbase, id)
authorized = request.user.has_credential(creds.COMMENT_DELETE, [comment.User])
if not authorized:
_ = l10n.get_translator_for_request(request)
raise HTTPException(
status_code=HTTPStatus.UNAUTHORIZED,
detail=_("You are not allowed to delete this comment."),
)
now = time.utcnow()
with db.begin():
comment.Deleter = request.user
comment.DelTS = now
if not next:
next = f"/pkgbase/{name}"
return RedirectResponse(next, status_code=HTTPStatus.SEE_OTHER)
@db.async_retry_deadlock
@router.post("/pkgbase/{name}/comments/{id}/undelete")
@handle_form_exceptions
@requires_auth
async def pkgbase_comment_undelete(
request: Request, name: str, id: int, next: str = Form(default=None)
):
"""
Undelete a comment.
This action does **not** undelete any comment from the database, but
unsets PackageBase.DelTS and PackageBase.DeleterUID which restores
the comment to a standard state.
:param request: FastAPI Request
:param name: PackageBase.Name
:param id: PackageComment.ID
:param next: Optional `next` parameter used in the POST request
:return: RedirectResponse to `next`
"""
pkgbase = get_pkg_or_base(name, PackageBase)
comment = get_pkgbase_comment(pkgbase, id)
has_cred = request.user.has_credential(
creds.COMMENT_UNDELETE, approved=[comment.User]
)
if not has_cred:
_ = l10n.get_translator_for_request(request)
raise HTTPException(
status_code=HTTPStatus.UNAUTHORIZED,
detail=_("You are not allowed to undelete this comment."),
)
with db.begin():
comment.Deleter = None
comment.DelTS = None
if not next:
next = f"/pkgbase/{name}"
return RedirectResponse(next, status_code=HTTPStatus.SEE_OTHER)
@db.async_retry_deadlock
@router.post("/pkgbase/{name}/vote")
@handle_form_exceptions
@requires_auth
async def pkgbase_vote(request: Request, name: str):
pkgbase = get_pkg_or_base(name, PackageBase)
vote = pkgbase.package_votes.filter(PackageVote.UsersID == request.user.ID).first()
has_cred = request.user.has_credential(creds.PKGBASE_VOTE)
if has_cred and not vote:
now = time.utcnow()
with db.begin():
db.create(PackageVote, User=request.user, PackageBase=pkgbase, VoteTS=now)
# Update NumVotes/Popularity.
popupdate.run_single(pkgbase)
return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER)
@db.async_retry_deadlock
@router.post("/pkgbase/{name}/unvote")
@handle_form_exceptions
@requires_auth
async def pkgbase_unvote(request: Request, name: str):
pkgbase = get_pkg_or_base(name, PackageBase)
vote = pkgbase.package_votes.filter(PackageVote.UsersID == request.user.ID).first()
has_cred = request.user.has_credential(creds.PKGBASE_VOTE)
if has_cred and vote:
with db.begin():
db.delete(vote)
# Update NumVotes/Popularity.
popupdate.run_single(pkgbase)
return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER)
@db.async_retry_deadlock
@router.post("/pkgbase/{name}/notify")
@handle_form_exceptions
@requires_auth
async def pkgbase_notify(request: Request, name: str):
pkgbase = get_pkg_or_base(name, PackageBase)
actions.pkgbase_notify_instance(request, pkgbase)
return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER)
@db.async_retry_deadlock
@router.post("/pkgbase/{name}/unnotify")
@handle_form_exceptions
@requires_auth
async def pkgbase_unnotify(request: Request, name: str):
pkgbase = get_pkg_or_base(name, PackageBase)
actions.pkgbase_unnotify_instance(request, pkgbase)
return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER)
@db.async_retry_deadlock
@router.post("/pkgbase/{name}/unflag")
@handle_form_exceptions
@requires_auth
async def pkgbase_unflag(request: Request, name: str):
pkgbase = get_pkg_or_base(name, PackageBase)
actions.pkgbase_unflag_instance(request, pkgbase)
return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER)
@router.get("/pkgbase/{name}/disown")
@requires_auth
async def pkgbase_disown_get(
request: Request, name: str, next: str = Query(default=str())
):
pkgbase = get_pkg_or_base(name, PackageBase)
comaints = {c.User for c in pkgbase.comaintainers}
approved = [pkgbase.Maintainer] + list(comaints)
has_cred = request.user.has_credential(creds.PKGBASE_DISOWN, approved=approved)
if not has_cred:
return RedirectResponse(f"/pkgbase/{name}", HTTPStatus.SEE_OTHER)
context = templates.make_context(request, "Disown Package")
context["pkgbase"] = pkgbase
context["next"] = next or "/pkgbase/{name}"
context["is_maint"] = request.user == pkgbase.Maintainer
context["is_comaint"] = request.user in comaints
return render_template(request, "pkgbase/disown.html", context)
@db.async_retry_deadlock
@router.post("/pkgbase/{name}/disown")
@handle_form_exceptions
@requires_auth
async def pkgbase_disown_post(
request: Request,
name: str,
comments: str = Form(default=str()),
confirm: bool = Form(default=False),
next: str = Form(default=str()),
):
pkgbase = get_pkg_or_base(name, PackageBase)
if comments:
validate.comment_raise_http_ex(comments)
comaints = {c.User for c in pkgbase.comaintainers}
approved = [pkgbase.Maintainer] + list(comaints)
has_cred = request.user.has_credential(creds.PKGBASE_DISOWN, approved=approved)
if not has_cred:
return RedirectResponse(f"/pkgbase/{name}", HTTPStatus.SEE_OTHER)
context = templates.make_context(request, "Disown Package")
context["pkgbase"] = pkgbase
context["is_maint"] = request.user == pkgbase.Maintainer
context["is_comaint"] = request.user in comaints
if not confirm:
context["errors"] = [
(
"The selected packages have not been disowned, "
"check the confirmation checkbox."
)
]
return render_template(
request, "pkgbase/disown.html", context, status_code=HTTPStatus.BAD_REQUEST
)
if request.user != pkgbase.Maintainer and request.user not in comaints:
with db.begin():
update_closure_comment(pkgbase, ORPHAN_ID, comments)
try:
actions.pkgbase_disown_instance(request, pkgbase)
except InvariantError as exc:
context["errors"] = [str(exc)]
return render_template(
request, "pkgbase/disown.html", context, status_code=HTTPStatus.BAD_REQUEST
)
next = next or f"/pkgbase/{name}"
return RedirectResponse(next, status_code=HTTPStatus.SEE_OTHER)
@db.async_retry_deadlock
@router.post("/pkgbase/{name}/adopt")
@handle_form_exceptions
@requires_auth
async def pkgbase_adopt_post(request: Request, name: str):
pkgbase = get_pkg_or_base(name, PackageBase)
has_cred = request.user.has_credential(creds.PKGBASE_ADOPT)
if has_cred or not pkgbase.Maintainer:
# If the user has credentials, they'll adopt the package regardless
# of maintainership. Otherwise, we'll promote the user to maintainer
# if no maintainer currently exists.
actions.pkgbase_adopt_instance(request, pkgbase)
return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER)
@router.get("/pkgbase/{name}/comaintainers")
@requires_auth
async def pkgbase_comaintainers(request: Request, name: str) -> Response:
# Get the PackageBase.
pkgbase = get_pkg_or_base(name, PackageBase)
# Unauthorized users (Non-TU/Dev and not the pkgbase maintainer)
# get redirected to the package base's page.
has_creds = request.user.has_credential(
creds.PKGBASE_EDIT_COMAINTAINERS, approved=[pkgbase.Maintainer]
)
if not has_creds:
return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER)
# Add our base information.
context = templates.make_context(request, "Manage Co-maintainers")
context.update(
{
"pkgbase": pkgbase,
"comaintainers": [c.User.Username for c in pkgbase.comaintainers],
}
)
return render_template(request, "pkgbase/comaintainers.html", context)
@db.async_retry_deadlock
@router.post("/pkgbase/{name}/comaintainers")
@handle_form_exceptions
@requires_auth
async def pkgbase_comaintainers_post(
request: Request, name: str, users: str = Form(default=str())
) -> Response:
# Get the PackageBase.
pkgbase = get_pkg_or_base(name, PackageBase)
# Unauthorized users (Non-TU/Dev and not the pkgbase maintainer)
# get redirected to the package base's page.
has_creds = request.user.has_credential(
creds.PKGBASE_EDIT_COMAINTAINERS, approved=[pkgbase.Maintainer]
)
if not has_creds:
return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER)
users = {e.strip() for e in users.split("\n") if bool(e.strip())}
records = {c.User.Username for c in pkgbase.comaintainers}
users_to_rm = records.difference(users)
pkgbaseutil.remove_comaintainers(pkgbase, users_to_rm)
logger.debug(
f"{request.user} removed comaintainers from " f"{pkgbase.Name}: {users_to_rm}"
)
users_to_add = users.difference(records)
error = pkgbaseutil.add_comaintainers(request, pkgbase, users_to_add)
if error:
context = templates.make_context(request, "Manage Co-maintainers")
context["pkgbase"] = pkgbase
context["comaintainers"] = [c.User.Username for c in pkgbase.comaintainers]
context["errors"] = [error]
return render_template(request, "pkgbase/comaintainers.html", context)
logger.debug(
f"{request.user} added comaintainers to " f"{pkgbase.Name}: {users_to_add}"
)
return RedirectResponse(
f"/pkgbase/{pkgbase.Name}", status_code=HTTPStatus.SEE_OTHER
)
@router.get("/pkgbase/{name}/request")
@requires_auth
async def pkgbase_request(
request: Request, name: str, next: str = Query(default=str())
):
pkgbase = get_pkg_or_base(name, PackageBase)
context = await make_variable_context(request, "Submit Request")
context["pkgbase"] = pkgbase
context["next"] = next or f"/pkgbase/{name}"
return render_template(request, "pkgbase/request.html", context)
@db.async_retry_deadlock
@router.post("/pkgbase/{name}/request")
@handle_form_exceptions
@requires_auth
async def pkgbase_request_post(
request: Request,
name: str,
type: str = Form(...),
merge_into: str = Form(default=None),
comments: str = Form(default=str()),
next: str = Form(default=str()),
):
pkgbase = get_pkg_or_base(name, PackageBase)
# Create our render context.
context = await make_variable_context(request, "Submit Request")
context["pkgbase"] = pkgbase
types = {"deletion": DELETION_ID, "merge": MERGE_ID, "orphan": ORPHAN_ID}
if type not in types:
# In the case that someone crafted a POST request with an invalid
# type, just return them to the request form with BAD_REQUEST status.
return render_template(
request, "pkgbase/request.html", context, status_code=HTTPStatus.BAD_REQUEST
)
try:
validate.request(pkgbase, type, comments, merge_into, context)
except ValidationError as exc:
logger.error(f"Request Validation Error: {str(exc.data)}")
context["errors"] = exc.data
return render_template(request, "pkgbase/request.html", context)
# All good. Create a new PackageRequest based on the given type.
now = time.utcnow()
with db.begin():
pkgreq = db.create(
PackageRequest,
ReqTypeID=types.get(type),
User=request.user,
RequestTS=now,
PackageBase=pkgbase,
PackageBaseName=pkgbase.Name,
MergeBaseName=merge_into,
Comments=comments,
ClosureComment=str(),
)
# Prepare notification object.
notif = notify.RequestOpenNotification(
request.user.ID,
pkgreq.ID,
type,
pkgreq.PackageBase.ID,
merge_into=merge_into or None,
)
# Send the notification now that we're out of the DB scope.
notif.send()
auto_orphan_age = config.getint("options", "auto_orphan_age")
auto_delete_age = config.getint("options", "auto_delete_age")
ood_ts = pkgbase.OutOfDateTS or 0
flagged = ood_ts and (now - ood_ts) >= auto_orphan_age
is_maintainer = pkgbase.Maintainer == request.user
outdated = (now - pkgbase.SubmittedTS) <= auto_delete_age
if type == "orphan" and flagged:
# This request should be auto-accepted.
with db.begin():
pkgbase.Maintainer = None
pkgreq.Status = ACCEPTED_ID
notif = notify.RequestCloseNotification(
request.user.ID, pkgreq.ID, pkgreq.status_display()
)
notif.send()
logger.debug(f"New request #{pkgreq.ID} is marked for auto-orphan.")
elif type == "deletion" and is_maintainer and outdated:
# This request should be auto-accepted.
notifs = actions.pkgbase_delete_instance(request, pkgbase, comments=comments)
util.apply_all(notifs, lambda n: n.send())
logger.debug(f"New request #{pkgreq.ID} is marked for auto-deletion.")
# Redirect the submitting user to /packages.
return RedirectResponse("/packages", status_code=HTTPStatus.SEE_OTHER)
@router.get("/pkgbase/{name}/delete")
@requires_auth
async def pkgbase_delete_get(
request: Request, name: str, next: str = Query(default=str())
):
if not request.user.has_credential(creds.PKGBASE_DELETE):
return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER)
context = templates.make_context(request, "Package Deletion")
context["pkgbase"] = get_pkg_or_base(name, PackageBase)
context["next"] = next or "/packages"
return render_template(request, "pkgbase/delete.html", context)
@db.async_retry_deadlock
@router.post("/pkgbase/{name}/delete")
@handle_form_exceptions
@requires_auth
async def pkgbase_delete_post(
request: Request,
name: str,
confirm: bool = Form(default=False),
comments: str = Form(default=str()),
next: str = Form(default="/packages"),
):
pkgbase = get_pkg_or_base(name, PackageBase)
if not request.user.has_credential(creds.PKGBASE_DELETE):
return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER)
if not confirm:
context = templates.make_context(request, "Package Deletion")
context["pkgbase"] = pkgbase
context["errors"] = [
(
"The selected packages have not been deleted, "
"check the confirmation checkbox."
)
]
return render_template(
request, "pkgbase/delete.html", context, status_code=HTTPStatus.BAD_REQUEST
)
if comments:
validate.comment_raise_http_ex(comments)
# Update any existing deletion requests' ClosureComment.
with db.begin():
requests = pkgbase.requests.filter(
and_(
PackageRequest.Status == PENDING_ID,
PackageRequest.ReqTypeID == DELETION_ID,
)
)
for pkgreq in requests:
pkgreq.ClosureComment = comments
notifs = actions.pkgbase_delete_instance(request, pkgbase, comments=comments)
util.apply_all(notifs, lambda n: n.send())
return RedirectResponse(next, status_code=HTTPStatus.SEE_OTHER)
@router.get("/pkgbase/{name}/merge")
@requires_auth
async def pkgbase_merge_get(
request: Request,
name: str,
into: str = Query(default=str()),
next: str = Query(default=str()),
):
pkgbase = get_pkg_or_base(name, PackageBase)
context = templates.make_context(request, "Package Merging")
context.update({"pkgbase": pkgbase, "into": into, "next": next})
status_code = HTTPStatus.OK
# TODO: Lookup errors from credential instead of hardcoding them.
# Idea: Something like credential_errors(creds.PKGBASE_MERGE).
# Perhaps additionally: bad_credential_status_code(creds.PKGBASE_MERGE).
# Don't take these examples verbatim. We should find good naming.
if not request.user.has_credential(creds.PKGBASE_MERGE):
context["errors"] = [
"Only Package Maintainers and Developers can merge packages."
]
status_code = HTTPStatus.UNAUTHORIZED
return render_template(
request, "pkgbase/merge.html", context, status_code=status_code
)
@db.async_retry_deadlock
@router.post("/pkgbase/{name}/merge")
@handle_form_exceptions
@requires_auth
async def pkgbase_merge_post(
request: Request,
name: str,
into: str = Form(default=str()),
comments: str = Form(default=str()),
confirm: bool = Form(default=False),
next: str = Form(default=str()),
):
pkgbase = get_pkg_or_base(name, PackageBase)
context = await make_variable_context(request, "Package Merging")
context["pkgbase"] = pkgbase
# TODO: Lookup errors from credential instead of hardcoding them.
if not request.user.has_credential(creds.PKGBASE_MERGE):
context["errors"] = [
"Only Package Maintainers and Developers can merge packages."
]
return render_template(
request, "pkgbase/merge.html", context, status_code=HTTPStatus.UNAUTHORIZED
)
if not confirm:
context["errors"] = [
"The selected packages have not been deleted, "
"check the confirmation checkbox."
]
return render_template(
request, "pkgbase/merge.html", context, status_code=HTTPStatus.BAD_REQUEST
)
try:
target = get_pkg_or_base(into, PackageBase)
except HTTPException:
context["errors"] = ["Cannot find package to merge votes and comments into."]
return render_template(
request, "pkgbase/merge.html", context, status_code=HTTPStatus.BAD_REQUEST
)
if pkgbase == target:
context["errors"] = ["Cannot merge a package base with itself."]
return render_template(
request, "pkgbase/merge.html", context, status_code=HTTPStatus.BAD_REQUEST
)
if comments:
validate.comment_raise_http_ex(comments)
with db.begin():
update_closure_comment(pkgbase, MERGE_ID, comments, target=target)
# Merge pkgbase into target.
actions.pkgbase_merge_instance(request, pkgbase, target, comments=comments)
if not next:
next = f"/pkgbase/{target.Name}"
# Redirect to the newly merged into package.
return RedirectResponse(next, status_code=HTTPStatus.SEE_OTHER)

166
aurweb/routers/requests.py Normal file
View file

@ -0,0 +1,166 @@
from http import HTTPStatus
from fastapi import APIRouter, Form, Query, Request
from fastapi.responses import RedirectResponse
from sqlalchemy import case, orm
from aurweb import db, defaults, time, util
from aurweb.auth import creds, requires_auth
from aurweb.exceptions import handle_form_exceptions
from aurweb.models import PackageBase, PackageRequest, User
from aurweb.models.package_request import (
ACCEPTED_ID,
CLOSED_ID,
PENDING_ID,
REJECTED_ID,
)
from aurweb.requests.util import get_pkgreq_by_id
from aurweb.scripts import notify
from aurweb.statistics import get_request_counts
from aurweb.templates import make_context, render_template
FILTER_PARAMS = {
"filter_pending",
"filter_closed",
"filter_accepted",
"filter_rejected",
"filter_maintainers_requests",
}
router = APIRouter()
@router.get("/requests")
@requires_auth
async def requests( # noqa: C901
request: Request,
O: int = Query(default=defaults.O),
PP: int = Query(default=defaults.PP),
filter_pending: bool = False,
filter_closed: bool = False,
filter_accepted: bool = False,
filter_rejected: bool = False,
filter_maintainer_requests: bool = False,
filter_pkg_name: str = None,
):
context = make_context(request, "Requests")
context["q"] = dict(request.query_params)
# Set pending filter by default if no status filter was provided.
# In case we got a package name filter, but no status filter,
# we enable the other ones too.
if not dict(request.query_params).keys() & FILTER_PARAMS:
filter_pending = True
if filter_pkg_name:
filter_closed = True
filter_accepted = True
filter_rejected = True
O, PP = util.sanitize_params(str(O), str(PP))
context["O"] = O
context["PP"] = PP
context["filter_pending"] = filter_pending
context["filter_closed"] = filter_closed
context["filter_accepted"] = filter_accepted
context["filter_rejected"] = filter_rejected
context["filter_maintainer_requests"] = filter_maintainer_requests
context["filter_pkg_name"] = filter_pkg_name
Maintainer = orm.aliased(User)
# A PackageRequest query
query = (
db.query(PackageRequest)
.join(PackageBase)
.join(User, PackageRequest.UsersID == User.ID, isouter=True)
.join(Maintainer, PackageBase.MaintainerUID == Maintainer.ID, isouter=True)
)
# Requests statistics
counts = get_request_counts()
for k in counts:
context[k] = counts[k]
# Apply status filters
in_filters = []
if filter_pending:
in_filters.append(PENDING_ID)
if filter_closed:
in_filters.append(CLOSED_ID)
if filter_accepted:
in_filters.append(ACCEPTED_ID)
if filter_rejected:
in_filters.append(REJECTED_ID)
filtered = query.filter(PackageRequest.Status.in_(in_filters))
# Name filter (contains)
if filter_pkg_name:
filtered = filtered.filter(PackageBase.Name.like(f"%{filter_pkg_name}%"))
# Additionally filter for requests made from package maintainer
if filter_maintainer_requests:
filtered = filtered.filter(PackageRequest.UsersID == PackageBase.MaintainerUID)
# If the request user is not elevated (TU or Dev), then
# filter PackageRequests which are owned by the request user.
if not request.user.is_elevated():
filtered = filtered.filter(PackageRequest.UsersID == request.user.ID)
context["total"] = filtered.count()
context["results"] = (
filtered.order_by(
# Order primarily by the Status column being PENDING_ID,
# and secondarily by RequestTS; both in descending order.
case([(PackageRequest.Status == PENDING_ID, 1)], else_=0).desc(),
PackageRequest.RequestTS.desc(),
)
.limit(PP)
.offset(O)
.all()
)
return render_template(request, "requests.html", context)
@router.get("/requests/{id}/close")
@requires_auth
async def request_close(request: Request, id: int):
pkgreq = get_pkgreq_by_id(id)
if not request.user.is_elevated() and request.user != pkgreq.User:
# Request user doesn't have permission here: redirect to '/'.
return RedirectResponse("/", status_code=HTTPStatus.SEE_OTHER)
context = make_context(request, "Close Request")
context["pkgreq"] = pkgreq
return render_template(request, "requests/close.html", context)
@db.async_retry_deadlock
@router.post("/requests/{id}/close")
@handle_form_exceptions
@requires_auth
async def request_close_post(
request: Request, id: int, comments: str = Form(default=str())
):
pkgreq = get_pkgreq_by_id(id)
# `pkgreq`.User can close their own request.
approved = [pkgreq.User]
if not request.user.has_credential(creds.PKGREQ_CLOSE, approved=approved):
# Request user doesn't have permission here: redirect to '/'.
return RedirectResponse("/", status_code=HTTPStatus.SEE_OTHER)
context = make_context(request, "Close Request")
context["pkgreq"] = pkgreq
now = time.utcnow()
with db.begin():
pkgreq.Closer = request.user
pkgreq.ClosureComment = comments
pkgreq.ClosedTS = now
pkgreq.Status = REJECTED_ID
notify_ = notify.RequestCloseNotification(
request.user.ID, pkgreq.ID, pkgreq.status_display()
)
notify_.send()
return RedirectResponse("/requests", status_code=HTTPStatus.SEE_OTHER)

320
aurweb/routers/rpc.py Normal file
View file

@ -0,0 +1,320 @@
"""
RPC API routing module
For legacy route documentation, see https://aur.archlinux.org/rpc
Legacy Routes:
- GET /rpc
- POST /rpc
Legacy example (version 5): /rpc?v=5&type=info&arg=my-package
For OpenAPI route documentation, see https://aur.archlinux.org/docs
OpenAPI Routes:
- GET /rpc/v{version}/info/{arg}
- GET /rpc/v{version}/info
- POST /rpc/v{version}/info
- GET /rpc/v{version}/search/{arg}
- GET /rpc/v{version}/search
- POST /rpc/v{version}/search
- GET /rpc/v{version}/suggest/{arg}
OpenAPI example (version 5): /rpc/v5/info/my-package
"""
import hashlib
import re
from http import HTTPStatus
from typing import Optional
from urllib.parse import unquote
import orjson
from fastapi import APIRouter, Form, Query, Request, Response
from fastapi.responses import JSONResponse
from aurweb import defaults
from aurweb.exceptions import handle_form_exceptions
from aurweb.ratelimit import check_ratelimit
from aurweb.rpc import RPC, documentation
router = APIRouter()
def parse_args(request: Request):
"""Handle legacy logic of 'arg' and 'arg[]' query parameter handling.
When 'arg' appears as the last argument given to the query string,
that argument is used by itself as one single argument, regardless
of any more 'arg' or 'arg[]' parameters supplied before it.
When 'arg[]' appears as the last argument given to the query string,
we iterate from last to first and build a list of arguments until
we hit an 'arg'.
TODO: This handling should be addressed in v6 of the RPC API. This
was most likely a bi-product of legacy handling of versions 1-4
which we no longer support.
:param request: FastAPI request
:returns: List of deduced arguments
"""
# Create a list of (key, value) pairs of the given 'arg' and 'arg[]'
# query parameters from last to first.
query = list(reversed(unquote(request.url.query).split("&")))
parts = [e.split("=", 1) for e in query if e.startswith(("arg=", "arg[]="))]
args = []
if parts:
# If we found 'arg' and/or 'arg[]' arguments, we begin processing
# the set of arguments depending on the last key found.
last = parts[0][0]
if last == "arg":
# If the last key was 'arg', then it is our sole argument.
args.append(parts[0][1])
else:
# Otherwise, it must be 'arg[]', so traverse backward
# until we reach a non-'arg[]' key.
for key, value in parts:
if key != last:
break
args.append(value)
return args
JSONP_EXPR = re.compile(r"^[a-zA-Z0-9()_.]{1,128}$")
async def rpc_request(
request: Request,
v: Optional[int] = None,
type: Optional[str] = None,
by: Optional[str] = defaults.RPC_SEARCH_BY,
arg: Optional[str] = None,
args: Optional[list[str]] = [],
callback: Optional[str] = None,
):
# Create a handle to our RPC class.
rpc = RPC(version=v, type=type)
# If ratelimit was exceeded, return a 429 Too Many Requests.
if check_ratelimit(request):
return JSONResponse(
rpc.error("Rate limit reached"),
status_code=int(HTTPStatus.TOO_MANY_REQUESTS),
)
# If `callback` was provided, produce a text/javascript response
# valid for the jsonp callback. Otherwise, by default, return
# application/json containing `output`.
content_type = "application/json"
if callback:
if not re.match(JSONP_EXPR, callback):
return rpc.error("Invalid callback name.")
content_type = "text/javascript"
# Prepare list of arguments for input. If 'arg' was given, it'll
# be a list with one element.
arguments = []
if request.url.query:
arguments = parse_args(request)
else:
if arg:
arguments.append(arg)
arguments += args
data = rpc.handle(by=by, args=arguments)
# Serialize `data` into JSON in a sorted fashion. This way, our
# ETag header produced below will never end up changed.
content = orjson.dumps(data, option=orjson.OPT_SORT_KEYS)
# Produce an md5 hash based on `output`.
md5 = hashlib.md5()
md5.update(content)
etag = md5.hexdigest()
# The ETag header expects quotes to surround any identifier.
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag
headers = {"Content-Type": content_type, "ETag": f'"{etag}"'}
if_none_match = request.headers.get("If-None-Match", str())
if if_none_match and if_none_match.strip('\t\n\r" ') == etag:
return Response(headers=headers, status_code=int(HTTPStatus.NOT_MODIFIED))
if callback:
content = f"/**/{callback}({content.decode()})"
return Response(content, headers=headers)
@router.get("/rpc.php/") # Temporary! Remove on 03/04
@router.get("/rpc.php") # Temporary! Remove on 03/04
@router.get("/rpc/")
@router.get("/rpc")
async def rpc(
request: Request,
v: Optional[int] = Query(default=None),
type: Optional[str] = Query(default=None),
by: Optional[str] = Query(default=defaults.RPC_SEARCH_BY),
arg: Optional[str] = Query(default=None),
args: Optional[list[str]] = Query(default=[], alias="arg[]"),
callback: Optional[str] = Query(default=None),
):
if not request.url.query:
return documentation()
return await rpc_request(request, v, type, by, arg, args, callback)
@router.get("/rpc.php/") # Temporary! Remove on 03/04
@router.get("/rpc.php") # Temporary! Remove on 03/04
@router.post("/rpc/")
@router.post("/rpc")
@handle_form_exceptions
async def rpc_post(
request: Request,
v: Optional[int] = Form(default=None),
type: Optional[str] = Form(default=None),
by: Optional[str] = Form(default=defaults.RPC_SEARCH_BY),
arg: Optional[str] = Form(default=None),
args: list[str] = Form(default=[], alias="arg[]"),
callback: Optional[str] = Form(default=None),
):
return await rpc_request(request, v, type, by, arg, args, callback)
@router.get("/rpc/v{version}/info/{name}")
async def rpc_openapi_info(request: Request, version: int, name: str):
return await rpc_request(
request,
version,
"info",
defaults.RPC_SEARCH_BY,
name,
[],
)
@router.get("/rpc/v{version}/info")
async def rpc_openapi_multiinfo(
request: Request,
version: int,
args: Optional[list[str]] = Query(default=[], alias="arg[]"),
):
arg = args.pop(0) if args else None
return await rpc_request(
request,
version,
"info",
defaults.RPC_SEARCH_BY,
arg,
args,
)
@router.post("/rpc/v{version}/info")
async def rpc_openapi_multiinfo_post(
request: Request,
version: int,
):
data = await request.json()
args = data.get("arg", [])
if not isinstance(args, list):
rpc = RPC(version, "info")
return JSONResponse(
rpc.error("the 'arg' parameter must be of array type"),
status_code=HTTPStatus.BAD_REQUEST,
)
arg = args.pop(0) if args else None
return await rpc_request(
request,
version,
"info",
defaults.RPC_SEARCH_BY,
arg,
args,
)
@router.get("/rpc/v{version}/search/{arg}")
async def rpc_openapi_search_arg(
request: Request,
version: int,
arg: str,
by: Optional[str] = Query(default=defaults.RPC_SEARCH_BY),
):
return await rpc_request(
request,
version,
"search",
by,
arg,
[],
)
@router.get("/rpc/v{version}/search")
async def rpc_openapi_search(
request: Request,
version: int,
arg: Optional[str] = Query(default=str()),
by: Optional[str] = Query(default=defaults.RPC_SEARCH_BY),
):
return await rpc_request(
request,
version,
"search",
by,
arg,
[],
)
@router.post("/rpc/v{version}/search")
async def rpc_openapi_search_post(
request: Request,
version: int,
):
data = await request.json()
by = data.get("by", defaults.RPC_SEARCH_BY)
if not isinstance(by, str):
rpc = RPC(version, "search")
return JSONResponse(
rpc.error("the 'by' parameter must be of string type"),
status_code=HTTPStatus.BAD_REQUEST,
)
arg = data.get("arg", str())
if not isinstance(arg, str):
rpc = RPC(version, "search")
return JSONResponse(
rpc.error("the 'arg' parameter must be of string type"),
status_code=HTTPStatus.BAD_REQUEST,
)
return await rpc_request(
request,
version,
"search",
by,
arg,
[],
)
@router.get("/rpc/v{version}/suggest/{arg}")
async def rpc_openapi_suggest(request: Request, version: int, arg: str):
return await rpc_request(
request,
version,
"suggest",
defaults.RPC_SEARCH_BY,
arg,
[],
)

89
aurweb/routers/rss.py Normal file
View file

@ -0,0 +1,89 @@
from fastapi import APIRouter, Request
from fastapi.responses import Response
from feedgen.feed import FeedGenerator
from aurweb import config, db, filters
from aurweb.cache import lambda_cache
from aurweb.models import Package, PackageBase
router = APIRouter()
def make_rss_feed(request: Request, packages: list):
"""Create an RSS Feed string for some packages.
:param request: A FastAPI request
:param packages: A list of packages to add to the RSS feed
:return: RSS Feed string
"""
feed = FeedGenerator()
feed.title("AUR Newest Packages")
feed.description("The latest and greatest packages in the AUR")
base = f"{request.url.scheme}://{request.url.netloc}"
feed.link(href=base, rel="alternate")
feed.link(href=f"{base}/rss", rel="self")
feed.image(
title="AUR Newest Packages",
url=f"{base}/static/css/archnavbar/aurlogo.png",
link=base,
description="AUR Newest Packages Feed",
)
for pkg in packages:
entry = feed.add_entry(order="append")
entry.title(pkg.Name)
entry.link(href=f"{base}/packages/{pkg.Name}", rel="alternate")
entry.description(pkg.Description or str())
dt = filters.timestamp_to_datetime(pkg.Timestamp)
dt = filters.as_timezone(dt, request.user.Timezone)
entry.pubDate(dt.strftime("%Y-%m-%d %H:%M:%S%z"))
entry.guid(f"{pkg.Name}-{pkg.Timestamp}")
return feed.rss_str()
@router.get("/rss/")
async def rss(request: Request):
packages = (
db.query(Package)
.join(PackageBase)
.order_by(PackageBase.SubmittedTS.desc())
.limit(100)
.with_entities(
Package.Name,
Package.Description,
PackageBase.SubmittedTS.label("Timestamp"),
)
)
# we use redis for caching the results of the feedgen
cache_expire = config.getint("cache", "expiry_time_rss", 300)
feed = lambda_cache("rss", lambda: make_rss_feed(request, packages), cache_expire)
response = Response(feed, media_type="application/rss+xml")
return response
@router.get("/rss/modified")
async def rss_modified(request: Request):
packages = (
db.query(Package)
.join(PackageBase)
.order_by(PackageBase.ModifiedTS.desc())
.limit(100)
.with_entities(
Package.Name,
Package.Description,
PackageBase.ModifiedTS.label("Timestamp"),
)
)
# we use redis for caching the results of the feedgen
cache_expire = config.getint("cache", "expiry_time_rss", 300)
feed = lambda_cache(
"rss_modified", lambda: make_rss_feed(request, packages), cache_expire
)
response = Response(feed, media_type="application/rss+xml")
return response

Some files were not shown because too many files have changed in this diff Show more