From 4c8d4dc8b3b70543071c6ee74995a081d7fdfad2 Mon Sep 17 00:00:00 2001 From: Luca Beltrame Date: Sat, 29 Jan 2022 09:51:35 +0100 Subject: [PATCH 01/28] New configuration options: aliases and secret --- base-config.yaml | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/base-config.yaml b/base-config.yaml index 5cedd97..96c03f6 100644 --- a/base-config.yaml +++ b/base-config.yaml @@ -6,9 +6,31 @@ api_url: https://api.opensuse.org instance_url: https://build.opensuse.org # Build Service username username: changeme -# Build Service password +# Used to encrypt the password. Set it to a random string +secret: changeme +# Build Service password - will be encrypted on first run password: changeme # Build service access token with the "rebuild" capability rebuild_token: changeme # Build service access token with the "runservices" capability trigger_token: changeme +# Aliases for repositories +repo_aliases: + kua_failed: + project: "KDE:Unstable:Applications" + package: "all" + repository: "KDE_Unstable_Frameworks_openSUSE_Factory" + state: "failed" + arch: "all" + kuf_failed: + project: "KDE:Unstable:Frameworks" + package: "all" + repository: "openSUSE_Factory" + state: "failed" + arch: "all" + kue_failed: + project: "KDE:Unstable:Extra" + package: "all" + repository: "KDE_Unstable_Frameworks_openSUSE_Factory" + state: "failed" + arch: "all" From 2c5fbcc2fd1954c788c215f6c014b15913c81bc4 Mon Sep 17 00:00:00 2001 From: Luca Beltrame Date: Sat, 29 Jan 2022 09:52:08 +0100 Subject: [PATCH 02/28] Encrypt the OBS password on first run This makes sure it is at least not stored in plaintext somewhere. --- oscbot/__init__.py | 61 ++++++++++++++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/oscbot/__init__.py b/oscbot/__init__.py index 43cc5a3..9c21962 100644 --- a/oscbot/__init__.py +++ b/oscbot/__init__.py @@ -1,10 +1,11 @@ # SPDX-FileCopyrightText: 2022 Luca Beltrame # SPDX-License-Identifier: AGPL-3.0-or-later -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Optional, List, Type, Tuple import aiohttp +import cryptocode from lxml import objectify from jinja2 import BaseLoader, Environment @@ -46,7 +47,7 @@ class BuildResult: class BuildRepository: name: str arch: str - packages: List[BuildResult] + packages: List[BuildResult] = field(default_factory=list) class Config(BaseProxyConfig): @@ -56,8 +57,17 @@ class Config(BaseProxyConfig): helper.copy("instance_url") helper.copy("rebuild_token") helper.copy("trigger_token") + helper.copy("secret") + + password = self["password"] + if len(password) < 91 and not password.endswith("=="): + encrypted_password = cryptocode.encrypt(password, self["secret"]) + helper.base["password"] = encrypted_password + else: + helper.copy("password") + helper.copy("username") - helper.copy("password") + helper.copy("repo_aliases") class OSCBot(Plugin): @@ -76,6 +86,19 @@ class OSCBot(Plugin): def get_config_class(cls) -> Type[BaseProxyConfig]: return Config + def get_alias(self, project_alias: str) -> Tuple[str, str, str, str, str]: + data = self.config["repo_aliases"][project_alias] + # There is no concept of non-positional arguments in maubot + # So we just use "all" in case we want to skip something + package = data["package"] if data["package"] != "all" else None + repository = (data["repository"] if data["repository"] != "all" + else None) + arch = data["arch"] if data["arch"] != "all" else None + project = data["project"] + state = data["state"] if data["state"] != "all" else None + + return (project, package, repository, state, arch) + async def parse_rebuilpac( self, project: str, @@ -112,7 +135,8 @@ class OSCBot(Plugin): arch: Optional[str] = None) -> List[BuildRepository]: username = self.config["username"] - password = self.config["password"] + password = cryptocode.decrypt(self.config["password"], + self.config["secret"]) api_url = self.config["api_url"] api_call = f"{api_url}/build/{project}/_result" @@ -179,7 +203,7 @@ class OSCBot(Plugin): @osc.subcommand( "rebuildpac", aliases=("rb",), help="Rebuild a package or all packages in the repositories") - @command.argument("project", "project name") + @command.argument("project", "project name/alias") @command.argument("package", "package name (or \"all\" for all packages)") @command.argument("repository", "repository (optional)", required=False) @command.argument("arch", "architecture (optional)", required=False) @@ -189,12 +213,9 @@ class OSCBot(Plugin): repository: Optional[str] = None, arch: Optional[str] = None) -> None: - if package == "all": - package = None - if repository == "all": - repository = None - if arch == "all": - arch = None + package = None if package == "all" else package + repository = None if repository == "all" else repository + arch = None if arch == "all" else arch result, status = await self.parse_rebuilpac(project, package, repository, @@ -244,16 +265,14 @@ class OSCBot(Plugin): repository: Optional[str] = None, arch: Optional[str] = None) -> None: - # There is no concept of non-positional arguments in maubot - # So we just use "all" in case we want to skip something - if state == "all": - state = None - if package == "all": - package = None - if repository == "all": - repository = None - if arch == "all": - arch = None + if project in self.config["repo_aliases"]: + project, package, repository, state, arch = self.get_alias(project) + else: + # There is no concept of non-positional arguments in maubot + # So we just use "all" in case we want to skip something + package = None if package == "all" else package + repository = None if repository == "all" else repository + arch = None if arch == "all" else arch response = await self.parse_status(project, package, state=state, repo=repository, arch=arch) From 5a02d4d2af1b138faa6337a388bc7be46e93b254 Mon Sep 17 00:00:00 2001 From: Luca Beltrame Date: Sat, 29 Jan 2022 09:53:08 +0100 Subject: [PATCH 03/28] Bump version and dependencies --- maubot.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/maubot.yaml b/maubot.yaml index 74723ca..27aeaa8 100644 --- a/maubot.yaml +++ b/maubot.yaml @@ -2,7 +2,7 @@ # SPDX-License-Identifier: CC0-1.0 maubot: 0.1.0 id: org.dennogumi.osc -version: 0.1.0 +version: 0.2.0 license: AGPL-3.0-or-later modules: - oscbot @@ -12,5 +12,6 @@ extra_files: dependencies: - lxml - jinja2 + - cryptocode database: false config: true From 754c2c6e3de905260668f64011e681f8efb64e4c Mon Sep 17 00:00:00 2001 From: Luca Beltrame Date: Sat, 29 Jan 2022 09:57:58 +0100 Subject: [PATCH 04/28] Update README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6c0e402..495cf87 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,10 @@ Then you need to modify a few configuration parameters for your instance: - `api_url`: the URL to make API requests to the OBS instance. By default it points to the [public OBS instance of the openSUSE project](https://build.opensuse.org) (`https://api.opensuse.org`). - `instance_url`: the main URL of the Build Service instance. Defaults to `https://build.opensuse.org`. - `username`: A valid username for the instance -- `password`: A password for the username. **Currently kept in cleartext**. Suggestions on how to improve this are welcome. +- `password`: A password for the username. Suggestions on how to improve this are welcome. - `trigger_token`: An OBS access token with the `runservice` capability. See "Creating tokens" below. - `rebuild_token`: An OBS access token with the `rebuild` capability. See "Creating tokens" below. +- `secret`: A random string used to encrypt the password in the configuration file. ### Creating tokens From 601ebbf357f06280a7c6f5cd6eedd2cac7ba7b67 Mon Sep 17 00:00:00 2001 From: Luca Beltrame Date: Sun, 30 Jan 2022 00:24:26 +0100 Subject: [PATCH 05/28] Add CI (if it works) --- .woodpecker.yml | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .woodpecker.yml diff --git a/.woodpecker.yml b/.woodpecker.yml new file mode 100644 index 0000000..34bbde3 --- /dev/null +++ b/.woodpecker.yml @@ -0,0 +1,36 @@ +workspace: + base: /data + path: sources + +pipeline: + build: + image: dock.mau.dev/maubot/maubot:latest + commands: + - mbc build + release: + image: plugins/gitea-release + settings: + base_url: https://git.dennogumi.org + api_key: + from_secret: gitea_token + files: + - /data/sources/*.mbp + checksum: + - sha256 + when: + event: tag + notify: + image: plugins/matrix + settings: + homeserver: https://conference.heavensinferno.net + roomid: + from_secret: roomid + accesstoken: + from_secret: access_token + userid: + from_secret: user_id + when: + status: + - failure + - success + From 6902625405d4061eecbe9b1548d15c6f02075656 Mon Sep 17 00:00:00 2001 From: Luca Beltrame Date: Sun, 30 Jan 2022 00:25:40 +0100 Subject: [PATCH 06/28] Second test for CI --- .woodpecker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.woodpecker.yml b/.woodpecker.yml index 34bbde3..094012d 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -6,7 +6,7 @@ pipeline: build: image: dock.mau.dev/maubot/maubot:latest commands: - - mbc build + - mbc build /data/sources release: image: plugins/gitea-release settings: From 10fcbadc85e440e14b739777f8fa127ec42c5410 Mon Sep 17 00:00:00 2001 From: Luca Beltrame Date: Sun, 30 Jan 2022 00:35:13 +0100 Subject: [PATCH 07/28] Just a simple test to see if uploading works --- .woodpecker.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.woodpecker.yml b/.woodpecker.yml index 094012d..1671261 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -6,7 +6,10 @@ pipeline: build: image: dock.mau.dev/maubot/maubot:latest commands: - - mbc build /data/sources + - mkdir -p $HOME/.config + - echo $${MBC_CONFIG}} > $HOME/.config/maubot-cli.json + - mbc build /data/sources --upload + secrets: [mbc_config] release: image: plugins/gitea-release settings: From d2f37850289e6d27bb490849e9f8e9fb4b818427 Mon Sep 17 00:00:00 2001 From: Luca Beltrame Date: Sun, 30 Jan 2022 00:36:14 +0100 Subject: [PATCH 08/28] Fix typo --- .woodpecker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.woodpecker.yml b/.woodpecker.yml index 1671261..59a5506 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -7,7 +7,7 @@ pipeline: image: dock.mau.dev/maubot/maubot:latest commands: - mkdir -p $HOME/.config - - echo $${MBC_CONFIG}} > $HOME/.config/maubot-cli.json + - echo $${MBC_CONFIG} > $HOME/.config/maubot-cli.json - mbc build /data/sources --upload secrets: [mbc_config] release: From 77125e921f159d5d1ff9ad3dc95be074b0e51838 Mon Sep 17 00:00:00 2001 From: Luca Beltrame Date: Sun, 30 Jan 2022 00:36:51 +0100 Subject: [PATCH 09/28] Some debug for the maubot config --- .woodpecker.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.woodpecker.yml b/.woodpecker.yml index 59a5506..8bc3c59 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -8,6 +8,7 @@ pipeline: commands: - mkdir -p $HOME/.config - echo $${MBC_CONFIG} > $HOME/.config/maubot-cli.json + - cat $HOME/.config/maubot-cli.json - mbc build /data/sources --upload secrets: [mbc_config] release: From ac10d9bc294655507330f328608ea1fa569ee114 Mon Sep 17 00:00:00 2001 From: Luca Beltrame Date: Sun, 30 Jan 2022 00:38:24 +0100 Subject: [PATCH 10/28] More tests to see why it does not work --- .woodpecker.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.woodpecker.yml b/.woodpecker.yml index 8bc3c59..6b6280a 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -7,8 +7,7 @@ pipeline: image: dock.mau.dev/maubot/maubot:latest commands: - mkdir -p $HOME/.config - - echo $${MBC_CONFIG} > $HOME/.config/maubot-cli.json - - cat $HOME/.config/maubot-cli.json + - echo $${MBC_CONFIG} - mbc build /data/sources --upload secrets: [mbc_config] release: From 60c0b7adf666e8f4fd03cc04a5978408add6acc5 Mon Sep 17 00:00:00 2001 From: Luca Beltrame Date: Sun, 30 Jan 2022 00:39:25 +0100 Subject: [PATCH 11/28] And yet more tests --- .woodpecker.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.woodpecker.yml b/.woodpecker.yml index 6b6280a..3a85f3e 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -7,7 +7,8 @@ pipeline: image: dock.mau.dev/maubot/maubot:latest commands: - mkdir -p $HOME/.config - - echo $${MBC_CONFIG} + - echo $${MBC_CONFIG} > $HOME/.config/maubot-cli.json + - echo $HOME - mbc build /data/sources --upload secrets: [mbc_config] release: From e5dd6edc28bb6ce4ad9634f90cca2489e2cdaf47 Mon Sep 17 00:00:00 2001 From: Luca Beltrame Date: Sun, 30 Jan 2022 00:40:18 +0100 Subject: [PATCH 12/28] And yet another --- .woodpecker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.woodpecker.yml b/.woodpecker.yml index 3a85f3e..738a248 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -8,7 +8,7 @@ pipeline: commands: - mkdir -p $HOME/.config - echo $${MBC_CONFIG} > $HOME/.config/maubot-cli.json - - echo $HOME + - grep server $HOME/.config/maubot-cli.json - mbc build /data/sources --upload secrets: [mbc_config] release: From 08a2f4f9c4925ac9dd52fd725ce4f1f27ea1b55d Mon Sep 17 00:00:00 2001 From: Luca Beltrame Date: Sun, 30 Jan 2022 00:41:50 +0100 Subject: [PATCH 13/28] Use an explicit URL --- .woodpecker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.woodpecker.yml b/.woodpecker.yml index 738a248..ce49a4f 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -9,7 +9,7 @@ pipeline: - mkdir -p $HOME/.config - echo $${MBC_CONFIG} > $HOME/.config/maubot-cli.json - grep server $HOME/.config/maubot-cli.json - - mbc build /data/sources --upload + - mbc build /data/sources --upload -s https://conference.heavensinferno.net secrets: [mbc_config] release: image: plugins/gitea-release From 30b14bbe316170d6d6df89c8bddfac498ab8e8e5 Mon Sep 17 00:00:00 2001 From: Luca Beltrame Date: Sun, 30 Jan 2022 00:43:15 +0100 Subject: [PATCH 14/28] See if this path works --- .woodpecker.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.woodpecker.yml b/.woodpecker.yml index ce49a4f..08fcc1a 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -6,9 +6,8 @@ pipeline: build: image: dock.mau.dev/maubot/maubot:latest commands: - - mkdir -p $HOME/.config - - echo $${MBC_CONFIG} > $HOME/.config/maubot-cli.json - - grep server $HOME/.config/maubot-cli.json + - mkdir -p /data/.config + - echo $${MBC_CONFIG} > /data/.config/maubot-cli.json - mbc build /data/sources --upload -s https://conference.heavensinferno.net secrets: [mbc_config] release: From 28f6e562bb7a1f687ff32ff4de69f20a7c6331f8 Mon Sep 17 00:00:00 2001 From: Luca Beltrame Date: Sun, 30 Jan 2022 00:44:31 +0100 Subject: [PATCH 15/28] More path adjustments --- .woodpecker.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.woodpecker.yml b/.woodpecker.yml index 08fcc1a..01d36ed 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -6,9 +6,8 @@ pipeline: build: image: dock.mau.dev/maubot/maubot:latest commands: - - mkdir -p /data/.config - - echo $${MBC_CONFIG} > /data/.config/maubot-cli.json - - mbc build /data/sources --upload -s https://conference.heavensinferno.net + - echo $${MBC_CONFIG} > /data/maubot-cli.json + - mbc build /data/sources --upload secrets: [mbc_config] release: image: plugins/gitea-release From a0044208ca201719ef3570fe92d01c142a4e6b36 Mon Sep 17 00:00:00 2001 From: Luca Beltrame Date: Sun, 30 Jan 2022 00:49:08 +0100 Subject: [PATCH 16/28] Change of plans, perform a login --- .woodpecker.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.woodpecker.yml b/.woodpecker.yml index 01d36ed..6c6dbef 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -6,9 +6,9 @@ pipeline: build: image: dock.mau.dev/maubot/maubot:latest commands: - - echo $${MBC_CONFIG} > /data/maubot-cli.json + - mbc login -u $${MBC_USERNAME} -p $${MBC_PASSWORD} -s https://conference.heavensinferno.net -a default - mbc build /data/sources --upload - secrets: [mbc_config] + secrets: [mbc_username, mbc_password] release: image: plugins/gitea-release settings: From 47da86f24ce052fae1dc5b9967c31e8048762132 Mon Sep 17 00:00:00 2001 From: Luca Beltrame Date: Sun, 30 Jan 2022 00:51:39 +0100 Subject: [PATCH 17/28] Reshuffle paths --- .woodpecker.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.woodpecker.yml b/.woodpecker.yml index 6c6dbef..8e73bfd 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -1,13 +1,12 @@ workspace: - base: /data - path: sources + base: /maubot-osc + path: src pipeline: build: image: dock.mau.dev/maubot/maubot:latest commands: - - mbc login -u $${MBC_USERNAME} -p $${MBC_PASSWORD} -s https://conference.heavensinferno.net -a default - - mbc build /data/sources --upload + - mbc build /maubot-osc/src secrets: [mbc_username, mbc_password] release: image: plugins/gitea-release @@ -16,7 +15,7 @@ pipeline: api_key: from_secret: gitea_token files: - - /data/sources/*.mbp + - /maubot-osc/src/*.mbp checksum: - sha256 when: From 01cb708ffe844a09a13deb9f422129a77c0537fb Mon Sep 17 00:00:00 2001 From: Luca Beltrame Date: Sun, 30 Jan 2022 00:52:12 +0100 Subject: [PATCH 18/28] Fix save path --- .woodpecker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.woodpecker.yml b/.woodpecker.yml index 8e73bfd..e42fe5a 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -6,7 +6,7 @@ pipeline: build: image: dock.mau.dev/maubot/maubot:latest commands: - - mbc build /maubot-osc/src + - mbc build /maubot-osc/src -o /maubot-osc/src/ secrets: [mbc_username, mbc_password] release: image: plugins/gitea-release From a2e3bccf3ce08741533cf4af4be2ac62dd09d6cb Mon Sep 17 00:00:00 2001 From: Luca Beltrame Date: Sun, 30 Jan 2022 10:08:40 +0100 Subject: [PATCH 19/28] Use the live build log URL It's easier then to check the package and what not and the full log is one click away anyway. --- oscbot/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/oscbot/__init__.py b/oscbot/__init__.py index 9c21962..1469901 100644 --- a/oscbot/__init__.py +++ b/oscbot/__init__.py @@ -27,8 +27,8 @@ REPO_TEMPLATE = """ {% for package in repo.packages %} {%if package.status != "disabled" %} -{% set build_log_url = "{0}/public/build/{1}/{2}/{3}/{4}/_log".format( - base_url,project, repo.name, repo.arch, package.name) %} +{% set build_log_url = "{0}/package/live_build_log/{1}/{2}/{3}/{4}/".format( + base_url, project, package.name, repo.name, repo.arch) %} - {{ package.name }} - *[{{ package.status }}]({{ build_log_url }})* {% else %} - {{ package.name }} - *{{ package.status }}* From fe751231911f92d6b94289a4aca8561102ed9657 Mon Sep 17 00:00:00 2001 From: Luca Beltrame Date: Sun, 30 Jan 2022 10:17:07 +0100 Subject: [PATCH 20/28] Whoops, forgot version bump --- maubot.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maubot.yaml b/maubot.yaml index 27aeaa8..6bcfc78 100644 --- a/maubot.yaml +++ b/maubot.yaml @@ -2,7 +2,7 @@ # SPDX-License-Identifier: CC0-1.0 maubot: 0.1.0 id: org.dennogumi.osc -version: 0.2.0 +version: 0.2.2 license: AGPL-3.0-or-later modules: - oscbot From cc676a082d75a47d3566e0d5127608103c5fd1a1 Mon Sep 17 00:00:00 2001 From: Luca Beltrame Date: Sun, 30 Jan 2022 11:10:34 +0100 Subject: [PATCH 21/28] Paginate status results and implement rate limiting Packages are chunked in groups of 100 to avoid generating too large messages, and each chunk is sent spaced by 100 ms, to avoid rate limiting. In parallel, avoid room flooding by refusing to print statuses with more than 50 repositories or 1000 packages. Fixes issue #1. --- oscbot/__init__.py | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/oscbot/__init__.py b/oscbot/__init__.py index 1469901..979fe56 100644 --- a/oscbot/__init__.py +++ b/oscbot/__init__.py @@ -2,10 +2,12 @@ # SPDX-License-Identifier: AGPL-3.0-or-later from dataclasses import dataclass, field +from time import sleep from typing import Optional, List, Type, Tuple import aiohttp import cryptocode +import more_itertools as mit from lxml import objectify from jinja2 import BaseLoader, Environment @@ -22,10 +24,8 @@ Only showing packages with {{ state }} state. {% endif %}""" -REPO_TEMPLATE = """ -#### {{ repo.name }} - {{ repo.arch }} - -{% for package in repo.packages %} +STATUS_TEMPLATE = """ +{% for package in packages %} {%if package.status != "disabled" %} {% set build_log_url = "{0}/package/live_build_log/{1}/{2}/{3}/{4}/".format( base_url, project, package.name, repo.name, repo.arch) %} @@ -287,8 +287,29 @@ class OSCBot(Plugin): base_url = self.config["instance_url"] + if len(response) > 50: + await evt.respond(f"Too many repositories ({len(response)})" + f"to display in project {project}, " + "please narrow down your search.") + return + for repository in response: - body = self.template.from_string(REPO_TEMPLATE) - message = body.render(repo=repository, project=project, - base_url=base_url) - await evt.respond(message, markdown=True) + await evt.respond(f"#### {repository.name} - {repository.arch}", + markdown=True) + + # Just so that someone doesn't flood by listing the whole + # openSUSE:Factory + if len(repository.packages) > 1000: + await evt.respond( + f"Too many packages ({len(repository.packages)}) " + "to display, please narrow down your search.") + continue + # To avoid creating too large messages, we chunk packages in + # groups of 100 + for packagelist in mit.chunked(repository.packages, 100): + body = self.template.from_string(STATUS_TEMPLATE) + message = body.render(packages=packagelist, base_url=base_url, + project=project, repo=repository) + await evt.respond(message, markdown=True) + # Wait 100 milliseconds to avoid triggering rate limiting + sleep(0.1) From 485dffaafc5caba1d96ba294b393dd3bdd8ce961 Mon Sep 17 00:00:00 2001 From: Luca Beltrame Date: Sun, 30 Jan 2022 11:16:46 +0100 Subject: [PATCH 22/28] And again I forgot to version bump... --- maubot.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maubot.yaml b/maubot.yaml index 6bcfc78..c1456f6 100644 --- a/maubot.yaml +++ b/maubot.yaml @@ -2,7 +2,7 @@ # SPDX-License-Identifier: CC0-1.0 maubot: 0.1.0 id: org.dennogumi.osc -version: 0.2.2 +version: 0.3.1 license: AGPL-3.0-or-later modules: - oscbot From aea53325adc10e03ead81225e7fe1934c85c452b Mon Sep 17 00:00:00 2001 From: Luca Beltrame Date: Tue, 8 Feb 2022 23:38:26 +0100 Subject: [PATCH 23/28] Initial support for aliases maubot's mandatory positional arguments make things a little difficult. --- oscbot/__init__.py | 121 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 119 insertions(+), 2 deletions(-) diff --git a/oscbot/__init__.py b/oscbot/__init__.py index 979fe56..02ff417 100644 --- a/oscbot/__init__.py +++ b/oscbot/__init__.py @@ -36,6 +36,21 @@ STATUS_TEMPLATE = """ {% endfor %} """ +ALIAS_TEMPLATE = """ +### Configured aliases + +{% for alias, contents in repo_aliases.items() %} +#### {{ alias }} + +- project: {{ contents.project }} +- repository: {{ contents.repository }} +- package(s): {{ contents.package }} +- architecture(s): {{ contents.arch }} +- state: {{ contents.state }} + +{% endfor %} +""" + @dataclass class BuildResult: @@ -200,6 +215,103 @@ class OSCBot(Plugin): async def osc(self) -> None: pass + @osc.subcommand("alias", help="Manage aliases") + async def alias(self) -> None: + pass + + @alias.subcommand("list", help="List configured aliases") + async def list_aliases(self, evt: MessageEvent) -> None: + + body = self.template.from_string(ALIAS_TEMPLATE) + + if self.config.get("repo_aliases", None) is None: + await evt.reply("No aliases defined.") + return + + message = body.render(repo_aliases=self.config["repo_aliases"]) + await evt.respond(message, markdown=True) + + @alias.subcommand("edit", help="Edit configured aliases") + @command.argument("alias", "alias name") + @command.argument("project", "project name") + @command.argument("package", "package name", required=False) + @command.argument("repository", "repository", required=False) + @command.argument("arch", "architecture", required=False) + @command.argument("state", "state", required=False) + async def edit_alias(self, evt: MessageEvent, + alias: str, + project: Optional[str] = None, + package: Optional[str] = None, + repository: Optional[str] = None, + arch: Optional[str] = None, + state: Optional[str] = None) -> None: + + if alias not in self.config["repo_aliases"]: + await evt.respond(f"Unknown alias {alias}") + return + + alias_data = self.config["repo_aliases"][alias] + + if project: + alias_data["project"] = project + + if repository: + alias_data["repository"] = repository + + if package: + alias_data["package"] = package + + if arch: + alias_data["arch"] = alias + + if state: + alias_data["state"] = state + + self.config["repo_aliases"][alias] = alias_data + self.config.save() + await evt.reply(f"Alias {alias} successfully changed.") + + @alias.subcommand("create", help="Create a new alias") + @command.argument("alias", "alias name") + @command.argument("project", "project name") + @command.argument("package", "package name", required=False) + @command.argument("repository", "repository", required=False) + @command.argument("arch", "architecture", required=False) + @command.argument("state", "state", required=False) + async def create_alias(self, evt: MessageEvent, + alias: str, + project: Optional[str] = None, + package: Optional[str] = None, + repository: Optional[str] = None, + arch: Optional[str] = None, + state: Optional[str] = None) -> None: + + repository = "all" if not repository else repository + package = "all" if not package else package + arch = "all" if not arch else arch + state = "all" if not state else state + alias_data = { + "project": project, + "package": package, + "repository": repository, + "arch": arch, + "state": state + } + self.config["repo_aliases"][alias] = alias_data + self.config.save() + await evt.reply(f"Alias {alias} successfully created.") + + @alias.subcommand("delete", help="Delete an alias", aliases=("rm", )) + @command.argument("alias", "alias name") + async def delete_alias(self, evt: MessageEvent, alias: str): + if alias not in self.config["repo_aliases"]: + await evt.respond(f"Unknown alias {alias}") + return + + del self.config["repo_aliases"][alias] + self.config.save() + await evt.reply(f"Alias {alias} successfully deleted.") + @osc.subcommand( "rebuildpac", aliases=("rb",), help="Rebuild a package or all packages in the repositories") @@ -213,9 +325,14 @@ class OSCBot(Plugin): repository: Optional[str] = None, arch: Optional[str] = None) -> None: + if project in self.config["repo_aliases"]: + # We're not interested in state and we query package explicitly + project, _, repository, _, arch = self.get_alias(project) + else: + repository = None if repository == "all" else repository + arch = None if arch == "all" else arch + package = None if package == "all" else package - repository = None if repository == "all" else repository - arch = None if arch == "all" else arch result, status = await self.parse_rebuilpac(project, package, repository, From c9d4e0312d4149a5d4de3cbadae3b9bb8826dc3e Mon Sep 17 00:00:00 2001 From: Luca Beltrame Date: Sat, 12 Feb 2022 10:19:06 +0100 Subject: [PATCH 24/28] Support for ACLs Users in the "admin" level can set aliases, rebuild packages, and trigger services. On the other hand, users in the "user" level can only perform read-only operations (reading status at this point). --- base-config.yaml | 5 +++++ oscbot/__init__.py | 46 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/base-config.yaml b/base-config.yaml index 96c03f6..cf823be 100644 --- a/base-config.yaml +++ b/base-config.yaml @@ -34,3 +34,8 @@ repo_aliases: repository: "KDE_Unstable_Frameworks_openSUSE_Factory" state: "failed" arch: "all" +acl: + admin: + - "@CHANGE_ME" + user: [] + diff --git a/oscbot/__init__.py b/oscbot/__init__.py index 02ff417..73735b5 100644 --- a/oscbot/__init__.py +++ b/oscbot/__init__.py @@ -83,6 +83,7 @@ class Config(BaseProxyConfig): helper.copy("username") helper.copy("repo_aliases") + helper.copy("acl") class OSCBot(Plugin): @@ -114,6 +115,18 @@ class OSCBot(Plugin): return (project, package, repository, state, arch) + def check_acl(self, user_id: str, need_admin: bool = False) -> bool: + + acls = self.config["acl"] + if not need_admin: + if user_id in acls["admin"] or acls["user"]: + return True + else: + if user_id in acls["admin"]: + return True + self.log.debug(f"Denied operation as {user_id}") + return False + async def parse_rebuilpac( self, project: str, @@ -222,6 +235,10 @@ class OSCBot(Plugin): @alias.subcommand("list", help="List configured aliases") async def list_aliases(self, evt: MessageEvent) -> None: + if not self.check_acl(evt.sender, need_admin=False): + await evt.reply("You are not authorized to perform this action.") + return + body = self.template.from_string(ALIAS_TEMPLATE) if self.config.get("repo_aliases", None) is None: @@ -246,6 +263,10 @@ class OSCBot(Plugin): arch: Optional[str] = None, state: Optional[str] = None) -> None: + if not self.check_acl(evt.sender, need_admin=True): + await evt.reply("You are not authorized to perform this action.") + return + if alias not in self.config["repo_aliases"]: await evt.respond(f"Unknown alias {alias}") return @@ -286,6 +307,10 @@ class OSCBot(Plugin): arch: Optional[str] = None, state: Optional[str] = None) -> None: + if not self.check_acl(evt.sender, need_admin=True): + await evt.reply("You are not authorized to perform this action.") + return + repository = "all" if not repository else repository package = "all" if not package else package arch = "all" if not arch else arch @@ -304,6 +329,11 @@ class OSCBot(Plugin): @alias.subcommand("delete", help="Delete an alias", aliases=("rm", )) @command.argument("alias", "alias name") async def delete_alias(self, evt: MessageEvent, alias: str): + + if not self.check_acl(evt.sender, need_admin=True): + await evt.reply("You are not authorized to perform this action.") + return + if alias not in self.config["repo_aliases"]: await evt.respond(f"Unknown alias {alias}") return @@ -325,6 +355,10 @@ class OSCBot(Plugin): repository: Optional[str] = None, arch: Optional[str] = None) -> None: + if not self.check_acl(evt.sender, need_admin=True): + await evt.reply("You are not authorized to perform this action.") + return + if project in self.config["repo_aliases"]: # We're not interested in state and we query package explicitly project, _, repository, _, arch = self.get_alias(project) @@ -353,6 +387,10 @@ class OSCBot(Plugin): project: str, package: str) -> None: + if not self.check_acl(evt.sender, need_admin=True): + await evt.reply("You are not authorized to perform this action.") + return + token = self.config["trigger_token"] trigger_url = f"{self.config['api_url']}/trigger/runservice" params = {"project": project, "package": package} @@ -382,6 +420,10 @@ class OSCBot(Plugin): repository: Optional[str] = None, arch: Optional[str] = None) -> None: + if not self.check_acl(evt.sender, need_admin=False): + await evt.reply("You are not authorized to perform this action.") + return + if project in self.config["repo_aliases"]: project, package, repository, state, arch = self.get_alias(project) else: @@ -428,5 +470,5 @@ class OSCBot(Plugin): message = body.render(packages=packagelist, base_url=base_url, project=project, repo=repository) await evt.respond(message, markdown=True) - # Wait 100 milliseconds to avoid triggering rate limiting - sleep(0.1) + # Wait 200 milliseconds to avoid triggering rate limiting + sleep(0.2) From 26d34967a06252b1da008eb6cdd1ebc81e41f9f0 Mon Sep 17 00:00:00 2001 From: Luca Beltrame Date: Sat, 12 Feb 2022 10:20:14 +0100 Subject: [PATCH 25/28] Bump version --- maubot.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maubot.yaml b/maubot.yaml index c1456f6..403a0ee 100644 --- a/maubot.yaml +++ b/maubot.yaml @@ -2,7 +2,7 @@ # SPDX-License-Identifier: CC0-1.0 maubot: 0.1.0 id: org.dennogumi.osc -version: 0.3.1 +version: 0.4.0 license: AGPL-3.0-or-later modules: - oscbot From 34a24e599af2a226b97d518baf3f288a5bfaa932 Mon Sep 17 00:00:00 2001 From: Luca Beltrame Date: Sat, 12 Feb 2022 10:30:58 +0100 Subject: [PATCH 26/28] [skip ci] Update README --- README.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 495cf87..98a580f 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ Then you need to modify a few configuration parameters for your instance: - `trigger_token`: An OBS access token with the `runservice` capability. See "Creating tokens" below. - `rebuild_token`: An OBS access token with the `rebuild` capability. See "Creating tokens" below. - `secret`: A random string used to encrypt the password in the configuration file. +- `acl`: User IDs (`@user:homeserver.com`) which are allowed to interact with the bot. See "Access control lists" below. ### Creating tokens @@ -59,9 +60,26 @@ Save both tokens in the configuration. Should you need to, you can view them lat Make sure your bot instance is in a room (refer to the maubot docs for how) and then type `!osc help` for help. +### Access control lists (ACLs) + +By default, no user is allowed to perform any operation. You need to change the `acl` section of the configuration to add user IDs (`@user:homeserver.com`) allowed to interact with the bot. Users in the `admin` list have full powers over the bot, while users in the `user` list can only perform read-only (e.g. status querying) actions. + +This example shows how it works in practice: + +```yaml +acl: + admin: + # Fred and Sue will be able to perform all commands + - @fred:myhome.com + - @sue:otherserver.com + user: + # Phil will only be able to run read-only (status, etc.) commands + - @phil:elsewhere.com +``` + ### Notice -I made this in about three or four hours for my own use. Don't expect high quality code or a lot of polish. +I made this for my own use. Don't expect high quality code or a lot of polish. ### License From 8089dd36e5a0c455a1ee41b1b17d13f499af7492 Mon Sep 17 00:00:00 2001 From: Luca Beltrame Date: Sat, 12 Feb 2022 10:34:07 +0100 Subject: [PATCH 27/28] [skip ci] Update README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 98a580f..cc659e3 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ These are in addition to what maubot requires by itself. - lxml - jinja2 +- [cryptocode](https://github.com/gdavid7/cryptocode) ### Installation From f4bc26ac1328ded5ee6f0e580ac0f18051a073f2 Mon Sep 17 00:00:00 2001 From: Luca Beltrame Date: Sat, 12 Feb 2022 10:35:38 +0100 Subject: [PATCH 28/28] [skip ci] Update README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index cc659e3..559df52 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ SPDX-License-Identifier: CC0-1.0 ## maubot-osc +![status-badge](https://ci.dennogumi.org/api/badges/einar/maubot-osc/status.svg) + `maubot-osc` is a simple [maubot](https://maubot.xyz/) plugin to query an [Open Build Service](https://openbuildservice.org/) instance. Basically it replicates a few of the features of the `osc` command line tool used to interact with the OBS. ### Features