aboutsummaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
authorIsmaël Bouya <ismael.bouya@normalesup.org>2019-05-22 20:55:28 +0200
committerIsmaël Bouya <ismael.bouya@normalesup.org>2019-05-22 20:55:28 +0200
commit8d213e2b1c934f6861f76aad5eb7c11097fa97de (patch)
tree23f8a2d5692deaeffffa1ab5f098b2d24b9e2217 /modules
parenta1a8649a2be768685eb04c246c114fce36b8096f (diff)
downloadNix-8d213e2b1c934f6861f76aad5eb7c11097fa97de.tar.gz
Nix-8d213e2b1c934f6861f76aad5eb7c11097fa97de.tar.zst
Nix-8d213e2b1c934f6861f76aad5eb7c11097fa97de.zip
Move rest of the modules outside of nixops
Diffstat (limited to 'modules')
-rw-r--r--modules/private/buildbot/common/build_helpers.py256
-rw-r--r--modules/private/buildbot/common/master.cfg69
-rw-r--r--modules/private/buildbot/default.nix198
-rw-r--r--modules/private/buildbot/projects/caldance/__init__.py190
-rw-r--r--modules/private/buildbot/projects/cryptoportfolio/__init__.py169
-rw-r--r--modules/private/buildbot/projects/test/__init__.py188
-rw-r--r--modules/private/certificates.nix52
-rw-r--r--modules/private/default.nix12
-rw-r--r--modules/private/dns.nix132
-rw-r--r--modules/private/ftp.nix118
-rw-r--r--modules/private/gitolite/default.nix63
-rwxr-xr-xmodules/private/gitolite/gitolite_ldap_groups.sh15
-rw-r--r--modules/private/mail.nix13
-rw-r--r--modules/private/mpd.nix56
-rw-r--r--modules/private/pub/default.nix52
-rw-r--r--modules/private/pub/restrict64
-rw-r--r--modules/private/pub/tmux.restrict.conf43
-rw-r--r--modules/private/ssh/default.nix40
-rwxr-xr-xmodules/private/ssh/ldap_authorized_keys.sh152
-rw-r--r--modules/private/system.nix30
-rw-r--r--modules/private/tasks/default.nix327
-rw-r--r--modules/private/tasks/www/index.php157
-rw-r--r--modules/private/websites/tools/git/default.nix4
23 files changed, 2399 insertions, 1 deletions
diff --git a/modules/private/buildbot/common/build_helpers.py b/modules/private/buildbot/common/build_helpers.py
new file mode 100644
index 0000000..384b1ac
--- /dev/null
+++ b/modules/private/buildbot/common/build_helpers.py
@@ -0,0 +1,256 @@
1from buildbot.plugins import util, steps, schedulers
2from buildbot_buildslist import BuildsList
3
4__all__ = [
5 "force_scheduler", "deploy_scheduler", "hook_scheduler",
6 "clean_branch", "package_and_upload", "SlackStatusPush",
7 "XMPPStatusPush"
8 ]
9
10# Small helpers"
11@util.renderer
12def clean_branch(props):
13 if props.hasProperty("branch") and len(props["branch"]) > 0:
14 return props["branch"].replace("/", "_")
15 else:
16 return "HEAD"
17
18def package_and_upload(package, package_dest, package_url):
19 return [
20 steps.ShellCommand(name="build package",
21 logEnviron=False, haltOnFailure=True, workdir="source",
22 command=["git", "archive", "HEAD", "-o", package]),
23
24 steps.FileUpload(name="upload package", workersrc=package,
25 workdir="source", masterdest=package_dest,
26 url=package_url, mode=0o644),
27
28 steps.ShellCommand(name="cleanup package", logEnviron=False,
29 haltOnFailure=True, workdir="source", alwaysRun=True,
30 command=["rm", "-f", package]),
31 ]
32
33# Schedulers
34def force_scheduler(name, builders):
35 return schedulers.ForceScheduler(name=name,
36 label="Force build", buttonName="Force build",
37 reason=util.StringParameter(name="reason", label="Reason", default="Force build"),
38 codebases=[
39 util.CodebaseParameter("",
40 branch=util.StringParameter(
41 name="branch", label="Git reference (tag, branch)", required=True),
42 revision=util.FixedParameter(name="revision", default=""),
43 repository=util.FixedParameter(name="repository", default=""),
44 project=util.FixedParameter(name="project", default=""),
45 ),
46 ],
47 username=util.FixedParameter(name="username", default="Web button"),
48 builderNames=builders)
49
50def deploy_scheduler(name, builders):
51 return schedulers.ForceScheduler(name=name,
52 builderNames=builders,
53 label="Deploy built package", buttonName="Deploy",
54 username=util.FixedParameter(name="username", default="Web button"),
55 codebases=[
56 util.CodebaseParameter(codebase="",
57 branch=util.FixedParameter(name="branch", default=""),
58 revision=util.FixedParameter(name="revision", default=""),
59 repository=util.FixedParameter(name="repository", default=""),
60 project=util.FixedParameter(name="project", default=""))],
61 reason=util.FixedParameter(name="reason", default="Deploy"),
62 properties=[
63 util.ChoiceStringParameter(label="Environment",
64 name="environment", default="integration",
65 choices=["integration", "production"]),
66 BuildsList(label="Build to deploy", name="build"),
67 ]
68 )
69
70def hook_scheduler(project, timer=10):
71 return schedulers.AnyBranchScheduler(
72 change_filter=util.ChangeFilter(category="hooks", project=project),
73 name=project, treeStableTimer=timer, builderNames=["{}_build".format(project)])
74
75# Slack/XMPP status push
76from buildbot.reporters.http import HttpStatusPushBase
77from twisted.internet import defer
78from twisted.python import log
79from buildbot.util import httpclientservice
80from buildbot.reporters import utils
81from buildbot.process import results
82from twisted.words.protocols.jabber.jid import JID
83from wokkel import client, xmppim
84from functools import partial
85
86class SlackStatusPush(HttpStatusPushBase):
87 name = "SlackStatusPush"
88
89 @defer.inlineCallbacks
90 def reconfigService(self, serverUrl, **kwargs):
91 yield HttpStatusPushBase.reconfigService(self, **kwargs)
92 self._http = yield httpclientservice.HTTPClientService.getService(
93 self.master, serverUrl)
94
95 @defer.inlineCallbacks
96 def send(self, build):
97 yield utils.getDetailsForBuild(self.master, build, wantProperties=True)
98 response = yield self._http.post("", json=self.format(build))
99 if response.code != 200:
100 log.msg("%s: unable to upload status: %s" %
101 (response.code, response.content))
102
103 def format(self, build):
104 colors = [
105 "#36A64F", # success
106 "#F1E903", # warnings
107 "#DA0505", # failure
108 "#FFFFFF", # skipped
109 "#000000", # exception
110 "#FFFFFF", # retry
111 "#D02CA9", # cancelled
112 ]
113
114 if "environment" in build["properties"]:
115 msg = "{} environment".format(build["properties"]["environment"][0])
116 if "build" in build["properties"]:
117 msg = "of archive {} in ".format(build["properties"]["build"][0]) + msg
118 elif len(build["buildset"]["sourcestamps"][0]["branch"]) > 0:
119 msg = "revision {}".format(build["buildset"]["sourcestamps"][0]["branch"])
120 else:
121 msg = "build"
122
123 if build["complete"]:
124 timedelta = int((build["complete_at"] - build["started_at"]).total_seconds())
125 hours, rest = divmod(timedelta, 3600)
126 minutes, seconds = divmod(rest, 60)
127 if hours > 0:
128 duration = "{}h {}min {}s".format(hours, minutes, seconds)
129 elif minutes > 0:
130 duration = "{}min {}s".format(minutes, seconds)
131 else:
132 duration = "{}s".format(seconds)
133
134 text = "Build <{}|{}> of {}'s {} was {} in {}.".format(
135 build["url"], build["buildid"],
136 build["builder"]["name"],
137 msg,
138 results.Results[build["results"]],
139 duration,
140 )
141 fields = [
142 {
143 "title": "Build",
144 "value": "<{}|{}>".format(build["url"], build["buildid"]),
145 "short": True,
146 },
147 {
148 "title": "Project",
149 "value": build["builder"]["name"],
150 "short": True,
151 },
152 {
153 "title": "Build status",
154 "value": results.Results[build["results"]],
155 "short": True,
156 },
157 {
158 "title": "Build duration",
159 "value": duration,
160 "short": True,
161 },
162 ]
163 if "environment" in build["properties"]:
164 fields.append({
165 "title": "Environment",
166 "value": build["properties"]["environment"][0],
167 "short": True,
168 })
169 if "build" in build["properties"]:
170 fields.append({
171 "title": "Archive",
172 "value": build["properties"]["build"][0],
173 "short": True,
174 })
175 attachments = [{
176 "fallback": "",
177 "color": colors[build["results"]],
178 "fields": fields
179 }]
180 else:
181 text = "Build <{}|{}> of {}'s {} started.".format(
182 build["url"], build["buildid"],
183 build["builder"]["name"],
184 msg,
185 )
186 attachments = []
187
188 return {
189 "username": "Buildbot",
190 "icon_url": "http://docs.buildbot.net/current/_static/icon.png",
191 "text": text,
192 "attachments": attachments,
193 }
194
195class XMPPStatusPush(HttpStatusPushBase):
196 name = "XMPPStatusPush"
197
198 @defer.inlineCallbacks
199 def reconfigService(self, password, recipients, **kwargs):
200 yield HttpStatusPushBase.reconfigService(self, **kwargs)
201 self.password = password
202 self.recipients = recipients
203
204 @defer.inlineCallbacks
205 def send(self, build):
206 yield utils.getDetailsForBuild(self.master, build, wantProperties=True)
207 body = self.format(build)
208 factory = client.DeferredClientFactory(JID("notify_bot@immae.fr/buildbot"), self.password)
209 d = client.clientCreator(factory)
210 def send_message(recipient, stream):
211 message = xmppim.Message(recipient=JID(recipient), body=body)
212 message.stanzaType = 'chat'
213 stream.send(message.toElement())
214 # To allow chaining
215 return stream
216 for recipient in self.recipients:
217 d.addCallback(partial(send_message, recipient))
218 d.addCallback(lambda _: factory.streamManager.xmlstream.sendFooter())
219 d.addErrback(log.err)
220
221 def format(self, build):
222 if "environment" in build["properties"]:
223 msg = "{} environment".format(build["properties"]["environment"][0])
224 if "build" in build["properties"]:
225 msg = "of archive {} in ".format(build["properties"]["build"][0]) + msg
226 elif len(build["buildset"]["sourcestamps"][0]["branch"]) > 0:
227 msg = "revision {}".format(build["buildset"]["sourcestamps"][0]["branch"])
228 else:
229 msg = "build"
230
231 if build["complete"]:
232 timedelta = int((build["complete_at"] - build["started_at"]).total_seconds())
233 hours, rest = divmod(timedelta, 3600)
234 minutes, seconds = divmod(rest, 60)
235 if hours > 0:
236 duration = "{}h {}min {}s".format(hours, minutes, seconds)
237 elif minutes > 0:
238 duration = "{}min {}s".format(minutes, seconds)
239 else:
240 duration = "{}s".format(seconds)
241
242 text = "Build {} ( {} ) of {}'s {} was {} in {}.".format(
243 build["buildid"], build["url"],
244 build["builder"]["name"],
245 msg,
246 results.Results[build["results"]],
247 duration,
248 )
249 else:
250 text = "Build {} ( {} ) of {}'s {} started.".format(
251 build["buildid"], build["url"],
252 build["builder"]["name"],
253 msg,
254 )
255
256 return text
diff --git a/modules/private/buildbot/common/master.cfg b/modules/private/buildbot/common/master.cfg
new file mode 100644
index 0000000..abe08e0
--- /dev/null
+++ b/modules/private/buildbot/common/master.cfg
@@ -0,0 +1,69 @@
1# -*- python -*-
2# ex: set filetype=python:
3
4from buildbot.plugins import secrets, util, webhooks
5from buildbot.util import bytes2unicode
6import re
7import os
8from buildbot_config import E, configure
9import json
10
11class CustomBase(webhooks.base):
12 def getChanges(self, request):
13 try:
14 content = request.content.read()
15 args = json.loads(bytes2unicode(content))
16 except Exception as e:
17 raise ValueError("Error loading JSON: " + str(e))
18
19 args.setdefault("comments", "")
20 args.setdefault("repository", "")
21 args.setdefault("author", args.get("who"))
22
23 return ([args], None)
24
25userInfoProvider = util.LdapUserInfo(
26 uri=E.LDAP_URL,
27 bindUser=E.LDAP_ADMIN_USER,
28 bindPw=open(E.SECRETS_FILE + "/ldap", "r").read().rstrip(),
29 accountBase=E.LDAP_BASE,
30 accountPattern=E.LDAP_PATTERN,
31 accountFullName='cn',
32 accountEmail='mail',
33 avatarData="jpegPhoto",
34 groupBase=E.LDAP_BASE,
35 groupName="cn",
36 groupMemberPattern=E.LDAP_GROUP_PATTERN,
37 )
38
39c = BuildmasterConfig = {
40 "title": E.TITLE,
41 "titleURL": E.TITLE_URL,
42 "db": {
43 "db_url": "sqlite:///state.sqlite"
44 },
45 "protocols": { "pb": { "port": E.PB_SOCKET } },
46 "workers": [],
47 "change_source": [],
48 "schedulers": [],
49 "builders": [],
50 "services": [],
51 "secretsProviders": [
52 secrets.SecretInAFile(E.SECRETS_FILE),
53 ],
54 "www": {
55 "change_hook_dialects": { "base": { "custom_class": CustomBase } },
56 "plugins": {
57 "waterfall_view": {},
58 "console_view": {},
59 "grid_view": {},
60 "buildslist": {},
61 },
62 "auth": util.RemoteUserAuth(
63 header=b"X-Remote-User",
64 userInfoProvider=userInfoProvider,
65 headerRegex=re.compile(br"(?P<username>[^ @]+)")),
66 }
67 }
68
69configure(c)
diff --git a/modules/private/buildbot/default.nix b/modules/private/buildbot/default.nix
new file mode 100644
index 0000000..fa6a6f2
--- /dev/null
+++ b/modules/private/buildbot/default.nix
@@ -0,0 +1,198 @@
1{ lib, pkgs, config, myconfig, ... }:
2let
3 varDir = "/var/lib/buildbot";
4 buildbot_common = pkgs.python3Packages.buildPythonPackage rec {
5 name = "buildbot_common";
6 src = ./common;
7 format = "other";
8 installPhase = ''
9 mkdir -p $out/${pkgs.python3.pythonForBuild.sitePackages}
10 cp -a $src $out/${pkgs.python3.pythonForBuild.sitePackages}/buildbot_common
11 '';
12 };
13 buildbot = pkgs.python3Packages.buildbot-full;
14in
15{
16 options = {
17 myServices.buildbot.enable = lib.mkOption {
18 type = lib.types.bool;
19 default = false;
20 description = ''
21 Whether to enable buildbot.
22 '';
23 };
24 };
25
26 config = lib.mkIf config.myServices.buildbot.enable {
27 ids.uids.buildbot = myconfig.env.buildbot.user.uid;
28 ids.gids.buildbot = myconfig.env.buildbot.user.gid;
29
30 users.groups.buildbot.gid = config.ids.gids.buildbot;
31 users.users.buildbot = {
32 name = "buildbot";
33 uid = config.ids.uids.buildbot;
34 group = "buildbot";
35 description = "Buildbot user";
36 home = varDir;
37 extraGroups = [ "keys" ];
38 };
39
40 services.websites.tools.vhostConfs.git.extraConfig = lib.attrsets.mapAttrsToList (k: project: ''
41 RedirectMatch permanent "^/buildbot/${project.name}$" "/buildbot/${project.name}/"
42 RewriteEngine On
43 RewriteRule ^/buildbot/${project.name}/ws(.*)$ unix:///run/buildbot/${project.name}.sock|ws://git.immae.eu/ws$1 [P,NE,QSA,L]
44 ProxyPass /buildbot/${project.name}/ unix:///run/buildbot/${project.name}.sock|http://${project.name}-git.immae.eu/
45 ProxyPassReverse /buildbot/${project.name}/ unix:///run/buildbot/${project.name}.sock|http://${project.name}-git.immae.eu/
46 <Location /buildbot/${project.name}/>
47 Use LDAPConnect
48 Require ldap-group cn=users,ou=${project.name},cn=buildbot,ou=services,dc=immae,dc=eu
49
50 SetEnvIf X-Url-Scheme https HTTPS=1
51 ProxyPreserveHost On
52 </Location>
53 <Location /buildbot/${project.name}/change_hook/base>
54 <RequireAny>
55 Require local
56 Require ldap-group cn=users,ou=${project.name},cn=buildbot,ou=services,dc=immae,dc=eu
57 Include /var/secrets/buildbot/${project.name}/webhook-httpd-include
58 </RequireAny>
59 </Location>
60 '') myconfig.env.buildbot.projects;
61
62 system.activationScripts = lib.attrsets.mapAttrs' (k: project: lib.attrsets.nameValuePair "buildbot-${project.name}" {
63 deps = [ "users" "wrappers" ];
64 text = project.activationScript;
65 }) myconfig.env.buildbot.projects;
66
67 secrets.keys = (
68 lib.lists.flatten (
69 lib.attrsets.mapAttrsToList (k: project:
70 lib.attrsets.mapAttrsToList (k: v:
71 {
72 permissions = "0600";
73 user = "buildbot";
74 group = "buildbot";
75 text = v;
76 dest = "buildbot/${project.name}/${k}";
77 }
78 ) project.secrets
79 ++ [
80 {
81 permissions = "0600";
82 user = "wwwrun";
83 group = "wwwrun";
84 text = lib.optionalString (lib.attrsets.hasAttr "webhookTokens" project) ''
85 Require expr "req('Access-Key') in { ${builtins.concatStringsSep ", " (map (x: "'${x}'") project.webhookTokens)} }"
86 '';
87 dest = "buildbot/${project.name}/webhook-httpd-include";
88 }
89 ]
90 ) myconfig.env.buildbot.projects
91 )
92 ) ++ [
93 {
94 permissions = "0600";
95 user = "buildbot";
96 group = "buildbot";
97 text = myconfig.env.buildbot.ldap.password;
98 dest = "buildbot/ldap";
99 }
100 {
101 permissions = "0600";
102 user = "buildbot";
103 group = "buildbot";
104 text = builtins.readFile "${myconfig.privateFiles}/buildbot_ssh_key";
105 dest = "buildbot/ssh_key";
106 }
107 ];
108
109 systemd.services = lib.attrsets.mapAttrs' (k: project: lib.attrsets.nameValuePair "buildbot-${project.name}" {
110 description = "Buildbot Continuous Integration Server ${project.name}.";
111 after = [ "network-online.target" ];
112 wantedBy = [ "multi-user.target" ];
113 path = project.packages pkgs ++ (project.pythonPackages buildbot.pythonModule pkgs);
114 preStart = let
115 master-cfg = "${buildbot_common}/${pkgs.python3.pythonForBuild.sitePackages}/buildbot_common/master.cfg";
116 tac_file = pkgs.writeText "buildbot.tac" ''
117 import os
118
119 from twisted.application import service
120 from buildbot.master import BuildMaster
121
122 basedir = '${varDir}/${project.name}'
123 rotateLength = 10000000
124 maxRotatedFiles = 10
125 configfile = '${master-cfg}'
126
127 # Default umask for server
128 umask = None
129
130 # if this is a relocatable tac file, get the directory containing the TAC
131 if basedir == '.':
132 import os
133 basedir = os.path.abspath(os.path.dirname(__file__))
134
135 # note: this line is matched against to check that this is a buildmaster
136 # directory; do not edit it.
137 application = service.Application('buildmaster')
138 from twisted.python.logfile import LogFile
139 from twisted.python.log import ILogObserver, FileLogObserver
140 logfile = LogFile.fromFullPath(os.path.join(basedir, "twistd.log"), rotateLength=rotateLength,
141 maxRotatedFiles=maxRotatedFiles)
142 application.setComponent(ILogObserver, FileLogObserver(logfile).emit)
143
144 m = BuildMaster(basedir, configfile, umask)
145 m.setServiceParent(application)
146 m.log_rotation.rotateLength = rotateLength
147 m.log_rotation.maxRotatedFiles = maxRotatedFiles
148 '';
149 in ''
150 if [ ! -f ${varDir}/${project.name}/buildbot.tac ]; then
151 ${buildbot}/bin/buildbot create-master -c "${master-cfg}" "${varDir}/${project.name}"
152 rm -f ${varDir}/${project.name}/master.cfg.sample
153 rm -f ${varDir}/${project.name}/buildbot.tac
154 fi
155 ln -sf ${tac_file} ${varDir}/${project.name}/buildbot.tac
156 # different buildbots may be trying that simultaneously, add the || true to avoid complaining in case of race
157 install -Dm600 -o buildbot -g buildbot -T /var/secrets/buildbot/ssh_key ${varDir}/buildbot_key || true
158 buildbot_secrets=${varDir}/${project.name}/secrets
159 install -m 0700 -o buildbot -g buildbot -d $buildbot_secrets
160 install -Dm600 -o buildbot -g buildbot -T /var/secrets/buildbot/ldap $buildbot_secrets/ldap
161 ${builtins.concatStringsSep "\n" (lib.attrsets.mapAttrsToList
162 (k: v: "install -Dm600 -o buildbot -g buildbot -T /var/secrets/buildbot/${project.name}/${k} $buildbot_secrets/${k}") project.secrets
163 )}
164 '';
165 environment = let
166 project_env = lib.attrsets.mapAttrs' (k: v: lib.attrsets.nameValuePair "BUILDBOT_${k}" v) project.environment;
167 buildbot_config = pkgs.python3Packages.buildPythonPackage (rec {
168 name = "buildbot_config-${project.name}";
169 src = ./projects + "/${project.name}";
170 format = "other";
171 installPhase = ''
172 mkdir -p $out/${pkgs.python3.pythonForBuild.sitePackages}
173 cp -a $src $out/${pkgs.python3.pythonForBuild.sitePackages}/buildbot_config
174 '';
175 });
176 HOME = "${varDir}/${project.name}";
177 PYTHONPATH = "${buildbot.pythonModule.withPackages (self: project.pythonPackages self pkgs ++ [
178 pkgs.python3Packages.wokkel
179 pkgs.python3Packages.treq pkgs.python3Packages.ldap3 buildbot
180 pkgs.python3Packages.buildbot-worker
181 buildbot_common buildbot_config
182 ])}/${buildbot.pythonModule.sitePackages}${if project.pythonPathHome then ":${varDir}/${project.name}/.local/${pkgs.python3.pythonForBuild.sitePackages}" else ""}";
183 in project_env // { inherit PYTHONPATH HOME; };
184
185 serviceConfig = {
186 Type = "forking";
187 User = "buildbot";
188 Group = "buildbot";
189 RuntimeDirectory = "buildbot";
190 RuntimeDirectoryPreserve = "yes";
191 StateDirectory = "buildbot";
192 SupplementaryGroups = "keys";
193 WorkingDirectory = "${varDir}/${project.name}";
194 ExecStart = "${buildbot}/bin/buildbot start";
195 };
196 }) myconfig.env.buildbot.projects;
197 };
198}
diff --git a/modules/private/buildbot/projects/caldance/__init__.py b/modules/private/buildbot/projects/caldance/__init__.py
new file mode 100644
index 0000000..2c0bad5
--- /dev/null
+++ b/modules/private/buildbot/projects/caldance/__init__.py
@@ -0,0 +1,190 @@
1from buildbot.plugins import *
2from buildbot_common.build_helpers import *
3import os
4from buildbot.util import bytes2unicode
5import json
6
7__all__ = [ "configure", "E" ]
8
9class E():
10 PROJECT = "caldance"
11 BUILDBOT_URL = "https://git.immae.eu/buildbot/{}/".format(PROJECT)
12 SOCKET = "unix:/run/buildbot/{}.sock".format(PROJECT)
13 PB_SOCKET = "unix:address=/run/buildbot/{}_pb.sock".format(PROJECT)
14 RELEASE_PATH = "/var/lib/ftp/release.immae.eu/{}".format(PROJECT)
15 RELEASE_URL = "https://release.immae.eu/{}".format(PROJECT)
16 GIT_URL = "gitolite@git.immae.eu:perso/simon_descarpentries/www.cal-dance.com"
17 SSH_KEY_PATH = "/var/lib/buildbot/buildbot_key"
18 SSH_HOST_KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIFbhFTl2A2RJn5L51yxJM4XfCS2ZaiSX/jo9jFSdghF"
19 LDAP_HOST = "ldap.immae.eu"
20 LDAP_DN = "cn=buildbot,ou=services,dc=immae,dc=eu"
21 LDAP_ROLES_BASE = "ou=roles,ou=hosts,dc=immae,dc=eu"
22 XMPP_RECIPIENTS = os.environ["BUILDBOT_XMPP_RECIPIENTS"].split(" ")
23
24 PUPPET_HOST = {
25 "integration": "root@caldance.immae.eu",
26 }
27
28 # master.cfg
29 SECRETS_FILE = os.getcwd() + "/secrets"
30 LDAP_URL = "ldaps://ldap.immae.eu:636"
31 LDAP_ADMIN_USER = "cn=buildbot,ou=services,dc=immae,dc=eu"
32 LDAP_BASE = "dc=immae,dc=eu"
33 LDAP_PATTERN = "(uid=%(username)s)"
34 LDAP_GROUP_PATTERN = "(&(memberOf=cn=groups,ou=caldance,cn=buildbot,ou=services,dc=immae,dc=eu)(member=%(dn)s))"
35 TITLE_URL = "https://caldance.immae.eu"
36 TITLE = "Caldance"
37
38class CustomBase(webhooks.base):
39 def getChanges(self, request):
40 try:
41 content = request.content.read()
42 args = json.loads(bytes2unicode(content))
43 except Exception as e:
44 raise ValueError("Error loading JSON: " + str(e))
45
46 args.setdefault("comments", "")
47 args.setdefault("repository", "")
48 args.setdefault("author", args.get("who", "unknown"))
49
50 if args["category"] == "deploy_webhook":
51 args = {
52 "category": "deploy_webhook",
53 "comments": "",
54 "repository": "",
55 "author": "webhook",
56 "project": "Caldance",
57 "properties": {
58 "environment": args.get("environment", "integration"),
59 "build": "caldance_{}.tar.gz".format(args.get("build", "master"))
60 }
61 }
62
63 return ([args], None)
64
65def deploy_hook_scheduler(project, timer=1):
66 return schedulers.AnyBranchScheduler(
67 change_filter=util.ChangeFilter(category="deploy_webhook", project=project),
68 name="{}_deploy".format(project), treeStableTimer=timer, builderNames=["{}_deploy".format(project)])
69
70def configure(c):
71 c["buildbotURL"] = E.BUILDBOT_URL
72 c["www"]["port"] = E.SOCKET
73
74 c["www"]["change_hook_dialects"]["base"] = {
75 "custom_class": CustomBase
76 }
77 c['workers'].append(worker.LocalWorker("generic-worker"))
78 c['workers'].append(worker.LocalWorker("deploy-worker"))
79
80 c['schedulers'].append(hook_scheduler("Caldance", timer=1))
81 c['schedulers'].append(force_scheduler("force_caldance", ["Caldance_build"]))
82 c['schedulers'].append(deploy_scheduler("deploy_caldance", ["Caldance_deploy"]))
83 c['schedulers'].append(deploy_hook_scheduler("Caldance", timer=1))
84
85 c['builders'].append(factory("caldance"))
86
87 c['builders'].append(deploy_factory("caldance"))
88
89 c['services'].append(SlackStatusPush(
90 name="slack_status_caldance",
91 builders=["Caldance_build", "Caldance_deploy"],
92 serverUrl=open(E.SECRETS_FILE + "/slack_webhook", "r").read().rstrip()))
93 c['services'].append(XMPPStatusPush(
94 name="xmpp_status_caldance",
95 builders=["Caldance_build", "Caldance_deploy"],
96 recipients=E.XMPP_RECIPIENTS,
97 password=open(E.SECRETS_FILE + "/notify_xmpp_password", "r").read().rstrip()))
98
99def factory(project, ignore_fails=False):
100 release_file = "{1}/{0}_%(kw:clean_branch)s.tar.gz"
101
102 package = util.Interpolate("{0}_%(kw:clean_branch)s.tar.gz".format(project), clean_branch=clean_branch)
103 package_dest = util.Interpolate(release_file.format(project, E.RELEASE_PATH), clean_branch=clean_branch)
104 package_url = util.Interpolate(release_file.format(project, E.RELEASE_URL), clean_branch=clean_branch)
105
106 factory = util.BuildFactory()
107 factory.addStep(steps.Git(logEnviron=False, repourl=E.GIT_URL,
108 sshPrivateKey=open(E.SSH_KEY_PATH).read().rstrip(),
109 sshHostKey=E.SSH_HOST_KEY, mode="full", method="copy"))
110 factory.addSteps(package_and_upload(package, package_dest, package_url))
111
112 return util.BuilderConfig(
113 name="{}_build".format(project.capitalize()),
114 workernames=["generic-worker"], factory=factory)
115
116def compute_build_infos(project):
117 @util.renderer
118 def compute(props):
119 import re, hashlib
120 build_file = props.getProperty("build")
121 package_dest = "{1}/{0}".format(build_file, E.RELEASE_PATH)
122 version = re.match(r"{0}_(.*).tar.gz".format(project), build_file).group(1)
123 with open(package_dest, "rb") as f:
124 sha = hashlib.sha256(f.read()).hexdigest()
125 return {
126 "build_version": version,
127 "build_hash": sha,
128 }
129 return compute
130
131@util.renderer
132def puppet_host(props):
133 environment = props["environment"] if props.hasProperty("environment") else "integration"
134 return E.PUPPET_HOST.get(environment, "host.invalid")
135
136def deploy_factory(project):
137 package_dest = util.Interpolate("{0}/%(prop:build)s".format(E.RELEASE_PATH))
138
139 factory = util.BuildFactory()
140 factory.addStep(steps.MasterShellCommand(command=["test", "-f", package_dest]))
141 factory.addStep(steps.SetProperties(properties=compute_build_infos(project)))
142 factory.addStep(LdapPush(environment=util.Property("environment"),
143 project=project, build_version=util.Property("build_version"),
144 build_hash=util.Property("build_hash"), ldap_password=util.Secret("ldap")))
145 factory.addStep(steps.MasterShellCommand(command=[
146 "ssh", "-o", "UserKnownHostsFile=/dev/null", "-o", "StrictHostKeyChecking=no", "-o", "CheckHostIP=no", "-i", E.SSH_KEY_PATH, puppet_host]))
147 return util.BuilderConfig(name="{}_deploy".format(project.capitalize()), workernames=["deploy-worker"], factory=factory)
148
149from twisted.internet import defer
150from buildbot.process.buildstep import FAILURE
151from buildbot.process.buildstep import SUCCESS
152from buildbot.process.buildstep import BuildStep
153
154class LdapPush(BuildStep):
155 name = "LdapPush"
156 renderables = ["environment", "project", "build_version", "build_hash", "ldap_password"]
157
158 def __init__(self, **kwargs):
159 self.environment = kwargs.pop("environment")
160 self.project = kwargs.pop("project")
161 self.build_version = kwargs.pop("build_version")
162 self.build_hash = kwargs.pop("build_hash")
163 self.ldap_password = kwargs.pop("ldap_password")
164 self.ldap_host = kwargs.pop("ldap_host", E.LDAP_HOST)
165 super().__init__(**kwargs)
166
167 def run(self):
168 import json
169 from ldap3 import Reader, Writer, Server, Connection, ObjectDef
170 server = Server(self.ldap_host)
171 conn = Connection(server,
172 user=E.LDAP_DN,
173 password=self.ldap_password)
174 conn.bind()
175 obj = ObjectDef("immaePuppetClass", conn)
176 r = Reader(conn, obj,
177 "cn=caldance.{},{}".format(self.environment, E.LDAP_ROLES_BASE))
178 r.search()
179 if len(r) > 0:
180 w = Writer.from_cursor(r)
181 for value in w[0].immaePuppetJson.values:
182 config = json.loads(value)
183 if "role::caldance::{}_version".format(self.project) in config:
184 config["role::caldance::{}_version".format(self.project)] = self.build_version
185 config["role::caldance::{}_sha256".format(self.project)] = self.build_hash
186 w[0].immaePuppetJson -= value
187 w[0].immaePuppetJson += json.dumps(config, indent=" ")
188 w.commit()
189 return defer.succeed(SUCCESS)
190 return defer.succeed(FAILURE)
diff --git a/modules/private/buildbot/projects/cryptoportfolio/__init__.py b/modules/private/buildbot/projects/cryptoportfolio/__init__.py
new file mode 100644
index 0000000..5d70f95
--- /dev/null
+++ b/modules/private/buildbot/projects/cryptoportfolio/__init__.py
@@ -0,0 +1,169 @@
1from buildbot.plugins import *
2from buildbot_common.build_helpers import *
3import os
4
5__all__ = [ "configure", "E" ]
6
7class E():
8 PROJECT = "cryptoportfolio"
9 BUILDBOT_URL = "https://git.immae.eu/buildbot/{}/".format(PROJECT)
10 SOCKET = "unix:/run/buildbot/{}.sock".format(PROJECT)
11 PB_SOCKET = "unix:address=/run/buildbot/{}_pb.sock".format(PROJECT)
12 RELEASE_PATH = "/var/lib/ftp/release.immae.eu/{}".format(PROJECT)
13 RELEASE_URL = "https://release.immae.eu/{}".format(PROJECT)
14 GIT_URL = "https://git.immae.eu/perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/{0}.git"
15 SSH_KEY_PATH = "/var/lib/buildbot/buildbot_key"
16 LDAP_HOST = "ldap.immae.eu"
17 LDAP_DN = "cn=buildbot,ou=services,dc=immae,dc=eu"
18 LDAP_ROLES_BASE = "ou=roles,ou=hosts,dc=immae,dc=eu"
19
20 PUPPET_HOST = {
21 "production": "root@cryptoportfolio.immae.eu",
22 "integration": "root@cryptoportfolio-dev.immae.eu"
23 }
24
25 # master.cfg
26 SECRETS_FILE = os.getcwd() + "/secrets"
27 LDAP_URL = "ldaps://ldap.immae.eu:636"
28 LDAP_ADMIN_USER = "cn=buildbot,ou=services,dc=immae,dc=eu"
29 LDAP_BASE = "dc=immae,dc=eu"
30 LDAP_PATTERN = "(uid=%(username)s)"
31 LDAP_GROUP_PATTERN = "(&(memberOf=cn=groups,ou=cryptoportfolio,cn=buildbot,ou=services,dc=immae,dc=eu)(member=%(dn)s))"
32 TITLE_URL = "https://git.immae.eu"
33 TITLE = "Cryptoportfolio"
34
35# eval .. dans .zshrc_local
36# mkdir -p $BUILD/go
37# export GOPATH=$BUILD/go
38# go get -u github.com/golang/dep/cmd/dep
39# export PATH=$PATH:$BUILD/go/bin
40# go get git.immae.eu/Cryptoportfolio/Front.git
41# cd $BUILD/go/src/git.immae.eu/Cryptoportfolio/Front.git
42# git checkout dev
43# dep ensure
44def configure(c):
45 c["buildbotURL"] = E.BUILDBOT_URL
46 c["www"]["port"] = E.SOCKET
47
48 c['workers'].append(worker.LocalWorker("generic-worker"))
49 c['workers'].append(worker.LocalWorker("deploy-worker"))
50
51 c['schedulers'].append(hook_scheduler("Trader"))
52 c['schedulers'].append(hook_scheduler("Front"))
53 c['schedulers'].append(force_scheduler(
54 "force_cryptoportfolio", ["Trader_build", "Front_build"]))
55 c['schedulers'].append(deploy_scheduler("deploy_cryptoportfolio",
56 ["Trader_deploy", "Front_deploy"]))
57
58 c['builders'].append(factory("trader"))
59 c['builders'].append(factory("front", ignore_fails=True))
60
61 c['builders'].append(deploy_factory("trader"))
62 c['builders'].append(deploy_factory("front"))
63
64 c['services'].append(SlackStatusPush(
65 name="slack_status_cryptoportfolio",
66 builders=["Front_build", "Trader_build", "Front_deploy", "Trader_deploy"],
67 serverUrl=open(E.SECRETS_FILE + "/slack_webhook", "r").read().rstrip()))
68
69def factory(project, ignore_fails=False):
70 release_file = "{1}/{0}/{0}_%(kw:clean_branch)s.tar.gz"
71
72 url = E.GIT_URL.format(project.capitalize())
73
74 package = util.Interpolate("{0}_%(kw:clean_branch)s.tar.gz".format(project), clean_branch=clean_branch)
75 package_dest = util.Interpolate(release_file.format(project, E.RELEASE_PATH), clean_branch=clean_branch)
76 package_url = util.Interpolate(release_file.format(project, E.RELEASE_URL), clean_branch=clean_branch)
77
78 factory = util.BuildFactory()
79 factory.addStep(steps.Git(logEnviron=False, repourl=url,
80 mode="full", method="copy"))
81 factory.addStep(steps.ShellCommand(name="make install",
82 logEnviron=False, haltOnFailure=(not ignore_fails),
83 warnOnFailure=ignore_fails, flunkOnFailure=(not ignore_fails),
84 command=["make", "install"]))
85 factory.addStep(steps.ShellCommand(name="make test",
86 logEnviron=False, haltOnFailure=(not ignore_fails),
87 warnOnFailure=ignore_fails, flunkOnFailure=(not ignore_fails),
88 command=["make", "test"]))
89 factory.addSteps(package_and_upload(package, package_dest, package_url))
90
91 return util.BuilderConfig(
92 name="{}_build".format(project.capitalize()),
93 workernames=["generic-worker"], factory=factory)
94
95def compute_build_infos(project):
96 @util.renderer
97 def compute(props):
98 import re, hashlib
99 build_file = props.getProperty("build")
100 package_dest = "{2}/{0}/{1}".format(project, build_file, E.RELEASE_PATH)
101 version = re.match(r"{0}_(.*).tar.gz".format(project), build_file).group(1)
102 with open(package_dest, "rb") as f:
103 sha = hashlib.sha256(f.read()).hexdigest()
104 return {
105 "build_version": version,
106 "build_hash": sha,
107 }
108 return compute
109
110@util.renderer
111def puppet_host(props):
112 environment = props["environment"] if props.hasProperty("environment") else "integration"
113 return E.PUPPET_HOST.get(environment, "host.invalid")
114
115def deploy_factory(project):
116 package_dest = util.Interpolate("{1}/{0}/%(prop:build)s".format(project, E.RELEASE_PATH))
117
118 factory = util.BuildFactory()
119 factory.addStep(steps.MasterShellCommand(command=["test", "-f", package_dest]))
120 factory.addStep(steps.SetProperties(properties=compute_build_infos(project)))
121 factory.addStep(LdapPush(environment=util.Property("environment"),
122 project=project, build_version=util.Property("build_version"),
123 build_hash=util.Property("build_hash"), ldap_password=util.Secret("ldap")))
124 factory.addStep(steps.MasterShellCommand(command=[
125 "ssh", "-o", "UserKnownHostsFile=/dev/null", "-o", "StrictHostKeyChecking=no", "-o", "CheckHostIP=no", "-i", E.SSH_KEY_PATH, puppet_host]))
126 return util.BuilderConfig(name="{}_deploy".format(project.capitalize()), workernames=["deploy-worker"], factory=factory)
127
128from twisted.internet import defer
129from buildbot.process.buildstep import FAILURE
130from buildbot.process.buildstep import SUCCESS
131from buildbot.process.buildstep import BuildStep
132
133class LdapPush(BuildStep):
134 name = "LdapPush"
135 renderables = ["environment", "project", "build_version", "build_hash", "ldap_password"]
136
137 def __init__(self, **kwargs):
138 self.environment = kwargs.pop("environment")
139 self.project = kwargs.pop("project")
140 self.build_version = kwargs.pop("build_version")
141 self.build_hash = kwargs.pop("build_hash")
142 self.ldap_password = kwargs.pop("ldap_password")
143 self.ldap_host = kwargs.pop("ldap_host", E.LDAP_HOST)
144 super().__init__(**kwargs)
145
146 def run(self):
147 import json
148 from ldap3 import Reader, Writer, Server, Connection, ObjectDef
149 server = Server(self.ldap_host)
150 conn = Connection(server,
151 user=E.LDAP_DN,
152 password=self.ldap_password)
153 conn.bind()
154 obj = ObjectDef("immaePuppetClass", conn)
155 r = Reader(conn, obj,
156 "cn=cryptoportfolio.{},{}".format(self.environment, E.LDAP_ROLES_BASE))
157 r.search()
158 if len(r) > 0:
159 w = Writer.from_cursor(r)
160 for value in w[0].immaePuppetJson.values:
161 config = json.loads(value)
162 if "role::cryptoportfolio::{}_version".format(self.project) in config:
163 config["role::cryptoportfolio::{}_version".format(self.project)] = self.build_version
164 config["role::cryptoportfolio::{}_sha256".format(self.project)] = self.build_hash
165 w[0].immaePuppetJson -= value
166 w[0].immaePuppetJson += json.dumps(config, indent=" ")
167 w.commit()
168 return defer.succeed(SUCCESS)
169 return defer.succeed(FAILURE)
diff --git a/modules/private/buildbot/projects/test/__init__.py b/modules/private/buildbot/projects/test/__init__.py
new file mode 100644
index 0000000..e6b8d51
--- /dev/null
+++ b/modules/private/buildbot/projects/test/__init__.py
@@ -0,0 +1,188 @@
1from buildbot.plugins import *
2from buildbot_common.build_helpers import *
3import os
4from buildbot.util import bytes2unicode
5import json
6
7__all__ = [ "configure", "E" ]
8
9class E():
10 PROJECT = "test"
11 BUILDBOT_URL = "https://git.immae.eu/buildbot/{}/".format(PROJECT)
12 SOCKET = "unix:/run/buildbot/{}.sock".format(PROJECT)
13 PB_SOCKET = "unix:address=/run/buildbot/{}_pb.sock".format(PROJECT)
14 RELEASE_PATH = "/var/lib/ftp/release.immae.eu/{}".format(PROJECT)
15 RELEASE_URL = "https://release.immae.eu/{}".format(PROJECT)
16 GIT_URL = "https://git.immae.eu/perso/Immae/TestProject.git"
17 SSH_KEY_PATH = "/var/lib/buildbot/buildbot_key"
18 PUPPET_HOST = "root@backup-1.v.immae.eu"
19 LDAP_HOST = "ldap.immae.eu"
20 LDAP_DN = "cn=buildbot,ou=services,dc=immae,dc=eu"
21 LDAP_ROLES_BASE = "ou=roles,ou=hosts,dc=immae,dc=eu"
22 XMPP_RECIPIENTS = os.environ["BUILDBOT_XMPP_RECIPIENTS"].split(" ")
23
24 # master.cfg
25 SECRETS_FILE = os.getcwd() + "/secrets"
26 LDAP_URL = "ldaps://ldap.immae.eu:636"
27 LDAP_ADMIN_USER = "cn=buildbot,ou=services,dc=immae,dc=eu"
28 LDAP_BASE = "dc=immae,dc=eu"
29 LDAP_PATTERN = "(uid=%(username)s)"
30 LDAP_GROUP_PATTERN = "(&(memberOf=cn=groups,ou=test,cn=buildbot,ou=services,dc=immae,dc=eu)(member=%(dn)s))"
31 TITLE_URL = "https://git.immae.eu/?p=perso/Immae/TestProject.git;a=summary"
32 TITLE = "Test project"
33
34class CustomBase(webhooks.base):
35 def getChanges(self, request):
36 try:
37 content = request.content.read()
38 args = json.loads(bytes2unicode(content))
39 except Exception as e:
40 raise ValueError("Error loading JSON: " + str(e))
41
42 args.setdefault("comments", "")
43 args.setdefault("repository", "")
44 args.setdefault("author", args.get("who", "unknown"))
45
46 if args["category"] == "deploy_webhook":
47 args = {
48 "category": "deploy_webhook",
49 "comments": "",
50 "repository": "",
51 "author": "unknown",
52 "project": "TestProject",
53 "properties": {
54 "environment": args.get("environment", "integration"),
55 "build": "test_{}.tar.gz".format(args.get("branch", "master"))
56 }
57 }
58
59 return ([args], None)
60
61def deploy_hook_scheduler(project, timer=1):
62 return schedulers.AnyBranchScheduler(
63 change_filter=util.ChangeFilter(category="deploy_webhook", project=project),
64 name="{}_deploy".format(project), treeStableTimer=timer, builderNames=["{}_deploy".format(project)])
65
66def configure(c):
67 c["buildbotURL"] = E.BUILDBOT_URL
68 c["www"]["port"] = E.SOCKET
69
70 c["www"]["change_hook_dialects"]["base"] = {
71 "custom_class": CustomBase
72 }
73 c['workers'].append(worker.LocalWorker("generic-worker-test"))
74 c['workers'].append(worker.LocalWorker("deploy-worker-test"))
75
76 c['schedulers'].append(hook_scheduler("TestProject", timer=1))
77 c['schedulers'].append(force_scheduler("force_test", ["TestProject_build"]))
78 c['schedulers'].append(deploy_scheduler("deploy_test", ["TestProject_deploy"]))
79 c['schedulers'].append(deploy_hook_scheduler("TestProject", timer=1))
80
81 c['builders'].append(factory())
82 c['builders'].append(deploy_factory())
83
84 c['services'].append(SlackStatusPush(
85 name="slack_status_test_project",
86 builders=["TestProject_build", "TestProject_deploy"],
87 serverUrl=open(E.SECRETS_FILE + "/slack_webhook", "r").read().rstrip()))
88 c['services'].append(XMPPStatusPush(
89 name="xmpp_status_test_project",
90 builders=["TestProject_build", "TestProject_deploy"],
91 recipients=E.XMPP_RECIPIENTS,
92 password=open(E.SECRETS_FILE + "/notify_xmpp_password", "r").read().rstrip()))
93
94def factory():
95 package = util.Interpolate("test_%(kw:clean_branch)s.tar.gz", clean_branch=clean_branch)
96 package_dest = util.Interpolate("{}/test_%(kw:clean_branch)s.tar.gz".format(E.RELEASE_PATH), clean_branch=clean_branch)
97 package_url = util.Interpolate("{}/test_%(kw:clean_branch)s.tar.gz".format(E.RELEASE_URL), clean_branch=clean_branch)
98
99 factory = util.BuildFactory()
100 factory.addStep(steps.Git(logEnviron=False,
101 repourl=E.GIT_URL, mode="full", method="copy"))
102 factory.addStep(steps.ShellCommand(name="env",
103 logEnviron=False, command=["env"]))
104 factory.addStep(steps.ShellCommand(name="pwd",
105 logEnviron=False, command=["pwd"]))
106 factory.addStep(steps.ShellCommand(name="true",
107 logEnviron=False, command=["true"]))
108 factory.addStep(steps.ShellCommand(name="echo",
109 logEnviron=False, command=["echo", package]))
110 factory.addSteps(package_and_upload(package, package_dest, package_url))
111
112 return util.BuilderConfig(name="TestProject_build", workernames=["generic-worker-test"], factory=factory)
113
114
115def compute_build_infos():
116 @util.renderer
117 def compute(props):
118 import re, hashlib
119 build_file = props.getProperty("build")
120 package_dest = "{}/{}".format(E.RELEASE_PATH, build_file)
121 version = re.match(r"{0}_(.*).tar.gz".format("test"), build_file).group(1)
122 with open(package_dest, "rb") as f:
123 sha = hashlib.sha256(f.read()).hexdigest()
124 return {
125 "build_version": version,
126 "build_hash": sha,
127 }
128 return compute
129
130@util.renderer
131def puppet_host(props):
132 return E.PUPPET_HOST
133
134def deploy_factory():
135 package_dest = util.Interpolate("{}/%(prop:build)s".format(E.RELEASE_PATH))
136
137 factory = util.BuildFactory()
138 factory.addStep(steps.MasterShellCommand(command=["test", "-f", package_dest]))
139 factory.addStep(steps.SetProperties(properties=compute_build_infos()))
140 factory.addStep(LdapPush(environment=util.Property("environment"),
141 build_version=util.Property("build_version"),
142 build_hash=util.Property("build_hash"),
143 ldap_password=util.Secret("ldap")))
144 factory.addStep(steps.MasterShellCommand(command=[
145 "ssh", "-o", "UserKnownHostsFile=/dev/null", "-o", "StrictHostKeyChecking=no", "-o", "CheckHostIP=no", "-i", E.SSH_KEY_PATH, puppet_host]))
146 return util.BuilderConfig(name="TestProject_deploy", workernames=["deploy-worker-test"], factory=factory)
147
148from twisted.internet import defer
149from buildbot.process.buildstep import FAILURE
150from buildbot.process.buildstep import SUCCESS
151from buildbot.process.buildstep import BuildStep
152
153class LdapPush(BuildStep):
154 name = "LdapPush"
155 renderables = ["environment", "build_version", "build_hash", "ldap_password"]
156
157 def __init__(self, **kwargs):
158 self.environment = kwargs.pop("environment")
159 self.build_version = kwargs.pop("build_version")
160 self.build_hash = kwargs.pop("build_hash")
161 self.ldap_password = kwargs.pop("ldap_password")
162 self.ldap_host = kwargs.pop("ldap_host", E.LDAP_HOST)
163 super().__init__(**kwargs)
164
165 def run(self):
166 import json
167 from ldap3 import Reader, Writer, Server, Connection, ObjectDef
168 server = Server(self.ldap_host)
169 conn = Connection(server,
170 user=E.LDAP_DN,
171 password=self.ldap_password)
172 conn.bind()
173 obj = ObjectDef("immaePuppetClass", conn)
174 r = Reader(conn, obj,
175 "cn=test.{},{}".format(self.environment, E.LDAP_ROLES_BASE))
176 r.search()
177 if len(r) > 0:
178 w = Writer.from_cursor(r)
179 for value in w[0].immaePuppetJson.values:
180 config = json.loads(value)
181 if "test_version" in config:
182 config["test_version"] = self.build_version
183 config["test_sha256"] = self.build_hash
184 w[0].immaePuppetJson -= value
185 w[0].immaePuppetJson += json.dumps(config, indent=" ")
186 w.commit()
187 return defer.succeed(SUCCESS)
188 return defer.succeed(FAILURE)
diff --git a/modules/private/certificates.nix b/modules/private/certificates.nix
new file mode 100644
index 0000000..43f6a23
--- /dev/null
+++ b/modules/private/certificates.nix
@@ -0,0 +1,52 @@
1{ lib, pkgs, config, ... }:
2{
3 options.services.myCertificates = {
4 certConfig = lib.mkOption {
5 default = {
6 webroot = "${config.security.acme.directory}/acme-challenge";
7 email = "ismael@bouya.org";
8 postRun = ''
9 systemctl reload httpdTools.service httpdInte.service httpdProd.service
10 '';
11 plugins = [ "cert.pem" "chain.pem" "fullchain.pem" "full.pem" "key.pem" "account_key.json" ];
12 };
13 description = "Default configuration for certificates";
14 };
15 };
16
17 config = {
18 services.websitesCerts = config.services.myCertificates.certConfig;
19 myServices.databasesCerts = config.services.myCertificates.certConfig;
20 myServices.ircCerts = config.services.myCertificates.certConfig;
21
22 security.acme.preliminarySelfsigned = true;
23
24 security.acme.certs = {
25 "eldiron" = config.services.myCertificates.certConfig // {
26 domain = "eldiron.immae.eu";
27 };
28 };
29
30 systemd.services = lib.attrsets.mapAttrs' (k: v:
31 lib.attrsets.nameValuePair "acme-selfsigned-${k}" (lib.mkBefore { script =
32 (lib.optionalString (builtins.elem "cert.pem" v.plugins) ''
33 cp $workdir/server.crt ${config.security.acme.directory}/${k}/cert.pem
34 chown '${v.user}:${v.group}' ${config.security.acme.directory}/${k}/cert.pem
35 chmod ${if v.allowKeysForGroup then "750" else "700"} ${config.security.acme.directory}/${k}/cert.pem
36 '') +
37 (lib.optionalString (builtins.elem "chain.pem" v.plugins) ''
38 cp $workdir/ca.crt ${config.security.acme.directory}/${k}/chain.pem
39 chown '${v.user}:${v.group}' ${config.security.acme.directory}/${k}/chain.pem
40 chmod ${if v.allowKeysForGroup then "750" else "700"} ${config.security.acme.directory}/${k}/chain.pem
41 '')
42 ; })
43 ) config.security.acme.certs // {
44 httpdProd.after = [ "acme-selfsigned-certificates.target" ];
45 httpdProd.wants = [ "acme-selfsigned-certificates.target" ];
46 httpdTools.after = [ "acme-selfsigned-certificates.target" ];
47 httpdTools.wants = [ "acme-selfsigned-certificates.target" ];
48 httpdInte.after = [ "acme-selfsigned-certificates.target" ];
49 httpdInte.wants = [ "acme-selfsigned-certificates.target" ];
50 };
51 };
52}
diff --git a/modules/private/default.nix b/modules/private/default.nix
index 242eeb9..894efb7 100644
--- a/modules/private/default.nix
+++ b/modules/private/default.nix
@@ -47,7 +47,19 @@ set = {
47 peertubeTool = ./websites/tools/peertube; 47 peertubeTool = ./websites/tools/peertube;
48 toolsTool = ./websites/tools/tools; 48 toolsTool = ./websites/tools/tools;
49 49
50 buildbot = ./buildbot;
51 certificates = ./certificates.nix;
52 gitolite = ./gitolite;
50 irc = ./irc.nix; 53 irc = ./irc.nix;
54 pub = ./pub;
55 tasks = ./tasks;
56 dns = ./dns.nix;
57 ftp = ./ftp.nix;
58 mail = ./mail.nix;
59 mpd = ./mpd.nix;
60 ssh = ./ssh;
61
62 system = ./system.nix;
51}; 63};
52in 64in
53builtins.listToAttrs (map (attr: { name = "priv${attr}"; value = set.${attr}; }) (builtins.attrNames set)) 65builtins.listToAttrs (map (attr: { name = "priv${attr}"; value = set.${attr}; }) (builtins.attrNames set))
diff --git a/modules/private/dns.nix b/modules/private/dns.nix
new file mode 100644
index 0000000..ced8d9b
--- /dev/null
+++ b/modules/private/dns.nix
@@ -0,0 +1,132 @@
1{ lib, pkgs, config, myconfig, ... }:
2{
3 config = let
4 cfg = config.services.bind;
5 configFile = pkgs.writeText "named.conf" ''
6 include "/etc/bind/rndc.key";
7 controls {
8 inet 127.0.0.1 allow {localhost;} keys {"rndc-key";};
9 };
10
11 acl cachenetworks { ${lib.concatMapStrings (entry: " ${entry}; ") cfg.cacheNetworks} };
12 acl badnetworks { ${lib.concatMapStrings (entry: " ${entry}; ") cfg.blockedNetworks} };
13
14 options {
15 listen-on { ${lib.concatMapStrings (entry: " ${entry}; ") cfg.listenOn} };
16 listen-on-v6 { ${lib.concatMapStrings (entry: " ${entry}; ") cfg.listenOnIpv6} };
17 allow-query { cachenetworks; };
18 blackhole { badnetworks; };
19 forward first;
20 forwarders { ${lib.concatMapStrings (entry: " ${entry}; ") cfg.forwarders} };
21 directory "/var/run/named";
22 pid-file "/var/run/named/named.pid";
23 ${cfg.extraOptions}
24 };
25
26 ${cfg.extraConfig}
27
28 ${ lib.concatMapStrings
29 ({ name, file, master ? true, extra ? "", slaves ? [], masters ? [] }:
30 ''
31 zone "${name}" {
32 type ${if master then "master" else "slave"};
33 file "${file}";
34 ${ if lib.lists.length slaves > 0 then
35 ''
36 allow-transfer {
37 ${lib.concatMapStrings (ip: "${ip};\n") slaves}
38 };
39 '' else ""}
40 ${ if lib.lists.length masters > 0 then
41 ''
42 masters {
43 ${lib.concatMapStrings (ip: "${ip};\n") masters}
44 };
45 '' else ""}
46 allow-query { any; };
47 ${extra}
48 };
49 '')
50 cfg.zones }
51 '';
52 in
53 {
54 networking.firewall.allowedUDPPorts = [ 53 ];
55 networking.firewall.allowedTCPPorts = [ 53 ];
56 services.bind = {
57 enable = true;
58 cacheNetworks = ["any"];
59 configFile = configFile;
60 extraOptions = ''
61 allow-recursion { 127.0.0.1; };
62 allow-transfer { none; };
63
64 notify-source ${myconfig.env.servers.eldiron.ips.main.ip4};
65 notify-source-v6 ${lib.head myconfig.env.servers.eldiron.ips.main.ip6};
66 version none;
67 hostname none;
68 server-id none;
69 '';
70 zones = with myconfig.env.dns;
71 assert (builtins.substring ((builtins.stringLength soa.email)-1) 1 soa.email) != ".";
72 assert (builtins.substring ((builtins.stringLength soa.primary)-1) 1 soa.primary) != ".";
73 (map (conf: {
74 name = conf.name;
75 master = false;
76 file = "/var/run/named/${conf.name}.zone";
77 masters = if lib.attrsets.hasAttr "masters" conf
78 then lib.lists.flatten (map (n: lib.attrsets.attrValues ns.${n}) conf.masters)
79 else [];
80 }) slaveZones)
81 ++ (map (conf: {
82 name = conf.name;
83 master = true;
84 extra = if lib.attrsets.hasAttr "extra" conf then conf.extra else "";
85 slaves = if lib.attrsets.hasAttr "slaves" conf
86 then lib.lists.flatten (map (n: lib.attrsets.attrValues ns.${n}) conf.slaves)
87 else [];
88 file = pkgs.writeText "${conf.name}.zone" ''
89 $TTL 10800
90 @ IN SOA ${soa.primary}. ${builtins.replaceStrings ["@"] ["."] soa.email}. ${soa.serial} ${soa.refresh} ${soa.retry} ${soa.expire} ${soa.ttl}
91
92 ${lib.concatStringsSep "\n" (map (x: "@ IN NS ${x}.") (lib.concatMap (n: lib.attrsets.mapAttrsToList (k: v: k) ns.${n}) conf.ns))}
93
94 ${conf.entries}
95
96 ${if lib.attrsets.hasAttr "withEmail" conf && lib.lists.length conf.withEmail > 0 then ''
97 mail IN A ${myconfig.env.servers.immaeEu.ips.main.ip4}
98 mx-1 IN A ${myconfig.env.servers.eldiron.ips.main.ip4}
99 ${builtins.concatStringsSep "\n" (map (i: "mail IN AAAA ${i}") myconfig.env.servers.immaeEu.ips.main.ip6)}
100 ${builtins.concatStringsSep "\n" (map (i: "mx-1 IN AAAA ${i}") myconfig.env.servers.eldiron.ips.main.ip6)}
101 ${lib.concatStringsSep "\n\n" (map (e:
102 let
103 n = if e.domain == "" then "@" else "${e.domain} ";
104 suffix = if e.domain == "" then "" else ".${e.domain}";
105 in
106 ''
107 ; ------------------ mail: ${n} ---------------------------
108 ${if e.receive then "${n} IN MX 10 mail.${conf.name}." else ""}
109 ;${if e.receive then "${n} IN MX 50 mx-1.${conf.name}." else ""}
110
111 ; Mail sender authentications
112 ${n} IN TXT "v=spf1 mx ~all"
113 _dmarc${suffix} IN TXT "v=DMARC1; p=none; adkim=r; aspf=r; fo=1; rua=mailto:postmaster+rua@immae.eu; ruf=mailto:postmaster+ruf@immae.eu;"
114 ${if e.send then ''
115 immae_eu._domainkey${suffix} IN TXT ( "v=DKIM1; k=rsa; s=email; "
116 "p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzl3vLd8W5YAuumC5+ZT9OV7/14Pmh5JYtwyqKI3cfe9NnAqInt3xO4bZ7oqIxRKWN4SD39vm7O/QOvFdBt00ENOOzdP90s5gKw6eIP/4+vPTh0IWltAsmu9B2agzdtWUE7t2xFKIzEn8l9niRE2QYbVaqZv4sub98vY55fIgFoHtjkmNC7325S8fjDJGp6OPbyhAs6Xl5/adjF"
117 "0ko4Y2p6RaxLQfjlS0bxmK4Qg6C14pIXHtzVeqOuWrwApqt5+AULSn97iUtqV/IJlEEjC6DUR44t3C/G0G/k46iFclCqRRi0hdPrOHCtZDbtMubnTN9eaUiNpkXh1WnCflHwtjQwIDAQAB" )
118 '' else ""}
119 '') conf.withEmail)}
120 '' + (if conf.name == "immae.eu" then ''
121 ; ----------------- Accept DMARC reports -------------------
122 ${lib.concatStringsSep "\n" (
123 lib.flatten (
124 map (z: map (e: "${e.domain}${if builtins.stringLength e.domain > 0 then "." else ""}${z.name}._report._dmarc IN TXT \"v=DMARC1;\"") (z.withEmail or [])) masterZones
125 )
126 )}
127 '' else "") else ""}
128 '';
129 }) masterZones);
130 };
131 };
132}
diff --git a/modules/private/ftp.nix b/modules/private/ftp.nix
new file mode 100644
index 0000000..842d2d6
--- /dev/null
+++ b/modules/private/ftp.nix
@@ -0,0 +1,118 @@
1{ lib, pkgs, config, myconfig, ... }:
2{
3 options = {
4 services.pure-ftpd.enable = lib.mkOption {
5 type = lib.types.bool;
6 default = false;
7 description = ''
8 Whether to enable pure-ftpd.
9 '';
10 };
11 };
12
13 config = lib.mkIf config.services.pure-ftpd.enable {
14 security.acme.certs."ftp" = config.services.myCertificates.certConfig // {
15 domain = "eldiron.immae.eu";
16 postRun = ''
17 systemctl restart pure-ftpd.service
18 '';
19 extraDomains = { "ftp.immae.eu" = null; };
20 };
21
22 networking = {
23 firewall = {
24 allowedTCPPorts = [ 21 ];
25 allowedTCPPortRanges = [ { from = 40000; to = 50000; } ];
26 };
27 };
28
29 users.users = [
30 {
31 name = "ftp";
32 uid = config.ids.uids.ftp; # 8
33 group = "ftp";
34 description = "Anonymous FTP user";
35 home = "/homeless-shelter";
36 extraGroups = [ "keys" ];
37 }
38 ];
39
40 users.groups.ftp.gid = config.ids.gids.ftp;
41
42 system.activationScripts.pure-ftpd = ''
43 install -m 0755 -o ftp -g ftp -d /var/lib/ftp
44 '';
45
46 secrets.keys = [{
47 dest = "pure-ftpd-ldap";
48 permissions = "0400";
49 user = "ftp";
50 group = "ftp";
51 text = ''
52 LDAPServer ${myconfig.env.ftp.ldap.host}
53 LDAPPort 389
54 LDAPUseTLS True
55 LDAPBaseDN ${myconfig.env.ftp.ldap.base}
56 LDAPBindDN ${myconfig.env.ftp.ldap.dn}
57 LDAPBindPW ${myconfig.env.ftp.ldap.password}
58 LDAPDefaultUID 500
59 LDAPForceDefaultUID False
60 LDAPDefaultGID 100
61 LDAPForceDefaultGID False
62 LDAPFilter ${myconfig.env.ftp.ldap.filter}
63
64 LDAPAuthMethod BIND
65
66 # Pas de possibilite de donner l'Uid/Gid !
67 # Compile dans pure-ftpd directement avec immaeFtpUid / immaeFtpGid
68 LDAPHomeDir immaeFtpDirectory
69 '';
70 }];
71
72 systemd.services.pure-ftpd = let
73 configFile = pkgs.writeText "pure-ftpd.conf" ''
74 PassivePortRange 40000 50000
75 ChrootEveryone yes
76 CreateHomeDir yes
77 BrokenClientsCompatibility yes
78 MaxClientsNumber 50
79 Daemonize yes
80 MaxClientsPerIP 8
81 VerboseLog no
82 DisplayDotFiles yes
83 AnonymousOnly no
84 NoAnonymous no
85 SyslogFacility ftp
86 DontResolve yes
87 MaxIdleTime 15
88 LDAPConfigFile /var/secrets/pure-ftpd-ldap
89 LimitRecursion 10000 8
90 AnonymousCanCreateDirs no
91 MaxLoad 4
92 AntiWarez yes
93 Umask 133:022
94 # ftp
95 MinUID 8
96 AllowUserFXP no
97 AllowAnonymousFXP no
98 ProhibitDotFilesWrite no
99 ProhibitDotFilesRead no
100 AutoRename no
101 AnonymousCantUpload no
102 MaxDiskUsage 99
103 CustomerProof yes
104 TLS 1
105 CertFile ${config.security.acme.directory}/ftp/full.pem
106 '';
107 in {
108 description = "Pure-FTPd server";
109 wantedBy = [ "multi-user.target" ];
110 after = [ "network.target" ];
111
112 serviceConfig.ExecStart = "${pkgs.pure-ftpd}/bin/pure-ftpd ${configFile}";
113 serviceConfig.Type = "forking";
114 serviceConfig.PIDFile = "/run/pure-ftpd.pid";
115 };
116 };
117
118}
diff --git a/modules/private/gitolite/default.nix b/modules/private/gitolite/default.nix
new file mode 100644
index 0000000..b9914a1
--- /dev/null
+++ b/modules/private/gitolite/default.nix
@@ -0,0 +1,63 @@
1{ lib, pkgs, config, myconfig, ... }:
2let
3 cfg = config.myServices.gitolite;
4in {
5 options.myServices.gitolite = {
6 enable = lib.mkEnableOption "my gitolite service";
7 gitoliteDir = lib.mkOption {
8 type = lib.types.string;
9 default = "/var/lib/gitolite";
10 };
11 };
12
13 config = lib.mkIf cfg.enable {
14 networking.firewall.allowedTCPPorts = [ 9418 ];
15
16 services.gitDaemon = {
17 enable = true;
18 user = "gitolite";
19 group = "gitolite";
20 basePath = "${cfg.gitoliteDir}/repositories";
21 };
22
23 system.activationScripts.gitolite = let
24 gitolite_ldap_groups = pkgs.mylibs.wrap {
25 name = "gitolite_ldap_groups.sh";
26 file = ./gitolite_ldap_groups.sh;
27 vars = {
28 LDAP_PASS = myconfig.env.tools.gitolite.ldap.password;
29 };
30 paths = [ pkgs.openldap pkgs.stdenv.shellPackage pkgs.gnugrep pkgs.coreutils ];
31 };
32 in {
33 deps = [ "users" ];
34 text = ''
35 if [ -d ${cfg.gitoliteDir} ]; then
36 ln -sf ${gitolite_ldap_groups} ${cfg.gitoliteDir}/gitolite_ldap_groups.sh
37 chmod g+rx ${cfg.gitoliteDir}
38 fi
39 if [ -f ${cfg.gitoliteDir}/projects.list ]; then
40 chmod g+r ${cfg.gitoliteDir}/projects.list
41 fi
42 '';
43 };
44
45 users.users.wwwrun.extraGroups = [ "gitolite" ];
46
47 users.users.gitolite.packages = let
48 python-packages = python-packages: with python-packages; [
49 simplejson
50 urllib3
51 sleekxmpp
52 ];
53 in
54 [
55 (pkgs.python3.withPackages python-packages)
56 ];
57 # Installation: https://git.immae.eu/mantisbt/view.php?id=93
58 services.gitolite = {
59 enable = true;
60 adminPubkey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDXqRbiHw7QoHADNIEuo4nUT9fSOIEBMdJZH0bkQAxXyJFyCM1IMz0pxsHV0wu9tdkkr36bPEUj2aV5bkYLBN6nxcV2Y49X8bjOSCPfx3n6Own1h+NeZVBj4ZByrFmqCbTxUJIZ2bZKcWOFncML39VmWdsVhNjg0X4NBBehqXRIKr2gt3E/ESAxTYJFm0BnU0baciw9cN0bsRGqvFgf5h2P48CIAfwhVcGmPQnnAwabnosYQzRWxR0OygH5Kd8mePh6FheIRIigfXsDO8f/jdxwut8buvNIf3m5EBr3tUbTsvM+eV3M5vKGt7sk8T64DVtepTSdOOWtp+47ktsnHOMh immae@immae.eu";
61 };
62 };
63}
diff --git a/modules/private/gitolite/gitolite_ldap_groups.sh b/modules/private/gitolite/gitolite_ldap_groups.sh
new file mode 100755
index 0000000..7db0da4
--- /dev/null
+++ b/modules/private/gitolite/gitolite_ldap_groups.sh
@@ -0,0 +1,15 @@
1#!/usr/bin/env bash
2
3uid_param="$1"
4ldap_host="ldap.immae.eu"
5ldap_binddn="cn=gitolite,ou=services,dc=immae,dc=eu"
6ldap_bindpw="$LDAP_PASS"
7ldap_searchbase="dc=immae,dc=eu"
8ldap_scope="subtree"
9
10ldap_options="-h ${ldap_host} -ZZ -x -D ${ldap_binddn} -w ${ldap_bindpw} -b ${ldap_searchbase} -s ${ldap_scope}"
11
12ldap_filter="(&(memberOf=cn=groups,cn=gitolite,ou=services,dc=immae,dc=eu)(|(member=uid=${uid_param},ou=users,dc=immae,dc=eu)(member=uid=${uid_param},ou=group_users,dc=immae,dc=eu)))"
13ldap_result=$(ldapsearch ${ldap_options} -LLL "${ldap_filter}" cn | grep 'cn:' | cut -d' ' -f2)
14
15echo "$ldap_result"
diff --git a/modules/private/mail.nix b/modules/private/mail.nix
new file mode 100644
index 0000000..611c8b4
--- /dev/null
+++ b/modules/private/mail.nix
@@ -0,0 +1,13 @@
1{ lib, pkgs, config, myconfig, ... }:
2{
3 config.users.users.nullmailer.uid = config.ids.uids.nullmailer;
4 config.users.groups.nullmailer.gid = config.ids.gids.nullmailer;
5
6 config.services.nullmailer = {
7 enable = true;
8 config = {
9 me = myconfig.env.mail.host;
10 remotes = "${myconfig.env.mail.relay} smtp";
11 };
12 };
13}
diff --git a/modules/private/mpd.nix b/modules/private/mpd.nix
new file mode 100644
index 0000000..9903bdf
--- /dev/null
+++ b/modules/private/mpd.nix
@@ -0,0 +1,56 @@
1{ lib, pkgs, config, myconfig, ... }:
2{
3 config = {
4 secrets.keys = [
5 {
6 dest = "mpd";
7 permissions = "0400";
8 text = myconfig.env.mpd.password;
9 }
10 {
11 dest = "mpd-config";
12 permissions = "0400";
13 user = "mpd";
14 group = "mpd";
15 text = ''
16 password "${myconfig.env.mpd.password}@read,add,control,admin"
17 '';
18 }
19 ];
20 networking.firewall.allowedTCPPorts = [ 6600 ];
21 users.users.mpd.extraGroups = [ "wwwrun" "keys" ];
22 systemd.services.mpd.serviceConfig.RuntimeDirectory = "mpd";
23 services.mpd = {
24 enable = true;
25 network.listenAddress = "any";
26 musicDirectory = myconfig.env.mpd.folder;
27 extraConfig = ''
28 include "/var/secrets/mpd-config"
29 audio_output {
30 type "null"
31 name "No Output"
32 mixer_type "none"
33 }
34 audio_output {
35 type "httpd"
36 name "OGG"
37 encoder "vorbis"
38 bind_to_address "/run/mpd/ogg.sock"
39 quality "5.0"
40 format "44100:16:1"
41 }
42 audio_output {
43 type "httpd"
44 name "MP3"
45 encoder "lame"
46 bind_to_address "/run/mpd/mp3.sock"
47 quality "5.0"
48 format "44100:16:1"
49 }
50
51
52 '';
53 };
54 };
55}
56
diff --git a/modules/private/pub/default.nix b/modules/private/pub/default.nix
new file mode 100644
index 0000000..c31c8eb
--- /dev/null
+++ b/modules/private/pub/default.nix
@@ -0,0 +1,52 @@
1{ lib, pkgs, config, myconfig, ... }:
2{
3 options = {
4 myServices.pub.enable = lib.mkOption {
5 type = lib.types.bool;
6 default = false;
7 description = ''
8 Whether to enable pub user.
9 '';
10 };
11 };
12
13 config = lib.mkIf config.myServices.pub.enable {
14 users.users.pub = let
15 restrict = pkgs.runCommand "restrict" {
16 file = ./restrict;
17 buildInputs = [ pkgs.makeWrapper ];
18 } ''
19 mkdir -p $out/bin
20 cp $file $out/bin/restrict
21 chmod a+x $out/bin/restrict
22 patchShebangs $out/bin/restrict
23 wrapProgram $out/bin/restrict \
24 --prefix PATH : ${lib.makeBinPath [ pkgs.bubblewrap pkgs.rrsync ]} \
25 --set TMUX_RESTRICT ${./tmux.restrict.conf}
26 '';
27 purple-hangouts = pkgs.purple-hangouts.overrideAttrs(old: {
28 installPhase = ''
29 install -Dm755 -t $out/lib/purple-2/ libhangouts.so
30 for size in 16 22 24 48; do
31 install -TDm644 hangouts$size.png $out/share/pixmaps/pidgin/protocols/$size/hangouts.png
32 done
33 '';
34 });
35 in {
36 createHome = true;
37 description = "Restricted shell user";
38 home = "/var/lib/pub";
39 uid = myconfig.env.users.pub.uid;
40 useDefaultShell = true;
41 packages = [
42 restrict
43 pkgs.tmux
44 (pkgs.pidgin.override { plugins = [
45 pkgs.purple-plugin-pack purple-hangouts
46 pkgs.purple-discord pkgs.purple-facebook
47 pkgs.telegram-purple
48 ]; })
49 ];
50 };
51 };
52}
diff --git a/modules/private/pub/restrict b/modules/private/pub/restrict
new file mode 100644
index 0000000..b2f3be3
--- /dev/null
+++ b/modules/private/pub/restrict
@@ -0,0 +1,64 @@
1#!/usr/bin/env bash
2user="$1"
3rootuser="$HOME/$user/"
4mkdir -p $rootuser
5
6orig="$SSH_ORIGINAL_COMMAND"
7if [ -z "$orig" ]; then
8 orig="/bin/bash -l"
9fi
10if [ "${orig:0:7}" = "command" ]; then
11 orig="${orig:8}"
12fi
13
14case "$orig" in
15rsync*)
16 rrsync $HOME/$user/
17 ;;
18*)
19 nix_store_paths() {
20 nix-store -q -R \
21 /run/current-system/sw \
22 /etc/profiles/per-user/pub \
23 /etc/ssl/certs/ca-bundle.crt \
24 | while read i; do
25 printf '%s--ro-bind\0'$i'\0'$i'\0' ''
26 done
27 }
28
29 set -euo pipefail
30 (exec -c bwrap --ro-bind /usr /usr \
31 --args 10 \
32 --dir /tmp \
33 --dir /var \
34 --symlink ../tmp var/tmp \
35 --proc /proc \
36 --dev /dev \
37 --ro-bind /etc/resolv.conf /etc/resolv.conf \
38 --ro-bind /etc/zoneinfo /etc/zoneinfo \
39 --ro-bind /etc/ssl /etc/ssl \
40 --ro-bind /etc/static/ssl/certs /etc/static/ssl/certs \
41 --ro-bind /run/current-system/sw/lib/locale/locale-archive /etc/locale-archive \
42 --ro-bind /run/current-system/sw/bin /bin \
43 --ro-bind /etc/profiles/per-user/pub/bin /bin-pub \
44 --bind /var/lib/pub/$user /var/lib/pub \
45 --dir /var/lib/commons \
46 --ro-bind $TMUX_RESTRICT /var/lib/commons/tmux.restrict.conf \
47 --chdir /var/lib/pub \
48 --unshare-all \
49 --share-net \
50 --dir /run/user/$(id -u) \
51 --setenv TERM "$TERM" \
52 --setenv LOCALE_ARCHIVE "/etc/locale-archive" \
53 --setenv XDG_RUNTIME_DIR "/run/user/`id -u`" \
54 --setenv PS1 "$user@pub $ " \
55 --setenv PATH "/bin:/bin-pub" \
56 --setenv HOME "/var/lib/pub" \
57 --file 11 /etc/passwd \
58 --file 12 /etc/group \
59 -- $orig) \
60 10< <(nix_store_paths) \
61 11< <(getent passwd $UID 65534) \
62 12< <(getent group $(id -g) 65534)
63 ;;
64esac
diff --git a/modules/private/pub/tmux.restrict.conf b/modules/private/pub/tmux.restrict.conf
new file mode 100644
index 0000000..5aefd1c
--- /dev/null
+++ b/modules/private/pub/tmux.restrict.conf
@@ -0,0 +1,43 @@
1# Pour les nostalgiques de screen
2# comme les raccourcis ne sont pas les mêmes, j'évite
3set -g prefix C-a
4unbind-key C-b
5
6unbind-key -a
7bind-key -n C-h list-keys
8bind-key C-d detach
9bind-key & confirm-before -p "kill-window #W? (y/n)" kill-window
10
11# même hack que sur screen lorsqu'on veut profiter du scroll du terminal
12# (xterm ...)
13set -g terminal-overrides 'xterm*:smcup@:rmcup@'
14
15#Pour les ctrl+arrow
16set-option -g xterm-keys on
17
18# c'est un minimum (defaut 2000)
19set-option -g history-limit 10000
20
21# lorsque j'ai encore un tmux ailleurs seule
22# sa fenetre active réduit la taille de ma fenetre locale
23setw -g aggressive-resize on
24
25# Pour etre alerté sur un changement dans une autre fenêtre
26setw -g monitor-activity on
27#set -g visual-activity on
28#set -g visual-bell on
29
30set -g base-index 1
31
32# repercuter le contenu de la fenetre dans la barre de titre
33# reference des string : man tmux (status-left)
34set -g set-titles on
35set -g set-titles-string '#H #W #T' # host window command
36
37#Dans les valeurs par defaut deja, avec le ssh-agent
38set -g update-environment "DISPLAY SSH_ASKPASS SSH_AUTH_SOCK SSH_AGENT_PID SSH_CONNECTION WINDOWID XAUTHORITY PATH"
39
40set -g status off
41set -g status-left ''
42set -g status-right ''
43
diff --git a/modules/private/ssh/default.nix b/modules/private/ssh/default.nix
new file mode 100644
index 0000000..beedaff
--- /dev/null
+++ b/modules/private/ssh/default.nix
@@ -0,0 +1,40 @@
1{ lib, pkgs, config, myconfig, ... }:
2{
3 config = {
4 networking.firewall.allowedTCPPorts = [ 22 ];
5
6 services.openssh.extraConfig = ''
7 AuthorizedKeysCommand /etc/ssh/ldap_authorized_keys
8 AuthorizedKeysCommandUser nobody
9 '';
10
11 secrets.keys = [{
12 dest = "ssh-ldap";
13 user = "nobody";
14 group = "nogroup";
15 permissions = "0400";
16 text = myconfig.env.sshd.ldap.password;
17 }];
18 system.activationScripts.sshd = {
19 deps = [ "secrets" ];
20 text = ''
21 install -Dm400 -o nobody -g nogroup -T /var/secrets/ssh-ldap /etc/ssh/ldap_password
22 '';
23 };
24 # ssh is strict about parent directory having correct rights, don't
25 # move it in the nix store.
26 environment.etc."ssh/ldap_authorized_keys" = let
27 ldap_authorized_keys =
28 pkgs.mylibs.wrap {
29 name = "ldap_authorized_keys";
30 file = ./ldap_authorized_keys.sh;
31 paths = [ pkgs.which pkgs.gitolite pkgs.openldap pkgs.stdenv.shellPackage pkgs.gnugrep pkgs.gnused pkgs.coreutils ];
32 };
33 in {
34 enable = true;
35 mode = "0755";
36 user = "root";
37 source = ldap_authorized_keys;
38 };
39 };
40}
diff --git a/modules/private/ssh/ldap_authorized_keys.sh b/modules/private/ssh/ldap_authorized_keys.sh
new file mode 100755
index 0000000..d556452
--- /dev/null
+++ b/modules/private/ssh/ldap_authorized_keys.sh
@@ -0,0 +1,152 @@
1#!/usr/bin/env bash
2
3LDAPSEARCH=ldapsearch
4KEY="immaeSshKey"
5LDAP_BIND="cn=ssh,ou=services,dc=immae,dc=eu"
6LDAP_PASS=$(cat /etc/ssh/ldap_password)
7LDAP_HOST="ldap.immae.eu"
8LDAP_MEMBER="cn=users,cn=ssh,ou=services,dc=immae,dc=eu"
9LDAP_GITOLITE_MEMBER="cn=users,cn=gitolite,ou=services,dc=immae,dc=eu"
10LDAP_PUB_RESTRICT_MEMBER="cn=restrict,cn=pub,ou=services,dc=immae,dc=eu"
11LDAP_PUB_FORWARD_MEMBER="cn=forward,cn=pub,ou=services,dc=immae,dc=eu"
12LDAP_BASE="dc=immae,dc=eu"
13GITOLITE_SHELL=$(which gitolite-shell)
14ECHO=$(which echo)
15
16suitable_for() {
17 type_for="$1"
18 key="$2"
19
20 if [[ $key != *$'\n'* ]] && [[ $key == ssh-* ]]; then
21 echo "$key"
22 else
23 key_type=$(cut -d " " -f 1 <<< "$key")
24
25 if grep -q "\b-$type_for\b" <<< "$key_type"; then
26 echo ""
27 elif grep -q "\b$type_for\b" <<< "$key_type"; then
28 echo $(sed -e "s/^[^ ]* //g" <<< "$key")
29 else
30 echo ""
31 fi
32 fi
33}
34
35clean_key_line() {
36 type_for="$1"
37 line="$2"
38
39 if [[ "$line" == $KEY::* ]]; then
40 # base64 keys should't happen, unless wrong copy-pasting
41 key=""
42 else
43 key=$(sed -e "s/^$KEY: *//" -e "s/ *$//" <<< "$line")
44 fi
45
46 suitable_for "$type_for" "$key"
47}
48
49ldap_search() {
50 $LDAPSEARCH -h $LDAP_HOST -ZZ -b $LDAP_BASE -D $LDAP_BIND -w "$LDAP_PASS" -x -o ldif-wrap=no -LLL "$@"
51}
52
53ldap_keys() {
54 user=$1;
55 if [[ $user == gitolite ]]; then
56 ldap_search '(&(memberOf='$LDAP_GITOLITE_MEMBER')('$KEY'=*))' $KEY | \
57 while read line ;
58 do
59 if [ ! -z "$line" ]; then
60 if [[ $line == dn* ]]; then
61 user=$(sed -n 's/.*uid=\([^,]*\).*/\1/p' <<< "$line")
62 if [ -n "$user" ]; then
63 if [[ $user == "immae" ]] || [[ $user == "denise" ]]; then
64 # Capitalize first letter (backward compatibility)
65 user=$(sed -r 's/^([a-z])/\U\1/' <<< "$user")
66 fi
67 else
68 # Service fake user
69 user=$(sed -n 's/.*cn=\([^,]*\).*/\1/p' <<< "$line")
70 fi
71 elif [[ $line == $KEY* ]]; then
72 key=$(clean_key_line git "$line")
73 if [ ! -z "$key" ]; then
74 if [[ $key != *$'\n'* ]] && [[ $key == ssh-* ]]; then
75 echo -n 'command="'$GITOLITE_SHELL' '$user'",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty '
76 echo $key
77 fi
78 fi
79 fi
80 fi
81 done
82 exit 0
83 elif [[ $user == pub ]]; then
84 ldap_search '(&(memberOf='$LDAP_PUB_RESTRICT_MEMBER')('$KEY'=*))' $KEY | \
85 while read line ;
86 do
87 if [ ! -z "$line" ]; then
88 if [[ $line == dn* ]]; then
89 echo ""
90 user=$(sed -n 's/.*uid=\([^,]*\).*/\1/p' <<< "$line")
91 echo "# $user"
92 elif [[ $line == $KEY* ]]; then
93 key=$(clean_key_line pub "$line")
94 key_forward=$(clean_key_line forward "$line")
95 if [ ! -z "$key" ]; then
96 if [[ $key != *$'\n'* ]] && [[ $key == ssh-* ]]; then
97 echo -n 'command="/etc/profiles/per-user/pub/bin/restrict '$user'" '
98 echo $key
99 fi
100 elif [ ! -z "$key_forward" ]; then
101 if [[ $key_forward != *$'\n'* ]] && [[ $key_forward == ssh-* ]]; then
102 echo "# forward only"
103 echo -n 'no-pty,no-X11-forwarding,command="'$ECHO' forward only" '
104 echo $key_forward
105 fi
106 fi
107 fi
108 fi
109 done
110
111 echo ""
112 ldap_search '(&(memberOf='$LDAP_PUB_FORWARD_MEMBER')('$KEY'=*))' $KEY | \
113 while read line ;
114 do
115 if [ ! -z "$line" ]; then
116 if [[ $line == dn* ]]; then
117 echo ""
118 user=$(sed -n 's/.*uid=\([^,]*\).*/\1/p' <<< "$line")
119 echo "# $user"
120 elif [[ $line == $KEY* ]]; then
121 key=$(clean_key_line forward "$line")
122 if [ ! -z "$key" ]; then
123 if [[ $key != *$'\n'* ]] && [[ $key == ssh-* ]]; then
124 echo -n 'no-pty,no-X11-forwarding,command="'$ECHO' forward only" '
125 echo $key
126 fi
127 fi
128 fi
129 fi
130 done
131 exit 0
132 else
133 ldap_search '(&(memberOf='$LDAP_MEMBER')('$KEY'=*)(uid='$user'))' $KEY | \
134 while read line ;
135 do
136 if [ ! -z "$line" ]; then
137 if [[ $line == dn* ]]; then
138 user=$(sed -n 's/.*uid=\([^,]*\).*/\1/p' <<< "$line")
139 elif [[ $line == $KEY* ]]; then
140 key=$(clean_key_line ssh "$line")
141 if [ ! -z "$key" ]; then
142 if [[ $key != *$'\n'* ]] && [[ $key == ssh-* ]]; then
143 echo $key
144 fi
145 fi
146 fi
147 fi
148 done
149 fi
150}
151
152ldap_keys $@
diff --git a/modules/private/system.nix b/modules/private/system.nix
new file mode 100644
index 0000000..fba504e
--- /dev/null
+++ b/modules/private/system.nix
@@ -0,0 +1,30 @@
1{ pkgs, privateFiles, ... }:
2{
3 config = {
4 nixpkgs.overlays = builtins.attrValues (import ../../overlays);
5 _module.args = {
6 pkgsNext = import <nixpkgsNext> {};
7 pkgsPrevious = import <nixpkgsPrevious> {};
8 myconfig = {
9 inherit privateFiles;
10 env = import "${privateFiles}/environment.nix";
11 };
12 };
13
14 services.journald.extraConfig = ''
15 MaxLevelStore="warning"
16 MaxRetentionSec="1year"
17 '';
18
19 users.users.root.packages = [
20 pkgs.telnet
21 pkgs.htop
22 pkgs.iftop
23 ];
24
25 environment.systemPackages = [
26 pkgs.vim
27 ];
28
29 };
30}
diff --git a/modules/private/tasks/default.nix b/modules/private/tasks/default.nix
new file mode 100644
index 0000000..30f49ee
--- /dev/null
+++ b/modules/private/tasks/default.nix
@@ -0,0 +1,327 @@
1{ lib, pkgs, config, myconfig, ... }:
2let
3 cfg = config.myServices.tasks;
4 server_vardir = config.services.taskserver.dataDir;
5 fqdn = "task.immae.eu";
6 user = config.services.taskserver.user;
7 env = myconfig.env.tools.task;
8 group = config.services.taskserver.group;
9 taskserver-user-certs = pkgs.runCommand "taskserver-user-certs" {} ''
10 mkdir -p $out/bin
11 cat > $out/bin/taskserver-user-certs <<"EOF"
12 #!/usr/bin/env bash
13
14 user=$1
15
16 silent_certtool() {
17 if ! output="$("${pkgs.gnutls.bin}/bin/certtool" "$@" 2>&1)"; then
18 echo "GNUTLS certtool invocation failed with output:" >&2
19 echo "$output" >&2
20 fi
21 }
22
23 silent_certtool -p \
24 --bits 4096 \
25 --outfile "${server_vardir}/userkeys/$user.key.pem"
26 ${pkgs.gnused}/bin/sed -i -n -e '/^-----BEGIN RSA PRIVATE KEY-----$/,$p' "${server_vardir}/userkeys/$user.key.pem"
27
28 silent_certtool -c \
29 --template "${pkgs.writeText "taskserver-ca.template" ''
30 tls_www_client
31 encryption_key
32 signing_key
33 expiration_days = 3650
34 ''}" \
35 --load-ca-certificate "${server_vardir}/keys/ca.cert" \
36 --load-ca-privkey "${server_vardir}/keys/ca.key" \
37 --load-privkey "${server_vardir}/userkeys/$user.key.pem" \
38 --outfile "${server_vardir}/userkeys/$user.cert.pem"
39 EOF
40 chmod a+x $out/bin/taskserver-user-certs
41 patchShebangs $out/bin/taskserver-user-certs
42 '';
43 taskwarrior-web = pkgs.webapps.taskwarrior-web;
44 socketsDir = "/run/taskwarrior-web";
45 varDir = "/var/lib/taskwarrior-web";
46 taskwebPages = let
47 uidPages = lib.attrsets.zipAttrs (
48 lib.lists.flatten
49 (lib.attrsets.mapAttrsToList (k: c: map (v: { "${v}" = k; }) c.uid) env.taskwarrior-web)
50 );
51 pages = lib.attrsets.mapAttrs (uid: items:
52 if lib.lists.length items == 1 then
53 ''
54 <html>
55 <head>
56 <meta http-equiv="refresh" content="0; url=/taskweb/${lib.lists.head items}/" />
57 </head>
58 <body></body>
59 </html>
60 ''
61 else
62 ''
63 <html>
64 <head>
65 <title>To-do list disponibles</title>
66 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
67 <meta name="viewport" content="width=device-width, initial-scale=1" />
68 </head>
69 <body>
70 <ul>
71 ${builtins.concatStringsSep "\n" (map (item: "<li><a href='/taskweb/${item}'>${item}</a></li>") items)}
72 </ul>
73 </body>
74 </html>
75 ''
76 ) uidPages;
77 in
78 pkgs.runCommand "taskwerver-pages" {} ''
79 mkdir -p $out/
80 ${builtins.concatStringsSep "\n" (lib.attrsets.mapAttrsToList (k: v: "cp ${pkgs.writeText k v} $out/${k}.html") pages)}
81 echo "Please login" > $out/index.html
82 '';
83in {
84 options.myServices.tasks = {
85 enable = lib.mkEnableOption "my tasks service";
86 };
87
88 config = lib.mkIf cfg.enable {
89 secrets.keys = [{
90 dest = "webapps/tools-taskwarrior-web";
91 user = "wwwrun";
92 group = "wwwrun";
93 permissions = "0400";
94 text = ''
95 SetEnv TASKD_HOST "${fqdn}:${toString config.services.taskserver.listenPort}"
96 SetEnv TASKD_VARDIR "${server_vardir}"
97 SetEnv TASKD_LDAP_HOST "ldaps://${env.ldap.host}"
98 SetEnv TASKD_LDAP_DN "${env.ldap.dn}"
99 SetEnv TASKD_LDAP_PASSWORD "${env.ldap.password}"
100 SetEnv TASKD_LDAP_BASE "${env.ldap.base}"
101 SetEnv TASKD_LDAP_FILTER "${env.ldap.search}"
102 '';
103 }];
104 services.websites.tools.modules = [ "proxy_fcgi" "sed" ];
105 services.websites.tools.vhostConfs.task = {
106 certName = "eldiron";
107 addToCerts = true;
108 hosts = [ "task.immae.eu" ];
109 root = "/run/current-system/webapps/_task";
110 extraConfig = [ ''
111 <Directory /run/current-system/webapps/_task>
112 DirectoryIndex index.php
113 Use LDAPConnect
114 Require ldap-group cn=users,cn=taskwarrior,ou=services,dc=immae,dc=eu
115 <FilesMatch "\.php$">
116 SetHandler "proxy:unix:/var/run/phpfpm/task.sock|fcgi://localhost"
117 </FilesMatch>
118 Include /var/secrets/webapps/tools-taskwarrior-web
119 </Directory>
120 ''
121 ''
122 <Macro Taskwarrior %{folderName}>
123 ProxyPass "unix://${socketsDir}/%{folderName}.sock|http://localhost-%{folderName}/"
124 ProxyPassReverse "unix://${socketsDir}/%{folderName}.sock|http://localhost-%{folderName}/"
125 ProxyPassReverse http://${fqdn}/
126
127 SetOutputFilter Sed
128 OutputSed "s|/ajax|/taskweb/%{folderName}/ajax|g"
129 OutputSed "s|\([^x]\)/tasks|\1/taskweb/%{folderName}/tasks|g"
130 OutputSed "s|\([^x]\)/projects|\1/taskweb/%{folderName}/projects|g"
131 OutputSed "s|http://${fqdn}/|/taskweb/%{folderName}/|g"
132 OutputSed "s|/img/relax.jpg|/taskweb/%{folderName}/img/relax.jpg|g"
133 </Macro>
134 ''
135 ''
136 Alias /taskweb ${taskwebPages}
137 <Directory "${taskwebPages}">
138 DirectoryIndex index.html
139 Require all granted
140 </Directory>
141
142 RewriteEngine on
143 RewriteRule ^/taskweb$ /taskweb/ [R=301,L]
144 RedirectMatch permanent ^/taskweb/([^/]+)$ /taskweb/$1/
145
146 RewriteCond %{LA-U:REMOTE_USER} !=""
147 RewriteCond ${taskwebPages}/%{LA-U:REMOTE_USER}.html -f
148 RewriteRule ^/taskweb/?$ ${taskwebPages}/%{LA-U:REMOTE_USER}.html [L]
149
150 <Location /taskweb/>
151 Use LDAPConnect
152 Require ldap-group cn=users,cn=taskwarrior,ou=services,dc=immae,dc=eu
153 </Location>
154 ''
155 ] ++ (lib.attrsets.mapAttrsToList (k: v: ''
156 <Location /taskweb/${k}/>
157 ${builtins.concatStringsSep "\n" (map (uid: "Require ldap-attribute uid=${uid}") v.uid)}
158
159 Use Taskwarrior ${k}
160 </Location>
161 '') env.taskwarrior-web);
162 };
163 services.phpfpm.poolConfigs = {
164 tasks = ''
165 listen = /var/run/phpfpm/task.sock
166 user = ${user}
167 group = ${group}
168 listen.owner = wwwrun
169 listen.group = wwwrun
170 pm = dynamic
171 pm.max_children = 60
172 pm.start_servers = 2
173 pm.min_spare_servers = 1
174 pm.max_spare_servers = 10
175
176 ; Needed to avoid clashes in browser cookies (same domain)
177 env[PATH] = "/etc/profiles/per-user/${user}/bin"
178 php_value[session.name] = TaskPHPSESSID
179 php_admin_value[open_basedir] = "${./www}:/tmp:${server_vardir}:/etc/profiles/per-user/${user}/bin/"
180 '';
181 };
182
183 myServices.websites.webappDirs._task = ./www;
184
185 security.acme.certs."task" = config.services.myCertificates.certConfig // {
186 inherit user group;
187 plugins = [ "fullchain.pem" "key.pem" "cert.pem" "account_key.json" ];
188 domain = fqdn;
189 postRun = ''
190 systemctl restart taskserver.service
191 '';
192 };
193
194 users.users.${user}.packages = [ taskserver-user-certs ];
195
196 system.activationScripts.taskserver = {
197 deps = [ "users" ];
198 text = ''
199 install -m 0750 -o ${user} -g ${group} -d ${server_vardir}
200 install -m 0750 -o ${user} -g ${group} -d ${server_vardir}/userkeys
201 install -m 0750 -o ${user} -g ${group} -d ${server_vardir}/keys
202
203 if [ ! -e "${server_vardir}/keys/ca.key" ]; then
204 silent_certtool() {
205 if ! output="$("${pkgs.gnutls.bin}/bin/certtool" "$@" 2>&1)"; then
206 echo "GNUTLS certtool invocation failed with output:" >&2
207 echo "$output" >&2
208 fi
209 }
210
211 silent_certtool -p \
212 --bits 4096 \
213 --outfile "${server_vardir}/keys/ca.key"
214
215 silent_certtool -s \
216 --template "${pkgs.writeText "taskserver-ca.template" ''
217 cn = ${fqdn}
218 expiration_days = -1
219 cert_signing_key
220 ca
221 ''}" \
222 --load-privkey "${server_vardir}/keys/ca.key" \
223 --outfile "${server_vardir}/keys/ca.cert"
224
225 chown :${group} "${server_vardir}/keys/ca.key"
226 chmod g+r "${server_vardir}/keys/ca.key"
227 fi
228 '';
229 };
230
231 services.taskserver = {
232 enable = true;
233 allowedClientIDs = [ "^task [2-9]" "^Mirakel [1-9]" ];
234 inherit fqdn;
235 listenHost = "::";
236 pki.manual.ca.cert = "${server_vardir}/keys/ca.cert";
237 pki.manual.server.cert = "${config.security.acme.directory}/task/fullchain.pem";
238 pki.manual.server.crl = "${config.security.acme.directory}/task/invalid.crl";
239 pki.manual.server.key = "${config.security.acme.directory}/task/key.pem";
240 requestLimit = 104857600;
241 };
242
243 system.activationScripts.taskwarrior-web = {
244 deps = [ "users" ];
245 text = ''
246 if [ ! -f ${server_vardir}/userkeys/taskwarrior-web.cert.pem ]; then
247 ${taskserver-user-certs}/bin/taskserver-user-certs taskwarrior-web
248 chown taskd:taskd ${server_vardir}/userkeys/taskwarrior-web.cert.pem ${server_vardir}/userkeys/taskwarrior-web.key.pem
249 fi
250 '';
251 };
252
253 systemd.services = (lib.attrsets.mapAttrs' (name: userConfig:
254 let
255 credentials = "${userConfig.org}/${name}/${userConfig.key}";
256 dateFormat = userConfig.date;
257 taskrc = pkgs.writeText "taskrc" ''
258 data.location=${varDir}/${name}
259 taskd.certificate=${server_vardir}/userkeys/taskwarrior-web.cert.pem
260 taskd.key=${server_vardir}/userkeys/taskwarrior-web.key.pem
261 # IdenTrust DST Root CA X3
262 # obtained here: https://letsencrypt.org/fr/certificates/
263 taskd.ca=${pkgs.writeText "ca.cert" ''
264 -----BEGIN CERTIFICATE-----
265 MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/
266 MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
267 DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow
268 PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD
269 Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
270 AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O
271 rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq
272 OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b
273 xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw
274 7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD
275 aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV
276 HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG
277 SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69
278 ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr
279 AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz
280 R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5
281 JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo
282 Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ
283 -----END CERTIFICATE-----''}
284 taskd.server=${fqdn}:${toString config.services.taskserver.listenPort}
285 taskd.credentials=${credentials}
286 dateformat=${dateFormat}
287 '';
288 in lib.attrsets.nameValuePair "taskwarrior-web-${name}" {
289 description = "Taskwarrior webapp for ${name}";
290 wantedBy = [ "multi-user.target" ];
291 after = [ "network.target" ];
292 path = [ pkgs.taskwarrior ];
293
294 environment.TASKRC = taskrc;
295 environment.BUNDLE_PATH = "${taskwarrior-web.gems}/${taskwarrior-web.gems.ruby.gemPath}";
296 environment.BUNDLE_GEMFILE = "${taskwarrior-web.gems.confFiles}/Gemfile";
297 environment.LC_ALL = "fr_FR.UTF-8";
298
299 script = ''
300 exec ${taskwarrior-web.gems}/${taskwarrior-web.gems.ruby.gemPath}/bin/bundle exec thin start -R config.ru -S ${socketsDir}/${name}.sock
301 '';
302
303 serviceConfig = {
304 User = user;
305 PrivateTmp = true;
306 Restart = "always";
307 TimeoutSec = 60;
308 Type = "simple";
309 WorkingDirectory = taskwarrior-web;
310 StateDirectoryMode = 0750;
311 StateDirectory = assert lib.strings.hasPrefix "/var/lib/" varDir;
312 (lib.strings.removePrefix "/var/lib/" varDir + "/${name}");
313 RuntimeDirectoryPreserve = "yes";
314 RuntimeDirectory = assert lib.strings.hasPrefix "/run/" socketsDir;
315 lib.strings.removePrefix "/run/" socketsDir;
316 };
317
318 unitConfig.RequiresMountsFor = varDir;
319 }) env.taskwarrior-web) // {
320 taskserver-ca.postStart = ''
321 chown :${group} "${server_vardir}/keys/ca.key"
322 chmod g+r "${server_vardir}/keys/ca.key"
323 '';
324 };
325
326 };
327}
diff --git a/modules/private/tasks/www/index.php b/modules/private/tasks/www/index.php
new file mode 100644
index 0000000..deaf8af
--- /dev/null
+++ b/modules/private/tasks/www/index.php
@@ -0,0 +1,157 @@
1<?php
2if (!isset($_SERVER["REMOTE_USER"])) {
3 die("please login");
4}
5$ldap_user = $_SERVER["REMOTE_USER"];
6$ldap_host = getenv("TASKD_LDAP_HOST");
7$ldap_dn = getenv('TASKD_LDAP_DN');
8$ldap_password = getenv('TASKD_LDAP_PASSWORD');
9$ldap_base = getenv('TASKD_LDAP_BASE');
10$ldap_filter = getenv('TASKD_LDAP_FILTER');
11$host = getenv('TASKD_HOST');
12$vardir = getenv('TASKD_VARDIR');
13
14$connect = ldap_connect($ldap_host);
15ldap_set_option($connect, LDAP_OPT_PROTOCOL_VERSION, 3);
16if (!$connect || !ldap_bind($connect, $ldap_dn, $ldap_password)) {
17 die("impossible to connect to LDAP");
18}
19
20$search_query = str_replace('%login%', ldap_escape($ldap_user), $ldap_filter);
21
22$search = ldap_search($connect, $ldap_base, $search_query);
23$info = ldap_get_entries($connect, $search);
24
25if (ldap_count_entries($connect, $search) != 1) {
26 die("Impossible to find user in LDAP");
27}
28
29$entries = [];
30foreach($info[0]["immaetaskid"] as $key => $value) {
31 if ($key !== "count") {
32 $entries[] = explode(":", $value);
33 }
34}
35
36if (isset($_GET["file"])) {
37 $basecert = $vardir . "/userkeys/" . $ldap_user;
38 if (!file_exists($basecert . ".cert.pem")) {
39 exec("taskserver-user-certs $ldap_user");
40 }
41 $certificate = file_get_contents($basecert . ".cert.pem");
42 $cert_key = file_get_contents($basecert . ".key.pem");
43
44 // IdenTrust DST Root CA X3
45 // obtained here: https://letsencrypt.org/fr/certificates/
46 $server_cert = "-----BEGIN CERTIFICATE-----
47MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/
48MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
49DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow
50PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD
51Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
52AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O
53rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq
54OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b
55xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw
567BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD
57aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV
58HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG
59SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69
60ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr
61AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz
62R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5
63JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo
64Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ
65-----END CERTIFICATE-----";
66
67 $file = $_GET["file"];
68 switch($file) {
69 case "ca.cert.pem":
70 $content = $server_cert;
71 $name = "ca.cert.pem";
72 $type = "application/x-x509-ca-cert";
73 break;
74 case "cert.pem":
75 $content = $certificate;
76 $name = $ldap_user . ".cert.pem";
77 $type = "application/x-x509-ca-cert";
78 break;
79 case "key.pem":
80 $content = $cert_key;
81 $name = $ldap_user . ".key.pem";
82 $type = "application/x-x509-ca-cert";
83 break;
84 case "mirakel";
85 foreach ($entries as $entry) {
86 list($org, $user, $key) = $entry;
87 if ($key == $_GET["key"]) { break; }
88 }
89 $name = $user . ".mirakel";
90 $type = "text/plain";
91 $content = "username: $user
92org: $org
93user key: $key
94server: $host
95client.cert:
96$certificate
97Client.key:
98$cert_key
99ca.cert:
100$server_cert
101";
102 break;
103 default:
104 die("invalid file name");
105 break;
106 }
107
108 header("Content-Type: $type");
109 header('Content-Disposition: attachment; filename="' . $name . '"');
110 header('Content-Transfer-Encoding: binary');
111 header('Accept-Ranges: bytes');
112 header('Cache-Control: private');
113 header('Pragma: private');
114 echo $content;
115 exit;
116}
117?>
118<html>
119<header>
120 <title>Taskwarrior configuration</title>
121</header>
122<body>
123<ul>
124 <li><a href="?file=ca.cert.pem">ca.cert.pem</a></li>
125 <li><a href="?file=cert.pem"><?php echo $ldap_user; ?>.cert.pem</a></li>
126 <li><a href="?file=key.pem"><?php echo $ldap_user; ?>.key.pem</a></li>
127</ul>
128For command line interface, download the files, put them near your Taskwarrior
129configuration files, and add that to your Taskwarrior configuration:
130<pre>
131taskd.certificate=/path/to/<?php echo $ldap_user; ?>.cert.pem
132taskd.key=/path/to/<?php echo $ldap_user; ?>.key.pem
133taskd.server=<?php echo $host ."\n"; ?>
134<?php if (count($entries) > 1) {
135 echo "# Chose one of them\n";
136 foreach($entries as $entry) {
137 list($org, $user, $key) = $entry;
138 echo "# taskd.credentials=$org/$user/$key\n";
139 }
140} else { ?>
141taskd.credentials=<?php echo $entries[0][0]; ?>/<?php echo $entries[0][1]; ?>/<?php echo $entries[0][2]; ?>
142<?php } ?>
143taskd.ca=/path/to/ca.cert.pem
144</pre>
145For Mirakel, download and import the file:
146<ul>
147<?php
148foreach ($entries as $entry) {
149 list($org, $user, $key) = $entry;
150 echo '<li><a href="?file=mirakel&key='.$key.'">' . $user . '.mirakel</a></li>';
151}
152?>
153</ul>
154For Android Taskwarrior app, see instructions <a href="https://bitbucket.org/kvorobyev/taskwarriorandroid/wiki/Configuration">here</a>.
155</body>
156</html>
157
diff --git a/modules/private/websites/tools/git/default.nix b/modules/private/websites/tools/git/default.nix
index 3e8b605..75d0240 100644
--- a/modules/private/websites/tools/git/default.nix
+++ b/modules/private/websites/tools/git/default.nix
@@ -4,7 +4,9 @@ let
4 inherit (pkgs.webapps) mantisbt_2 mantisbt_2-plugins; 4 inherit (pkgs.webapps) mantisbt_2 mantisbt_2-plugins;
5 env = myconfig.env.tools.mantisbt; 5 env = myconfig.env.tools.mantisbt;
6 }; 6 };
7 gitweb = pkgs.callPackage ./gitweb.nix { gitoliteDir = config.services.myGitolite.gitoliteDir; }; 7 gitweb = pkgs.callPackage ./gitweb.nix {
8 gitoliteDir = config.myServices.gitolite.gitoliteDir;
9 };
8 10
9 cfg = config.myServices.websites.tools.git; 11 cfg = config.myServices.websites.tools.git;
10in { 12in {