From 1f3b5addd3812ec328af84a1cb6919c1e2098294 Mon Sep 17 00:00:00 2001 From: h7x4 Date: Wed, 12 Jul 2023 02:30:00 +0200 Subject: [PATCH] tsuki/hedgedoc: misc: - configure oauth2 (this requires a custom module for now, will be resolved in 23.11) - harden systemd service - add systemd requires list - use socket postgres uri --- hosts/tsuki/configuration.nix | 2 +- hosts/tsuki/services/hedgedoc/default.nix | 93 ++ hosts/tsuki/services/hedgedoc/hedgedoc.nix | 1075 ++++++++++++++++++++ 3 files changed, 1169 insertions(+), 1 deletion(-) create mode 100644 hosts/tsuki/services/hedgedoc/default.nix create mode 100644 hosts/tsuki/services/hedgedoc/hedgedoc.nix diff --git a/hosts/tsuki/configuration.nix b/hosts/tsuki/configuration.nix index 9fbd90f..ddbecf9 100644 --- a/hosts/tsuki/configuration.nix +++ b/hosts/tsuki/configuration.nix @@ -9,7 +9,7 @@ ./services/gitea ./services/grafana ./services/headscale.nix - ./services/hedgedoc.nix + ./services/hedgedoc ./services/hydra.nix ./services/invidious.nix # ./services/jitsi.nix diff --git a/hosts/tsuki/services/hedgedoc/default.nix b/hosts/tsuki/services/hedgedoc/default.nix new file mode 100644 index 0000000..777356b --- /dev/null +++ b/hosts/tsuki/services/hedgedoc/default.nix @@ -0,0 +1,93 @@ +{ pkgs, lib, config, options, ... }: let + cfg = config.services.hedgedoc; +in { + imports = [ ./hedgedoc.nix ]; + disabledModules = [ "services/web-apps/hedgedoc.nix" ]; + + config = { + # Contains CMD_SESSION_SECRET and CMD_OAUTH2_CLIENT_SECRET + sops.secrets."hedgedoc/env" = { + restartUnits = [ "hedgedoc.service" ]; + }; + + services.hedgedoc = { + enable = true; + workDir = "${config.machineVars.dataDrives.default}/var/hedgedoc"; + environmentFile = config.sops.secrets."hedgedoc/env".path; + settings = { + domain = "docs.nani.wtf"; + email = false; + allowAnonymous = false; + allowAnonymousEdits = true; + protocolUseSSL = true; + + db = { + username = "hedgedoc"; + # TODO: set a password + database = "hedgedoc"; + host = "/var/run/postgresql"; + dialect = "postgresql"; + }; + + oauth2 = let + authServerUrl = config.services.kanidm.serverSettings.origin; + in rec { + baseURL = "${authServerUrl}/oauth2"; + tokenURL = "${authServerUrl}/oauth2/token"; + authorizationURL = "${authServerUrl}/ui/oauth2"; + userProfileURL = "${authServerUrl}/oauth2/openid/${clientID}/userinfo"; + + clientID = "hedgedoc"; + + scope = "openid email profile"; + userProfileUsernameAttr = "name"; + userProfileEmailAttr = "email"; + userProfileDisplayNameAttr = "displayname"; + + providerName = "KaniDM"; + }; + }; + }; + + services.postgresql = { + ensureDatabases = [ "hedgedoc" ]; + ensureUsers = [{ + name = "hedgedoc"; + ensurePermissions = { + "DATABASE \"hedgedoc\"" = "ALL PRIVILEGES"; + }; + }]; + }; + + systemd.services.hedgedoc = { + requires = [ + "postgresql.service" + "kanidm.service" + ]; + serviceConfig = { + CapabilityBoundingSet = ""; + LockPersonality = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateMounts = true; + PrivateTmp = true; + PrivateUsers = true; + ProtectClock = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "strict"; + ReadWritePaths = [ cfg.workDir ]; + RemoveIPC = true; + RestrictSUIDSGID = true; + UMask = "0007"; + RestrictAddressFamilies = [ "AF_UNIX AF_INET AF_INET6" ]; + SystemCallArchitectures = "native"; + SystemCallFilter = "~@clock @cpu-emulation @debug @keyring @module @mount @obsolete @raw-io @reboot @setuid @swap"; + }; + }; + }; +} diff --git a/hosts/tsuki/services/hedgedoc/hedgedoc.nix b/hosts/tsuki/services/hedgedoc/hedgedoc.nix new file mode 100644 index 0000000..e2014a9 --- /dev/null +++ b/hosts/tsuki/services/hedgedoc/hedgedoc.nix @@ -0,0 +1,1075 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.hedgedoc; + + # 21.03 will not be an official release - it was instead 21.05. This + # versionAtLeast statement remains set to 21.03 for backwards compatibility. + # See https://github.com/NixOS/nixpkgs/pull/108899 and + # https://github.com/NixOS/rfcs/blob/master/rfcs/0080-nixos-release-schedule.md. + name = if versionAtLeast config.system.stateVersion "21.03" + then "hedgedoc" + else "codimd"; + + settingsFormat = pkgs.formats.json {}; + + prettyJSON = conf: + pkgs.runCommandLocal "hedgedoc-config.json" { + nativeBuildInputs = [ pkgs.jq ]; + } '' + jq '{production:del(.[]|nulls)|del(.[][]?|nulls)}' \ + < ${settingsFormat.generate "hedgedoc-ugly.json" cfg.settings} \ + > $out + ''; +in +{ + imports = [ + (mkRenamedOptionModule [ "services" "codimd" ] [ "services" "hedgedoc" ]) + (mkRenamedOptionModule + [ "services" "hedgedoc" "configuration" ] [ "services" "hedgedoc" "settings" ]) + ]; + + options.services.hedgedoc = { + enable = mkEnableOption (lib.mdDoc "the HedgeDoc Markdown Editor"); + + groups = mkOption { + type = types.listOf types.str; + default = []; + description = lib.mdDoc '' + Groups to which the service user should be added. + ''; + }; + + workDir = mkOption { + type = types.path; + default = "/var/lib/${name}"; + description = lib.mdDoc '' + Working directory for the HedgeDoc service. + ''; + }; + + settings = let options = { + debug = mkEnableOption (lib.mdDoc "debug mode"); + domain = mkOption { + type = types.nullOr types.str; + default = null; + example = "hedgedoc.org"; + description = lib.mdDoc '' + Domain name for the HedgeDoc instance. + ''; + }; + urlPath = mkOption { + type = types.nullOr types.str; + default = null; + example = "/url/path/to/hedgedoc"; + description = lib.mdDoc '' + Path under which HedgeDoc is accessible. + ''; + }; + host = mkOption { + type = types.str; + default = "localhost"; + description = lib.mdDoc '' + Address to listen on. + ''; + }; + port = mkOption { + type = types.port; + default = 3000; + example = 80; + description = lib.mdDoc '' + Port to listen on. + ''; + }; + path = mkOption { + type = types.nullOr types.str; + default = null; + example = "/run/hedgedoc.sock"; + description = lib.mdDoc '' + Specify where a UNIX domain socket should be placed. + ''; + }; + allowOrigin = mkOption { + type = types.listOf types.str; + default = []; + example = [ "localhost" "hedgedoc.org" ]; + description = lib.mdDoc '' + List of domains to whitelist. + ''; + }; + useSSL = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Enable to use SSL server. This will also enable + {option}`protocolUseSSL`. + ''; + }; + hsts = { + enable = mkOption { + type = types.bool; + default = true; + description = lib.mdDoc '' + Whether to enable HSTS if HTTPS is also enabled. + ''; + }; + maxAgeSeconds = mkOption { + type = types.int; + default = 31536000; + description = lib.mdDoc '' + Max duration for clients to keep the HSTS status. + ''; + }; + includeSubdomains = mkOption { + type = types.bool; + default = true; + description = lib.mdDoc '' + Whether to include subdomains in HSTS. + ''; + }; + preload = mkOption { + type = types.bool; + default = true; + description = lib.mdDoc '' + Whether to allow preloading of the site's HSTS status. + ''; + }; + }; + csp = mkOption { + type = types.nullOr types.attrs; + default = null; + example = literalExpression '' + { + enable = true; + directives = { + scriptSrc = "trustworthy.scripts.example.com"; + }; + upgradeInsecureRequest = "auto"; + addDefaults = true; + } + ''; + description = lib.mdDoc '' + Specify the Content Security Policy which is passed to Helmet. + For configuration details see . + ''; + }; + protocolUseSSL = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Enable to use TLS for resource paths. + This only applies when {option}`domain` is set. + ''; + }; + urlAddPort = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Enable to add the port to callback URLs. + This only applies when {option}`domain` is set + and only for ports other than 80 and 443. + ''; + }; + useCDN = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Whether to use CDN resources or not. + ''; + }; + allowAnonymous = mkOption { + type = types.bool; + default = true; + description = lib.mdDoc '' + Whether to allow anonymous usage. + ''; + }; + allowAnonymousEdits = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Whether to allow guests to edit existing notes with the `freely` permission, + when {option}`allowAnonymous` is enabled. + ''; + }; + allowFreeURL = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Whether to allow note creation by accessing a nonexistent note URL. + ''; + }; + requireFreeURLAuthentication = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Whether to require authentication for FreeURL mode style note creation. + ''; + }; + defaultPermission = mkOption { + type = types.enum [ "freely" "editable" "limited" "locked" "private" ]; + default = "editable"; + description = lib.mdDoc '' + Default permissions for notes. + This only applies for signed-in users. + ''; + }; + dbURL = mkOption { + type = types.nullOr types.str; + default = null; + example = '' + postgres://user:pass@host:5432/dbname + ''; + description = lib.mdDoc '' + Specify which database to use. + HedgeDoc supports mysql, postgres, sqlite and mssql. + See [ + https://sequelize.readthedocs.io/en/v3/](https://sequelize.readthedocs.io/en/v3/) for more information. + Note: This option overrides {option}`db`. + ''; + }; + db = mkOption { + type = types.attrs; + default = {}; + example = literalExpression '' + { + dialect = "sqlite"; + storage = "/var/lib/${name}/db.${name}.sqlite"; + } + ''; + description = lib.mdDoc '' + Specify the configuration for sequelize. + HedgeDoc supports mysql, postgres, sqlite and mssql. + See [ + https://sequelize.readthedocs.io/en/v3/](https://sequelize.readthedocs.io/en/v3/) for more information. + Note: This option overrides {option}`db`. + ''; + }; + sslKeyPath= mkOption { + type = types.nullOr types.str; + default = null; + example = "/var/lib/hedgedoc/hedgedoc.key"; + description = lib.mdDoc '' + Path to the SSL key. Needed when {option}`useSSL` is enabled. + ''; + }; + sslCertPath = mkOption { + type = types.nullOr types.str; + default = null; + example = "/var/lib/hedgedoc/hedgedoc.crt"; + description = lib.mdDoc '' + Path to the SSL cert. Needed when {option}`useSSL` is enabled. + ''; + }; + sslCAPath = mkOption { + type = types.listOf types.str; + default = []; + example = [ "/var/lib/hedgedoc/ca.crt" ]; + description = lib.mdDoc '' + SSL ca chain. Needed when {option}`useSSL` is enabled. + ''; + }; + dhParamPath = mkOption { + type = types.nullOr types.str; + default = null; + example = "/var/lib/hedgedoc/dhparam.pem"; + description = lib.mdDoc '' + Path to the SSL dh params. Needed when {option}`useSSL` is enabled. + ''; + }; + tmpPath = mkOption { + type = types.str; + default = "/tmp"; + description = lib.mdDoc '' + Path to the temp directory HedgeDoc should use. + Note that {option}`serviceConfig.PrivateTmp` is enabled for + the HedgeDoc systemd service by default. + (Non-canonical paths are relative to HedgeDoc's base directory) + ''; + }; + defaultNotePath = mkOption { + type = types.nullOr types.str; + default = "${cfg.package}/public/default.md"; + defaultText = literalExpression "\"\${cfg.package}/public/default.md\""; + description = lib.mdDoc '' + Path to the default Note file. + (Non-canonical paths are relative to HedgeDoc's base directory) + ''; + }; + docsPath = mkOption { + type = types.nullOr types.str; + default = "${cfg.package}/public/docs"; + defaultText = literalExpression "\"\${cfg.package}/public/docs\""; + description = lib.mdDoc '' + Path to the docs directory. + (Non-canonical paths are relative to HedgeDoc's base directory) + ''; + }; + indexPath = mkOption { + type = types.nullOr types.str; + default = "${cfg.package}/public/views/index.ejs"; + defaultText = literalExpression "\"\${cfg.package}/public/views/index.ejs\""; + description = lib.mdDoc '' + Path to the index template file. + (Non-canonical paths are relative to HedgeDoc's base directory) + ''; + }; + hackmdPath = mkOption { + type = types.nullOr types.str; + default = "${cfg.package}/public/views/hackmd.ejs"; + defaultText = literalExpression "\"\${cfg.package}/public/views/hackmd.ejs\""; + description = lib.mdDoc '' + Path to the hackmd template file. + (Non-canonical paths are relative to HedgeDoc's base directory) + ''; + }; + errorPath = mkOption { + type = types.nullOr types.str; + default = "${cfg.package}/public/views/error.ejs"; + defaultText = literalExpression "\"\${cfg.package}/public/views/error.ejs\""; + description = lib.mdDoc '' + Path to the error template file. + (Non-canonical paths are relative to HedgeDoc's base directory) + ''; + }; + prettyPath = mkOption { + type = types.nullOr types.str; + default = "${cfg.package}/public/views/pretty.ejs"; + defaultText = literalExpression "\"\${cfg.package}/public/views/pretty.ejs\""; + description = lib.mdDoc '' + Path to the pretty template file. + (Non-canonical paths are relative to HedgeDoc's base directory) + ''; + }; + slidePath = mkOption { + type = types.nullOr types.str; + default = "${cfg.package}/public/views/slide.hbs"; + defaultText = literalExpression "\"\${cfg.package}/public/views/slide.hbs\""; + description = lib.mdDoc '' + Path to the slide template file. + (Non-canonical paths are relative to HedgeDoc's base directory) + ''; + }; + uploadsPath = mkOption { + type = types.str; + default = "${cfg.workDir}/uploads"; + defaultText = literalExpression "\"\${cfg.workDir}/uploads\""; + description = lib.mdDoc '' + Path under which uploaded files are saved. + ''; + }; + sessionName = mkOption { + type = types.str; + default = "connect.sid"; + description = lib.mdDoc '' + Specify the name of the session cookie. + ''; + }; + sessionSecret = mkOption { + type = types.nullOr types.str; + default = null; + description = lib.mdDoc '' + Specify the secret used to sign the session cookie. + If unset, one will be generated on startup. + ''; + }; + sessionLife = mkOption { + type = types.int; + default = 1209600000; + description = lib.mdDoc '' + Session life time in milliseconds. + ''; + }; + heartbeatInterval = mkOption { + type = types.int; + default = 5000; + description = lib.mdDoc '' + Specify the socket.io heartbeat interval. + ''; + }; + heartbeatTimeout = mkOption { + type = types.int; + default = 10000; + description = lib.mdDoc '' + Specify the socket.io heartbeat timeout. + ''; + }; + documentMaxLength = mkOption { + type = types.int; + default = 100000; + description = lib.mdDoc '' + Specify the maximum document length. + ''; + }; + email = mkOption { + type = types.bool; + default = true; + description = lib.mdDoc '' + Whether to enable email sign-in. + ''; + }; + allowEmailRegister = mkOption { + type = types.bool; + default = true; + description = lib.mdDoc '' + Whether to enable email registration. + ''; + }; + allowGravatar = mkOption { + type = types.bool; + default = true; + description = lib.mdDoc '' + Whether to use gravatar as profile picture source. + ''; + }; + imageUploadType = mkOption { + type = types.enum [ "imgur" "s3" "minio" "filesystem" ]; + default = "filesystem"; + description = lib.mdDoc '' + Specify where to upload images. + ''; + }; + minio = mkOption { + type = types.nullOr (types.submodule { + options = { + accessKey = mkOption { + type = types.str; + description = lib.mdDoc '' + Minio access key. + ''; + }; + secretKey = mkOption { + type = types.str; + description = lib.mdDoc '' + Minio secret key. + ''; + }; + endPoint = mkOption { + type = types.str; + description = lib.mdDoc '' + Minio endpoint. + ''; + }; + port = mkOption { + type = types.port; + default = 9000; + description = lib.mdDoc '' + Minio listen port. + ''; + }; + secure = mkOption { + type = types.bool; + default = true; + description = lib.mdDoc '' + Whether to use HTTPS for Minio. + ''; + }; + }; + }); + default = null; + description = lib.mdDoc "Configure the minio third-party integration."; + }; + s3 = mkOption { + type = types.nullOr (types.submodule { + options = { + accessKeyId = mkOption { + type = types.str; + description = lib.mdDoc '' + AWS access key id. + ''; + }; + secretAccessKey = mkOption { + type = types.str; + description = lib.mdDoc '' + AWS access key. + ''; + }; + region = mkOption { + type = types.str; + description = lib.mdDoc '' + AWS S3 region. + ''; + }; + }; + }); + default = null; + description = lib.mdDoc "Configure the s3 third-party integration."; + }; + s3bucket = mkOption { + type = types.nullOr types.str; + default = null; + description = lib.mdDoc '' + Specify the bucket name for upload types `s3` and `minio`. + ''; + }; + allowPDFExport = mkOption { + type = types.bool; + default = true; + description = lib.mdDoc '' + Whether to enable PDF exports. + ''; + }; + imgur.clientId = mkOption { + type = types.nullOr types.str; + default = null; + description = lib.mdDoc '' + Imgur API client ID. + ''; + }; + azure = mkOption { + type = types.nullOr (types.submodule { + options = { + connectionString = mkOption { + type = types.str; + description = lib.mdDoc '' + Azure Blob Storage connection string. + ''; + }; + container = mkOption { + type = types.str; + description = lib.mdDoc '' + Azure Blob Storage container name. + It will be created if non-existent. + ''; + }; + }; + }); + default = null; + description = lib.mdDoc "Configure the azure third-party integration."; + }; + oauth2 = mkOption { + type = types.nullOr (types.submodule { + options = { + authorizationURL = mkOption { + type = types.str; + description = lib.mdDoc '' + Specify the OAuth authorization URL. + ''; + }; + tokenURL = mkOption { + type = types.str; + description = lib.mdDoc '' + Specify the OAuth token URL. + ''; + }; + baseURL = mkOption { + type = with types; nullOr str; + default = null; + description = lib.mdDoc '' + Specify the OAuth base URL. + ''; + }; + userProfileURL = mkOption { + type = with types; nullOr str; + default = null; + description = lib.mdDoc '' + Specify the OAuth userprofile URL. + ''; + }; + userProfileUsernameAttr = mkOption { + type = with types; nullOr str; + default = null; + description = lib.mdDoc '' + Specify the name of the attribute for the username from the claim. + ''; + }; + userProfileDisplayNameAttr = mkOption { + type = with types; nullOr str; + default = null; + description = lib.mdDoc '' + Specify the name of the attribute for the display name from the claim. + ''; + }; + userProfileEmailAttr = mkOption { + type = with types; nullOr str; + default = null; + description = lib.mdDoc '' + Specify the name of the attribute for the email from the claim. + ''; + }; + scope = mkOption { + type = with types; nullOr str; + default = null; + description = lib.mdDoc '' + Specify the OAuth scope. + ''; + }; + providerName = mkOption { + type = with types; nullOr str; + default = null; + description = lib.mdDoc '' + Specify the name to be displayed for this strategy. + ''; + }; + rolesClaim = mkOption { + type = with types; nullOr str; + default = null; + description = lib.mdDoc '' + Specify the role claim name. + ''; + }; + accessRole = mkOption { + type = with types; nullOr str; + default = null; + description = lib.mdDoc '' + Specify role which should be included in the ID token roles claim to grant access + ''; + }; + clientID = mkOption { + type = types.str; + description = lib.mdDoc '' + Specify the OAuth client ID. + ''; + }; + clientSecret = mkOption { + type = with types; nullOr str; + default = null; + description = lib.mdDoc '' + Specify the OAuth client secret. + ''; + }; + }; + }); + default = null; + description = lib.mdDoc "Configure the OAuth integration."; + }; + facebook = mkOption { + type = types.nullOr (types.submodule { + options = { + clientID = mkOption { + type = types.str; + description = lib.mdDoc '' + Facebook API client ID. + ''; + }; + clientSecret = mkOption { + type = types.str; + description = lib.mdDoc '' + Facebook API client secret. + ''; + }; + }; + }); + default = null; + description = lib.mdDoc "Configure the facebook third-party integration"; + }; + twitter = mkOption { + type = types.nullOr (types.submodule { + options = { + consumerKey = mkOption { + type = types.str; + description = lib.mdDoc '' + Twitter API consumer key. + ''; + }; + consumerSecret = mkOption { + type = types.str; + description = lib.mdDoc '' + Twitter API consumer secret. + ''; + }; + }; + }); + default = null; + description = lib.mdDoc "Configure the Twitter third-party integration."; + }; + github = mkOption { + type = types.nullOr (types.submodule { + options = { + clientID = mkOption { + type = types.str; + description = lib.mdDoc '' + GitHub API client ID. + ''; + }; + clientSecret = mkOption { + type = types.str; + description = lib.mdDoc '' + Github API client secret. + ''; + }; + }; + }); + default = null; + description = lib.mdDoc "Configure the GitHub third-party integration."; + }; + gitlab = mkOption { + type = types.nullOr (types.submodule { + options = { + baseURL = mkOption { + type = types.str; + default = ""; + description = lib.mdDoc '' + GitLab API authentication endpoint. + Only needed for other endpoints than gitlab.com. + ''; + }; + clientID = mkOption { + type = types.str; + description = lib.mdDoc '' + GitLab API client ID. + ''; + }; + clientSecret = mkOption { + type = types.str; + description = lib.mdDoc '' + GitLab API client secret. + ''; + }; + scope = mkOption { + type = types.enum [ "api" "read_user" ]; + default = "api"; + description = lib.mdDoc '' + GitLab API requested scope. + GitLab snippet import/export requires api scope. + ''; + }; + }; + }); + default = null; + description = lib.mdDoc "Configure the GitLab third-party integration."; + }; + mattermost = mkOption { + type = types.nullOr (types.submodule { + options = { + baseURL = mkOption { + type = types.str; + description = lib.mdDoc '' + Mattermost authentication endpoint. + ''; + }; + clientID = mkOption { + type = types.str; + description = lib.mdDoc '' + Mattermost API client ID. + ''; + }; + clientSecret = mkOption { + type = types.str; + description = lib.mdDoc '' + Mattermost API client secret. + ''; + }; + }; + }); + default = null; + description = lib.mdDoc "Configure the Mattermost third-party integration."; + }; + dropbox = mkOption { + type = types.nullOr (types.submodule { + options = { + clientID = mkOption { + type = types.str; + description = lib.mdDoc '' + Dropbox API client ID. + ''; + }; + clientSecret = mkOption { + type = types.str; + description = lib.mdDoc '' + Dropbox API client secret. + ''; + }; + appKey = mkOption { + type = types.str; + description = lib.mdDoc '' + Dropbox app key. + ''; + }; + }; + }); + default = null; + description = lib.mdDoc "Configure the Dropbox third-party integration."; + }; + google = mkOption { + type = types.nullOr (types.submodule { + options = { + clientID = mkOption { + type = types.str; + description = lib.mdDoc '' + Google API client ID. + ''; + }; + clientSecret = mkOption { + type = types.str; + description = lib.mdDoc '' + Google API client secret. + ''; + }; + }; + }); + default = null; + description = lib.mdDoc "Configure the Google third-party integration."; + }; + ldap = mkOption { + type = types.nullOr (types.submodule { + options = { + providerName = mkOption { + type = types.str; + default = ""; + description = lib.mdDoc '' + Optional name to be displayed at login form, indicating the LDAP provider. + ''; + }; + url = mkOption { + type = types.str; + example = "ldap://localhost"; + description = lib.mdDoc '' + URL of LDAP server. + ''; + }; + bindDn = mkOption { + type = types.str; + description = lib.mdDoc '' + Bind DN for LDAP access. + ''; + }; + bindCredentials = mkOption { + type = types.str; + description = lib.mdDoc '' + Bind credentials for LDAP access. + ''; + }; + searchBase = mkOption { + type = types.str; + example = "o=users,dc=example,dc=com"; + description = lib.mdDoc '' + LDAP directory to begin search from. + ''; + }; + searchFilter = mkOption { + type = types.str; + example = "(uid={{username}})"; + description = lib.mdDoc '' + LDAP filter to search with. + ''; + }; + searchAttributes = mkOption { + type = types.nullOr (types.listOf types.str); + default = null; + example = [ "displayName" "mail" ]; + description = lib.mdDoc '' + LDAP attributes to search with. + ''; + }; + userNameField = mkOption { + type = types.str; + default = ""; + description = lib.mdDoc '' + LDAP field which is used as the username on HedgeDoc. + By default {option}`useridField` is used. + ''; + }; + useridField = mkOption { + type = types.str; + example = "uid"; + description = lib.mdDoc '' + LDAP field which is a unique identifier for users on HedgeDoc. + ''; + }; + tlsca = mkOption { + type = types.str; + default = "/etc/ssl/certs/ca-certificates.crt"; + example = "server-cert.pem,root.pem"; + description = lib.mdDoc '' + Root CA for LDAP TLS in PEM format. + ''; + }; + }; + }); + default = null; + description = lib.mdDoc "Configure the LDAP integration."; + }; + saml = mkOption { + type = types.nullOr (types.submodule { + options = { + idpSsoUrl = mkOption { + type = types.str; + example = "https://idp.example.com/sso"; + description = lib.mdDoc '' + IdP authentication endpoint. + ''; + }; + idpCert = mkOption { + type = types.path; + example = "/path/to/cert.pem"; + description = lib.mdDoc '' + Path to IdP certificate file in PEM format. + ''; + }; + issuer = mkOption { + type = types.str; + default = ""; + description = lib.mdDoc '' + Optional identity of the service provider. + This defaults to the server URL. + ''; + }; + identifierFormat = mkOption { + type = types.str; + default = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"; + description = lib.mdDoc '' + Optional name identifier format. + ''; + }; + groupAttribute = mkOption { + type = types.str; + default = ""; + example = "memberOf"; + description = lib.mdDoc '' + Optional attribute name for group list. + ''; + }; + externalGroups = mkOption { + type = types.listOf types.str; + default = []; + example = [ "Temporary-staff" "External-users" ]; + description = lib.mdDoc '' + Excluded group names. + ''; + }; + requiredGroups = mkOption { + type = types.listOf types.str; + default = []; + example = [ "Hedgedoc-Users" ]; + description = lib.mdDoc '' + Required group names. + ''; + }; + providerName = mkOption { + type = types.str; + default = ""; + example = "My institution"; + description = lib.mdDoc '' + Optional name to be displayed at login form indicating the SAML provider. + ''; + }; + attribute = { + id = mkOption { + type = types.str; + default = ""; + description = lib.mdDoc '' + Attribute map for `id`. + Defaults to `NameID` of SAML response. + ''; + }; + username = mkOption { + type = types.str; + default = ""; + description = lib.mdDoc '' + Attribute map for `username`. + Defaults to `NameID` of SAML response. + ''; + }; + email = mkOption { + type = types.str; + default = ""; + description = lib.mdDoc '' + Attribute map for `email`. + Defaults to `NameID` of SAML response if + {option}`identifierFormat` has + the default value. + ''; + }; + }; + }; + }); + default = null; + description = lib.mdDoc "Configure the SAML integration."; + }; + }; in lib.mkOption { + type = lib.types.submodule { + freeformType = settingsFormat.type; + inherit options; + }; + description = lib.mdDoc '' + HedgeDoc configuration, see + + for documentation. + ''; + }; + + environmentFile = mkOption { + type = with types; nullOr path; + default = null; + example = "/var/lib/hedgedoc/hedgedoc.env"; + description = lib.mdDoc '' + Environment file as defined in {manpage}`systemd.exec(5)`. + + Secrets may be passed to the service without adding them to the world-readable + Nix store, by specifying placeholder variables as the option value in Nix and + setting these variables accordingly in the environment file. + + ``` + # snippet of HedgeDoc-related config + services.hedgedoc.settings.dbURL = "postgres://hedgedoc:\''${DB_PASSWORD}@db-host:5432/hedgedocdb"; + services.hedgedoc.settings.minio.secretKey = "$MINIO_SECRET_KEY"; + ``` + + ``` + # content of the environment file + DB_PASSWORD=verysecretdbpassword + MINIO_SECRET_KEY=verysecretminiokey + ``` + + Note that this file needs to be available on the host on which + `HedgeDoc` is running. + ''; + }; + + package = mkOption { + type = types.package; + default = pkgs.hedgedoc; + defaultText = literalExpression "pkgs.hedgedoc"; + description = lib.mdDoc '' + Package that provides HedgeDoc. + ''; + }; + + }; + + config = mkIf cfg.enable { + assertions = [ + { assertion = cfg.settings.db == {} -> ( + cfg.settings.dbURL != "" && cfg.settings.dbURL != null + ); + message = "Database configuration for HedgeDoc missing."; } + ]; + users.groups.${name} = {}; + users.users.${name} = { + description = "HedgeDoc service user"; + group = name; + extraGroups = cfg.groups; + home = cfg.workDir; + createHome = true; + isSystemUser = true; + }; + + systemd.services.hedgedoc = { + description = "HedgeDoc Service"; + wantedBy = [ "multi-user.target" ]; + after = [ "networking.target" ]; + preStart = '' + ${pkgs.envsubst}/bin/envsubst \ + -o ${cfg.workDir}/config.json \ + -i ${prettyJSON cfg.settings} + mkdir -p ${cfg.settings.uploadsPath} + ''; + serviceConfig = { + WorkingDirectory = cfg.workDir; + StateDirectory = [ cfg.workDir cfg.settings.uploadsPath ]; + ExecStart = "${cfg.package}/bin/hedgedoc"; + EnvironmentFile = mkIf (cfg.environmentFile != null) [ cfg.environmentFile ]; + Environment = [ + "CMD_CONFIG_FILE=${cfg.workDir}/config.json" + "NODE_ENV=production" + ]; + Restart = "always"; + User = name; + PrivateTmp = true; + }; + }; + }; +}