Skip to content

Commit 5f030db

Browse files
authored
Merge pull request #251 from MagicRB/support-fully-private-mode
Support fully private mode
2 parents ffd44ea + d990365 commit 5f030db

File tree

7 files changed

+348
-15
lines changed

7 files changed

+348
-15
lines changed

README.md

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,19 @@ examples to guide you:
7676

7777
### Authentication backend
7878

79-
At the moment all projects are visible without authentication.
79+
At the moment `buildbot-nix` offers two access modes, `public` and
80+
`fullyPrivate`. `public` is the default and gives read-only access to all of
81+
buildbot, including builds, logs and builders. For read-write access,
82+
authentication is still needed, this is controlled by the `authBackend` option.
83+
84+
`fullyPrivate` will hide buildbot behind `oauth2-proxy` which protects the whole
85+
buildbot instance. buildbot fetches the currently authenticated user from
86+
`oauth2-proxy` so the same admin, organisation rules apply.
87+
88+
`fullyPrivate` acccess mode is a workaround as buildbot does not support hiding
89+
information natively as now.
90+
91+
#### Public
8092

8193
For some actions a login is required. This login can either be based on GitHub
8294
or on Gitea (more logins may follow). The backend is set by the
@@ -92,9 +104,9 @@ We have the following two roles:
92104
- All member of the organisation where this repository is located
93105
- They can restart builds
94106

95-
### Integration with GitHub
107+
##### Integration with GitHub
96108

97-
#### GitHub App
109+
###### GitHub App
98110

99111
This is the preferred option to setup buildbot-nix for GitHub.
100112

@@ -128,15 +140,15 @@ To integrate with GitHub using app authentication:
128140
changes (new repositories or installations) automatically, it is therefore
129141
necessary to manually trigger a reload or wait for the next periodic reload.
130142

131-
#### Token Auth
143+
###### Token Auth
132144

133145
To integrate with GitHub using legacy token authentication:
134146

135147
1. **GitHub Token**: Obtain a GitHub token with `admin:repo_hook` and `repo`
136148
permissions. For GitHub organizations, it's advisable to create a separate
137149
GitHub user for managing repository webhooks.
138150

139-
### Optional when using GitHub login
151+
##### Optional when using GitHub login
140152

141153
1. **GitHub App**: Set up a GitHub app for Buildbot to enable GitHub user
142154
authentication on the Buildbot dashboard. (can be the same as for GitHub App
@@ -149,7 +161,7 @@ Afterwards add the configured github topic to every project that should build
149161
with buildbot-nix. Notice that the buildbot user needs to have admin access to
150162
this repository because it needs to install a webhook.
151163

152-
### Integration with Gitea
164+
##### Integration with Gitea
153165

154166
To integrate with Gitea
155167

@@ -171,6 +183,22 @@ with buildbot-nix. Notice that the buildbot user needs to have repository write
171183
access to this repository because it needs to install a webhook in the
172184
repository.
173185

186+
#### Fully Private
187+
188+
To enable fully private mode, set `acessMode.fullyPrivate` to an attrset
189+
containing the required options for fully private use, refer to the examples and
190+
module implementation (`nix/master.nix`).
191+
192+
This access mode honors the `admins` option in addition to the
193+
`accessMode.fullyPrivate.organisations` option. To allow access from certain
194+
organisations, you must explicitly list them.
195+
196+
If you've set `authBackend` previously, unset it, or you will get an error about
197+
a conflicting definitions. `fullyPrivate` requires the `authBackend` to be set
198+
to `basichttpauth` to function (this is handled by the module, which is why you
199+
can leave it unset). For a concrete example please refer to
200+
[fully-private-github](./examples/fully-private-github.nix)
201+
174202
## Binary caches
175203

176204
To access the build results on other machines there are two options at the

buildbot_nix/__init__.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@
3636
from .github_projects import (
3737
GithubBackend,
3838
)
39-
from .models import BuildbotNixConfig
39+
from .models import AuthBackendConfig, BuildbotNixConfig
40+
from .oauth2_proxy_auth import OAuth2ProxyAuth
4041
from .projects import GitBackend, GitProject
4142

4243
SKIPPED_BUILDER_NAME = "skipped-builds"
@@ -913,11 +914,13 @@ def configure(self, config: dict[str, Any]) -> None:
913914
if self.config.gitea is not None:
914915
backends["gitea"] = GiteaBackend(self.config.gitea, self.config.url)
915916

916-
auth: AuthBase | None = (
917-
backends[self.config.auth_backend].create_auth()
918-
if self.config.auth_backend != "none"
919-
else None
920-
)
917+
auth: AuthBase | None = None
918+
if self.config.auth_backend == AuthBackendConfig.httpbasicauth:
919+
auth = OAuth2ProxyAuth(self.config.http_basic_auth_password)
920+
elif self.config.auth_backend == AuthBackendConfig.none:
921+
pass
922+
elif backends[self.config.auth_backend] is not None:
923+
auth = backends[self.config.auth_backend].create_auth()
921924

922925
projects: list[GitProject] = []
923926

buildbot_nix/models.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ def exclude_fields(fields: list[str]) -> dict[str, dict[str, bool]]:
1818
class AuthBackendConfig(str, Enum):
1919
github = "github"
2020
gitea = "gitea"
21+
httpbasicauth = "httpbasicauth"
2122
none = "none"
2223

2324

@@ -180,7 +181,14 @@ class BuildbotNixConfig(BaseModel):
180181
url: str
181182
post_build_steps: list[PostBuildStep]
182183
job_report_limit: int | None
184+
http_basic_auth_password_file: Path | None
183185

184186
@property
185187
def nix_workers_secret(self) -> str:
186188
return read_secret_file(self.nix_workers_secret_file)
189+
190+
@property
191+
def http_basic_auth_password(self) -> str:
192+
if self.http_basic_auth_password_file is None:
193+
raise InternalError
194+
return read_secret_file(self.http_basic_auth_password_file)

buildbot_nix/oauth2_proxy_auth.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import base64
2+
import typing
3+
from typing import Any, ClassVar
4+
5+
from buildbot.util import bytes2unicode, unicode2bytes
6+
from buildbot.www.auth import AuthBase, UserInfoProviderBase
7+
from twisted.internet import defer
8+
from twisted.internet.defer import Generator
9+
from twisted.logger import Logger
10+
from twisted.web.error import Error
11+
from twisted.web.pages import forbidden
12+
from twisted.web.resource import IResource
13+
from twisted.web.server import Request
14+
from twisted.web.util import Redirect
15+
16+
log = Logger()
17+
18+
19+
class OAuth2ProxyAuth(AuthBase):
20+
header: ClassVar[bytes] = b"Authorization"
21+
prefix: ClassVar[bytes] = b"Basic "
22+
user_info_provider: UserInfoProviderBase = None
23+
password: bytes
24+
25+
def __init__(self, password: str, **kwargs: Any) -> None:
26+
super().__init__(**kwargs)
27+
if self.user_info_provider is None:
28+
self.user_info_provider = UserInfoProviderBase()
29+
self.password = unicode2bytes(password)
30+
31+
def getLoginResource(self) -> IResource: # noqa: N802
32+
return forbidden(message="URL is not supported for authentication")
33+
34+
def getLogoutResource(self) -> IResource: # noqa: N802
35+
return typing.cast(IResource, Redirect(b"/oauth2/sign_out"))
36+
37+
@defer.inlineCallbacks
38+
def maybeAutoLogin( # noqa: N802
39+
self, request: Request
40+
) -> Generator[defer.Deferred[None], object, None]:
41+
header = request.getHeader(self.header)
42+
if header is None:
43+
msg = (
44+
b"missing http header "
45+
+ self.header
46+
+ b". Check your oauth2-proxy config!"
47+
)
48+
raise Error(403, msg)
49+
if not header.startswith(self.prefix):
50+
msg = (
51+
b"invalid http header "
52+
+ self.header
53+
+ b". Check your oauth2-proxy config!"
54+
)
55+
raise Error(403, msg)
56+
header = header.removeprefix(self.prefix)
57+
(username, password) = base64.b64decode(header).split(b":")
58+
username = bytes2unicode(username)
59+
60+
if password != self.password:
61+
msg = b"invalid password given. Check your oauth2-proxy config!!"
62+
raise Error(403, msg)
63+
64+
session = request.getSession() # type: ignore[no-untyped-call]
65+
user_info = {"username": username}
66+
if session.user_info != user_info:
67+
session.user_info = user_info
68+
yield self.updateUserInfo(request)

examples/default.nix

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,14 @@ in
6565
{ services.buildbot-nix.worker.masterUrl = ''tcp:host=2a09\:80c0\:102\:\:1:port=9989''; }
6666
];
6767
};
68+
69+
"example-oauth2-proxy-github-${system}" = nixosSystem {
70+
inherit system;
71+
modules = [
72+
dummy
73+
buildbot-nix.nixosModules.buildbot-master
74+
buildbot-nix.nixosModules.buildbot-worker
75+
./fully-private-github.nix
76+
];
77+
};
6878
}

