diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 17c21be22f1..b128568bdf5 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -683,6 +683,7 @@ ./services/web-apps/atlassian/confluence.nix ./services/web-apps/atlassian/crowd.nix ./services/web-apps/atlassian/jira.nix + ./services/web-apps/codimd.nix ./services/web-apps/frab.nix ./services/web-apps/mattermost.nix ./services/web-apps/nexus.nix diff --git a/nixos/modules/services/web-apps/codimd.nix b/nixos/modules/services/web-apps/codimd.nix new file mode 100644 index 00000000000..5368e8b0e66 --- /dev/null +++ b/nixos/modules/services/web-apps/codimd.nix @@ -0,0 +1,958 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.codimd; + + prettyJSON = conf: + pkgs.runCommand "codimd-config.json" { } '' + echo '${builtins.toJSON conf}' | ${pkgs.jq}/bin/jq \ + '{production:del(.[]|nulls)|del(.[][]?|nulls)}' > $out + ''; +in +{ + options.services.codimd = { + enable = mkEnableOption "the CodiMD Markdown Editor"; + + groups = mkOption { + type = types.listOf types.str; + default = []; + description = '' + Groups to which the codimd user should be added. + ''; + }; + + workDir = mkOption { + type = types.path; + default = "/var/lib/codimd"; + description = '' + Working directory for the CodiMD service. + ''; + }; + + configuration = { + debug = mkEnableOption "debug mode"; + domain = mkOption { + type = types.nullOr types.str; + default = null; + example = "codimd.org"; + description = '' + Domain name for the CodiMD instance. + ''; + }; + urlPath = mkOption { + type = types.nullOr types.str; + default = null; + example = "/url/path/to/codimd"; + description = '' + Path under which CodiMD is accessible. + ''; + }; + host = mkOption { + type = types.str; + default = "localhost"; + description = '' + Address to listen on. + ''; + }; + port = mkOption { + type = types.int; + default = 3000; + example = "80"; + description = '' + Port to listen on. + ''; + }; + path = mkOption { + type = types.nullOr types.str; + default = null; + example = "/var/run/codimd.sock"; + description = '' + Specify where a UNIX domain socket should be placed. + ''; + }; + allowOrigin = mkOption { + type = types.listOf types.str; + default = []; + example = [ "localhost" "codimd.org" ]; + description = '' + List of domains to whitelist. + ''; + }; + useSSL = mkOption { + type = types.bool; + default = false; + description = '' + Enable to use SSL server. This will also enable + . + ''; + }; + hsts = { + enable = mkOption { + type = types.bool; + default = true; + description = '' + Wheter to enable HSTS if HTTPS is also enabled. + ''; + }; + maxAgeSeconds = mkOption { + type = types.int; + default = 31536000; + description = '' + Max duration for clients to keep the HSTS status. + ''; + }; + includeSubdomains = mkOption { + type = types.bool; + default = true; + description = '' + Whether to include subdomains in HSTS. + ''; + }; + preload = mkOption { + type = types.bool; + default = true; + description = '' + Whether to allow preloading of the site's HSTS status. + ''; + }; + }; + csp = mkOption { + type = types.nullOr types.attrs; + default = null; + example = literalExample '' + { + enable = true; + directives = { + scriptSrc = "trustworthy.scripts.example.com"; + }; + upgradeInsecureRequest = "auto"; + addDefaults = true; + } + ''; + description = '' + Specify the Content Security Policy which is passed to Helmet. + For configuration details see https://helmetjs.github.io/docs/csp/. + ''; + }; + protocolUseSSL = mkOption { + type = types.bool; + default = false; + description = '' + Enable to use TLS for resource paths. + This only applies when is set. + ''; + }; + urlAddPort = mkOption { + type = types.bool; + default = false; + description = '' + Enable to add the port to callback URLs. + This only applies when is set + and only for ports other than 80 and 443. + ''; + }; + useCDN = mkOption { + type = types.bool; + default = true; + description = '' + Whether to use CDN resources or not. + ''; + }; + allowAnonymous = mkOption { + type = types.bool; + default = true; + description = '' + Whether to allow anonymous usage. + ''; + }; + allowAnonymousEdits = mkOption { + type = types.bool; + default = false; + description = '' + Whether to allow guests to edit existing notes with the `freely' permission, + when is enabled. + ''; + }; + allowFreeURL = mkOption { + type = types.bool; + default = false; + description = '' + Whether to allow note creation by accessing a nonexistent note URL. + ''; + }; + defaultPermission = mkOption { + type = types.enum [ "freely" "editable" "limited" "locked" "private" ]; + default = "editable"; + description = '' + 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 = '' + Specify which database to use. + CodiMD supports mysql, postgres, sqlite and mssql. + See + https://sequelize.readthedocs.io/en/v3/ for more information. + Note: This option overrides . + ''; + }; + db = mkOption { + type = types.attrs; + default = {}; + example = literalExample '' + { + dialect = "sqlite"; + storage = "/var/lib/codimd/db.codimd.sqlite"; + } + ''; + description = '' + Specify the configuration for sequelize. + CodiMD supports mysql, postgres, sqlite and mssql. + See + https://sequelize.readthedocs.io/en/v3/ for more information. + Note: This option overrides . + ''; + }; + sslKeyPath= mkOption { + type = types.nullOr types.str; + default = null; + example = "/var/lib/codimd/codimd.key"; + description = '' + Path to the SSL key. Needed when is enabled. + ''; + }; + sslCertPath = mkOption { + type = types.nullOr types.str; + default = null; + example = "/var/lib/codimd/codimd.crt"; + description = '' + Path to the SSL cert. Needed when is enabled. + ''; + }; + sslCAPath = mkOption { + type = types.listOf types.str; + default = []; + example = [ "/var/lib/codimd/ca.crt" ]; + description = '' + SSL ca chain. Needed when is enabled. + ''; + }; + dhParamPath = mkOption { + type = types.nullOr types.str; + default = null; + example = "/var/lib/codimd/dhparam.pem"; + description = '' + Path to the SSL dh params. Needed when is enabled. + ''; + }; + tmpPath = mkOption { + type = types.str; + default = "/tmp"; + description = '' + Path to the temp directory CodiMD should use. + Note that is enabled for + the CodiMD systemd service by default. + (Non-canonical paths are relative to CodiMD's base directory) + ''; + }; + defaultNotePath = mkOption { + type = types.nullOr types.str; + default = "./public/default.md"; + description = '' + Path to the default Note file. + (Non-canonical paths are relative to CodiMD's base directory) + ''; + }; + docsPath = mkOption { + type = types.nullOr types.str; + default = "./public/docs"; + description = '' + Path to the docs directory. + (Non-canonical paths are relative to CodiMD's base directory) + ''; + }; + indexPath = mkOption { + type = types.nullOr types.str; + default = "./public/views/index.ejs"; + description = '' + Path to the index template file. + (Non-canonical paths are relative to CodiMD's base directory) + ''; + }; + hackmdPath = mkOption { + type = types.nullOr types.str; + default = "./public/views/hackmd.ejs"; + description = '' + Path to the hackmd template file. + (Non-canonical paths are relative to CodiMD's base directory) + ''; + }; + errorPath = mkOption { + type = types.nullOr types.str; + default = null; + defaultText = "./public/views/error.ejs"; + description = '' + Path to the error template file. + (Non-canonical paths are relative to CodiMD's base directory) + ''; + }; + prettyPath = mkOption { + type = types.nullOr types.str; + default = null; + defaultText = "./public/views/pretty.ejs"; + description = '' + Path to the pretty template file. + (Non-canonical paths are relative to CodiMD's base directory) + ''; + }; + slidePath = mkOption { + type = types.nullOr types.str; + default = null; + defaultText = "./public/views/slide.hbs"; + description = '' + Path to the slide template file. + (Non-canonical paths are relative to CodiMD's base directory) + ''; + }; + uploadsPath = mkOption { + type = types.str; + default = "${cfg.workDir}/uploads"; + defaultText = "/var/lib/codimd/uploads"; + description = '' + Path under which uploaded files are saved. + ''; + }; + sessionName = mkOption { + type = types.str; + default = "connect.sid"; + description = '' + Specify the name of the session cookie. + ''; + }; + sessionSecret = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + 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 = '' + Session life time in milliseconds. + ''; + }; + heartbeatInterval = mkOption { + type = types.int; + default = 5000; + description = '' + Specify the socket.io heartbeat interval. + ''; + }; + heartbeatTimeout = mkOption { + type = types.int; + default = 10000; + description = '' + Specify the socket.io heartbeat timeout. + ''; + }; + documentMaxLength = mkOption { + type = types.int; + default = 100000; + description = '' + Specify the maximum document length. + ''; + }; + email = mkOption { + type = types.bool; + default = true; + description = '' + Whether to enable email sign-in. + ''; + }; + allowEmailRegister = mkOption { + type = types.bool; + default = true; + description = '' + Wether to enable email registration. + ''; + }; + allowGravatar = mkOption { + type = types.bool; + default = true; + description = '' + Whether to use gravatar as profile picture source. + ''; + }; + imageUploadType = mkOption { + type = types.enum [ "imgur" "s3" "minio" "filesystem" ]; + default = "filesystem"; + description = '' + Specify where to upload images. + ''; + }; + minio = mkOption { + type = types.nullOr (types.submodule { + options = { + accessKey = mkOption { + type = types.str; + default = ""; + description = '' + Minio access key. + ''; + }; + secretKey = mkOption { + type = types.str; + default = ""; + description = '' + Minio secret key. + ''; + }; + endpoint = mkOption { + type = types.str; + default = ""; + description = '' + Minio endpoint. + ''; + }; + port = mkOption { + type = types.int; + default = 9000; + description = '' + Minio listen port. + ''; + }; + secure = mkOption { + type = types.bool; + default = true; + description = '' + Whether to use HTTPS for Minio. + ''; + }; + }; + }); + default = null; + description = "Configure the minio third-party integration."; + }; + s3 = mkOption { + type = types.nullOr (types.submodule { + options = { + accessKeyId = mkOption { + type = types.str; + default = ""; + description = '' + AWS access key id. + ''; + }; + secretAccessKey = mkOption { + type = types.str; + default = ""; + description = '' + AWS access key. + ''; + }; + region = mkOption { + type = types.str; + default = ""; + description = '' + AWS S3 region. + ''; + }; + }; + }); + default = null; + description = "Configure the s3 third-party integration."; + }; + s3bucket = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Specify the bucket name for upload types s3 and minio. + ''; + }; + allowPDFExport = mkOption { + type = types.bool; + default = true; + description = '' + Whether to enable PDF exports. + ''; + }; + imgur.clientId = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Imgur API client ID. + ''; + }; + azure = mkOption { + type = types.nullOr (types.submodule { + options = { + connectionString = mkOption { + type = types.str; + default = ""; + description = '' + Azure Blob Storage connection string. + ''; + }; + container = mkOption { + type = types.str; + default = ""; + description = '' + Azure Blob Storage container name. + It will be created if non-existent. + ''; + }; + }; + }); + default = null; + description = "Configure the azure third-party integration."; + }; + oauth2 = mkOption { + type = types.nullOr (types.submodule { + options = { + authorizationURL = mkOption { + type = types.str; + default = ""; + description = '' + Specify the OAuth authorization URL. + ''; + }; + tokenURL = mkOption { + type = types.str; + default = ""; + description = '' + Specify the OAuth token URL. + ''; + }; + clientID = mkOption { + type = types.str; + default = ""; + description = '' + Specify the OAuth client ID. + ''; + }; + clientSecret = mkOption { + type = types.str; + default = ""; + description = '' + Specify the OAuth client secret. + ''; + }; + }; + }); + default = null; + description = "Configure the OAuth integration."; + }; + facebook = mkOption { + type = types.nullOr (types.submodule { + options = { + clientID = mkOption { + type = types.str; + default = ""; + description = '' + Facebook API client ID. + ''; + }; + clientSecret = mkOption { + type = types.str; + default = ""; + description = '' + Facebook API client secret. + ''; + }; + }; + }); + default = null; + description = "Configure the facebook third-party integration"; + }; + twitter = mkOption { + type = types.nullOr (types.submodule { + options = { + consumerKey = mkOption { + type = types.str; + default = ""; + description = '' + Twitter API consumer key. + ''; + }; + consumerSecret = mkOption { + type = types.str; + default = ""; + description = '' + Twitter API consumer secret. + ''; + }; + }; + }); + default = null; + description = "Configure the Twitter third-party integration."; + }; + github = mkOption { + type = types.nullOr (types.submodule { + options = { + clientID = mkOption { + type = types.str; + default = ""; + description = '' + GitHub API client ID. + ''; + }; + clientSecret = mkOption { + type = types.str; + default = ""; + description = '' + Github API client secret. + ''; + }; + }; + }); + default = null; + description = "Configure the GitHub third-party integration."; + }; + gitlab = mkOption { + type = types.nullOr (types.submodule { + options = { + baseURL = mkOption { + type = types.str; + default = ""; + description = '' + GitLab API authentication endpoint. + Only needed for other endpoints than gitlab.com. + ''; + }; + clientID = mkOption { + type = types.str; + default = ""; + description = '' + GitLab API client ID. + ''; + }; + clientSecret = mkOption { + type = types.str; + default = ""; + description = '' + GitLab API client secret. + ''; + }; + scope = mkOption { + type = types.enum [ "api" "read_user" ]; + default = "api"; + description = '' + GitLab API requested scope. + GitLab snippet import/export requires api scope. + ''; + }; + }; + }); + default = null; + description = "Configure the GitLab third-party integration."; + }; + mattermost = mkOption { + type = types.nullOr (types.submodule { + options = { + baseURL = mkOption { + type = types.str; + default = ""; + description = '' + Mattermost authentication endpoint. + ''; + }; + clientID = mkOption { + type = types.str; + default = ""; + description = '' + Mattermost API client ID. + ''; + }; + clientSecret = mkOption { + type = types.str; + default = ""; + description = '' + Mattermost API client secret. + ''; + }; + }; + }); + default = null; + description = "Configure the Mattermost third-party integration."; + }; + dropbox = mkOption { + type = types.nullOr (types.submodule { + options = { + clientID = mkOption { + type = types.str; + default = ""; + description = '' + Dropbox API client ID. + ''; + }; + clientSecret = mkOption { + type = types.str; + default = ""; + description = '' + Dropbox API client secret. + ''; + }; + appKey = mkOption { + type = types.str; + default = ""; + description = '' + Dropbox app key. + ''; + }; + }; + }); + default = null; + description = "Configure the Dropbox third-party integration."; + }; + google = mkOption { + type = types.nullOr (types.submodule { + options = { + clientID = mkOption { + type = types.str; + default = ""; + description = '' + Google API client ID. + ''; + }; + clientSecret = mkOption { + type = types.str; + default = ""; + description = '' + Google API client secret. + ''; + }; + }; + }); + default = null; + description = "Configure the Google third-party integration."; + }; + ldap = mkOption { + type = types.nullOr (types.submodule { + options = { + providerName = mkOption { + type = types.str; + default = ""; + description = '' + Optional name to be displayed at login form, indicating the LDAP provider. + ''; + }; + url = mkOption { + type = types.str; + default = ""; + example = "ldap://localhost"; + description = '' + URL of LDAP server. + ''; + }; + bindDn = mkOption { + type = types.str; + default = ""; + description = '' + Bind DN for LDAP access. + ''; + }; + bindCredentials = mkOption { + type = types.str; + default = ""; + description = '' + Bind credentials for LDAP access. + ''; + }; + searchBase = mkOption { + type = types.str; + default = ""; + example = "o=users,dc=example,dc=com"; + description = '' + LDAP directory to begin search from. + ''; + }; + searchFilter = mkOption { + type = types.str; + default = ""; + example = "(uid={{username}})"; + description = '' + LDAP filter to search with. + ''; + }; + searchAttributes = mkOption { + type = types.listOf types.str; + default = []; + example = [ "displayName" "mail" ]; + description = '' + LDAP attributes to search with. + ''; + }; + userNameField = mkOption { + type = types.str; + default = ""; + description = '' + LDAP field which is used as the username on CodiMD. + By default is used. + ''; + }; + useridField = mkOption { + type = types.str; + default = ""; + example = "uid"; + description = '' + LDAP field which is a unique identifier for users on CodiMD. + ''; + }; + tlsca = mkOption { + type = types.str; + default = ""; + example = "server-cert.pem,root.pem"; + description = '' + Root CA for LDAP TLS in PEM format. + ''; + }; + }; + }); + default = null; + description = "Configure the LDAP integration."; + }; + saml = mkOption { + type = types.nullOr (types.submodule { + options = { + idpSsoUrl = mkOption { + type = types.str; + default = ""; + example = "https://idp.example.com/sso"; + description = '' + IdP authentication endpoint. + ''; + }; + idPCert = mkOption { + type = types.str; + default = ""; + example = "/path/to/cert.pem"; + description = '' + Path to IdP certificate file in PEM format. + ''; + }; + issuer = mkOption { + type = types.str; + default = ""; + description = '' + 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 = '' + Optional name identifier format. + ''; + }; + groupAttribute = mkOption { + type = types.str; + default = ""; + example = "memberOf"; + description = '' + Optional attribute name for group list. + ''; + }; + externalGroups = mkOption { + type = types.listOf types.str; + default = []; + example = [ "Temporary-staff" "External-users" ]; + description = '' + Excluded group names. + ''; + }; + requiredGroups = mkOption { + type = types.listOf types.str; + default = []; + example = [ "Hackmd-users" "Codimd-users" ]; + description = '' + Required group names. + ''; + }; + attribute = { + id = mkOption { + type = types.str; + default = ""; + description = '' + Attribute map for `id'. + Defaults to `NameID' of SAML response. + ''; + }; + username = mkOption { + type = types.str; + default = ""; + description = '' + Attribute map for `username'. + Defaults to `NameID' of SAML response. + ''; + }; + email = mkOption { + type = types.str; + default = ""; + description = '' + Attribute map for `email'. + Defaults to `NameID' of SAML response if + has + the default value. + ''; + }; + }; + }; + }); + default = null; + description = "Configure the SAML integration."; + }; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { assertion = cfg.configuration.db == {} -> ( + cfg.configuration.dbURL != "" && cfg.configuration.dbURL != null + ); + message = "Database configuration for CodiMD missing."; } + ]; + users.groups.codimd = {}; + users.users.codimd = { + description = "CodiMD service user"; + group = "codimd"; + extraGroups = cfg.groups; + home = cfg.workDir; + createHome = true; + }; + + systemd.services.codimd = { + description = "CodiMD Service"; + wantedBy = [ "multi-user.target" ]; + after = [ "networking.target" ]; + preStart = '' + mkdir -p ${cfg.workDir} + chown -R codimd: ${cfg.workDir} + ''; + serviceConfig = { + WorkingDirectory = cfg.workDir; + ExecStart = "${pkgs.codimd}/bin/codimd"; + Environment = [ + "CMD_CONFIG_FILE=${prettyJSON cfg.configuration}" + "NODE_ENV=production" + ]; + Restart = "always"; + User = "codimd"; + PermissionsStartOnly = true; + PrivateTmp = true; + }; + }; + }; +}