mirror of
https://github.com/vale981/phoebe
synced 2025-03-04 09:21:40 -05:00
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:
parent
3a322a114a
commit
b2fd566c36
8 changed files with 380 additions and 72 deletions
|
@ -4,7 +4,7 @@
|
|||
|
||||
pkgs.stdenvNoCC.mkDerivation rec {
|
||||
name = "phoebe-${version}";
|
||||
version = "0.1";
|
||||
version = "0.2";
|
||||
src = ./.;
|
||||
|
||||
phases =
|
||||
|
|
79
modules/services/databases/postgresql/create-db.sh
Executable file
79
modules/services/databases/postgresql/create-db.sh
Executable 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
|
108
modules/services/databases/postgresql/create-grant.sh
Executable file
108
modules/services/databases/postgresql/create-grant.sh
Executable 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"
|
|
@ -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
|
||||
|
|
|
@ -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$;
|
||||
|
|
|
@ -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));
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
27
modules/services/databases/postgresql/nologin.sh
Executable file
27
modules/services/databases/postgresql/nologin.sh
Executable 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
|
|
@ -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; {
|
Loading…
Add table
Reference in a new issue