examples/fully-private-github.nix

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
{ pkgs, ... }:
2+
{
3+
# For a more full fledged and commented example refer to ./master.nix and ./worker.nix,
4+
# those two files give a better introductory example than this one
5+
services.buildbot-nix.master = {
6+
enable = true;
7+
8+
domain = "buildbot.example.org";
9+
workersFile = pkgs.writeText "workers.json" "changeMe";
10+
admins = [ ];
11+
12+
# `authBackend` can be omitted here, the module sets it itself
13+
authBackend = "httpbasicauth";
14+
# this is a randomly generated secret, which is only used to authenticate requests
15+
# from the oauth2 proxy to buildbot
16+
httpBasicAuthPasswordFile = pkgs.writeText "http-basic-auth-passwd" "changeMe";
17+
18+
gitea = {
19+
enable = true;
20+
tokenFile = "/secret/gitea_token";
21+
instanceUrl = "https://codeberg.org";
22+
webhookSecretFile = pkgs.writeText "webhook-secret" "changeMe";
23+
topic = "build-with-buildbot";
24+
};
25+
github = {
26+
enable = true;
27+
webhookSecretFile = pkgs.writeText "github_webhook_secret" "changeMe";
28+
topic = "build-with-buildbot";
29+
authType.app = {
30+
secretKeyFile = pkgs.writeText "github_app_secret_key" "changeMe";
31+
id = 0;
32+
};
33+
};
34+
35+
# optional nix-eval-jobs settings
36+
evalWorkerCount = 2; # limit number of concurrent evaluations
37+
evalMaxMemorySize = 4096; # limit memory usage per evaluation
38+
39+
accessMode.fullyPrivate = {
40+
backend = "github";
41+
# this is a randomly generated alphanumeric secret, which is used to encrypt the cookies set by
42+
# oauth2-proxy, it must be 8, 16, or 32 characters long
43+
cookieSecretFile = pkgs.writeText "github_cookie_secret" "changeMe";
44+
clientSecretFile = pkgs.writeText "github_oauth_secret" "changeMe";
45+
clientId = "Iv1.XXXXXXXXXXXXXXXX";
46+
};
47+
};
48+
49+
services.buildbot-nix.worker = {
50+
enable = true;
51+
workerPasswordFile = "/secret/worker_secret";
52+
};
53+
}

0 commit comments

Comments
 (0)