From 24f1150760d2d117a2e94f627f51ceabb9dce7a2 Mon Sep 17 00:00:00 2001 From: magic_rb Date: Wed, 12 Feb 2025 12:24:21 +0100 Subject: [PATCH] Add `users` option to both GitHub and Gitea restricting project owners Signed-off-by: magic_rb --- buildbot_nix/buildbot_nix/common.py | 24 ++++++++-- buildbot_nix/buildbot_nix/gitea_projects.py | 14 ++++-- buildbot_nix/buildbot_nix/github_projects.py | 46 +++++++++++++++++--- buildbot_nix/buildbot_nix/models.py | 5 +++ nixosModules/master.nix | 29 ++++++++++++ 5 files changed, 106 insertions(+), 12 deletions(-) diff --git a/buildbot_nix/buildbot_nix/common.py b/buildbot_nix/buildbot_nix/common.py index 8e2e9d174..985bd9a19 100644 --- a/buildbot_nix/buildbot_nix/common.py +++ b/buildbot_nix/buildbot_nix/common.py @@ -114,13 +114,29 @@ def atomic_write_file(file: Path, data: str) -> None: Y = TypeVar("Y") -def filter_repos_by_topic( - topic: str | None, repos: list[Y], topics: Callable[[Y], list[str]] +def filter_repos( + repo_allowlist: list[str] | None, + user_allowlist: list[str] | None, + topic: str | None, + repos: list[Y], + repo_name: Callable[[Y], str], + user: Callable[[Y], str], + topics: Callable[[Y], list[str]], ) -> list[Y]: + # This is a bit complicated so let me explain: + # If both `user_allowlist` and `repo_allowlist` are `None` then we want to allow everything, + # however if either are non-`None`, then the one that is non-`None` determines whether to + # allow a repo, or both if both are non-+None`. + return list( filter( - lambda repo: topic is None or topic in topics(repo), - repos, + lambda repo: (user_allowlist is None and repo_allowlist is None) + or (user_allowlist is not None and user(repo) in user_allowlist) + or (repo_allowlist is not None and repo_name(repo) in repo_allowlist), + filter( + lambda repo: topic is None or topic in topics(repo), + repos, + ), ) ) diff --git a/buildbot_nix/buildbot_nix/gitea_projects.py b/buildbot_nix/buildbot_nix/gitea_projects.py index 034fc3cc0..b84248a73 100644 --- a/buildbot_nix/buildbot_nix/gitea_projects.py +++ b/buildbot_nix/buildbot_nix/gitea_projects.py @@ -20,7 +20,7 @@ from .common import ( ThreadDeferredBuildStep, atomic_write_file, - filter_repos_by_topic, + filter_repos, http_request, model_dump_project_cache, model_validate_project_cache, @@ -185,12 +185,16 @@ def load_projects(self) -> list["GitProject"]: if not self.config.project_cache_file.exists(): return [] - repos: list[RepoData] = filter_repos_by_topic( + repos: list[RepoData] = filter_repos( + self.config.repo_allowlist, + self.config.user_allowlist, self.config.topic, sorted( model_validate_project_cache(RepoData, self.config.project_cache_file), key=lambda repo: repo.full_name, ), + lambda repo: repo.full_name, + lambda repo: repo.owner.login, lambda repo: repo.topics, ) repo_names: list[str] = [repo.owner.login + "/" + repo.name for repo in repos] @@ -320,9 +324,13 @@ def __init__( super().__init__(**kwargs) def run_deferred(self) -> None: - repos: list[RepoData] = filter_repos_by_topic( + repos: list[RepoData] = filter_repos( + self.config.repo_allowlist, + self.config.user_allowlist, self.config.topic, refresh_projects(self.config, self.project_cache_file), + lambda repo: repo.full_name, + lambda repo: repo.owner.login, lambda repo: repo.topics, ) diff --git a/buildbot_nix/buildbot_nix/github_projects.py b/buildbot_nix/buildbot_nix/github_projects.py index b1dac6aac..a1497ecc1 100644 --- a/buildbot_nix/buildbot_nix/github_projects.py +++ b/buildbot_nix/buildbot_nix/github_projects.py @@ -25,7 +25,7 @@ from .common import ( ThreadDeferredBuildStep, atomic_write_file, - filter_repos_by_topic, + filter_repos, http_request, model_dump_project_cache, model_validate_project_cache, @@ -145,6 +145,8 @@ class ReloadGithubInstallations(ThreadDeferredBuildStep): installation_token_map_name: Path project_id_map_name: Path topic: str | None + user_allowlist: list[str] | None + repo_allowlist: list[str] | None def __init__( self, @@ -153,6 +155,8 @@ def __init__( installation_token_map_name: Path, project_id_map_name: Path, topic: str | None, + user_allowlist: list[str] | None, + repo_allowlist: list[str] | None, **kwargs: Any, ) -> None: self.jwt_token = jwt_token @@ -160,6 +164,8 @@ def __init__( self.project_id_map_name = project_id_map_name self.project_cache_file = project_cache_file self.topic = topic + self.user_allowlist = user_allowlist + self.repo_allowlist = repo_allowlist super().__init__(**kwargs) def run_deferred(self) -> None: @@ -179,7 +185,9 @@ def run_deferred(self) -> None: repos = [] for k, v in installation_token_map.items(): - new_repos = filter_repos_by_topic( + new_repos = filter_repos( + self.repo_allowlist, + self.user_allowlist, self.topic, refresh_projects( v.get(), @@ -189,6 +197,8 @@ def run_deferred(self) -> None: subkey="repositories", require_admin=False, ), + lambda repo: repo.full_name, + lambda repo: repo.owner.login, lambda repo: repo.topics, ) @@ -259,23 +269,33 @@ class ReloadGithubProjects(ThreadDeferredBuildStep): token: RepoToken project_cache_file: Path topic: str | None + user_allowlist: list[str] | None + repo_allowlist: list[str] | None def __init__( self, token: RepoToken, project_cache_file: Path, topic: str | None, + user_allowlist: list[str] | None, + repo_allowlist: list[str] | None, **kwargs: Any, ) -> None: self.token = token self.project_cache_file = project_cache_file self.topic = topic + self.user_allowlist = user_allowlist + self.repo_allowlist = repo_allowlist super().__init__(**kwargs) def run_deferred(self) -> None: - repos: list[RepoData] = filter_repos_by_topic( + repos: list[RepoData] = filter_repos( + self.repo_allowlist, + self.user_allowlist, self.topic, refresh_projects(self.token.get(), self.project_cache_file), + lambda repo: repo.full_name, + lambda repo: repo.owner.login, lambda repo: repo.topics, ) @@ -309,6 +329,8 @@ def create_reload_builder_steps( webhook_secret: str, webhook_url: str, topic: str | None, + user_allowlist: list[str] | None, + repo_allowlist: list[str] | None, ) -> list[BuildStep]: pass @@ -349,12 +371,16 @@ def create_reload_builder_steps( webhook_secret: str, webhook_url: str, topic: str | None, + user_allowlist: list[str] | None, + repo_allowlist: list[str] | None, ) -> list[BuildStep]: return [ ReloadGithubProjects( token=self.token, project_cache_file=project_cache_file, topic=topic, + user_allowlist=user_allowlist, + repo_allowlist=repo_allowlist, ), CreateGitHubProjectHooks( token=self.token, @@ -447,6 +473,8 @@ def create_reload_builder_steps( webhook_secret: str, webhook_url: str, topic: str | None, + user_allowlist: list[str] | None, + repo_allowlist: list[str] | None, ) -> list[BuildStep]: return [ ReloadGithubInstallations( @@ -454,7 +482,9 @@ def create_reload_builder_steps( project_cache_file, self.auth_type.installation_token_map_file, self.auth_type.project_id_map_file, - topic, + topic=topic, + user_allowlist=user_allowlist, + repo_allowlist=repo_allowlist, ), CreateGitHubInstallationHooks( self.jwt_token, @@ -564,6 +594,8 @@ def create_reload_builder(self, worker_names: list[str]) -> BuilderConfig: self.webhook_secret, self.webhook_url, self.config.topic, + self.config.user_allowlist, + self.config.repo_allowlist, ) for step in steps: factory.addStep(step) @@ -610,12 +642,16 @@ def load_projects(self) -> list["GitProject"]: if not self.config.project_cache_file.exists(): return [] - repos: list[RepoData] = filter_repos_by_topic( + repos: list[RepoData] = filter_repos( + self.config.repo_allowlist, + self.config.user_allowlist, self.config.topic, sorted( model_validate_project_cache(RepoData, self.config.project_cache_file), key=lambda repo: repo.full_name, ), + lambda repo: repo.full_name, + lambda repo: repo.owner.login, lambda repo: repo.topics, ) diff --git a/buildbot_nix/buildbot_nix/models.py b/buildbot_nix/buildbot_nix/models.py index 0b5913bf5..3e3ff719b 100644 --- a/buildbot_nix/buildbot_nix/models.py +++ b/buildbot_nix/buildbot_nix/models.py @@ -58,6 +58,9 @@ class GiteaConfig(BaseModel): instance_url: str topic: str | None + user_allowlist: list[str] | None + repo_allowlist: list[str] | None + token_file: Path = Field(default=Path("gitea-token")) webhook_secret_file: Path = Field(default=Path("gitea-webhook-secret")) project_cache_file: Path = Field(default=Path("gitea-project-cache.json")) @@ -148,6 +151,8 @@ class GitHubConfig(BaseModel): auth_type: GitHubLegacyConfig | GitHubAppConfig topic: str | None + user_allowlist: list[str] | None + repo_allowlist: list[str] | None project_cache_file: Path = Field(default=Path("github-project-cache-v1.json")) webhook_secret_file: Path = Field(default=Path("github-webhook-secret")) diff --git a/nixosModules/master.nix b/nixosModules/master.nix index 595d982c1..1d04d099c 100644 --- a/nixosModules/master.nix +++ b/nixosModules/master.nix @@ -282,6 +282,18 @@ in default = cfg.authBackend == "gitea"; }; + userAllowlist = lib.mkOption { + type = lib.types.nullOr (lib.types.listOf lib.types.str); + default = null; + description = "If non-null, specifies users/organizations that are allowed to use buildbot, i.e. buildbot-nix will ignore any repositories not owned by these users/organizations."; + }; + + repoAllowlist = lib.mkOption { + type = lib.types.nullOr (lib.types.listOf lib.types.str); + default = null; + description = "If non-null, specifies an explicit set of repositories that are allowed to use buildbot, i.e. buildbot-nix will ignore any repositories not in this list."; + }; + tokenFile = lib.mkOption { type = lib.types.path; description = "Gitea token file"; @@ -336,6 +348,18 @@ in default = cfg.authBackend == "github"; }; + userAllowlist = lib.mkOption { + type = lib.types.nullOr (lib.types.listOf lib.types.str); + default = null; + description = "If non-null, specifies users/organizations that are allowed to use buildbot, i.e. buildbot-nix will ignore any repositories not owned by these users/organizations."; + }; + + repoAllowlist = lib.mkOption { + type = lib.types.nullOr (lib.types.listOf lib.types.str); + default = null; + description = "If non-null, specifies an explicit set of repositories that are allowed to use buildbot, i.e. buildbot-nix will ignore any repositories not in this list."; + }; + authType = lib.mkOption { type = lib.types.attrTag { legacy = lib.mkOption { @@ -492,6 +516,7 @@ in default = [ ]; description = "Users that are allowed to login to buildbot, trigger builds and change settings"; }; + workersFile = lib.mkOption { type = lib.types.path; description = "File containing a list of nix workers"; @@ -712,6 +737,8 @@ in null else { + user_allowlist = cfg.gitea.userAllowlist; + repo_allowlist = cfg.gitea.repoAllowlist; token_file = "gitea-token"; webhook_secret_file = "gitea-webhook-secret"; project_cache_file = "gitea-project-cache.json"; @@ -727,6 +754,8 @@ in null else { + user_allowlist = cfg.github.userAllowlist; + repo_allowlist = cfg.github.repoAllowlist; auth_type = if (cfg.github.authType ? "legacy") then { token_file = "github-token"; }