From 8d213e2b1c934f6861f76aad5eb7c11097fa97de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isma=C3=ABl=20Bouya?= Date: Wed, 22 May 2019 20:55:28 +0200 Subject: Move rest of the modules outside of nixops --- modules/private/buildbot/common/build_helpers.py | 256 ++++++++++++++++ modules/private/buildbot/common/master.cfg | 69 +++++ modules/private/buildbot/default.nix | 198 +++++++++++++ .../private/buildbot/projects/caldance/__init__.py | 190 ++++++++++++ .../buildbot/projects/cryptoportfolio/__init__.py | 169 +++++++++++ modules/private/buildbot/projects/test/__init__.py | 188 ++++++++++++ modules/private/certificates.nix | 52 ++++ modules/private/default.nix | 12 + modules/private/dns.nix | 132 +++++++++ modules/private/ftp.nix | 118 ++++++++ modules/private/gitolite/default.nix | 63 ++++ modules/private/gitolite/gitolite_ldap_groups.sh | 15 + modules/private/mail.nix | 13 + modules/private/mpd.nix | 56 ++++ modules/private/pub/default.nix | 52 ++++ modules/private/pub/restrict | 64 ++++ modules/private/pub/tmux.restrict.conf | 43 +++ modules/private/ssh/default.nix | 40 +++ modules/private/ssh/ldap_authorized_keys.sh | 152 ++++++++++ modules/private/system.nix | 30 ++ modules/private/tasks/default.nix | 327 +++++++++++++++++++++ modules/private/tasks/www/index.php | 157 ++++++++++ modules/private/websites/tools/git/default.nix | 4 +- 23 files changed, 2399 insertions(+), 1 deletion(-) create mode 100644 modules/private/buildbot/common/build_helpers.py create mode 100644 modules/private/buildbot/common/master.cfg create mode 100644 modules/private/buildbot/default.nix create mode 100644 modules/private/buildbot/projects/caldance/__init__.py create mode 100644 modules/private/buildbot/projects/cryptoportfolio/__init__.py create mode 100644 modules/private/buildbot/projects/test/__init__.py create mode 100644 modules/private/certificates.nix create mode 100644 modules/private/dns.nix create mode 100644 modules/private/ftp.nix create mode 100644 modules/private/gitolite/default.nix create mode 100755 modules/private/gitolite/gitolite_ldap_groups.sh create mode 100644 modules/private/mail.nix create mode 100644 modules/private/mpd.nix create mode 100644 modules/private/pub/default.nix create mode 100644 modules/private/pub/restrict create mode 100644 modules/private/pub/tmux.restrict.conf create mode 100644 modules/private/ssh/default.nix create mode 100755 modules/private/ssh/ldap_authorized_keys.sh create mode 100644 modules/private/system.nix create mode 100644 modules/private/tasks/default.nix create mode 100644 modules/private/tasks/www/index.php (limited to 'modules') 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 @@ +from buildbot.plugins import util, steps, schedulers +from buildbot_buildslist import BuildsList + +__all__ = [ + "force_scheduler", "deploy_scheduler", "hook_scheduler", + "clean_branch", "package_and_upload", "SlackStatusPush", + "XMPPStatusPush" + ] + +# Small helpers" +@util.renderer +def clean_branch(props): + if props.hasProperty("branch") and len(props["branch"]) > 0: + return props["branch"].replace("/", "_") + else: + return "HEAD" + +def package_and_upload(package, package_dest, package_url): + return [ + steps.ShellCommand(name="build package", + logEnviron=False, haltOnFailure=True, workdir="source", + command=["git", "archive", "HEAD", "-o", package]), + + steps.FileUpload(name="upload package", workersrc=package, + workdir="source", masterdest=package_dest, + url=package_url, mode=0o644), + + steps.ShellCommand(name="cleanup package", logEnviron=False, + haltOnFailure=True, workdir="source", alwaysRun=True, + command=["rm", "-f", package]), + ] + +# Schedulers +def force_scheduler(name, builders): + return schedulers.ForceScheduler(name=name, + label="Force build", buttonName="Force build", + reason=util.StringParameter(name="reason", label="Reason", default="Force build"), + codebases=[ + util.CodebaseParameter("", + branch=util.StringParameter( + name="branch", label="Git reference (tag, branch)", required=True), + revision=util.FixedParameter(name="revision", default=""), + repository=util.FixedParameter(name="repository", default=""), + project=util.FixedParameter(name="project", default=""), + ), + ], + username=util.FixedParameter(name="username", default="Web button"), + builderNames=builders) + +def deploy_scheduler(name, builders): + return schedulers.ForceScheduler(name=name, + builderNames=builders, + label="Deploy built package", buttonName="Deploy", + username=util.FixedParameter(name="username", default="Web button"), + codebases=[ + util.CodebaseParameter(codebase="", + branch=util.FixedParameter(name="branch", default=""), + revision=util.FixedParameter(name="revision", default=""), + repository=util.FixedParameter(name="repository", default=""), + project=util.FixedParameter(name="project", default=""))], + reason=util.FixedParameter(name="reason", default="Deploy"), + properties=[ + util.ChoiceStringParameter(label="Environment", + name="environment", default="integration", + choices=["integration", "production"]), + BuildsList(label="Build to deploy", name="build"), + ] + ) + +def hook_scheduler(project, timer=10): + return schedulers.AnyBranchScheduler( + change_filter=util.ChangeFilter(category="hooks", project=project), + name=project, treeStableTimer=timer, builderNames=["{}_build".format(project)]) + +# Slack/XMPP status push +from buildbot.reporters.http import HttpStatusPushBase +from twisted.internet import defer +from twisted.python import log +from buildbot.util import httpclientservice +from buildbot.reporters import utils +from buildbot.process import results +from twisted.words.protocols.jabber.jid import JID +from wokkel import client, xmppim +from functools import partial + +class SlackStatusPush(HttpStatusPushBase): + name = "SlackStatusPush" + + @defer.inlineCallbacks + def reconfigService(self, serverUrl, **kwargs): + yield HttpStatusPushBase.reconfigService(self, **kwargs) + self._http = yield httpclientservice.HTTPClientService.getService( + self.master, serverUrl) + + @defer.inlineCallbacks + def send(self, build): + yield utils.getDetailsForBuild(self.master, build, wantProperties=True) + response = yield self._http.post("", json=self.format(build)) + if response.code != 200: + log.msg("%s: unable to upload status: %s" % + (response.code, response.content)) + + def format(self, build): + colors = [ + "#36A64F", # success + "#F1E903", # warnings + "#DA0505", # failure + "#FFFFFF", # skipped + "#000000", # exception + "#FFFFFF", # retry + "#D02CA9", # cancelled + ] + + if "environment" in build["properties"]: + msg = "{} environment".format(build["properties"]["environment"][0]) + if "build" in build["properties"]: + msg = "of archive {} in ".format(build["properties"]["build"][0]) + msg + elif len(build["buildset"]["sourcestamps"][0]["branch"]) > 0: + msg = "revision {}".format(build["buildset"]["sourcestamps"][0]["branch"]) + else: + msg = "build" + + if build["complete"]: + timedelta = int((build["complete_at"] - build["started_at"]).total_seconds()) + hours, rest = divmod(timedelta, 3600) + minutes, seconds = divmod(rest, 60) + if hours > 0: + duration = "{}h {}min {}s".format(hours, minutes, seconds) + elif minutes > 0: + duration = "{}min {}s".format(minutes, seconds) + else: + duration = "{}s".format(seconds) + + text = "Build <{}|{}> of {}'s {} was {} in {}.".format( + build["url"], build["buildid"], + build["builder"]["name"], + msg, + results.Results[build["results"]], + duration, + ) + fields = [ + { + "title": "Build", + "value": "<{}|{}>".format(build["url"], build["buildid"]), + "short": True, + }, + { + "title": "Project", + "value": build["builder"]["name"], + "short": True, + }, + { + "title": "Build status", + "value": results.Results[build["results"]], + "short": True, + }, + { + "title": "Build duration", + "value": duration, + "short": True, + }, + ] + if "environment" in build["properties"]: + fields.append({ + "title": "Environment", + "value": build["properties"]["environment"][0], + "short": True, + }) + if "build" in build["properties"]: + fields.append({ + "title": "Archive", + "value": build["properties"]["build"][0], + "short": True, + }) + attachments = [{ + "fallback": "", + "color": colors[build["results"]], + "fields": fields + }] + else: + text = "Build <{}|{}> of {}'s {} started.".format( + build["url"], build["buildid"], + build["builder"]["name"], + msg, + ) + attachments = [] + + return { + "username": "Buildbot", + "icon_url": "http://docs.buildbot.net/current/_static/icon.png", + "text": text, + "attachments": attachments, + } + +class XMPPStatusPush(HttpStatusPushBase): + name = "XMPPStatusPush" + + @defer.inlineCallbacks + def reconfigService(self, password, recipients, **kwargs): + yield HttpStatusPushBase.reconfigService(self, **kwargs) + self.password = password + self.recipients = recipients + + @defer.inlineCallbacks + def send(self, build): + yield utils.getDetailsForBuild(self.master, build, wantProperties=True) + body = self.format(build) + factory = client.DeferredClientFactory(JID("notify_bot@immae.fr/buildbot"), self.password) + d = client.clientCreator(factory) + def send_message(recipient, stream): + message = xmppim.Message(recipient=JID(recipient), body=body) + message.stanzaType = 'chat' + stream.send(message.toElement()) + # To allow chaining + return stream + for recipient in self.recipients: + d.addCallback(partial(send_message, recipient)) + d.addCallback(lambda _: factory.streamManager.xmlstream.sendFooter()) + d.addErrback(log.err) + + def format(self, build): + if "environment" in build["properties"]: + msg = "{} environment".format(build["properties"]["environment"][0]) + if "build" in build["properties"]: + msg = "of archive {} in ".format(build["properties"]["build"][0]) + msg + elif len(build["buildset"]["sourcestamps"][0]["branch"]) > 0: + msg = "revision {}".format(build["buildset"]["sourcestamps"][0]["branch"]) + else: + msg = "build" + + if build["complete"]: + timedelta = int((build["complete_at"] - build["started_at"]).total_seconds()) + hours, rest = divmod(timedelta, 3600) + minutes, seconds = divmod(rest, 60) + if hours > 0: + duration = "{}h {}min {}s".format(hours, minutes, seconds) + elif minutes > 0: + duration = "{}min {}s".format(minutes, seconds) + else: + duration = "{}s".format(seconds) + + text = "Build {} ( {} ) of {}'s {} was {} in {}.".format( + build["buildid"], build["url"], + build["builder"]["name"], + msg, + results.Results[build["results"]], + duration, + ) + else: + text = "Build {} ( {} ) of {}'s {} started.".format( + build["buildid"], build["url"], + build["builder"]["name"], + msg, + ) + + 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 @@ +# -*- python -*- +# ex: set filetype=python: + +from buildbot.plugins import secrets, util, webhooks +from buildbot.util import bytes2unicode +import re +import os +from buildbot_config import E, configure +import json + +class CustomBase(webhooks.base): + def getChanges(self, request): + try: + content = request.content.read() + args = json.loads(bytes2unicode(content)) + except Exception as e: + raise ValueError("Error loading JSON: " + str(e)) + + args.setdefault("comments", "") + args.setdefault("repository", "") + args.setdefault("author", args.get("who")) + + return ([args], None) + +userInfoProvider = util.LdapUserInfo( + uri=E.LDAP_URL, + bindUser=E.LDAP_ADMIN_USER, + bindPw=open(E.SECRETS_FILE + "/ldap", "r").read().rstrip(), + accountBase=E.LDAP_BASE, + accountPattern=E.LDAP_PATTERN, + accountFullName='cn', + accountEmail='mail', + avatarData="jpegPhoto", + groupBase=E.LDAP_BASE, + groupName="cn", + groupMemberPattern=E.LDAP_GROUP_PATTERN, + ) + +c = BuildmasterConfig = { + "title": E.TITLE, + "titleURL": E.TITLE_URL, + "db": { + "db_url": "sqlite:///state.sqlite" + }, + "protocols": { "pb": { "port": E.PB_SOCKET } }, + "workers": [], + "change_source": [], + "schedulers": [], + "builders": [], + "services": [], + "secretsProviders": [ + secrets.SecretInAFile(E.SECRETS_FILE), + ], + "www": { + "change_hook_dialects": { "base": { "custom_class": CustomBase } }, + "plugins": { + "waterfall_view": {}, + "console_view": {}, + "grid_view": {}, + "buildslist": {}, + }, + "auth": util.RemoteUserAuth( + header=b"X-Remote-User", + userInfoProvider=userInfoProvider, + headerRegex=re.compile(br"(?P[^ @]+)")), + } + } + +configure(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 @@ +{ lib, pkgs, config, myconfig, ... }: +let + varDir = "/var/lib/buildbot"; + buildbot_common = pkgs.python3Packages.buildPythonPackage rec { + name = "buildbot_common"; + src = ./common; + format = "other"; + installPhase = '' + mkdir -p $out/${pkgs.python3.pythonForBuild.sitePackages} + cp -a $src $out/${pkgs.python3.pythonForBuild.sitePackages}/buildbot_common + ''; + }; + buildbot = pkgs.python3Packages.buildbot-full; +in +{ + options = { + myServices.buildbot.enable = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Whether to enable buildbot. + ''; + }; + }; + + config = lib.mkIf config.myServices.buildbot.enable { + ids.uids.buildbot = myconfig.env.buildbot.user.uid; + ids.gids.buildbot = myconfig.env.buildbot.user.gid; + + users.groups.buildbot.gid = config.ids.gids.buildbot; + users.users.buildbot = { + name = "buildbot"; + uid = config.ids.uids.buildbot; + group = "buildbot"; + description = "Buildbot user"; + home = varDir; + extraGroups = [ "keys" ]; + }; + + services.websites.tools.vhostConfs.git.extraConfig = lib.attrsets.mapAttrsToList (k: project: '' + RedirectMatch permanent "^/buildbot/${project.name}$" "/buildbot/${project.name}/" + RewriteEngine On + RewriteRule ^/buildbot/${project.name}/ws(.*)$ unix:///run/buildbot/${project.name}.sock|ws://git.immae.eu/ws$1 [P,NE,QSA,L] + ProxyPass /buildbot/${project.name}/ unix:///run/buildbot/${project.name}.sock|http://${project.name}-git.immae.eu/ + ProxyPassReverse /buildbot/${project.name}/ unix:///run/buildbot/${project.name}.sock|http://${project.name}-git.immae.eu/ + + Use LDAPConnect + Require ldap-group cn=users,ou=${project.name},cn=buildbot,ou=services,dc=immae,dc=eu + + SetEnvIf X-Url-Scheme https HTTPS=1 + ProxyPreserveHost On + + + + Require local + Require ldap-group cn=users,ou=${project.name},cn=buildbot,ou=services,dc=immae,dc=eu + Include /var/secrets/buildbot/${project.name}/webhook-httpd-include + + + '') myconfig.env.buildbot.projects; + + system.activationScripts = lib.attrsets.mapAttrs' (k: project: lib.attrsets.nameValuePair "buildbot-${project.name}" { + deps = [ "users" "wrappers" ]; + text = project.activationScript; + }) myconfig.env.buildbot.projects; + + secrets.keys = ( + lib.lists.flatten ( + lib.attrsets.mapAttrsToList (k: project: + lib.attrsets.mapAttrsToList (k: v: + { + permissions = "0600"; + user = "buildbot"; + group = "buildbot"; + text = v; + dest = "buildbot/${project.name}/${k}"; + } + ) project.secrets + ++ [ + { + permissions = "0600"; + user = "wwwrun"; + group = "wwwrun"; + text = lib.optionalString (lib.attrsets.hasAttr "webhookTokens" project) '' + Require expr "req('Access-Key') in { ${builtins.concatStringsSep ", " (map (x: "'${x}'") project.webhookTokens)} }" + ''; + dest = "buildbot/${project.name}/webhook-httpd-include"; + } + ] + ) myconfig.env.buildbot.projects + ) + ) ++ [ + { + permissions = "0600"; + user = "buildbot"; + group = "buildbot"; + text = myconfig.env.buildbot.ldap.password; + dest = "buildbot/ldap"; + } + { + permissions = "0600"; + user = "buildbot"; + group = "buildbot"; + text = builtins.readFile "${myconfig.privateFiles}/buildbot_ssh_key"; + dest = "buildbot/ssh_key"; + } + ]; + + systemd.services = lib.attrsets.mapAttrs' (k: project: lib.attrsets.nameValuePair "buildbot-${project.name}" { + description = "Buildbot Continuous Integration Server ${project.name}."; + after = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + path = project.packages pkgs ++ (project.pythonPackages buildbot.pythonModule pkgs); + preStart = let + master-cfg = "${buildbot_common}/${pkgs.python3.pythonForBuild.sitePackages}/buildbot_common/master.cfg"; + tac_file = pkgs.writeText "buildbot.tac" '' + import os + + from twisted.application import service + from buildbot.master import BuildMaster + + basedir = '${varDir}/${project.name}' + rotateLength = 10000000 + maxRotatedFiles = 10 + configfile = '${master-cfg}' + + # Default umask for server + umask = None + + # if this is a relocatable tac file, get the directory containing the TAC + if basedir == '.': + import os + basedir = os.path.abspath(os.path.dirname(__file__)) + + # note: this line is matched against to check that this is a buildmaster + # directory; do not edit it. + application = service.Application('buildmaster') + from twisted.python.logfile import LogFile + from twisted.python.log import ILogObserver, FileLogObserver + logfile = LogFile.fromFullPath(os.path.join(basedir, "twistd.log"), rotateLength=rotateLength, + maxRotatedFiles=maxRotatedFiles) + application.setComponent(ILogObserver, FileLogObserver(logfile).emit) + + m = BuildMaster(basedir, configfile, umask) + m.setServiceParent(application) + m.log_rotation.rotateLength = rotateLength + m.log_rotation.maxRotatedFiles = maxRotatedFiles + ''; + in '' + if [ ! -f ${varDir}/${project.name}/buildbot.tac ]; then + ${buildbot}/bin/buildbot create-master -c "${master-cfg}" "${varDir}/${project.name}" + rm -f ${varDir}/${project.name}/master.cfg.sample + rm -f ${varDir}/${project.name}/buildbot.tac + fi + ln -sf ${tac_file} ${varDir}/${project.name}/buildbot.tac + # different buildbots may be trying that simultaneously, add the || true to avoid complaining in case of race + install -Dm600 -o buildbot -g buildbot -T /var/secrets/buildbot/ssh_key ${varDir}/buildbot_key || true + buildbot_secrets=${varDir}/${project.name}/secrets + install -m 0700 -o buildbot -g buildbot -d $buildbot_secrets + install -Dm600 -o buildbot -g buildbot -T /var/secrets/buildbot/ldap $buildbot_secrets/ldap + ${builtins.concatStringsSep "\n" (lib.attrsets.mapAttrsToList + (k: v: "install -Dm600 -o buildbot -g buildbot -T /var/secrets/buildbot/${project.name}/${k} $buildbot_secrets/${k}") project.secrets + )} + ''; + environment = let + project_env = lib.attrsets.mapAttrs' (k: v: lib.attrsets.nameValuePair "BUILDBOT_${k}" v) project.environment; + buildbot_config = pkgs.python3Packages.buildPythonPackage (rec { + name = "buildbot_config-${project.name}"; + src = ./projects + "/${project.name}"; + format = "other"; + installPhase = '' + mkdir -p $out/${pkgs.python3.pythonForBuild.sitePackages} + cp -a $src $out/${pkgs.python3.pythonForBuild.sitePackages}/buildbot_config + ''; + }); + HOME = "${varDir}/${project.name}"; + PYTHONPATH = "${buildbot.pythonModule.withPackages (self: project.pythonPackages self pkgs ++ [ + pkgs.python3Packages.wokkel + pkgs.python3Packages.treq pkgs.python3Packages.ldap3 buildbot + pkgs.python3Packages.buildbot-worker + buildbot_common buildbot_config + ])}/${buildbot.pythonModule.sitePackages}${if project.pythonPathHome then ":${varDir}/${project.name}/.local/${pkgs.python3.pythonForBuild.sitePackages}" else ""}"; + in project_env // { inherit PYTHONPATH HOME; }; + + serviceConfig = { + Type = "forking"; + User = "buildbot"; + Group = "buildbot"; + RuntimeDirectory = "buildbot"; + RuntimeDirectoryPreserve = "yes"; + StateDirectory = "buildbot"; + SupplementaryGroups = "keys"; + WorkingDirectory = "${varDir}/${project.name}"; + ExecStart = "${buildbot}/bin/buildbot start"; + }; + }) myconfig.env.buildbot.projects; + }; +} 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 @@ +from buildbot.plugins import * +from buildbot_common.build_helpers import * +import os +from buildbot.util import bytes2unicode +import json + +__all__ = [ "configure", "E" ] + +class E(): + PROJECT = "caldance" + BUILDBOT_URL = "https://git.immae.eu/buildbot/{}/".format(PROJECT) + SOCKET = "unix:/run/buildbot/{}.sock".format(PROJECT) + PB_SOCKET = "unix:address=/run/buildbot/{}_pb.sock".format(PROJECT) + RELEASE_PATH = "/var/lib/ftp/release.immae.eu/{}".format(PROJECT) + RELEASE_URL = "https://release.immae.eu/{}".format(PROJECT) + GIT_URL = "gitolite@git.immae.eu:perso/simon_descarpentries/www.cal-dance.com" + SSH_KEY_PATH = "/var/lib/buildbot/buildbot_key" + SSH_HOST_KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIFbhFTl2A2RJn5L51yxJM4XfCS2ZaiSX/jo9jFSdghF" + LDAP_HOST = "ldap.immae.eu" + LDAP_DN = "cn=buildbot,ou=services,dc=immae,dc=eu" + LDAP_ROLES_BASE = "ou=roles,ou=hosts,dc=immae,dc=eu" + XMPP_RECIPIENTS = os.environ["BUILDBOT_XMPP_RECIPIENTS"].split(" ") + + PUPPET_HOST = { + "integration": "root@caldance.immae.eu", + } + + # master.cfg + SECRETS_FILE = os.getcwd() + "/secrets" + LDAP_URL = "ldaps://ldap.immae.eu:636" + LDAP_ADMIN_USER = "cn=buildbot,ou=services,dc=immae,dc=eu" + LDAP_BASE = "dc=immae,dc=eu" + LDAP_PATTERN = "(uid=%(username)s)" + LDAP_GROUP_PATTERN = "(&(memberOf=cn=groups,ou=caldance,cn=buildbot,ou=services,dc=immae,dc=eu)(member=%(dn)s))" + TITLE_URL = "https://caldance.immae.eu" + TITLE = "Caldance" + +class CustomBase(webhooks.base): + def getChanges(self, request): + try: + content = request.content.read() + args = json.loads(bytes2unicode(content)) + except Exception as e: + raise ValueError("Error loading JSON: " + str(e)) + + args.setdefault("comments", "") + args.setdefault("repository", "") + args.setdefault("author", args.get("who", "unknown")) + + if args["category"] == "deploy_webhook": + args = { + "category": "deploy_webhook", + "comments": "", + "repository": "", + "author": "webhook", + "project": "Caldance", + "properties": { + "environment": args.get("environment", "integration"), + "build": "caldance_{}.tar.gz".format(args.get("build", "master")) + } + } + + return ([args], None) + +def deploy_hook_scheduler(project, timer=1): + return schedulers.AnyBranchScheduler( + change_filter=util.ChangeFilter(category="deploy_webhook", project=project), + name="{}_deploy".format(project), treeStableTimer=timer, builderNames=["{}_deploy".format(project)]) + +def configure(c): + c["buildbotURL"] = E.BUILDBOT_URL + c["www"]["port"] = E.SOCKET + + c["www"]["change_hook_dialects"]["base"] = { + "custom_class": CustomBase + } + c['workers'].append(worker.LocalWorker("generic-worker")) + c['workers'].append(worker.LocalWorker("deploy-worker")) + + c['schedulers'].append(hook_scheduler("Caldance", timer=1)) + c['schedulers'].append(force_scheduler("force_caldance", ["Caldance_build"])) + c['schedulers'].append(deploy_scheduler("deploy_caldance", ["Caldance_deploy"])) + c['schedulers'].append(deploy_hook_scheduler("Caldance", timer=1)) + + c['builders'].append(factory("caldance")) + + c['builders'].append(deploy_factory("caldance")) + + c['services'].append(SlackStatusPush( + name="slack_status_caldance", + builders=["Caldance_build", "Caldance_deploy"], + serverUrl=open(E.SECRETS_FILE + "/slack_webhook", "r").read().rstrip())) + c['services'].append(XMPPStatusPush( + name="xmpp_status_caldance", + builders=["Caldance_build", "Caldance_deploy"], + recipients=E.XMPP_RECIPIENTS, + password=open(E.SECRETS_FILE + "/notify_xmpp_password", "r").read().rstrip())) + +def factory(project, ignore_fails=False): + release_file = "{1}/{0}_%(kw:clean_branch)s.tar.gz" + + package = util.Interpolate("{0}_%(kw:clean_branch)s.tar.gz".format(project), clean_branch=clean_branch) + package_dest = util.Interpolate(release_file.format(project, E.RELEASE_PATH), clean_branch=clean_branch) + package_url = util.Interpolate(release_file.format(project, E.RELEASE_URL), clean_branch=clean_branch) + + factory = util.BuildFactory() + factory.addStep(steps.Git(logEnviron=False, repourl=E.GIT_URL, + sshPrivateKey=open(E.SSH_KEY_PATH).read().rstrip(), + sshHostKey=E.SSH_HOST_KEY, mode="full", method="copy")) + factory.addSteps(package_and_upload(package, package_dest, package_url)) + + return util.BuilderConfig( + name="{}_build".format(project.capitalize()), + workernames=["generic-worker"], factory=factory) + +def compute_build_infos(project): + @util.renderer + def compute(props): + import re, hashlib + build_file = props.getProperty("build") + package_dest = "{1}/{0}".format(build_file, E.RELEASE_PATH) + version = re.match(r"{0}_(.*).tar.gz".format(project), build_file).group(1) + with open(package_dest, "rb") as f: + sha = hashlib.sha256(f.read()).hexdigest() + return { + "build_version": version, + "build_hash": sha, + } + return compute + +@util.renderer +def puppet_host(props): + environment = props["environment"] if props.hasProperty("environment") else "integration" + return E.PUPPET_HOST.get(environment, "host.invalid") + +def deploy_factory(project): + package_dest = util.Interpolate("{0}/%(prop:build)s".format(E.RELEASE_PATH)) + + factory = util.BuildFactory() + factory.addStep(steps.MasterShellCommand(command=["test", "-f", package_dest])) + factory.addStep(steps.SetProperties(properties=compute_build_infos(project))) + factory.addStep(LdapPush(environment=util.Property("environment"), + project=project, build_version=util.Property("build_version"), + build_hash=util.Property("build_hash"), ldap_password=util.Secret("ldap"))) + factory.addStep(steps.MasterShellCommand(command=[ + "ssh", "-o", "UserKnownHostsFile=/dev/null", "-o", "StrictHostKeyChecking=no", "-o", "CheckHostIP=no", "-i", E.SSH_KEY_PATH, puppet_host])) + return util.BuilderConfig(name="{}_deploy".format(project.capitalize()), workernames=["deploy-worker"], factory=factory) + +from twisted.internet import defer +from buildbot.process.buildstep import FAILURE +from buildbot.process.buildstep import SUCCESS +from buildbot.process.buildstep import BuildStep + +class LdapPush(BuildStep): + name = "LdapPush" + renderables = ["environment", "project", "build_version", "build_hash", "ldap_password"] + + def __init__(self, **kwargs): + self.environment = kwargs.pop("environment") + self.project = kwargs.pop("project") + self.build_version = kwargs.pop("build_version") + self.build_hash = kwargs.pop("build_hash") + self.ldap_password = kwargs.pop("ldap_password") + self.ldap_host = kwargs.pop("ldap_host", E.LDAP_HOST) + super().__init__(**kwargs) + + def run(self): + import json + from ldap3 import Reader, Writer, Server, Connection, ObjectDef + server = Server(self.ldap_host) + conn = Connection(server, + user=E.LDAP_DN, + password=self.ldap_password) + conn.bind() + obj = ObjectDef("immaePuppetClass", conn) + r = Reader(conn, obj, + "cn=caldance.{},{}".format(self.environment, E.LDAP_ROLES_BASE)) + r.search() + if len(r) > 0: + w = Writer.from_cursor(r) + for value in w[0].immaePuppetJson.values: + config = json.loads(value) + if "role::caldance::{}_version".format(self.project) in config: + config["role::caldance::{}_version".format(self.project)] = self.build_version + config["role::caldance::{}_sha256".format(self.project)] = self.build_hash + w[0].immaePuppetJson -= value + w[0].immaePuppetJson += json.dumps(config, indent=" ") + w.commit() + return defer.succeed(SUCCESS) + 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 @@ +from buildbot.plugins import * +from buildbot_common.build_helpers import * +import os + +__all__ = [ "configure", "E" ] + +class E(): + PROJECT = "cryptoportfolio" + BUILDBOT_URL = "https://git.immae.eu/buildbot/{}/".format(PROJECT) + SOCKET = "unix:/run/buildbot/{}.sock".format(PROJECT) + PB_SOCKET = "unix:address=/run/buildbot/{}_pb.sock".format(PROJECT) + RELEASE_PATH = "/var/lib/ftp/release.immae.eu/{}".format(PROJECT) + RELEASE_URL = "https://release.immae.eu/{}".format(PROJECT) + GIT_URL = "https://git.immae.eu/perso/Immae/Projets/Cryptomonnaies/Cryptoportfolio/{0}.git" + SSH_KEY_PATH = "/var/lib/buildbot/buildbot_key" + LDAP_HOST = "ldap.immae.eu" + LDAP_DN = "cn=buildbot,ou=services,dc=immae,dc=eu" + LDAP_ROLES_BASE = "ou=roles,ou=hosts,dc=immae,dc=eu" + + PUPPET_HOST = { + "production": "root@cryptoportfolio.immae.eu", + "integration": "root@cryptoportfolio-dev.immae.eu" + } + + # master.cfg + SECRETS_FILE = os.getcwd() + "/secrets" + LDAP_URL = "ldaps://ldap.immae.eu:636" + LDAP_ADMIN_USER = "cn=buildbot,ou=services,dc=immae,dc=eu" + LDAP_BASE = "dc=immae,dc=eu" + LDAP_PATTERN = "(uid=%(username)s)" + LDAP_GROUP_PATTERN = "(&(memberOf=cn=groups,ou=cryptoportfolio,cn=buildbot,ou=services,dc=immae,dc=eu)(member=%(dn)s))" + TITLE_URL = "https://git.immae.eu" + TITLE = "Cryptoportfolio" + +# eval .. dans .zshrc_local +# mkdir -p $BUILD/go +# export GOPATH=$BUILD/go +# go get -u github.com/golang/dep/cmd/dep +# export PATH=$PATH:$BUILD/go/bin +# go get git.immae.eu/Cryptoportfolio/Front.git +# cd $BUILD/go/src/git.immae.eu/Cryptoportfolio/Front.git +# git checkout dev +# dep ensure +def configure(c): + c["buildbotURL"] = E.BUILDBOT_URL + c["www"]["port"] = E.SOCKET + + c['workers'].append(worker.LocalWorker("generic-worker")) + c['workers'].append(worker.LocalWorker("deploy-worker")) + + c['schedulers'].append(hook_scheduler("Trader")) + c['schedulers'].append(hook_scheduler("Front")) + c['schedulers'].append(force_scheduler( + "force_cryptoportfolio", ["Trader_build", "Front_build"])) + c['schedulers'].append(deploy_scheduler("deploy_cryptoportfolio", + ["Trader_deploy", "Front_deploy"])) + + c['builders'].append(factory("trader")) + c['builders'].append(factory("front", ignore_fails=True)) + + c['builders'].append(deploy_factory("trader")) + c['builders'].append(deploy_factory("front")) + + c['services'].append(SlackStatusPush( + name="slack_status_cryptoportfolio", + builders=["Front_build", "Trader_build", "Front_deploy", "Trader_deploy"], + serverUrl=open(E.SECRETS_FILE + "/slack_webhook", "r").read().rstrip())) + +def factory(project, ignore_fails=False): + release_file = "{1}/{0}/{0}_%(kw:clean_branch)s.tar.gz" + + url = E.GIT_URL.format(project.capitalize()) + + package = util.Interpolate("{0}_%(kw:clean_branch)s.tar.gz".format(project), clean_branch=clean_branch) + package_dest = util.Interpolate(release_file.format(project, E.RELEASE_PATH), clean_branch=clean_branch) + package_url = util.Interpolate(release_file.format(project, E.RELEASE_URL), clean_branch=clean_branch) + + factory = util.BuildFactory() + factory.addStep(steps.Git(logEnviron=False, repourl=url, + mode="full", method="copy")) + factory.addStep(steps.ShellCommand(name="make install", + logEnviron=False, haltOnFailure=(not ignore_fails), + warnOnFailure=ignore_fails, flunkOnFailure=(not ignore_fails), + command=["make", "install"])) + factory.addStep(steps.ShellCommand(name="make test", + logEnviron=False, haltOnFailure=(not ignore_fails), + warnOnFailure=ignore_fails, flunkOnFailure=(not ignore_fails), + command=["make", "test"])) + factory.addSteps(package_and_upload(package, package_dest, package_url)) + + return util.BuilderConfig( + name="{}_build".format(project.capitalize()), + workernames=["generic-worker"], factory=factory) + +def compute_build_infos(project): + @util.renderer + def compute(props): + import re, hashlib + build_file = props.getProperty("build") + package_dest = "{2}/{0}/{1}".format(project, build_file, E.RELEASE_PATH) + version = re.match(r"{0}_(.*).tar.gz".format(project), build_file).group(1) + with open(package_dest, "rb") as f: + sha = hashlib.sha256(f.read()).hexdigest() + return { + "build_version": version, + "build_hash": sha, + } + return compute + +@util.renderer +def puppet_host(props): + environment = props["environment"] if props.hasProperty("environment") else "integration" + return E.PUPPET_HOST.get(environment, "host.invalid") + +def deploy_factory(project): + package_dest = util.Interpolate("{1}/{0}/%(prop:build)s".format(project, E.RELEASE_PATH)) + + factory = util.BuildFactory() + factory.addStep(steps.MasterShellCommand(command=["test", "-f", package_dest])) + factory.addStep(steps.SetProperties(properties=compute_build_infos(project))) + factory.addStep(LdapPush(environment=util.Property("environment"), + project=project, build_version=util.Property("build_version"), + build_hash=util.Property("build_hash"), ldap_password=util.Secret("ldap"))) + factory.addStep(steps.MasterShellCommand(command=[ + "ssh", "-o", "UserKnownHostsFile=/dev/null", "-o", "StrictHostKeyChecking=no", "-o", "CheckHostIP=no", "-i", E.SSH_KEY_PATH, puppet_host])) + return util.BuilderConfig(name="{}_deploy".format(project.capitalize()), workernames=["deploy-worker"], factory=factory) + +from twisted.internet import defer +from buildbot.process.buildstep import FAILURE +from buildbot.process.buildstep import SUCCESS +from buildbot.process.buildstep import BuildStep + +class LdapPush(BuildStep): + name = "LdapPush" + renderables = ["environment", "project", "build_version", "build_hash", "ldap_password"] + + def __init__(self, **kwargs): + self.environment = kwargs.pop("environment") + self.project = kwargs.pop("project") + self.build_version = kwargs.pop("build_version") + self.build_hash = kwargs.pop("build_hash") + self.ldap_password = kwargs.pop("ldap_password") + self.ldap_host = kwargs.pop("ldap_host", E.LDAP_HOST) + super().__init__(**kwargs) + + def run(self): + import json + from ldap3 import Reader, Writer, Server, Connection, ObjectDef + server = Server(self.ldap_host) + conn = Connection(server, + user=E.LDAP_DN, + password=self.ldap_password) + conn.bind() + obj = ObjectDef("immaePuppetClass", conn) + r = Reader(conn, obj, + "cn=cryptoportfolio.{},{}".format(self.environment, E.LDAP_ROLES_BASE)) + r.search() + if len(r) > 0: + w = Writer.from_cursor(r) + for value in w[0].immaePuppetJson.values: + config = json.loads(value) + if "role::cryptoportfolio::{}_version".format(self.project) in config: + config["role::cryptoportfolio::{}_version".format(self.project)] = self.build_version + config["role::cryptoportfolio::{}_sha256".format(self.project)] = self.build_hash + w[0].immaePuppetJson -= value + w[0].immaePuppetJson += json.dumps(config, indent=" ") + w.commit() + return defer.succeed(SUCCESS) + 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 @@ +from buildbot.plugins import * +from buildbot_common.build_helpers import * +import os +from buildbot.util import bytes2unicode +import json + +__all__ = [ "configure", "E" ] + +class E(): + PROJECT = "test" + BUILDBOT_URL = "https://git.immae.eu/buildbot/{}/".format(PROJECT) + SOCKET = "unix:/run/buildbot/{}.sock".format(PROJECT) + PB_SOCKET = "unix:address=/run/buildbot/{}_pb.sock".format(PROJECT) + RELEASE_PATH = "/var/lib/ftp/release.immae.eu/{}".format(PROJECT) + RELEASE_URL = "https://release.immae.eu/{}".format(PROJECT) + GIT_URL = "https://git.immae.eu/perso/Immae/TestProject.git" + SSH_KEY_PATH = "/var/lib/buildbot/buildbot_key" + PUPPET_HOST = "root@backup-1.v.immae.eu" + LDAP_HOST = "ldap.immae.eu" + LDAP_DN = "cn=buildbot,ou=services,dc=immae,dc=eu" + LDAP_ROLES_BASE = "ou=roles,ou=hosts,dc=immae,dc=eu" + XMPP_RECIPIENTS = os.environ["BUILDBOT_XMPP_RECIPIENTS"].split(" ") + + # master.cfg + SECRETS_FILE = os.getcwd() + "/secrets" + LDAP_URL = "ldaps://ldap.immae.eu:636" + LDAP_ADMIN_USER = "cn=buildbot,ou=services,dc=immae,dc=eu" + LDAP_BASE = "dc=immae,dc=eu" + LDAP_PATTERN = "(uid=%(username)s)" + LDAP_GROUP_PATTERN = "(&(memberOf=cn=groups,ou=test,cn=buildbot,ou=services,dc=immae,dc=eu)(member=%(dn)s))" + TITLE_URL = "https://git.immae.eu/?p=perso/Immae/TestProject.git;a=summary" + TITLE = "Test project" + +class CustomBase(webhooks.base): + def getChanges(self, request): + try: + content = request.content.read() + args = json.loads(bytes2unicode(content)) + except Exception as e: + raise ValueError("Error loading JSON: " + str(e)) + + args.setdefault("comments", "") + args.setdefault("repository", "") + args.setdefault("author", args.get("who", "unknown")) + + if args["category"] == "deploy_webhook": + args = { + "category": "deploy_webhook", + "comments": "", + "repository": "", + "author": "unknown", + "project": "TestProject", + "properties": { + "environment": args.get("environment", "integration"), + "build": "test_{}.tar.gz".format(args.get("branch", "master")) + } + } + + return ([args], None) + +def deploy_hook_scheduler(project, timer=1): + return schedulers.AnyBranchScheduler( + change_filter=util.ChangeFilter(category="deploy_webhook", project=project), + name="{}_deploy".format(project), treeStableTimer=timer, builderNames=["{}_deploy".format(project)]) + +def configure(c): + c["buildbotURL"] = E.BUILDBOT_URL + c["www"]["port"] = E.SOCKET + + c["www"]["change_hook_dialects"]["base"] = { + "custom_class": CustomBase + } + c['workers'].append(worker.LocalWorker("generic-worker-test")) + c['workers'].append(worker.LocalWorker("deploy-worker-test")) + + c['schedulers'].append(hook_scheduler("TestProject", timer=1)) + c['schedulers'].append(force_scheduler("force_test", ["TestProject_build"])) + c['schedulers'].append(deploy_scheduler("deploy_test", ["TestProject_deploy"])) + c['schedulers'].append(deploy_hook_scheduler("TestProject", timer=1)) + + c['builders'].append(factory()) + c['builders'].append(deploy_factory()) + + c['services'].append(SlackStatusPush( + name="slack_status_test_project", + builders=["TestProject_build", "TestProject_deploy"], + serverUrl=open(E.SECRETS_FILE + "/slack_webhook", "r").read().rstrip())) + c['services'].append(XMPPStatusPush( + name="xmpp_status_test_project", + builders=["TestProject_build", "TestProject_deploy"], + recipients=E.XMPP_RECIPIENTS, + password=open(E.SECRETS_FILE + "/notify_xmpp_password", "r").read().rstrip())) + +def factory(): + package = util.Interpolate("test_%(kw:clean_branch)s.tar.gz", clean_branch=clean_branch) + package_dest = util.Interpolate("{}/test_%(kw:clean_branch)s.tar.gz".format(E.RELEASE_PATH), clean_branch=clean_branch) + package_url = util.Interpolate("{}/test_%(kw:clean_branch)s.tar.gz".format(E.RELEASE_URL), clean_branch=clean_branch) + + factory = util.BuildFactory() + factory.addStep(steps.Git(logEnviron=False, + repourl=E.GIT_URL, mode="full", method="copy")) + factory.addStep(steps.ShellCommand(name="env", + logEnviron=False, command=["env"])) + factory.addStep(steps.ShellCommand(name="pwd", + logEnviron=False, command=["pwd"])) + factory.addStep(steps.ShellCommand(name="true", + logEnviron=False, command=["true"])) + factory.addStep(steps.ShellCommand(name="echo", + logEnviron=False, command=["echo", package])) + factory.addSteps(package_and_upload(package, package_dest, package_url)) + + return util.BuilderConfig(name="TestProject_build", workernames=["generic-worker-test"], factory=factory) + + +def compute_build_infos(): + @util.renderer + def compute(props): + import re, hashlib + build_file = props.getProperty("build") + package_dest = "{}/{}".format(E.RELEASE_PATH, build_file) + version = re.match(r"{0}_(.*).tar.gz".format("test"), build_file).group(1) + with open(package_dest, "rb") as f: + sha = hashlib.sha256(f.read()).hexdigest() + return { + "build_version": version, + "build_hash": sha, + } + return compute + +@util.renderer +def puppet_host(props): + return E.PUPPET_HOST + +def deploy_factory(): + package_dest = util.Interpolate("{}/%(prop:build)s".format(E.RELEASE_PATH)) + + factory = util.BuildFactory() + factory.addStep(steps.MasterShellCommand(command=["test", "-f", package_dest])) + factory.addStep(steps.SetProperties(properties=compute_build_infos())) + factory.addStep(LdapPush(environment=util.Property("environment"), + build_version=util.Property("build_version"), + build_hash=util.Property("build_hash"), + ldap_password=util.Secret("ldap"))) + factory.addStep(steps.MasterShellCommand(command=[ + "ssh", "-o", "UserKnownHostsFile=/dev/null", "-o", "StrictHostKeyChecking=no", "-o", "CheckHostIP=no", "-i", E.SSH_KEY_PATH, puppet_host])) + return util.BuilderConfig(name="TestProject_deploy", workernames=["deploy-worker-test"], factory=factory) + +from twisted.internet import defer +from buildbot.process.buildstep import FAILURE +from buildbot.process.buildstep import SUCCESS +from buildbot.process.buildstep import BuildStep + +class LdapPush(BuildStep): + name = "LdapPush" + renderables = ["environment", "build_version", "build_hash", "ldap_password"] + + def __init__(self, **kwargs): + self.environment = kwargs.pop("environment") + self.build_version = kwargs.pop("build_version") + self.build_hash = kwargs.pop("build_hash") + self.ldap_password = kwargs.pop("ldap_password") + self.ldap_host = kwargs.pop("ldap_host", E.LDAP_HOST) + super().__init__(**kwargs) + + def run(self): + import json + from ldap3 import Reader, Writer, Server, Connection, ObjectDef + server = Server(self.ldap_host) + conn = Connection(server, + user=E.LDAP_DN, + password=self.ldap_password) + conn.bind() + obj = ObjectDef("immaePuppetClass", conn) + r = Reader(conn, obj, + "cn=test.{},{}".format(self.environment, E.LDAP_ROLES_BASE)) + r.search() + if len(r) > 0: + w = Writer.from_cursor(r) + for value in w[0].immaePuppetJson.values: + config = json.loads(value) + if "test_version" in config: + config["test_version"] = self.build_version + config["test_sha256"] = self.build_hash + w[0].immaePuppetJson -= value + w[0].immaePuppetJson += json.dumps(config, indent=" ") + w.commit() + return defer.succeed(SUCCESS) + 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 @@ +{ lib, pkgs, config, ... }: +{ + options.services.myCertificates = { + certConfig = lib.mkOption { + default = { + webroot = "${config.security.acme.directory}/acme-challenge"; + email = "ismael@bouya.org"; + postRun = '' + systemctl reload httpdTools.service httpdInte.service httpdProd.service + ''; + plugins = [ "cert.pem" "chain.pem" "fullchain.pem" "full.pem" "key.pem" "account_key.json" ]; + }; + description = "Default configuration for certificates"; + }; + }; + + config = { + services.websitesCerts = config.services.myCertificates.certConfig; + myServices.databasesCerts = config.services.myCertificates.certConfig; + myServices.ircCerts = config.services.myCertificates.certConfig; + + security.acme.preliminarySelfsigned = true; + + security.acme.certs = { + "eldiron" = config.services.myCertificates.certConfig // { + domain = "eldiron.immae.eu"; + }; + }; + + systemd.services = lib.attrsets.mapAttrs' (k: v: + lib.attrsets.nameValuePair "acme-selfsigned-${k}" (lib.mkBefore { script = + (lib.optionalString (builtins.elem "cert.pem" v.plugins) '' + cp $workdir/server.crt ${config.security.acme.directory}/${k}/cert.pem + chown '${v.user}:${v.group}' ${config.security.acme.directory}/${k}/cert.pem + chmod ${if v.allowKeysForGroup then "750" else "700"} ${config.security.acme.directory}/${k}/cert.pem + '') + + (lib.optionalString (builtins.elem "chain.pem" v.plugins) '' + cp $workdir/ca.crt ${config.security.acme.directory}/${k}/chain.pem + chown '${v.user}:${v.group}' ${config.security.acme.directory}/${k}/chain.pem + chmod ${if v.allowKeysForGroup then "750" else "700"} ${config.security.acme.directory}/${k}/chain.pem + '') + ; }) + ) config.security.acme.certs // { + httpdProd.after = [ "acme-selfsigned-certificates.target" ]; + httpdProd.wants = [ "acme-selfsigned-certificates.target" ]; + httpdTools.after = [ "acme-selfsigned-certificates.target" ]; + httpdTools.wants = [ "acme-selfsigned-certificates.target" ]; + httpdInte.after = [ "acme-selfsigned-certificates.target" ]; + httpdInte.wants = [ "acme-selfsigned-certificates.target" ]; + }; + }; +} 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 = { peertubeTool = ./websites/tools/peertube; toolsTool = ./websites/tools/tools; + buildbot = ./buildbot; + certificates = ./certificates.nix; + gitolite = ./gitolite; irc = ./irc.nix; + pub = ./pub; + tasks = ./tasks; + dns = ./dns.nix; + ftp = ./ftp.nix; + mail = ./mail.nix; + mpd = ./mpd.nix; + ssh = ./ssh; + + system = ./system.nix; }; in builtins.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 @@ +{ lib, pkgs, config, myconfig, ... }: +{ + config = let + cfg = config.services.bind; + configFile = pkgs.writeText "named.conf" '' + include "/etc/bind/rndc.key"; + controls { + inet 127.0.0.1 allow {localhost;} keys {"rndc-key";}; + }; + + acl cachenetworks { ${lib.concatMapStrings (entry: " ${entry}; ") cfg.cacheNetworks} }; + acl badnetworks { ${lib.concatMapStrings (entry: " ${entry}; ") cfg.blockedNetworks} }; + + options { + listen-on { ${lib.concatMapStrings (entry: " ${entry}; ") cfg.listenOn} }; + listen-on-v6 { ${lib.concatMapStrings (entry: " ${entry}; ") cfg.listenOnIpv6} }; + allow-query { cachenetworks; }; + blackhole { badnetworks; }; + forward first; + forwarders { ${lib.concatMapStrings (entry: " ${entry}; ") cfg.forwarders} }; + directory "/var/run/named"; + pid-file "/var/run/named/named.pid"; + ${cfg.extraOptions} + }; + + ${cfg.extraConfig} + + ${ lib.concatMapStrings + ({ name, file, master ? true, extra ? "", slaves ? [], masters ? [] }: + '' + zone "${name}" { + type ${if master then "master" else "slave"}; + file "${file}"; + ${ if lib.lists.length slaves > 0 then + '' + allow-transfer { + ${lib.concatMapStrings (ip: "${ip};\n") slaves} + }; + '' else ""} + ${ if lib.lists.length masters > 0 then + '' + masters { + ${lib.concatMapStrings (ip: "${ip};\n") masters} + }; + '' else ""} + allow-query { any; }; + ${extra} + }; + '') + cfg.zones } + ''; + in + { + networking.firewall.allowedUDPPorts = [ 53 ]; + networking.firewall.allowedTCPPorts = [ 53 ]; + services.bind = { + enable = true; + cacheNetworks = ["any"]; + configFile = configFile; + extraOptions = '' + allow-recursion { 127.0.0.1; }; + allow-transfer { none; }; + + notify-source ${myconfig.env.servers.eldiron.ips.main.ip4}; + notify-source-v6 ${lib.head myconfig.env.servers.eldiron.ips.main.ip6}; + version none; + hostname none; + server-id none; + ''; + zones = with myconfig.env.dns; + assert (builtins.substring ((builtins.stringLength soa.email)-1) 1 soa.email) != "."; + assert (builtins.substring ((builtins.stringLength soa.primary)-1) 1 soa.primary) != "."; + (map (conf: { + name = conf.name; + master = false; + file = "/var/run/named/${conf.name}.zone"; + masters = if lib.attrsets.hasAttr "masters" conf + then lib.lists.flatten (map (n: lib.attrsets.attrValues ns.${n}) conf.masters) + else []; + }) slaveZones) + ++ (map (conf: { + name = conf.name; + master = true; + extra = if lib.attrsets.hasAttr "extra" conf then conf.extra else ""; + slaves = if lib.attrsets.hasAttr "slaves" conf + then lib.lists.flatten (map (n: lib.attrsets.attrValues ns.${n}) conf.slaves) + else []; + file = pkgs.writeText "${conf.name}.zone" '' + $TTL 10800 + @ IN SOA ${soa.primary}. ${builtins.replaceStrings ["@"] ["."] soa.email}. ${soa.serial} ${soa.refresh} ${soa.retry} ${soa.expire} ${soa.ttl} + + ${lib.concatStringsSep "\n" (map (x: "@ IN NS ${x}.") (lib.concatMap (n: lib.attrsets.mapAttrsToList (k: v: k) ns.${n}) conf.ns))} + + ${conf.entries} + + ${if lib.attrsets.hasAttr "withEmail" conf && lib.lists.length conf.withEmail > 0 then '' + mail IN A ${myconfig.env.servers.immaeEu.ips.main.ip4} + mx-1 IN A ${myconfig.env.servers.eldiron.ips.main.ip4} + ${builtins.concatStringsSep "\n" (map (i: "mail IN AAAA ${i}") myconfig.env.servers.immaeEu.ips.main.ip6)} + ${builtins.concatStringsSep "\n" (map (i: "mx-1 IN AAAA ${i}") myconfig.env.servers.eldiron.ips.main.ip6)} + ${lib.concatStringsSep "\n\n" (map (e: + let + n = if e.domain == "" then "@" else "${e.domain} "; + suffix = if e.domain == "" then "" else ".${e.domain}"; + in + '' + ; ------------------ mail: ${n} --------------------------- + ${if e.receive then "${n} IN MX 10 mail.${conf.name}." else ""} + ;${if e.receive then "${n} IN MX 50 mx-1.${conf.name}." else ""} + + ; Mail sender authentications + ${n} IN TXT "v=spf1 mx ~all" + _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;" + ${if e.send then '' + immae_eu._domainkey${suffix} IN TXT ( "v=DKIM1; k=rsa; s=email; " + "p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzl3vLd8W5YAuumC5+ZT9OV7/14Pmh5JYtwyqKI3cfe9NnAqInt3xO4bZ7oqIxRKWN4SD39vm7O/QOvFdBt00ENOOzdP90s5gKw6eIP/4+vPTh0IWltAsmu9B2agzdtWUE7t2xFKIzEn8l9niRE2QYbVaqZv4sub98vY55fIgFoHtjkmNC7325S8fjDJGp6OPbyhAs6Xl5/adjF" + "0ko4Y2p6RaxLQfjlS0bxmK4Qg6C14pIXHtzVeqOuWrwApqt5+AULSn97iUtqV/IJlEEjC6DUR44t3C/G0G/k46iFclCqRRi0hdPrOHCtZDbtMubnTN9eaUiNpkXh1WnCflHwtjQwIDAQAB" ) + '' else ""} + '') conf.withEmail)} + '' + (if conf.name == "immae.eu" then '' + ; ----------------- Accept DMARC reports ------------------- + ${lib.concatStringsSep "\n" ( + lib.flatten ( + 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 + ) + )} + '' else "") else ""} + ''; + }) masterZones); + }; + }; +} 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 @@ +{ lib, pkgs, config, myconfig, ... }: +{ + options = { + services.pure-ftpd.enable = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Whether to enable pure-ftpd. + ''; + }; + }; + + config = lib.mkIf config.services.pure-ftpd.enable { + security.acme.certs."ftp" = config.services.myCertificates.certConfig // { + domain = "eldiron.immae.eu"; + postRun = '' + systemctl restart pure-ftpd.service + ''; + extraDomains = { "ftp.immae.eu" = null; }; + }; + + networking = { + firewall = { + allowedTCPPorts = [ 21 ]; + allowedTCPPortRanges = [ { from = 40000; to = 50000; } ]; + }; + }; + + users.users = [ + { + name = "ftp"; + uid = config.ids.uids.ftp; # 8 + group = "ftp"; + description = "Anonymous FTP user"; + home = "/homeless-shelter"; + extraGroups = [ "keys" ]; + } + ]; + + users.groups.ftp.gid = config.ids.gids.ftp; + + system.activationScripts.pure-ftpd = '' + install -m 0755 -o ftp -g ftp -d /var/lib/ftp + ''; + + secrets.keys = [{ + dest = "pure-ftpd-ldap"; + permissions = "0400"; + user = "ftp"; + group = "ftp"; + text = '' + LDAPServer ${myconfig.env.ftp.ldap.host} + LDAPPort 389 + LDAPUseTLS True + LDAPBaseDN ${myconfig.env.ftp.ldap.base} + LDAPBindDN ${myconfig.env.ftp.ldap.dn} + LDAPBindPW ${myconfig.env.ftp.ldap.password} + LDAPDefaultUID 500 + LDAPForceDefaultUID False + LDAPDefaultGID 100 + LDAPForceDefaultGID False + LDAPFilter ${myconfig.env.ftp.ldap.filter} + + LDAPAuthMethod BIND + + # Pas de possibilite de donner l'Uid/Gid ! + # Compile dans pure-ftpd directement avec immaeFtpUid / immaeFtpGid + LDAPHomeDir immaeFtpDirectory + ''; + }]; + + systemd.services.pure-ftpd = let + configFile = pkgs.writeText "pure-ftpd.conf" '' + PassivePortRange 40000 50000 + ChrootEveryone yes + CreateHomeDir yes + BrokenClientsCompatibility yes + MaxClientsNumber 50 + Daemonize yes + MaxClientsPerIP 8 + VerboseLog no + DisplayDotFiles yes + AnonymousOnly no + NoAnonymous no + SyslogFacility ftp + DontResolve yes + MaxIdleTime 15 + LDAPConfigFile /var/secrets/pure-ftpd-ldap + LimitRecursion 10000 8 + AnonymousCanCreateDirs no + MaxLoad 4 + AntiWarez yes + Umask 133:022 + # ftp + MinUID 8 + AllowUserFXP no + AllowAnonymousFXP no + ProhibitDotFilesWrite no + ProhibitDotFilesRead no + AutoRename no + AnonymousCantUpload no + MaxDiskUsage 99 + CustomerProof yes + TLS 1 + CertFile ${config.security.acme.directory}/ftp/full.pem + ''; + in { + description = "Pure-FTPd server"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + + serviceConfig.ExecStart = "${pkgs.pure-ftpd}/bin/pure-ftpd ${configFile}"; + serviceConfig.Type = "forking"; + serviceConfig.PIDFile = "/run/pure-ftpd.pid"; + }; + }; + +} 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 @@ +{ lib, pkgs, config, myconfig, ... }: +let + cfg = config.myServices.gitolite; +in { + options.myServices.gitolite = { + enable = lib.mkEnableOption "my gitolite service"; + gitoliteDir = lib.mkOption { + type = lib.types.string; + default = "/var/lib/gitolite"; + }; + }; + + config = lib.mkIf cfg.enable { + networking.firewall.allowedTCPPorts = [ 9418 ]; + + services.gitDaemon = { + enable = true; + user = "gitolite"; + group = "gitolite"; + basePath = "${cfg.gitoliteDir}/repositories"; + }; + + system.activationScripts.gitolite = let + gitolite_ldap_groups = pkgs.mylibs.wrap { + name = "gitolite_ldap_groups.sh"; + file = ./gitolite_ldap_groups.sh; + vars = { + LDAP_PASS = myconfig.env.tools.gitolite.ldap.password; + }; + paths = [ pkgs.openldap pkgs.stdenv.shellPackage pkgs.gnugrep pkgs.coreutils ]; + }; + in { + deps = [ "users" ]; + text = '' + if [ -d ${cfg.gitoliteDir} ]; then + ln -sf ${gitolite_ldap_groups} ${cfg.gitoliteDir}/gitolite_ldap_groups.sh + chmod g+rx ${cfg.gitoliteDir} + fi + if [ -f ${cfg.gitoliteDir}/projects.list ]; then + chmod g+r ${cfg.gitoliteDir}/projects.list + fi + ''; + }; + + users.users.wwwrun.extraGroups = [ "gitolite" ]; + + users.users.gitolite.packages = let + python-packages = python-packages: with python-packages; [ + simplejson + urllib3 + sleekxmpp + ]; + in + [ + (pkgs.python3.withPackages python-packages) + ]; + # Installation: https://git.immae.eu/mantisbt/view.php?id=93 + services.gitolite = { + enable = true; + adminPubkey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDXqRbiHw7QoHADNIEuo4nUT9fSOIEBMdJZH0bkQAxXyJFyCM1IMz0pxsHV0wu9tdkkr36bPEUj2aV5bkYLBN6nxcV2Y49X8bjOSCPfx3n6Own1h+NeZVBj4ZByrFmqCbTxUJIZ2bZKcWOFncML39VmWdsVhNjg0X4NBBehqXRIKr2gt3E/ESAxTYJFm0BnU0baciw9cN0bsRGqvFgf5h2P48CIAfwhVcGmPQnnAwabnosYQzRWxR0OygH5Kd8mePh6FheIRIigfXsDO8f/jdxwut8buvNIf3m5EBr3tUbTsvM+eV3M5vKGt7sk8T64DVtepTSdOOWtp+47ktsnHOMh immae@immae.eu"; + }; + }; +} 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 @@ +#!/usr/bin/env bash + +uid_param="$1" +ldap_host="ldap.immae.eu" +ldap_binddn="cn=gitolite,ou=services,dc=immae,dc=eu" +ldap_bindpw="$LDAP_PASS" +ldap_searchbase="dc=immae,dc=eu" +ldap_scope="subtree" + +ldap_options="-h ${ldap_host} -ZZ -x -D ${ldap_binddn} -w ${ldap_bindpw} -b ${ldap_searchbase} -s ${ldap_scope}" + +ldap_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)))" +ldap_result=$(ldapsearch ${ldap_options} -LLL "${ldap_filter}" cn | grep 'cn:' | cut -d' ' -f2) + +echo "$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 @@ +{ lib, pkgs, config, myconfig, ... }: +{ + config.users.users.nullmailer.uid = config.ids.uids.nullmailer; + config.users.groups.nullmailer.gid = config.ids.gids.nullmailer; + + config.services.nullmailer = { + enable = true; + config = { + me = myconfig.env.mail.host; + remotes = "${myconfig.env.mail.relay} smtp"; + }; + }; +} 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 @@ +{ lib, pkgs, config, myconfig, ... }: +{ + config = { + secrets.keys = [ + { + dest = "mpd"; + permissions = "0400"; + text = myconfig.env.mpd.password; + } + { + dest = "mpd-config"; + permissions = "0400"; + user = "mpd"; + group = "mpd"; + text = '' + password "${myconfig.env.mpd.password}@read,add,control,admin" + ''; + } + ]; + networking.firewall.allowedTCPPorts = [ 6600 ]; + users.users.mpd.extraGroups = [ "wwwrun" "keys" ]; + systemd.services.mpd.serviceConfig.RuntimeDirectory = "mpd"; + services.mpd = { + enable = true; + network.listenAddress = "any"; + musicDirectory = myconfig.env.mpd.folder; + extraConfig = '' + include "/var/secrets/mpd-config" + audio_output { + type "null" + name "No Output" + mixer_type "none" + } + audio_output { + type "httpd" + name "OGG" + encoder "vorbis" + bind_to_address "/run/mpd/ogg.sock" + quality "5.0" + format "44100:16:1" + } + audio_output { + type "httpd" + name "MP3" + encoder "lame" + bind_to_address "/run/mpd/mp3.sock" + quality "5.0" + format "44100:16:1" + } + + + ''; + }; + }; +} + 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 @@ +{ lib, pkgs, config, myconfig, ... }: +{ + options = { + myServices.pub.enable = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Whether to enable pub user. + ''; + }; + }; + + config = lib.mkIf config.myServices.pub.enable { + users.users.pub = let + restrict = pkgs.runCommand "restrict" { + file = ./restrict; + buildInputs = [ pkgs.makeWrapper ]; + } '' + mkdir -p $out/bin + cp $file $out/bin/restrict + chmod a+x $out/bin/restrict + patchShebangs $out/bin/restrict + wrapProgram $out/bin/restrict \ + --prefix PATH : ${lib.makeBinPath [ pkgs.bubblewrap pkgs.rrsync ]} \ + --set TMUX_RESTRICT ${./tmux.restrict.conf} + ''; + purple-hangouts = pkgs.purple-hangouts.overrideAttrs(old: { + installPhase = '' + install -Dm755 -t $out/lib/purple-2/ libhangouts.so + for size in 16 22 24 48; do + install -TDm644 hangouts$size.png $out/share/pixmaps/pidgin/protocols/$size/hangouts.png + done + ''; + }); + in { + createHome = true; + description = "Restricted shell user"; + home = "/var/lib/pub"; + uid = myconfig.env.users.pub.uid; + useDefaultShell = true; + packages = [ + restrict + pkgs.tmux + (pkgs.pidgin.override { plugins = [ + pkgs.purple-plugin-pack purple-hangouts + pkgs.purple-discord pkgs.purple-facebook + pkgs.telegram-purple + ]; }) + ]; + }; + }; +} 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 @@ +#!/usr/bin/env bash +user="$1" +rootuser="$HOME/$user/" +mkdir -p $rootuser + +orig="$SSH_ORIGINAL_COMMAND" +if [ -z "$orig" ]; then + orig="/bin/bash -l" +fi +if [ "${orig:0:7}" = "command" ]; then + orig="${orig:8}" +fi + +case "$orig" in +rsync*) + rrsync $HOME/$user/ + ;; +*) + nix_store_paths() { + nix-store -q -R \ + /run/current-system/sw \ + /etc/profiles/per-user/pub \ + /etc/ssl/certs/ca-bundle.crt \ + | while read i; do + printf '%s--ro-bind\0'$i'\0'$i'\0' '' + done + } + + set -euo pipefail + (exec -c bwrap --ro-bind /usr /usr \ + --args 10 \ + --dir /tmp \ + --dir /var \ + --symlink ../tmp var/tmp \ + --proc /proc \ + --dev /dev \ + --ro-bind /etc/resolv.conf /etc/resolv.conf \ + --ro-bind /etc/zoneinfo /etc/zoneinfo \ + --ro-bind /etc/ssl /etc/ssl \ + --ro-bind /etc/static/ssl/certs /etc/static/ssl/certs \ + --ro-bind /run/current-system/sw/lib/locale/locale-archive /etc/locale-archive \ + --ro-bind /run/current-system/sw/bin /bin \ + --ro-bind /etc/profiles/per-user/pub/bin /bin-pub \ + --bind /var/lib/pub/$user /var/lib/pub \ + --dir /var/lib/commons \ + --ro-bind $TMUX_RESTRICT /var/lib/commons/tmux.restrict.conf \ + --chdir /var/lib/pub \ + --unshare-all \ + --share-net \ + --dir /run/user/$(id -u) \ + --setenv TERM "$TERM" \ + --setenv LOCALE_ARCHIVE "/etc/locale-archive" \ + --setenv XDG_RUNTIME_DIR "/run/user/`id -u`" \ + --setenv PS1 "$user@pub $ " \ + --setenv PATH "/bin:/bin-pub" \ + --setenv HOME "/var/lib/pub" \ + --file 11 /etc/passwd \ + --file 12 /etc/group \ + -- $orig) \ + 10< <(nix_store_paths) \ + 11< <(getent passwd $UID 65534) \ + 12< <(getent group $(id -g) 65534) + ;; +esac 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 @@ +# Pour les nostalgiques de screen +# comme les raccourcis ne sont pas les mêmes, j'évite +set -g prefix C-a +unbind-key C-b + +unbind-key -a +bind-key -n C-h list-keys +bind-key C-d detach +bind-key & confirm-before -p "kill-window #W? (y/n)" kill-window + +# même hack que sur screen lorsqu'on veut profiter du scroll du terminal +# (xterm ...) +set -g terminal-overrides 'xterm*:smcup@:rmcup@' + +#Pour les ctrl+arrow +set-option -g xterm-keys on + +# c'est un minimum (defaut 2000) +set-option -g history-limit 10000 + +# lorsque j'ai encore un tmux ailleurs seule +# sa fenetre active réduit la taille de ma fenetre locale +setw -g aggressive-resize on + +# Pour etre alerté sur un changement dans une autre fenêtre +setw -g monitor-activity on +#set -g visual-activity on +#set -g visual-bell on + +set -g base-index 1 + +# repercuter le contenu de la fenetre dans la barre de titre +# reference des string : man tmux (status-left) +set -g set-titles on +set -g set-titles-string '#H #W #T' # host window command + +#Dans les valeurs par defaut deja, avec le ssh-agent +set -g update-environment "DISPLAY SSH_ASKPASS SSH_AUTH_SOCK SSH_AGENT_PID SSH_CONNECTION WINDOWID XAUTHORITY PATH" + +set -g status off +set -g status-left '' +set -g status-right '' + 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 @@ +{ lib, pkgs, config, myconfig, ... }: +{ + config = { + networking.firewall.allowedTCPPorts = [ 22 ]; + + services.openssh.extraConfig = '' + AuthorizedKeysCommand /etc/ssh/ldap_authorized_keys + AuthorizedKeysCommandUser nobody + ''; + + secrets.keys = [{ + dest = "ssh-ldap"; + user = "nobody"; + group = "nogroup"; + permissions = "0400"; + text = myconfig.env.sshd.ldap.password; + }]; + system.activationScripts.sshd = { + deps = [ "secrets" ]; + text = '' + install -Dm400 -o nobody -g nogroup -T /var/secrets/ssh-ldap /etc/ssh/ldap_password + ''; + }; + # ssh is strict about parent directory having correct rights, don't + # move it in the nix store. + environment.etc."ssh/ldap_authorized_keys" = let + ldap_authorized_keys = + pkgs.mylibs.wrap { + name = "ldap_authorized_keys"; + file = ./ldap_authorized_keys.sh; + paths = [ pkgs.which pkgs.gitolite pkgs.openldap pkgs.stdenv.shellPackage pkgs.gnugrep pkgs.gnused pkgs.coreutils ]; + }; + in { + enable = true; + mode = "0755"; + user = "root"; + source = ldap_authorized_keys; + }; + }; +} 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 @@ +#!/usr/bin/env bash + +LDAPSEARCH=ldapsearch +KEY="immaeSshKey" +LDAP_BIND="cn=ssh,ou=services,dc=immae,dc=eu" +LDAP_PASS=$(cat /etc/ssh/ldap_password) +LDAP_HOST="ldap.immae.eu" +LDAP_MEMBER="cn=users,cn=ssh,ou=services,dc=immae,dc=eu" +LDAP_GITOLITE_MEMBER="cn=users,cn=gitolite,ou=services,dc=immae,dc=eu" +LDAP_PUB_RESTRICT_MEMBER="cn=restrict,cn=pub,ou=services,dc=immae,dc=eu" +LDAP_PUB_FORWARD_MEMBER="cn=forward,cn=pub,ou=services,dc=immae,dc=eu" +LDAP_BASE="dc=immae,dc=eu" +GITOLITE_SHELL=$(which gitolite-shell) +ECHO=$(which echo) + +suitable_for() { + type_for="$1" + key="$2" + + if [[ $key != *$'\n'* ]] && [[ $key == ssh-* ]]; then + echo "$key" + else + key_type=$(cut -d " " -f 1 <<< "$key") + + if grep -q "\b-$type_for\b" <<< "$key_type"; then + echo "" + elif grep -q "\b$type_for\b" <<< "$key_type"; then + echo $(sed -e "s/^[^ ]* //g" <<< "$key") + else + echo "" + fi + fi +} + +clean_key_line() { + type_for="$1" + line="$2" + + if [[ "$line" == $KEY::* ]]; then + # base64 keys should't happen, unless wrong copy-pasting + key="" + else + key=$(sed -e "s/^$KEY: *//" -e "s/ *$//" <<< "$line") + fi + + suitable_for "$type_for" "$key" +} + +ldap_search() { + $LDAPSEARCH -h $LDAP_HOST -ZZ -b $LDAP_BASE -D $LDAP_BIND -w "$LDAP_PASS" -x -o ldif-wrap=no -LLL "$@" +} + +ldap_keys() { + user=$1; + if [[ $user == gitolite ]]; then + ldap_search '(&(memberOf='$LDAP_GITOLITE_MEMBER')('$KEY'=*))' $KEY | \ + while read line ; + do + if [ ! -z "$line" ]; then + if [[ $line == dn* ]]; then + user=$(sed -n 's/.*uid=\([^,]*\).*/\1/p' <<< "$line") + if [ -n "$user" ]; then + if [[ $user == "immae" ]] || [[ $user == "denise" ]]; then + # Capitalize first letter (backward compatibility) + user=$(sed -r 's/^([a-z])/\U\1/' <<< "$user") + fi + else + # Service fake user + user=$(sed -n 's/.*cn=\([^,]*\).*/\1/p' <<< "$line") + fi + elif [[ $line == $KEY* ]]; then + key=$(clean_key_line git "$line") + if [ ! -z "$key" ]; then + if [[ $key != *$'\n'* ]] && [[ $key == ssh-* ]]; then + echo -n 'command="'$GITOLITE_SHELL' '$user'",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ' + echo $key + fi + fi + fi + fi + done + exit 0 + elif [[ $user == pub ]]; then + ldap_search '(&(memberOf='$LDAP_PUB_RESTRICT_MEMBER')('$KEY'=*))' $KEY | \ + while read line ; + do + if [ ! -z "$line" ]; then + if [[ $line == dn* ]]; then + echo "" + user=$(sed -n 's/.*uid=\([^,]*\).*/\1/p' <<< "$line") + echo "# $user" + elif [[ $line == $KEY* ]]; then + key=$(clean_key_line pub "$line") + key_forward=$(clean_key_line forward "$line") + if [ ! -z "$key" ]; then + if [[ $key != *$'\n'* ]] && [[ $key == ssh-* ]]; then + echo -n 'command="/etc/profiles/per-user/pub/bin/restrict '$user'" ' + echo $key + fi + elif [ ! -z "$key_forward" ]; then + if [[ $key_forward != *$'\n'* ]] && [[ $key_forward == ssh-* ]]; then + echo "# forward only" + echo -n 'no-pty,no-X11-forwarding,command="'$ECHO' forward only" ' + echo $key_forward + fi + fi + fi + fi + done + + echo "" + ldap_search '(&(memberOf='$LDAP_PUB_FORWARD_MEMBER')('$KEY'=*))' $KEY | \ + while read line ; + do + if [ ! -z "$line" ]; then + if [[ $line == dn* ]]; then + echo "" + user=$(sed -n 's/.*uid=\([^,]*\).*/\1/p' <<< "$line") + echo "# $user" + elif [[ $line == $KEY* ]]; then + key=$(clean_key_line forward "$line") + if [ ! -z "$key" ]; then + if [[ $key != *$'\n'* ]] && [[ $key == ssh-* ]]; then + echo -n 'no-pty,no-X11-forwarding,command="'$ECHO' forward only" ' + echo $key + fi + fi + fi + fi + done + exit 0 + else + ldap_search '(&(memberOf='$LDAP_MEMBER')('$KEY'=*)(uid='$user'))' $KEY | \ + while read line ; + do + if [ ! -z "$line" ]; then + if [[ $line == dn* ]]; then + user=$(sed -n 's/.*uid=\([^,]*\).*/\1/p' <<< "$line") + elif [[ $line == $KEY* ]]; then + key=$(clean_key_line ssh "$line") + if [ ! -z "$key" ]; then + if [[ $key != *$'\n'* ]] && [[ $key == ssh-* ]]; then + echo $key + fi + fi + fi + fi + done + fi +} + +ldap_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 @@ +{ pkgs, privateFiles, ... }: +{ + config = { + nixpkgs.overlays = builtins.attrValues (import ../../overlays); + _module.args = { + pkgsNext = import {}; + pkgsPrevious = import {}; + myconfig = { + inherit privateFiles; + env = import "${privateFiles}/environment.nix"; + }; + }; + + services.journald.extraConfig = '' + MaxLevelStore="warning" + MaxRetentionSec="1year" + ''; + + users.users.root.packages = [ + pkgs.telnet + pkgs.htop + pkgs.iftop + ]; + + environment.systemPackages = [ + pkgs.vim + ]; + + }; +} 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 @@ +{ lib, pkgs, config, myconfig, ... }: +let + cfg = config.myServices.tasks; + server_vardir = config.services.taskserver.dataDir; + fqdn = "task.immae.eu"; + user = config.services.taskserver.user; + env = myconfig.env.tools.task; + group = config.services.taskserver.group; + taskserver-user-certs = pkgs.runCommand "taskserver-user-certs" {} '' + mkdir -p $out/bin + cat > $out/bin/taskserver-user-certs <<"EOF" + #!/usr/bin/env bash + + user=$1 + + silent_certtool() { + if ! output="$("${pkgs.gnutls.bin}/bin/certtool" "$@" 2>&1)"; then + echo "GNUTLS certtool invocation failed with output:" >&2 + echo "$output" >&2 + fi + } + + silent_certtool -p \ + --bits 4096 \ + --outfile "${server_vardir}/userkeys/$user.key.pem" + ${pkgs.gnused}/bin/sed -i -n -e '/^-----BEGIN RSA PRIVATE KEY-----$/,$p' "${server_vardir}/userkeys/$user.key.pem" + + silent_certtool -c \ + --template "${pkgs.writeText "taskserver-ca.template" '' + tls_www_client + encryption_key + signing_key + expiration_days = 3650 + ''}" \ + --load-ca-certificate "${server_vardir}/keys/ca.cert" \ + --load-ca-privkey "${server_vardir}/keys/ca.key" \ + --load-privkey "${server_vardir}/userkeys/$user.key.pem" \ + --outfile "${server_vardir}/userkeys/$user.cert.pem" + EOF + chmod a+x $out/bin/taskserver-user-certs + patchShebangs $out/bin/taskserver-user-certs + ''; + taskwarrior-web = pkgs.webapps.taskwarrior-web; + socketsDir = "/run/taskwarrior-web"; + varDir = "/var/lib/taskwarrior-web"; + taskwebPages = let + uidPages = lib.attrsets.zipAttrs ( + lib.lists.flatten + (lib.attrsets.mapAttrsToList (k: c: map (v: { "${v}" = k; }) c.uid) env.taskwarrior-web) + ); + pages = lib.attrsets.mapAttrs (uid: items: + if lib.lists.length items == 1 then + '' + + + + + + + '' + else + '' + + + To-do list disponibles + + + + +
    + ${builtins.concatStringsSep "\n" (map (item: "
  • ${item}
  • ") items)} +
+ + + '' + ) uidPages; + in + pkgs.runCommand "taskwerver-pages" {} '' + mkdir -p $out/ + ${builtins.concatStringsSep "\n" (lib.attrsets.mapAttrsToList (k: v: "cp ${pkgs.writeText k v} $out/${k}.html") pages)} + echo "Please login" > $out/index.html + ''; +in { + options.myServices.tasks = { + enable = lib.mkEnableOption "my tasks service"; + }; + + config = lib.mkIf cfg.enable { + secrets.keys = [{ + dest = "webapps/tools-taskwarrior-web"; + user = "wwwrun"; + group = "wwwrun"; + permissions = "0400"; + text = '' + SetEnv TASKD_HOST "${fqdn}:${toString config.services.taskserver.listenPort}" + SetEnv TASKD_VARDIR "${server_vardir}" + SetEnv TASKD_LDAP_HOST "ldaps://${env.ldap.host}" + SetEnv TASKD_LDAP_DN "${env.ldap.dn}" + SetEnv TASKD_LDAP_PASSWORD "${env.ldap.password}" + SetEnv TASKD_LDAP_BASE "${env.ldap.base}" + SetEnv TASKD_LDAP_FILTER "${env.ldap.search}" + ''; + }]; + services.websites.tools.modules = [ "proxy_fcgi" "sed" ]; + services.websites.tools.vhostConfs.task = { + certName = "eldiron"; + addToCerts = true; + hosts = [ "task.immae.eu" ]; + root = "/run/current-system/webapps/_task"; + extraConfig = [ '' + + DirectoryIndex index.php + Use LDAPConnect + Require ldap-group cn=users,cn=taskwarrior,ou=services,dc=immae,dc=eu + + SetHandler "proxy:unix:/var/run/phpfpm/task.sock|fcgi://localhost" + + Include /var/secrets/webapps/tools-taskwarrior-web + + '' + '' + + ProxyPass "unix://${socketsDir}/%{folderName}.sock|http://localhost-%{folderName}/" + ProxyPassReverse "unix://${socketsDir}/%{folderName}.sock|http://localhost-%{folderName}/" + ProxyPassReverse http://${fqdn}/ + + SetOutputFilter Sed + OutputSed "s|/ajax|/taskweb/%{folderName}/ajax|g" + OutputSed "s|\([^x]\)/tasks|\1/taskweb/%{folderName}/tasks|g" + OutputSed "s|\([^x]\)/projects|\1/taskweb/%{folderName}/projects|g" + OutputSed "s|http://${fqdn}/|/taskweb/%{folderName}/|g" + OutputSed "s|/img/relax.jpg|/taskweb/%{folderName}/img/relax.jpg|g" + + '' + '' + Alias /taskweb ${taskwebPages} + + DirectoryIndex index.html + Require all granted + + + RewriteEngine on + RewriteRule ^/taskweb$ /taskweb/ [R=301,L] + RedirectMatch permanent ^/taskweb/([^/]+)$ /taskweb/$1/ + + RewriteCond %{LA-U:REMOTE_USER} !="" + RewriteCond ${taskwebPages}/%{LA-U:REMOTE_USER}.html -f + RewriteRule ^/taskweb/?$ ${taskwebPages}/%{LA-U:REMOTE_USER}.html [L] + + + Use LDAPConnect + Require ldap-group cn=users,cn=taskwarrior,ou=services,dc=immae,dc=eu + + '' + ] ++ (lib.attrsets.mapAttrsToList (k: v: '' + + ${builtins.concatStringsSep "\n" (map (uid: "Require ldap-attribute uid=${uid}") v.uid)} + + Use Taskwarrior ${k} + + '') env.taskwarrior-web); + }; + services.phpfpm.poolConfigs = { + tasks = '' + listen = /var/run/phpfpm/task.sock + user = ${user} + group = ${group} + listen.owner = wwwrun + listen.group = wwwrun + pm = dynamic + pm.max_children = 60 + pm.start_servers = 2 + pm.min_spare_servers = 1 + pm.max_spare_servers = 10 + + ; Needed to avoid clashes in browser cookies (same domain) + env[PATH] = "/etc/profiles/per-user/${user}/bin" + php_value[session.name] = TaskPHPSESSID + php_admin_value[open_basedir] = "${./www}:/tmp:${server_vardir}:/etc/profiles/per-user/${user}/bin/" + ''; + }; + + myServices.websites.webappDirs._task = ./www; + + security.acme.certs."task" = config.services.myCertificates.certConfig // { + inherit user group; + plugins = [ "fullchain.pem" "key.pem" "cert.pem" "account_key.json" ]; + domain = fqdn; + postRun = '' + systemctl restart taskserver.service + ''; + }; + + users.users.${user}.packages = [ taskserver-user-certs ]; + + system.activationScripts.taskserver = { + deps = [ "users" ]; + text = '' + install -m 0750 -o ${user} -g ${group} -d ${server_vardir} + install -m 0750 -o ${user} -g ${group} -d ${server_vardir}/userkeys + install -m 0750 -o ${user} -g ${group} -d ${server_vardir}/keys + + if [ ! -e "${server_vardir}/keys/ca.key" ]; then + silent_certtool() { + if ! output="$("${pkgs.gnutls.bin}/bin/certtool" "$@" 2>&1)"; then + echo "GNUTLS certtool invocation failed with output:" >&2 + echo "$output" >&2 + fi + } + + silent_certtool -p \ + --bits 4096 \ + --outfile "${server_vardir}/keys/ca.key" + + silent_certtool -s \ + --template "${pkgs.writeText "taskserver-ca.template" '' + cn = ${fqdn} + expiration_days = -1 + cert_signing_key + ca + ''}" \ + --load-privkey "${server_vardir}/keys/ca.key" \ + --outfile "${server_vardir}/keys/ca.cert" + + chown :${group} "${server_vardir}/keys/ca.key" + chmod g+r "${server_vardir}/keys/ca.key" + fi + ''; + }; + + services.taskserver = { + enable = true; + allowedClientIDs = [ "^task [2-9]" "^Mirakel [1-9]" ]; + inherit fqdn; + listenHost = "::"; + pki.manual.ca.cert = "${server_vardir}/keys/ca.cert"; + pki.manual.server.cert = "${config.security.acme.directory}/task/fullchain.pem"; + pki.manual.server.crl = "${config.security.acme.directory}/task/invalid.crl"; + pki.manual.server.key = "${config.security.acme.directory}/task/key.pem"; + requestLimit = 104857600; + }; + + system.activationScripts.taskwarrior-web = { + deps = [ "users" ]; + text = '' + if [ ! -f ${server_vardir}/userkeys/taskwarrior-web.cert.pem ]; then + ${taskserver-user-certs}/bin/taskserver-user-certs taskwarrior-web + chown taskd:taskd ${server_vardir}/userkeys/taskwarrior-web.cert.pem ${server_vardir}/userkeys/taskwarrior-web.key.pem + fi + ''; + }; + + systemd.services = (lib.attrsets.mapAttrs' (name: userConfig: + let + credentials = "${userConfig.org}/${name}/${userConfig.key}"; + dateFormat = userConfig.date; + taskrc = pkgs.writeText "taskrc" '' + data.location=${varDir}/${name} + taskd.certificate=${server_vardir}/userkeys/taskwarrior-web.cert.pem + taskd.key=${server_vardir}/userkeys/taskwarrior-web.key.pem + # IdenTrust DST Root CA X3 + # obtained here: https://letsencrypt.org/fr/certificates/ + taskd.ca=${pkgs.writeText "ca.cert" '' + -----BEGIN CERTIFICATE----- + MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/ + MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT + DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow + PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD + Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB + AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O + rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq + OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b + xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw + 7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD + aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV + HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG + SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69 + ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr + AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz + R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5 + JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo + Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ + -----END CERTIFICATE-----''} + taskd.server=${fqdn}:${toString config.services.taskserver.listenPort} + taskd.credentials=${credentials} + dateformat=${dateFormat} + ''; + in lib.attrsets.nameValuePair "taskwarrior-web-${name}" { + description = "Taskwarrior webapp for ${name}"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + path = [ pkgs.taskwarrior ]; + + environment.TASKRC = taskrc; + environment.BUNDLE_PATH = "${taskwarrior-web.gems}/${taskwarrior-web.gems.ruby.gemPath}"; + environment.BUNDLE_GEMFILE = "${taskwarrior-web.gems.confFiles}/Gemfile"; + environment.LC_ALL = "fr_FR.UTF-8"; + + script = '' + exec ${taskwarrior-web.gems}/${taskwarrior-web.gems.ruby.gemPath}/bin/bundle exec thin start -R config.ru -S ${socketsDir}/${name}.sock + ''; + + serviceConfig = { + User = user; + PrivateTmp = true; + Restart = "always"; + TimeoutSec = 60; + Type = "simple"; + WorkingDirectory = taskwarrior-web; + StateDirectoryMode = 0750; + StateDirectory = assert lib.strings.hasPrefix "/var/lib/" varDir; + (lib.strings.removePrefix "/var/lib/" varDir + "/${name}"); + RuntimeDirectoryPreserve = "yes"; + RuntimeDirectory = assert lib.strings.hasPrefix "/run/" socketsDir; + lib.strings.removePrefix "/run/" socketsDir; + }; + + unitConfig.RequiresMountsFor = varDir; + }) env.taskwarrior-web) // { + taskserver-ca.postStart = '' + chown :${group} "${server_vardir}/keys/ca.key" + chmod g+r "${server_vardir}/keys/ca.key" + ''; + }; + + }; +} 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 @@ + $value) { + if ($key !== "count") { + $entries[] = explode(":", $value); + } +} + +if (isset($_GET["file"])) { + $basecert = $vardir . "/userkeys/" . $ldap_user; + if (!file_exists($basecert . ".cert.pem")) { + exec("taskserver-user-certs $ldap_user"); + } + $certificate = file_get_contents($basecert . ".cert.pem"); + $cert_key = file_get_contents($basecert . ".key.pem"); + + // IdenTrust DST Root CA X3 + // obtained here: https://letsencrypt.org/fr/certificates/ + $server_cert = "-----BEGIN CERTIFICATE----- +MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/ +MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT +DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow +PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD +Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O +rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq +OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b +xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw +7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD +aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV +HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG +SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69 +ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr +AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz +R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5 +JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo +Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ +-----END CERTIFICATE-----"; + + $file = $_GET["file"]; + switch($file) { + case "ca.cert.pem": + $content = $server_cert; + $name = "ca.cert.pem"; + $type = "application/x-x509-ca-cert"; + break; + case "cert.pem": + $content = $certificate; + $name = $ldap_user . ".cert.pem"; + $type = "application/x-x509-ca-cert"; + break; + case "key.pem": + $content = $cert_key; + $name = $ldap_user . ".key.pem"; + $type = "application/x-x509-ca-cert"; + break; + case "mirakel"; + foreach ($entries as $entry) { + list($org, $user, $key) = $entry; + if ($key == $_GET["key"]) { break; } + } + $name = $user . ".mirakel"; + $type = "text/plain"; + $content = "username: $user +org: $org +user key: $key +server: $host +client.cert: +$certificate +Client.key: +$cert_key +ca.cert: +$server_cert +"; + break; + default: + die("invalid file name"); + break; + } + + header("Content-Type: $type"); + header('Content-Disposition: attachment; filename="' . $name . '"'); + header('Content-Transfer-Encoding: binary'); + header('Accept-Ranges: bytes'); + header('Cache-Control: private'); + header('Pragma: private'); + echo $content; + exit; +} +?> + +
+ Taskwarrior configuration +
+ + +For command line interface, download the files, put them near your Taskwarrior +configuration files, and add that to your Taskwarrior configuration: +
+taskd.certificate=/path/to/.cert.pem
+taskd.key=/path/to/.key.pem
+taskd.server=
+ 1) {
+  echo "# Chose one of them\n";
+  foreach($entries as $entry) {
+    list($org, $user, $key) = $entry;
+    echo "# taskd.credentials=$org/$user/$key\n";
+  }
+} else { ?>
+taskd.credentials=//
+
+taskd.ca=/path/to/ca.cert.pem
+
+For Mirakel, download and import the file: + +For Android Taskwarrior app, see instructions here. + + + 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 inherit (pkgs.webapps) mantisbt_2 mantisbt_2-plugins; env = myconfig.env.tools.mantisbt; }; - gitweb = pkgs.callPackage ./gitweb.nix { gitoliteDir = config.services.myGitolite.gitoliteDir; }; + gitweb = pkgs.callPackage ./gitweb.nix { + gitoliteDir = config.myServices.gitolite.gitoliteDir; + }; cfg = config.myServices.websites.tools.git; in { -- cgit v1.2.3