at master 8.0 kB view raw
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 DEFAULT_SERVER_PORT = cfg.port; 162 PASSWORD_LENGTH_MIN = cfg.minimumPasswordLength; 163 SERVER_MODE = true; 164 UPGRADE_CHECK_ENABLED = false; 165 } 166 // (lib.optionalAttrs cfg.openFirewall { 167 DEFAULT_SERVER = lib.mkDefault "::"; 168 }) 169 // (lib.optionalAttrs cfg.emailServer.enable { 170 MAIL_SERVER = cfg.emailServer.address; 171 MAIL_PORT = cfg.emailServer.port; 172 MAIL_USE_SSL = cfg.emailServer.useSSL; 173 MAIL_USE_TLS = cfg.emailServer.useTLS; 174 MAIL_USERNAME = cfg.emailServer.username; 175 SECURITY_EMAIL_SENDER = cfg.emailServer.sender; 176 }); 177 178 systemd.services.pgadmin = { 179 wantedBy = [ "multi-user.target" ]; 180 after = [ "network.target" ]; 181 requires = [ "network.target" ]; 182 # we're adding this optionally so just in case there's any race it'll be caught 183 # in case postgres doesn't start, pgadmin will just start normally 184 wants = [ "postgresql.target" ]; 185 186 path = [ 187 config.services.postgresql.package 188 pkgs.coreutils 189 pkgs.bash 190 ]; 191 192 preStart = '' 193 # NOTE: this is idempotent (aka running it twice has no effect) 194 # Check here for password length to prevent pgadmin from starting 195 # and presenting a hard to find error message 196 # see https://github.com/NixOS/nixpkgs/issues/270624 197 PW_FILE="$CREDENTIALS_DIRECTORY/initial_password" 198 PW_LENGTH=$(wc -m < "$PW_FILE") 199 if [ $PW_LENGTH -lt ${toString cfg.minimumPasswordLength} ]; then 200 echo "Password must be at least ${toString cfg.minimumPasswordLength} characters long" 201 exit 1 202 fi 203 ( 204 # Email address: 205 echo ${lib.escapeShellArg cfg.initialEmail} 206 207 # file might not contain newline. echo hack fixes that. 208 PW=$(cat "$PW_FILE") 209 210 # Password: 211 echo "$PW" 212 # Retype password: 213 echo "$PW" 214 ) | ${cfg.package}/bin/pgadmin4-cli setup-db 215 ''; 216 217 restartTriggers = [ 218 "/etc/pgadmin/config_system.py" 219 ]; 220 221 serviceConfig = { 222 User = "pgadmin"; 223 DynamicUser = true; 224 LogsDirectory = "pgadmin"; 225 StateDirectory = "pgadmin"; 226 ExecStart = "${cfg.package}/bin/pgadmin4"; 227 LoadCredential = [ 228 "initial_password:${cfg.initialPasswordFile}" 229 ] 230 ++ lib.optional cfg.emailServer.enable "email_password:${cfg.emailServer.passwordFile}"; 231 AmbientCapabilities = ""; 232 CapabilityBoundingSet = ""; 233 LockPersonality = true; 234 MemoryDenyWriteExecute = true; 235 NoNewPrivileges = true; 236 PrivateDevices = true; 237 PrivateMounts = true; 238 PrivateTmp = true; 239 ProtectClock = true; 240 ProtectControlGroups = true; 241 ProtectHome = true; 242 ProtectHostname = true; 243 ProtectKernelLogs = true; 244 ProtectKernelModules = true; 245 ProtectKernelTunables = true; 246 ProtectSystem = "full"; 247 RemoveIPC = true; 248 RestrictAddressFamilies = [ 249 "AF_UNIX" 250 "AF_INET" 251 "AF_INET6" 252 ]; 253 RestrictNamespaces = true; 254 RestrictRealtime = true; 255 RestrictSUIDSGID = true; 256 SystemCallArchitectures = "native"; 257 UMask = 27; 258 }; 259 }; 260 261 users.users.pgadmin = { 262 isSystemUser = true; 263 group = "pgadmin"; 264 }; 265 266 users.groups.pgadmin = { }; 267 268 environment.etc."pgadmin/config_system.py" = { 269 text = 270 lib.optionalString cfg.emailServer.enable '' 271 import os 272 with open(os.path.join(os.environ['CREDENTIALS_DIRECTORY'], 'email_password')) as f: 273 pw = f.read() 274 MAIL_PASSWORD = pw 275 '' 276 + formatPy cfg.settings; 277 mode = "0600"; 278 user = "pgadmin"; 279 group = "pgadmin"; 280 }; 281 }; 282}