1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.bitwarden_rs;
7 user = config.users.users.bitwarden_rs.name;
8 group = config.users.groups.bitwarden_rs.name;
9
10 # Convert name from camel case (e.g. disable2FARemember) to upper case snake case (e.g. DISABLE_2FA_REMEMBER).
11 nameToEnvVar = name:
12 let
13 parts = builtins.split "([A-Z0-9]+)" name;
14 partsToEnvVar = parts: foldl' (key: x: let last = stringLength key - 1; in
15 if isList x then key + optionalString (key != "" && substring last 1 key != "_") "_" + head x
16 else if key != "" && elem (substring 0 1 x) lowerChars then # to handle e.g. [ "disable" [ "2FAR" ] "emember" ]
17 substring 0 last key + optionalString (substring (last - 1) 1 key != "_") "_" + substring last 1 key + toUpper x
18 else key + toUpper x) "" parts;
19 in if builtins.match "[A-Z0-9_]+" name != null then name else partsToEnvVar parts;
20
21 # Due to the different naming schemes allowed for config keys,
22 # we can only check for values consistently after converting them to their corresponding environment variable name.
23 configEnv =
24 let
25 configEnv = listToAttrs (concatLists (mapAttrsToList (name: value:
26 if value != null then [ (nameValuePair (nameToEnvVar name) (if isBool value then boolToString value else toString value)) ] else []
27 ) cfg.config));
28 in { DATA_FOLDER = "/var/lib/bitwarden_rs"; } // optionalAttrs (!(configEnv ? WEB_VAULT_ENABLED) || configEnv.WEB_VAULT_ENABLED == "true") {
29 WEB_VAULT_FOLDER = "${pkgs.bitwarden_rs-vault}/share/bitwarden_rs/vault";
30 } // configEnv;
31
32 configFile = pkgs.writeText "bitwarden_rs.env" (concatStrings (mapAttrsToList (name: value: "${name}=${value}\n") configEnv));
33
34 bitwarden_rs = pkgs.bitwarden_rs.override { inherit (cfg) dbBackend; };
35
36in {
37 options.services.bitwarden_rs = with types; {
38 enable = mkEnableOption "bitwarden_rs";
39
40 dbBackend = mkOption {
41 type = enum [ "sqlite" "mysql" "postgresql" ];
42 default = "sqlite";
43 description = ''
44 Which database backend bitwarden_rs will be using.
45 '';
46 };
47
48 backupDir = mkOption {
49 type = nullOr str;
50 default = null;
51 description = ''
52 The directory under which bitwarden_rs will backup its persistent data.
53 '';
54 };
55
56 config = mkOption {
57 type = attrsOf (nullOr (oneOf [ bool int str ]));
58 default = {};
59 example = literalExample ''
60 {
61 domain = "https://bw.domain.tld:8443";
62 signupsAllowed = true;
63 rocketPort = 8222;
64 rocketLog = "critical";
65 }
66 '';
67 description = ''
68 The configuration of bitwarden_rs is done through environment variables,
69 therefore the names are converted from camel case (e.g. disable2FARemember)
70 to upper case snake case (e.g. DISABLE_2FA_REMEMBER).
71 In this conversion digits (0-9) are handled just like upper case characters,
72 so foo2 would be converted to FOO_2.
73 Names already in this format remain unchanged, so FOO2 remains FOO2 if passed as such,
74 even though foo2 would have been converted to FOO_2.
75 This allows working around any potential future conflicting naming conventions.
76
77 Based on the attributes passed to this config option an environment file will be generated
78 that is passed to bitwarden_rs's systemd service.
79
80 The available configuration options can be found in
81 <link xlink:href="https://github.com/dani-garcia/bitwarden_rs/blob/${bitwarden_rs.version}/.env.template">the environment template file</link>.
82 '';
83 };
84
85 environmentFile = mkOption {
86 type = with types; nullOr path;
87 default = null;
88 example = "/root/bitwarden_rs.env";
89 description = ''
90 Additional environment file as defined in <citerefentry>
91 <refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum>
92 </citerefentry>.
93
94 Secrets like <envar>ADMIN_TOKEN</envar> and <envar>SMTP_PASSWORD</envar>
95 may be passed to the service without adding them to the world-readable Nix store.
96
97 Note that this file needs to be available on the host on which
98 <literal>bitwarden_rs</literal> is running.
99 '';
100 };
101 };
102
103 config = mkIf cfg.enable {
104 assertions = [ {
105 assertion = cfg.backupDir != null -> cfg.dbBackend == "sqlite";
106 message = "Backups for database backends other than sqlite will need customization";
107 } ];
108
109 users.users.bitwarden_rs = {
110 inherit group;
111 isSystemUser = true;
112 };
113 users.groups.bitwarden_rs = { };
114
115 systemd.services.bitwarden_rs = {
116 after = [ "network.target" ];
117 path = with pkgs; [ openssl ];
118 serviceConfig = {
119 User = user;
120 Group = group;
121 EnvironmentFile = [ configFile ] ++ optional (cfg.environmentFile != null) cfg.environmentFile;
122 ExecStart = "${bitwarden_rs}/bin/bitwarden_rs";
123 LimitNOFILE = "1048576";
124 LimitNPROC = "64";
125 PrivateTmp = "true";
126 PrivateDevices = "true";
127 ProtectHome = "true";
128 ProtectSystem = "strict";
129 AmbientCapabilities = "CAP_NET_BIND_SERVICE";
130 StateDirectory = "bitwarden_rs";
131 };
132 wantedBy = [ "multi-user.target" ];
133 };
134
135 systemd.services.backup-bitwarden_rs = mkIf (cfg.backupDir != null) {
136 description = "Backup bitwarden_rs";
137 environment = {
138 DATA_FOLDER = "/var/lib/bitwarden_rs";
139 BACKUP_FOLDER = cfg.backupDir;
140 };
141 path = with pkgs; [ sqlite ];
142 serviceConfig = {
143 SyslogIdentifier = "backup-bitwarden_rs";
144 Type = "oneshot";
145 User = mkDefault user;
146 Group = mkDefault group;
147 ExecStart = "${pkgs.bash}/bin/bash ${./backup.sh}";
148 };
149 wantedBy = [ "multi-user.target" ];
150 };
151
152 systemd.timers.backup-bitwarden_rs = mkIf (cfg.backupDir != null) {
153 description = "Backup bitwarden_rs on time";
154 timerConfig = {
155 OnCalendar = mkDefault "23:00";
156 Persistent = "true";
157 Unit = "backup-bitwarden_rs.service";
158 };
159 wantedBy = [ "multi-user.target" ];
160 };
161 };
162}