340 lines
10 KiB
Nix
340 lines
10 KiB
Nix
{ config, lib, pkgs, environment, ... }:
|
|
|
|
with lib;
|
|
let
|
|
cfg = config.fudo.postgresql;
|
|
|
|
utils = import ../../lib/utils.nix { inherit lib; };
|
|
|
|
join-lines = lib.concatStringsSep "\n";
|
|
|
|
userDatabaseOpts = { database, ... }: {
|
|
options = {
|
|
access = mkOption {
|
|
type = types.str;
|
|
description = "Privileges for user on this database.";
|
|
default = "CONNECT";
|
|
};
|
|
|
|
entity-access = mkOption {
|
|
type = with types; attrsOf str;
|
|
description = "A list of entities mapped to the access this user should have.";
|
|
default = {};
|
|
example = {
|
|
"TABLE users" = "SELECT,DELETE";
|
|
"ALL SEQUENCES IN public" = "SELECT";
|
|
};
|
|
};
|
|
};
|
|
};
|
|
|
|
userOpts = { username, ... }: {
|
|
options = {
|
|
password-file = mkOption {
|
|
type = with types; nullOr str;
|
|
description = "A file containing the user's (plaintext) password.";
|
|
default = null;
|
|
};
|
|
|
|
databases = mkOption {
|
|
type = with types; attrsOf (submodule userDatabaseOpts);
|
|
description = "Map of databases to required database/table perms.";
|
|
default = {};
|
|
example = {
|
|
my_database = {
|
|
access = "ALL PRIVILEGES";
|
|
entity-access = {
|
|
"ALL TABLES" = "SELECT";
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
|
|
databaseOpts = { dbname, ... }: {
|
|
options = {
|
|
users = mkOption {
|
|
type = with types; listOf str;
|
|
description = "A list of users who should have full access to this database.";
|
|
default = [];
|
|
};
|
|
};
|
|
};
|
|
|
|
filterPasswordedUsers = filterAttrs (user: opts: opts.password-file != null);
|
|
|
|
password-setter-script = user: password-file: sql-file: ''
|
|
unset PASSWORD
|
|
if [ ! -f ${password-file} ]; then
|
|
echo "file does not exist: ${password-file}"
|
|
exit 1
|
|
fi
|
|
PASSWORD=$(cat ${password-file})
|
|
echo "setting password for user ${user}"
|
|
echo "ALTER USER ${user} ENCRYPTED PASSWORD '$PASSWORD';" >> ${sql-file}
|
|
'';
|
|
|
|
passwords-setter-script = users:
|
|
pkgs.writeScriptBin "postgres-set-passwords.sh" ''
|
|
#!${pkgs.bash}/bin/bash
|
|
|
|
if [ $# -ne 1 ]; then
|
|
echo "usage: $0 output-file.sql"
|
|
exit 1
|
|
fi
|
|
|
|
OUTPUT_FILE=$1
|
|
|
|
if [ ! -f $OUTPUT_FILE ]; then
|
|
echo "file doesn't exist: $OUTPUT_FILE"
|
|
exit 2
|
|
fi
|
|
|
|
${join-lines
|
|
(mapAttrsToList
|
|
(user: opts: password-setter-script user opts.password-file "$OUTPUT_FILE")
|
|
(filterPasswordedUsers users))}
|
|
'';
|
|
|
|
userDatabaseAccess = user: databases:
|
|
mapAttrs' (database: databaseOpts:
|
|
nameValuePair "DATABASE ${database}" databaseOpts.access)
|
|
databases;
|
|
|
|
makeEntry = nw:
|
|
"host all all ${nw} gss include_realm=0 krb_realm=FUDO.ORG";
|
|
|
|
makeNetworksEntry = networks: join-lines (map makeEntry networks);
|
|
|
|
makeLocalUserPasswordEntries = users:
|
|
join-lines (mapAttrsToList
|
|
(user: opts: join-lines
|
|
(map (db: ''
|
|
local ${db} ${user} md5
|
|
host ${db} ${user} 127.0.0.1/16 md5
|
|
host ${db} ${user} ::1/128 md5
|
|
'') (attrNames opts.databases)))
|
|
(filterPasswordedUsers users));
|
|
|
|
userTableAccessSql = user: entity: access: "GRANT ${access} ON ${entity} TO ${user};";
|
|
userDatabaseAccessSql = user: database: dbOpts: ''
|
|
\c ${database}
|
|
${join-lines (mapAttrsToList (userTableAccessSql user) dbOpts.entity-access)}
|
|
'';
|
|
userAccessSql = user: userOpts: join-lines (mapAttrsToList (userDatabaseAccessSql user) userOpts.databases);
|
|
usersAccessSql = users: join-lines (mapAttrsToList userAccessSql users);
|
|
|
|
in {
|
|
|
|
options.fudo.postgresql = {
|
|
enable = mkEnableOption "Fudo PostgreSQL Server";
|
|
|
|
ssl-private-key = mkOption {
|
|
type = types.str;
|
|
description = "Location of the server SSL private key.";
|
|
};
|
|
|
|
ssl-certificate = mkOption {
|
|
type = types.str;
|
|
description = "Location of the server SSL certificate.";
|
|
};
|
|
|
|
keytab = mkOption {
|
|
type = types.str;
|
|
description = "Location of the server Kerberos keytab.";
|
|
};
|
|
|
|
local-networks = mkOption {
|
|
type = with types; listOf str;
|
|
description = "A list of networks from which to accept connections.";
|
|
example = [
|
|
"10.0.0.1/16"
|
|
];
|
|
default = [];
|
|
};
|
|
|
|
users = mkOption {
|
|
type = with types; loaOf (submodule userOpts);
|
|
description = "A map of users to user attributes.";
|
|
example = {
|
|
sampleUser = {
|
|
password-file = "/path/to/password/file";
|
|
databases = {
|
|
some_database = {
|
|
access = "CONNECT";
|
|
entity-access = {
|
|
"TABLE some_table" = "SELECT,UPDATE";
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
default = {};
|
|
};
|
|
|
|
databases = mkOption {
|
|
type = with types; loaOf (submodule databaseOpts);
|
|
description = "A map of databases to database options.";
|
|
default = {};
|
|
};
|
|
|
|
socket-directory = mkOption {
|
|
type = types.str;
|
|
description = "Directory in which to place unix sockets.";
|
|
default = "/run/postgresql";
|
|
};
|
|
|
|
socket-group = mkOption {
|
|
type = types.str;
|
|
description = "Group for accessing sockets.";
|
|
default = "postgres_local";
|
|
};
|
|
|
|
local-users = mkOption {
|
|
type = with types; listOf str;
|
|
description = "Users able to access the server via local socket.";
|
|
default = [];
|
|
};
|
|
|
|
required-services = mkOption {
|
|
type = with types; listOf str;
|
|
description = "List of services that should run before postgresql.";
|
|
default = [];
|
|
example = [ "password-generator.service" ];
|
|
};
|
|
};
|
|
|
|
config = mkIf cfg.enable {
|
|
|
|
environment = {
|
|
systemPackages = with pkgs; [
|
|
postgresql_11_gssapi
|
|
];
|
|
|
|
etc = {
|
|
"postgresql/private/privkey.pem" = {
|
|
mode = "0400";
|
|
user = "postgres";
|
|
group = "postgres";
|
|
source = cfg.ssl-private-key;
|
|
};
|
|
|
|
"postgresql/cert.pem" = {
|
|
mode = "0444";
|
|
user = "postgres";
|
|
group = "postgres";
|
|
source = cfg.ssl-certificate;
|
|
};
|
|
|
|
"postgresql/private/postgres.keytab" = {
|
|
mode = "0400";
|
|
user = "postgres";
|
|
group = "postgres";
|
|
source = cfg.keytab;
|
|
};
|
|
};
|
|
};
|
|
|
|
users.groups = {
|
|
${cfg.socket-group} = {
|
|
members = ["postgres"] ++ cfg.local-users;
|
|
};
|
|
};
|
|
|
|
services.postgresql = {
|
|
enable = true;
|
|
package = pkgs.postgresql_11_gssapi;
|
|
enableTCPIP = true;
|
|
ensureDatabases = mapAttrsToList (name: value: name) cfg.databases;
|
|
ensureUsers = ((mapAttrsToList
|
|
(username: attrs:
|
|
{
|
|
name = username;
|
|
ensurePermissions = userDatabaseAccess username attrs.databases;
|
|
})
|
|
cfg.users) ++ (flatten (mapAttrsToList
|
|
(database: opts:
|
|
(map (username: {
|
|
name = username;
|
|
ensurePermissions = {
|
|
"DATABASE ${database}" = "ALL PRIVILEGES";
|
|
};
|
|
}) opts.users)) cfg.databases)));
|
|
|
|
extraConfig = ''
|
|
krb_server_keyfile = '/etc/postgresql/private/postgres.keytab'
|
|
|
|
ssl = true
|
|
ssl_cert_file = '/etc/postgresql/cert.pem'
|
|
ssl_key_file = '/etc/postgresql/private/privkey.pem'
|
|
|
|
unix_socket_directories = '${cfg.socket-directory}'
|
|
unix_socket_group = '${cfg.socket-group}'
|
|
unix_socket_permissions = 0777
|
|
'';
|
|
|
|
authentication = lib.mkForce ''
|
|
${makeLocalUserPasswordEntries cfg.users}
|
|
|
|
local all all ident
|
|
|
|
# host-local
|
|
host all all 127.0.0.1/32 gss include_realm=0 krb_realm=FUDO.ORG
|
|
host all all ::1/128 gss include_realm=0 krb_realm=FUDO.ORG
|
|
|
|
# local networks
|
|
${makeNetworksEntry cfg.local-networks}
|
|
'';
|
|
};
|
|
|
|
systemd = {
|
|
|
|
services = {
|
|
|
|
postgresql-password-setter = let
|
|
passwords-script = passwords-setter-script cfg.users;
|
|
password-wrapper-script = pkgs.writeScriptBin "password-script-wrapper.sh" ''
|
|
#!${pkgs.bash}/bin/bash
|
|
TMPDIR=$(${pkgs.coreutils}/bin/mktemp -d -t postgres-XXXXXXXXXX)
|
|
echo "using temp dir $TMPDIR"
|
|
PASSWORD_SQL_FILE=$TMPDIR/user-passwords.sql
|
|
echo "password file $PASSWORD_SQL_FILE"
|
|
touch $PASSWORD_SQL_FILE
|
|
chown ${config.services.postgresql.superUser} $PASSWORD_SQL_FILE
|
|
chmod go-rwx $PASSWORD_SQL_FILE
|
|
${passwords-script}/bin/postgres-set-passwords.sh $PASSWORD_SQL_FILE
|
|
echo "executing $PASSWORD_SQL_FILE"
|
|
${pkgs.postgresql}/bin/psql --port ${toString config.services.postgresql.port} -d postgres -f $PASSWORD_SQL_FILE
|
|
echo rm $PASSWORD_SQL_FILE
|
|
echo "Postgresql user passwords set.";
|
|
exit 0
|
|
'';
|
|
|
|
in {
|
|
description = "A service to set postgresql user passwords after the server has started.";
|
|
after = [ "postgresql.service" ] ++ cfg.required-services;
|
|
requires = [ "postgresql.service" ] ++ cfg.required-services;
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
User = config.services.postgresql.superUser;
|
|
};
|
|
script = "${password-wrapper-script}/bin/password-script-wrapper.sh";
|
|
};
|
|
|
|
postgresql.postStart = let
|
|
allow-user-login = user: "ALTER ROLE ${user} WITH LOGIN;";
|
|
|
|
extra-settings-sql = pkgs.writeText "settings.sql" ''
|
|
${concatStringsSep "\n" (map allow-user-login (mapAttrsToList (key: val: key) cfg.users))}
|
|
${usersAccessSql cfg.users}
|
|
'';
|
|
in ''
|
|
${pkgs.sudo}/bin/sudo -u ${config.services.postgresql.superUser} ${pkgs.postgresql}/bin/psql --port ${toString config.services.postgresql.port} -d postgres -f ${extra-settings-sql}
|
|
${pkgs.coreutils}/bin/chgrp ${cfg.socket-group} ${cfg.socket-directory}/.s.PGSQL*
|
|
'';
|
|
};
|
|
};
|
|
};
|
|
}
|