1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 cfg = config.services.pgadmin;
9
10 _base = with lib.types; [
11 int
12 bool
13 str
14 ];
15 base =
16 with lib.types;
17 oneOf (
18 [
19 (listOf (oneOf _base))
20 (attrsOf (oneOf _base))
21 ]
22 ++ _base
23 );
24
25 formatAttrset =
26 attr:
27 "{${
28 lib.concatStringsSep "\n" (
29 lib.mapAttrsToList (key: value: "${builtins.toJSON key}: ${formatPyValue value},") attr
30 )
31 }}";
32
33 formatPyValue =
34 value:
35 if builtins.isString value then
36 builtins.toJSON value
37 else if value ? _expr then
38 value._expr
39 else if builtins.isInt value then
40 toString value
41 else if builtins.isBool value then
42 (if value then "True" else "False")
43 else if builtins.isAttrs value then
44 (formatAttrset value)
45 else if builtins.isList value then
46 "[${lib.concatStringsSep "\n" (map (v: "${formatPyValue v},") value)}]"
47 else
48 throw "Unrecognized type";
49
50 formatPy =
51 attrs:
52 lib.concatStringsSep "\n" (
53 lib.mapAttrsToList (key: value: "${key} = ${formatPyValue value}") attrs
54 );
55
56 pyType =
57 with lib.types;
58 attrsOf (oneOf [
59 (attrsOf base)
60 (listOf base)
61 base
62 ]);
63in
64{
65 options.services.pgadmin = {
66 enable = lib.mkEnableOption "PostgreSQL Admin 4";
67
68 port = lib.mkOption {
69 description = "Port for pgadmin4 to run on";
70 type = lib.types.port;
71 default = 5050;
72 };
73
74 package = lib.mkPackageOption pkgs "pgadmin4" { };
75
76 initialEmail = lib.mkOption {
77 description = "Initial email for the pgAdmin account";
78 type = lib.types.str;
79 };
80
81 initialPasswordFile = lib.mkOption {
82 description = ''
83 Initial password file for the pgAdmin account. Minimum length by default is 6.
84 Please see `services.pgadmin.minimumPasswordLength`.
85 NOTE: Should be string not a store path, to prevent the password from being world readable
86 '';
87 type = lib.types.path;
88 };
89
90 minimumPasswordLength = lib.mkOption {
91 description = "Minimum length of the password";
92 type = lib.types.int;
93 default = 6;
94 };
95
96 emailServer = {
97 enable = lib.mkOption {
98 description = ''
99 Enable SMTP email server. This is necessary, if you want to use password recovery or change your own password
100 '';
101 type = lib.types.bool;
102 default = false;
103 };
104 address = lib.mkOption {
105 description = "SMTP server for email delivery";
106 type = lib.types.str;
107 default = "localhost";
108 };
109 port = lib.mkOption {
110 description = "SMTP server port for email delivery";
111 type = lib.types.port;
112 default = 25;
113 };
114 useSSL = lib.mkOption {
115 description = "SMTP server should use SSL";
116 type = lib.types.bool;
117 default = false;
118 };
119 useTLS = lib.mkOption {
120 description = "SMTP server should use TLS";
121 type = lib.types.bool;
122 default = false;
123 };
124 username = lib.mkOption {
125 description = "SMTP server username for email delivery";
126 type = lib.types.nullOr lib.types.str;
127 default = null;
128 };
129 sender = lib.mkOption {
130 description = ''
131 SMTP server sender email for email delivery. Some servers require this to be a valid email address from that server
132 '';
133 type = lib.types.str;
134 example = "noreply@example.com";
135 };
136 passwordFile = lib.mkOption {
137 description = ''
138 Password for SMTP email account.
139 NOTE: Should be string not a store path, to prevent the password from being world readable
140 '';
141 type = lib.types.path;
142 };
143 };
144
145 openFirewall = lib.mkEnableOption "firewall passthrough for pgadmin4";
146
147 settings = lib.mkOption {
148 description = ''
149 Settings for pgadmin4.
150 [Documentation](https://www.pgadmin.org/docs/pgadmin4/development/config_py.html)
151 '';
152 type = pyType;
153 default = { };
154 };
155 };
156
157 config = lib.mkIf (cfg.enable) {
158 networking.firewall.allowedTCPPorts = lib.mkIf (cfg.openFirewall) [ cfg.port ];
159
160 services.pgadmin.settings =
161 {
162 DEFAULT_SERVER_PORT = cfg.port;
163 PASSWORD_LENGTH_MIN = cfg.minimumPasswordLength;
164 SERVER_MODE = true;
165 UPGRADE_CHECK_ENABLED = false;
166 }
167 // (lib.optionalAttrs cfg.openFirewall {
168 DEFAULT_SERVER = lib.mkDefault "::";
169 })
170 // (lib.optionalAttrs cfg.emailServer.enable {
171 MAIL_SERVER = cfg.emailServer.address;
172 MAIL_PORT = cfg.emailServer.port;
173 MAIL_USE_SSL = cfg.emailServer.useSSL;
174 MAIL_USE_TLS = cfg.emailServer.useTLS;
175 MAIL_USERNAME = cfg.emailServer.username;
176 SECURITY_EMAIL_SENDER = cfg.emailServer.sender;
177 });
178
179 systemd.services.pgadmin = {
180 wantedBy = [ "multi-user.target" ];
181 after = [ "network.target" ];
182 requires = [ "network.target" ];
183 # we're adding this optionally so just in case there's any race it'll be caught
184 # in case postgres doesn't start, pgadmin will just start normally
185 wants = [ "postgresql.service" ];
186
187 path = [
188 config.services.postgresql.package
189 pkgs.coreutils
190 pkgs.bash
191 ];
192
193 preStart = ''
194 # NOTE: this is idempotent (aka running it twice has no effect)
195 # Check here for password length to prevent pgadmin from starting
196 # and presenting a hard to find error message
197 # see https://github.com/NixOS/nixpkgs/issues/270624
198 PW_FILE="$CREDENTIALS_DIRECTORY/initial_password"
199 PW_LENGTH=$(wc -m < "$PW_FILE")
200 if [ $PW_LENGTH -lt ${toString cfg.minimumPasswordLength} ]; then
201 echo "Password must be at least ${toString cfg.minimumPasswordLength} characters long"
202 exit 1
203 fi
204 (
205 # Email address:
206 echo ${lib.escapeShellArg cfg.initialEmail}
207
208 # file might not contain newline. echo hack fixes that.
209 PW=$(cat "$PW_FILE")
210
211 # Password:
212 echo "$PW"
213 # Retype password:
214 echo "$PW"
215 ) | ${cfg.package}/bin/pgadmin4-cli setup-db
216 '';
217
218 restartTriggers = [
219 "/etc/pgadmin/config_system.py"
220 ];
221
222 serviceConfig = {
223 User = "pgadmin";
224 DynamicUser = true;
225 LogsDirectory = "pgadmin";
226 StateDirectory = "pgadmin";
227 ExecStart = "${cfg.package}/bin/pgadmin4";
228 LoadCredential = [
229 "initial_password:${cfg.initialPasswordFile}"
230 ] ++ lib.optional cfg.emailServer.enable "email_password:${cfg.emailServer.passwordFile}";
231 };
232 };
233
234 users.users.pgadmin = {
235 isSystemUser = true;
236 group = "pgadmin";
237 };
238
239 users.groups.pgadmin = { };
240
241 environment.etc."pgadmin/config_system.py" = {
242 text =
243 lib.optionalString cfg.emailServer.enable ''
244 import os
245 with open(os.path.join(os.environ['CREDENTIALS_DIRECTORY'], 'email_password')) as f:
246 pw = f.read()
247 MAIL_PASSWORD = pw
248 ''
249 + formatPy cfg.settings;
250 mode = "0600";
251 user = "pgadmin";
252 group = "pgadmin";
253 };
254 };
255}