Breaking Change: Completely Change PostgreSQL Account Management

This is a breaking change that will require you to change your Phoebe
settings for PostgreSQL.

  * New database configuration options

  * Accounts no longer automatically create databases

  * Databases have `owners' that tie them back to an account

  * Databases have `users' that grant accounts full access

  * Databases have `readers' that grant read-only access to accounts

  * Accounts can use `ident' authentication for local connections if
    you enable the `allowIdent' option.

  * Existing accounts that are not configured via Phoebe will be
    locked so they cannot be used.  That way if you delete a user from
    Phoebe the account will continue to exist, but won't have access
    to anything.
This commit is contained in:
Peter Jones 2019-04-30 08:12:06 -07:00
parent 3a322a114a
commit b2fd566c36
No known key found for this signature in database
GPG key ID: 9DAFAA8D01941E49
8 changed files with 380 additions and 72 deletions

View file

@ -4,7 +4,7 @@
pkgs.stdenvNoCC.mkDerivation rec {
name = "phoebe-${version}";
version = "0.1";
version = "0.2";
src = ./.;
phases =

View file

@ -0,0 +1,79 @@
#!/bin/bash
################################################################################
# Create a database if it's missing.
set -e
################################################################################
option_database=""
option_owner="@superuser@"
option_extensions=""
################################################################################
usage () {
cat <<EOF
Usage: create-db.sh [options]
-d NAME Database name to create
-e LIST Space-separated list of extensions to enable
-h This message
-o USER The owner of the new database.
EOF
}
################################################################################
while getopts "d:e:ho:" o; do
case "${o}" in
d) option_database=$OPTARG
;;
e) option_extensions=$OPTARG
;;
h) usage
exit
;;
o) option_owner=$OPTARG
;;
*) exit 1
;;
esac
done
shift $((OPTIND-1))
################################################################################
_psql() {
@sudo@ -u @superuser@ -H psql "$@"
}
################################################################################
create_database() {
has_db=$(_psql -tAl | cut -d'|' -f1 | grep -cF "$option_database" || :)
if [ "$has_db" -eq 0 ]; then
@sudo@ -u @superuser@ -H \
createdb --owner "$option_owner" "$option_database"
fi
}
################################################################################
enable_extensions() {
if [ -n "$option_extensions" ]; then
for ext in $option_extensions; do
_psql "$option_database" -c "CREATE EXTENSION IF NOT EXISTS $ext"
done
fi
}
################################################################################
if [ -z "$option_database" ]; then
>&2 echo "ERROR: must give -d"
exit 1;
fi
################################################################################
create_database
enable_extensions

View file

@ -0,0 +1,108 @@
#!/bin/bash
################################################################################
# Grant a user specific rights to a database.
set -e
################################################################################
option_user=""
option_database=""
option_access="r"
################################################################################
usage () {
cat <<EOF
Usage: create-grant.sh [options]
-a LEVEL Access level (r, w, or rw)
-d NAME Database name to grant access to
-h This message
-u USER The user to grant access to
EOF
}
################################################################################
verify_access_level() {
local level=$1
case $level in
r|w|rw)
echo "$level"
;;
*)
>&2 echo "ERROR: invalid access level: $level"
exit 1
esac
}
################################################################################
while getopts "a:d:hu:" o; do
case "${o}" in
a) option_access=$(verify_access_level "$OPTARG")
;;
d) option_database=$OPTARG
;;
h) usage
exit
;;
u) option_user=$OPTARG
;;
*) exit 1
;;
esac
done
shift $((OPTIND-1))
################################################################################
_psql() {
@sudo@ -u @superuser@ -H psql "$@"
}
################################################################################
echo_grants() {
local r_list="SELECT"
local w_list="INSERT,UPDATE,DELETE,TRUNCATE,REFERENCES,TRIGGER"
# Needed to resolve ambiguous role memberships.
echo "SET ROLE @superuser@;"
# Start by removing all access then granting the ability to connect:
echo "REVOKE ALL PRIVILEGES ON DATABASE $option_database FROM $option_user;"
echo "GRANT CONNECT ON DATABASE $option_database TO $option_user;"
# Basic options:
echo "GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO $option_user;"
echo "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON FUNCTIONS TO $option_user;"
if [ "$option_access" = "r" ] || [ "$option_access" = "rw" ]; then
echo "GRANT USAGE ON SCHEMA public TO $option_user;"
echo "GRANT $r_list ON ALL TABLES IN SCHEMA public TO $option_user;"
echo "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT $r_list ON TABLES TO $option_user;"
echo "GRANT USAGE,SELECT ON ALL SEQUENCES IN SCHEMA public TO $option_user;"
echo "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE,SELECT ON SEQUENCES TO $option_user;"
fi
if [ "$option_access" = "w" ] || [ "$option_access" = "rw" ]; then
echo "GRANT $w_list ON ALL TABLES IN SCHEMA public TO $option_user;"
echo "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT $w_list ON TABLES TO $option_user;"
echo "GRANT UPDATE ON ALL SEQUENCES IN SCHEMA public TO $option_user;"
echo "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT UPDATE ON SEQUENCES TO $option_user;"
fi
}
################################################################################
# Let's do it!
sql_file=$(mktemp)
echo_grants > "$sql_file"
chown @superuser@ "$sql_file"
_psql --dbname="$option_database" --file="$sql_file" --single-transaction
rm "$sql_file"

View file

@ -6,8 +6,6 @@ set -e
################################################################################
option_username=""
option_password_file=""
option_database=""
option_extensions=""
option_sqlfile="@out@/sql/create-user.sql"
option_superuser=0
@ -16,8 +14,6 @@ usage () {
cat <<EOF
Usage: create-user.sh [options]
-d NAME Database name to create for USER
-e LIST Space-separated list of extensions to enable
-h This message
-p FILE File containing USER's password
-s FILE The SQL template file (pg-create-user.sql)
@ -27,14 +23,8 @@ EOF
}
################################################################################
while getopts "d:e:hp:s:Su:" o; do
while getopts "hp:s:Su:" o; do
case "${o}" in
d) option_database=$OPTARG
;;
e) option_extensions=$OPTARG
;;
h) usage
exit
;;
@ -109,26 +99,5 @@ create_user() {
_psql -d postgres -c "ALTER ROLE $option_username $superuser"
}
################################################################################
create_database() {
has_db=$(_psql -tAl | cut -d'|' -f1 | grep -cF "$option_database" || :)
if [ "$has_db" -eq 0 ]; then
@sudo@ -u @superuser@ -H \
createdb --owner "$option_username" "$option_database"
fi
}
################################################################################
enable_extensions() {
if [ -n "$option_extensions" ]; then
for ext in $option_extensions; do
_psql "$option_database" -c "CREATE EXTENSION IF NOT EXISTS $ext"
done
fi
}
################################################################################
create_user
create_database
enable_extensions

View file

@ -6,7 +6,7 @@ BEGIN
FROM pg_catalog.pg_roles
WHERE rolname = '@@USERNAME@@') THEN
CREATE ROLE @@USERNAME@@ LOGIN ENCRYPTED PASSWORD '@@PASSWORD@@';
CREATE ROLE @@USERNAME@@ NOINHERIT LOGIN ENCRYPTED PASSWORD '@@PASSWORD@@';
END IF;
END
$body$;

View file

@ -8,9 +8,55 @@ let
cfg = config.phoebe.services.postgresql;
plib = config.phoebe.lib;
superuser = config.services.postgresql.superUser;
create-user = import ./create-user.nix { inherit config lib pkgs; };
scripts = import ./scripts.nix { inherit config lib pkgs; };
afterservices = concatMap (a: plib.keyService a.passwordFile) (attrValues cfg.accounts);
# Per-database options:
database = { name, ...}: {
#### Interface:
options = {
name = mkOption {
type = types.str;
example = "sales";
description = "The name of the database.";
};
owner = mkOption {
type = types.str;
default = superuser;
example = "jdoe";
description = "Name of the account that owns the database.";
};
users = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "alice" ];
description = "List of user names who have full access to the database.";
};
readers = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "bob" ];
description = "List of user names who have read-only access to the database";
};
extensions = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "pg_trgm" ];
description = "A list of extension modules to enable for the database.";
};
};
#### Implementation:
config = {
name = mkDefault name;
};
};
# Per-account options:
account = { name, ... }: {
@ -38,23 +84,6 @@ let
'';
};
database = mkOption {
type = types.str;
default = null;
example = "jdoe";
description = ''
The name of the database this user can access. Defaults to
the account name.
'';
};
extensions = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "pg_trgm" ];
description = "A list of extension modules to enable for the database.";
};
superuser = mkOption {
type = types.bool;
default = false;
@ -70,6 +99,16 @@ let
'';
};
allowIdent = mkOption {
type = types.bool;
default = false;
example = true;
description = ''
Whether or not this account can use ident authentication
when connecting locally.
'';
};
netmask = mkOption {
type = types.nullOr types.str;
default = null;
@ -85,31 +124,100 @@ let
#### Implementation:
config = {
user = mkDefault name;
database = mkDefault name;
};
};
# Create HBA authentication entries:
accountToHBA = account:
''
local ${account.database} ${account.user} md5
host ${account.database} ${account.user} 127.0.0.1/32 md5
host ${account.database} ${account.user} ::1/28 md5
'' + optionalString (account.netmask != null) ''
host ${account.database} ${account.user} ${account.netmask} md5
let local = if account.allowIdent then "ident" else "md5";
template = database: (''
local ${database} ${account.user} ${local}
host ${database} ${account.user} 127.0.0.1/32 md5
host ${database} ${account.user} ::1/28 md5
'' + optionalString (account.netmask != null) ''
host ${database} ${account.user} ${account.netmask} md5
'');
databases = map (d: d.name)
(filter (d: d.owner == account.user ||
elem account.user d.users ||
elem account.user d.readers)
(attrValues cfg.databases));
in if account.superuser
then template "all"
else concatMapStringsSep "\n" template databases;
# Commands to run to create accounts:
createUser = account:
let options = [
''-u "${account.user}"''
''-p "${account.passwordFile}"''
] ++ optional account.superuser "-S";
in ''
${scripts}/bin/create-user.sh ${concatStringsSep " " options}
'';
# Commands to run to create accounts/databases:
createScript = account:
# Commands to run to create databases:
createDB = database:
''
${create-user}/bin/create-user.sh \
${scripts}/bin/create-db.sh \
-d "${database.name}" \
-o "${database.owner}" \
-e "${concatStringsSep " " database.extensions}"
'';
# Commands to run to create full grants:
createGrant = database: account:
''
${scripts}/bin/create-grant.sh \
-a rw \
-u "${account.user}" \
-d "${account.database}" \
-p "${account.passwordFile}" \
-e "${concatStringsSep " " account.extensions}" \
-S "${toString account.superuser}"
-d "${database.name}"
'';
# Commands to run to create read-only grants:
createReadGrant = database: account:
''
${scripts}/bin/create-grant.sh \
-a r \
-u "${account.user}" \
-d "${database.name}"
'';
# Generate a SQL statement that allows a user to login:
allowLogin = accounts: concatMapStringsSep "\n" (account: ''
echo "ALTER ROLE ${account.user} LOGIN;"
'') accounts;
# Lock out accounts that are not configured:
lockAccounts = accounts: ''
sql_file=$(mktemp)
# Lock all accounts:
${scripts}/bin/nologin.sh > "$sql_file"
# Unlock configured accounts:
(
${allowLogin accounts}
) >> "$sql_file"
chown ${superuser} "$sql_file"
${pkgs.sudo}/bin/sudo -u ${superuser} -H \
psql --dbname="postgres" --file="$sql_file" --single-transaction
rm "$sql_file"
'';
# Master grant creation function:
createGrants = database:
let find = names: map (name: cfg.accounts."${name}")
(filter (name: cfg.accounts ? "${name}")
names);
ro = find database.readers;
rw = find (database.users ++ [database.owner]);
owner = find [database.owner];
in (concatMapStringsSep "\n" (createReadGrant database) ro) +
(concatMapStringsSep "\n" (createGrant database) rw);
in
{
#### Interface
@ -119,7 +227,13 @@ in
accounts = mkOption {
type = types.attrsOf (types.submodule account);
default = { };
description = "Additional user accounts";
description = "Additional user accounts.";
};
databases = mkOption {
type = types.attrsOf (types.submodule database);
default = { };
description = "Additional databases to create.";
};
};
@ -142,13 +256,19 @@ in
};
# Create missing accounts:
systemd.services.pg-accounts = mkIf (length (attrValues cfg.accounts) > 0) {
systemd.services.postgres-account-manager = {
description = "PostgreSQL Account Manager";
path = [ pkgs.gawk config.services.postgresql.package ];
script = (concatMapStringsSep "\n" createScript (attrValues cfg.accounts));
wantedBy = [ "postgresql.service" ];
after = [ "postgresql.service" ] ++ afterservices;
wants = afterservices;
script = ''
set -e
'' + (concatMapStringsSep "\n" createUser (attrValues cfg.accounts))
+ (lockAccounts (attrValues cfg.accounts))
+ (concatMapStringsSep "\n" createDB (attrValues cfg.databases))
+ (concatMapStringsSep "\n" createGrants (attrValues cfg.databases));
};
};
}

View file

@ -0,0 +1,27 @@
#!/bin/bash
################################################################################
# Generate SQL that locks out all users (except the superuser).
set -e
set -u
################################################################################
_psql() {
@sudo@ -u @superuser@ -H psql "$@"
}
################################################################################
accounts() {
echo "SELECT rolname FROM pg_catalog.pg_roles;" | \
_psql --tuples-only postgres | \
sed 's/^[[:space:]]*//' | \
grep --fixed-strings --invert-match --line-regexp '@superuser@' | \
grep --extended-regexp --invert-match '^pg_'
}
################################################################################
for name in $(accounts); do
if [ -n "$name" ]; then
echo "ALTER ROLE $name NOLOGIN;"
fi
done

View file

@ -10,9 +10,14 @@ pkgs.stdenvNoCC.mkDerivation {
export superuser=${config.services.postgresql.superUser}
mkdir -p $out/bin $out/sql
cp ${./create-user.sql} $out/sql/create-user.sql
substituteAll ${./create-user.sh} $out/bin/create-user.sh
chmod 555 $out/bin/create-user.sh
cp ${./create-user.sql} $out/sql/create-user.sql
substituteAll ${./create-user.sh} $out/bin/create-user.sh
substituteAll ${./create-db.sh} $out/bin/create-db.sh
substituteAll ${./create-grant.sh} $out/bin/create-grant.sh
substituteAll ${./nologin.sh} $out/bin/nologin.sh
chmod 555 $out/bin/*.sh
'';
meta = with lib; {