Skip to content

Add users option to both GitHub and Gitea restricting project owners #394

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions buildbot_nix/buildbot_nix/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
),
)
)

Expand Down
14 changes: 11 additions & 3 deletions buildbot_nix/buildbot_nix/gitea_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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,
)

Expand Down
46 changes: 41 additions & 5 deletions buildbot_nix/buildbot_nix/github_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -153,13 +155,17 @@ 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
self.installation_token_map_name = installation_token_map_name
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:
Expand All @@ -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(),
Expand All @@ -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,
)

Expand Down Expand Up @@ -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,
)

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -447,14 +473,18 @@ 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(
self.jwt_token,
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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
)

Expand Down
5 changes: 5 additions & 0 deletions buildbot_nix/buildbot_nix/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down Expand Up @@ -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"))

Expand Down
29 changes: 29 additions & 0 deletions nixosModules/master.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be cool if you could add those options also to https://github.com/nix-community/buildbot-nix/blob/main/examples/master.nix
To have same example-driven documentation.

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 {
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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";
Expand All @@ -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"; }
Expand Down