Browse Source

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.
master
Peter J. Jones 1 month ago
parent
commit
b2fd566c36
Signed by: Peter Jones <pjones@devalot.com> GPG Key ID: 9DAFAA8D01941E49

+ 1
- 1
default.nix View File

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

+ 79
- 0
modules/services/databases/postgresql/create-db.sh View File

@@ -0,0 +1,79 @@
1
+#!/bin/bash
2
+
3
+################################################################################
4
+# Create a database if it's missing.
5
+set -e
6
+
7
+################################################################################
8
+option_database=""
9
+option_owner="@superuser@"
10
+option_extensions=""
11
+
12
+################################################################################
13
+usage () {
14
+cat <<EOF
15
+Usage: create-db.sh [options]
16
+
17
+  -d NAME Database name to create
18
+  -e LIST Space-separated list of extensions to enable
19
+  -h      This message
20
+  -o USER The owner of the new database.
21
+EOF
22
+}
23
+
24
+################################################################################
25
+while getopts "d:e:ho:" o; do
26
+  case "${o}" in
27
+    d) option_database=$OPTARG
28
+       ;;
29
+
30
+    e) option_extensions=$OPTARG
31
+       ;;
32
+
33
+    h) usage
34
+       exit
35
+       ;;
36
+
37
+    o) option_owner=$OPTARG
38
+       ;;
39
+
40
+    *) exit 1
41
+       ;;
42
+  esac
43
+done
44
+
45
+shift $((OPTIND-1))
46
+
47
+################################################################################
48
+_psql() {
49
+  @sudo@ -u @superuser@ -H psql "$@"
50
+}
51
+
52
+################################################################################
53
+create_database() {
54
+  has_db=$(_psql -tAl | cut -d'|' -f1 | grep -cF "$option_database" || :)
55
+
56
+  if [ "$has_db" -eq 0 ]; then
57
+    @sudo@ -u @superuser@ -H \
58
+     createdb --owner "$option_owner" "$option_database"
59
+  fi
60
+}
61
+
62
+################################################################################
63
+enable_extensions() {
64
+  if [ -n "$option_extensions" ]; then
65
+    for ext in $option_extensions; do
66
+      _psql "$option_database" -c "CREATE EXTENSION IF NOT EXISTS $ext"
67
+    done
68
+  fi
69
+}
70
+
71
+################################################################################
72
+if [ -z "$option_database" ]; then
73
+  >&2 echo "ERROR: must give -d"
74
+  exit 1;
75
+fi
76
+
77
+################################################################################
78
+create_database
79
+enable_extensions

+ 108
- 0
modules/services/databases/postgresql/create-grant.sh View File

@@ -0,0 +1,108 @@
1
+#!/bin/bash
2
+
3
+################################################################################
4
+# Grant a user specific rights to a database.
5
+set -e
6
+
7
+################################################################################
8
+option_user=""
9
+option_database=""
10
+option_access="r"
11
+
12
+################################################################################
13
+usage () {
14
+cat <<EOF
15
+Usage: create-grant.sh [options]
16
+
17
+  -a LEVEL Access level (r, w, or rw)
18
+  -d NAME  Database name to grant access to
19
+  -h       This message
20
+  -u USER  The user to grant access to
21
+EOF
22
+}
23
+
24
+################################################################################
25
+verify_access_level() {
26
+  local level=$1
27
+
28
+  case $level in
29
+    r|w|rw)
30
+      echo "$level"
31
+      ;;
32
+
33
+    *)
34
+      >&2 echo "ERROR: invalid access level: $level"
35
+      exit 1
36
+  esac
37
+}
38
+
39
+################################################################################
40
+while getopts "a:d:hu:" o; do
41
+  case "${o}" in
42
+    a) option_access=$(verify_access_level "$OPTARG")
43
+       ;;
44
+
45
+    d) option_database=$OPTARG
46
+       ;;
47
+
48
+    h) usage
49
+       exit
50
+       ;;
51
+
52
+    u) option_user=$OPTARG
53
+       ;;
54
+
55
+    *) exit 1
56
+       ;;
57
+  esac
58
+done
59
+
60
+shift $((OPTIND-1))
61
+
62
+################################################################################
63
+_psql() {
64
+  @sudo@ -u @superuser@ -H psql "$@"
65
+}
66
+
67
+################################################################################
68
+echo_grants() {
69
+  local r_list="SELECT"
70
+  local w_list="INSERT,UPDATE,DELETE,TRUNCATE,REFERENCES,TRIGGER"
71
+
72
+  # Needed to resolve ambiguous role memberships.
73
+  echo "SET ROLE @superuser@;"
74
+
75
+  # Start by removing all access then granting the ability to connect:
76
+  echo "REVOKE ALL PRIVILEGES ON DATABASE $option_database FROM $option_user;"
77
+  echo "GRANT CONNECT ON DATABASE $option_database TO $option_user;"
78
+
79
+  # Basic options:
80
+  echo "GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO $option_user;"
81
+  echo "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON FUNCTIONS TO $option_user;"
82
+
83
+  if [ "$option_access" = "r" ] || [ "$option_access" = "rw" ]; then
84
+    echo "GRANT USAGE ON SCHEMA public TO $option_user;"
85
+
86
+    echo "GRANT $r_list ON ALL TABLES IN SCHEMA public TO $option_user;"
87
+    echo "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT $r_list ON TABLES TO $option_user;"
88
+
89
+    echo "GRANT USAGE,SELECT ON ALL SEQUENCES IN SCHEMA public TO $option_user;"
90
+    echo "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE,SELECT ON SEQUENCES TO $option_user;"
91
+  fi
92
+
93
+  if [ "$option_access" = "w" ] || [ "$option_access" = "rw" ]; then
94
+    echo "GRANT $w_list ON ALL TABLES IN SCHEMA public TO $option_user;"
95
+    echo "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT $w_list ON TABLES TO $option_user;"
96
+
97
+    echo "GRANT UPDATE ON ALL SEQUENCES IN SCHEMA public TO $option_user;"
98
+    echo "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT UPDATE ON  SEQUENCES TO $option_user;"
99
+  fi
100
+}
101
+
102
+################################################################################
103
+# Let's do it!
104
+sql_file=$(mktemp)
105
+echo_grants > "$sql_file"
106
+chown @superuser@ "$sql_file"
107
+_psql --dbname="$option_database" --file="$sql_file" --single-transaction
108
+rm "$sql_file"

+ 1
- 32
modules/services/databases/postgresql/create-user.sh View File

@@ -6,8 +6,6 @@ set -e
6 6
 ################################################################################
7 7
 option_username=""
8 8
 option_password_file=""
9
-option_database=""
10
-option_extensions=""
11 9
 option_sqlfile="@out@/sql/create-user.sql"
12 10
 option_superuser=0
13 11
 
@@ -16,8 +14,6 @@ usage () {
16 14
 cat <<EOF
17 15
 Usage: create-user.sh [options]
18 16
 
19
-  -d NAME Database name to create for USER
20
-  -e LIST Space-separated list of extensions to enable
21 17
   -h      This message
22 18
   -p FILE File containing USER's password
23 19
   -s FILE The SQL template file (pg-create-user.sql)
@@ -27,14 +23,8 @@ EOF
27 23
 }
28 24
 
29 25
 ################################################################################
30
-while getopts "d:e:hp:s:Su:" o; do
26
+while getopts "hp:s:Su:" o; do
31 27
   case "${o}" in
32
-    d) option_database=$OPTARG
33
-       ;;
34
-
35
-    e) option_extensions=$OPTARG
36
-       ;;
37
-
38 28
     h) usage
39 29
        exit
40 30
        ;;
@@ -109,26 +99,5 @@ create_user() {
109 99
   _psql -d postgres -c "ALTER ROLE $option_username $superuser"
110 100
 }
111 101
 
112
-################################################################################
113
-create_database() {
114
-  has_db=$(_psql -tAl | cut -d'|' -f1 | grep -cF "$option_database" || :)
115
-
116
-  if [ "$has_db" -eq 0 ]; then
117
-    @sudo@ -u @superuser@ -H \
118
-     createdb --owner "$option_username" "$option_database"
119
-  fi
120
-}
121
-
122
-################################################################################
123
-enable_extensions() {
124
-  if [ -n "$option_extensions" ]; then
125
-    for ext in $option_extensions; do
126
-      _psql "$option_database" -c "CREATE EXTENSION IF NOT EXISTS $ext"
127
-    done
128
-  fi
129
-}
130
-
131 102
 ################################################################################
132 103
 create_user
133
-create_database
134
-enable_extensions

+ 1
- 1
modules/services/databases/postgresql/create-user.sql View File

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

+ 154
- 34
modules/services/databases/postgresql/default.nix View File

@@ -8,9 +8,55 @@ let
8 8
   cfg = config.phoebe.services.postgresql;
9 9
   plib = config.phoebe.lib;
10 10
   superuser = config.services.postgresql.superUser;
11
-  create-user = import ./create-user.nix { inherit config lib pkgs; };
11
+  scripts = import ./scripts.nix { inherit config lib pkgs; };
12 12
   afterservices = concatMap (a: plib.keyService a.passwordFile) (attrValues cfg.accounts);
13 13
 
14
+  # Per-database options:
15
+  database = { name, ...}: {
16
+
17
+    #### Interface:
18
+    options = {
19
+      name = mkOption {
20
+        type = types.str;
21
+        example = "sales";
22
+        description = "The name of the database.";
23
+      };
24
+
25
+      owner = mkOption {
26
+        type = types.str;
27
+        default = superuser;
28
+        example = "jdoe";
29
+        description = "Name of the account that owns the database.";
30
+      };
31
+
32
+      users = mkOption {
33
+        type = types.listOf types.str;
34
+        default = [ ];
35
+        example = [ "alice" ];
36
+        description = "List of user names who have full access to the database.";
37
+      };
38
+
39
+      readers = mkOption {
40
+        type = types.listOf types.str;
41
+        default = [ ];
42
+        example = [ "bob" ];
43
+        description = "List of user names who have read-only access to the database";
44
+      };
45
+
46
+      extensions = mkOption {
47
+        type = types.listOf types.str;
48
+        default = [ ];
49
+        example = [ "pg_trgm" ];
50
+        description = "A list of extension modules to enable for the database.";
51
+      };
52
+    };
53
+
54
+    #### Implementation:
55
+    config = {
56
+      name = mkDefault name;
57
+    };
58
+  };
59
+
14 60
   # Per-account options:
15 61
   account = { name, ... }: {
16 62
 
@@ -38,23 +84,6 @@ let
38 84
         '';
39 85
       };
40 86
 
41
-      database = mkOption {
42
-        type = types.str;
43
-        default = null;
44
-        example = "jdoe";
45
-        description = ''
46
-          The name of the database this user can access.  Defaults to
47
-          the account name.
48
-        '';
49
-      };
50
-
51
-      extensions = mkOption {
52
-        type = types.listOf types.str;
53
-        default = [ ];
54
-        example = [ "pg_trgm" ];
55
-        description = "A list of extension modules to enable for the database.";
56
-      };
57
-
58 87
       superuser = mkOption {
59 88
         type = types.bool;
60 89
         default = false;
@@ -70,6 +99,16 @@ let
70 99
         '';
71 100
       };
72 101
 
102
+      allowIdent = mkOption {
103
+        type = types.bool;
104
+        default = false;
105
+        example = true;
106
+        description = ''
107
+          Whether or not this account can use ident authentication
108
+          when connecting locally.
109
+        '';
110
+      };
111
+
73 112
       netmask = mkOption {
74 113
         type = types.nullOr types.str;
75 114
         default = null;
@@ -85,31 +124,100 @@ let
85 124
     #### Implementation:
86 125
     config = {
87 126
       user = mkDefault name;
88
-      database = mkDefault name;
89 127
     };
90 128
   };
91 129
 
92 130
   # Create HBA authentication entries:
93 131
   accountToHBA = account:
132
+    let local = if account.allowIdent then "ident" else "md5";
133
+        template = database: (''
134
+          local ${database} ${account.user}              ${local}
135
+          host  ${database} ${account.user} 127.0.0.1/32 md5
136
+          host  ${database} ${account.user} ::1/28       md5
137
+        '' + optionalString (account.netmask != null) ''
138
+          host  ${database} ${account.user} ${account.netmask} md5
139
+        '');
140
+        databases = map (d: d.name)
141
+          (filter (d: d.owner == account.user   ||
142
+                      elem account.user d.users ||
143
+                      elem account.user d.readers)
144
+            (attrValues cfg.databases));
145
+    in if account.superuser
146
+          then template "all"
147
+          else concatMapStringsSep "\n" template databases;
148
+
149
+  # Commands to run to create accounts:
150
+  createUser = account:
151
+    let options = [
152
+      ''-u "${account.user}"''
153
+      ''-p "${account.passwordFile}"''
154
+    ] ++ optional account.superuser "-S";
155
+    in ''
156
+      ${scripts}/bin/create-user.sh ${concatStringsSep " " options}
157
+    '';
158
+
159
+  # Commands to run to create databases:
160
+  createDB = database:
94 161
     ''
95
-      local ${account.database} ${account.user}              md5
96
-      host  ${account.database} ${account.user} 127.0.0.1/32 md5
97
-      host  ${account.database} ${account.user} ::1/28       md5
98
-    '' + optionalString (account.netmask != null) ''
99
-      host  ${account.database} ${account.user} ${account.netmask} md5
162
+      ${scripts}/bin/create-db.sh \
163
+        -d "${database.name}" \
164
+        -o "${database.owner}" \
165
+        -e "${concatStringsSep " " database.extensions}"
100 166
     '';
101 167
 
102
-  # Commands to run to create accounts/databases:
103
-  createScript = account:
168
+  # Commands to run to create full grants:
169
+  createGrant = database: account:
104 170
     ''
105
-      ${create-user}/bin/create-user.sh \
171
+      ${scripts}/bin/create-grant.sh \
172
+        -a rw \
106 173
         -u "${account.user}" \
107
-        -d "${account.database}" \
108
-        -p "${account.passwordFile}" \
109
-        -e "${concatStringsSep " " account.extensions}" \
110
-        -S "${toString account.superuser}"
174
+        -d "${database.name}"
111 175
     '';
112 176
 
177
+  # Commands to run to create read-only grants:
178
+  createReadGrant = database: account:
179
+    ''
180
+      ${scripts}/bin/create-grant.sh \
181
+        -a r \
182
+        -u "${account.user}" \
183
+        -d "${database.name}"
184
+    '';
185
+
186
+  # Generate a SQL statement that allows a user to login:
187
+  allowLogin = accounts: concatMapStringsSep "\n" (account: ''
188
+    echo "ALTER ROLE ${account.user} LOGIN;"
189
+  '') accounts;
190
+
191
+  # Lock out accounts that are not configured:
192
+  lockAccounts = accounts: ''
193
+    sql_file=$(mktemp)
194
+
195
+    # Lock all accounts:
196
+    ${scripts}/bin/nologin.sh > "$sql_file"
197
+
198
+    # Unlock configured accounts:
199
+    (
200
+      ${allowLogin accounts}
201
+    ) >> "$sql_file"
202
+
203
+    chown ${superuser} "$sql_file"
204
+
205
+    ${pkgs.sudo}/bin/sudo -u ${superuser} -H \
206
+      psql --dbname="postgres" --file="$sql_file" --single-transaction
207
+
208
+    rm "$sql_file"
209
+  '';
210
+
211
+  # Master grant creation function:
212
+  createGrants = database:
213
+    let find = names: map (name: cfg.accounts."${name}")
214
+                        (filter (name: cfg.accounts ? "${name}")
215
+                          names);
216
+        ro = find database.readers;
217
+        rw = find (database.users ++ [database.owner]);
218
+        owner = find [database.owner];
219
+    in (concatMapStringsSep "\n" (createReadGrant database) ro) +
220
+       (concatMapStringsSep "\n" (createGrant database) rw);
113 221
 in
114 222
 {
115 223
   #### Interface
@@ -119,7 +227,13 @@ in
119 227
     accounts = mkOption {
120 228
       type = types.attrsOf (types.submodule account);
121 229
       default = { };
122
-      description = "Additional user accounts";
230
+      description = "Additional user accounts.";
231
+    };
232
+
233
+    databases = mkOption {
234
+      type = types.attrsOf (types.submodule database);
235
+      default = { };
236
+      description = "Additional databases to create.";
123 237
     };
124 238
   };
125 239
 
@@ -142,13 +256,19 @@ in
142 256
     };
143 257
 
144 258
     # Create missing accounts:
145
-    systemd.services.pg-accounts = mkIf (length (attrValues cfg.accounts) > 0) {
259
+    systemd.services.postgres-account-manager = {
146 260
       description = "PostgreSQL Account Manager";
147 261
       path = [ pkgs.gawk config.services.postgresql.package ];
148
-      script = (concatMapStringsSep "\n" createScript (attrValues cfg.accounts));
149 262
       wantedBy = [ "postgresql.service" ];
150 263
       after = [ "postgresql.service" ] ++ afterservices;
151 264
       wants = afterservices;
265
+
266
+      script = ''
267
+        set -e
268
+      '' + (concatMapStringsSep "\n" createUser (attrValues cfg.accounts))
269
+         + (lockAccounts (attrValues cfg.accounts))
270
+         + (concatMapStringsSep "\n" createDB (attrValues cfg.databases))
271
+         + (concatMapStringsSep "\n" createGrants (attrValues cfg.databases));
152 272
     };
153 273
   };
154 274
 }

+ 27
- 0
modules/services/databases/postgresql/nologin.sh View File

@@ -0,0 +1,27 @@
1
+#!/bin/bash
2
+
3
+################################################################################
4
+# Generate SQL that locks out all users (except the superuser).
5
+set -e
6
+set -u
7
+
8
+################################################################################
9
+_psql() {
10
+  @sudo@ -u @superuser@ -H psql "$@"
11
+}
12
+
13
+################################################################################
14
+accounts() {
15
+  echo "SELECT rolname FROM pg_catalog.pg_roles;" | \
16
+    _psql --tuples-only postgres | \
17
+    sed 's/^[[:space:]]*//' | \
18
+    grep --fixed-strings --invert-match --line-regexp '@superuser@' | \
19
+    grep --extended-regexp --invert-match '^pg_'
20
+}
21
+
22
+################################################################################
23
+for name in $(accounts); do
24
+  if [ -n "$name" ]; then
25
+    echo "ALTER ROLE $name NOLOGIN;"
26
+  fi
27
+done

modules/services/databases/postgresql/create-user.nix → modules/services/databases/postgresql/scripts.nix View File

@@ -10,9 +10,14 @@ pkgs.stdenvNoCC.mkDerivation {
10 10
     export superuser=${config.services.postgresql.superUser}
11 11
 
12 12
     mkdir -p $out/bin $out/sql
13
-    cp ${./create-user.sql} $out/sql/create-user.sql
14
-    substituteAll ${./create-user.sh} $out/bin/create-user.sh
15
-    chmod 555 $out/bin/create-user.sh
13
+
14
+    cp ${./create-user.sql}            $out/sql/create-user.sql
15
+    substituteAll ${./create-user.sh}  $out/bin/create-user.sh
16
+    substituteAll ${./create-db.sh}    $out/bin/create-db.sh
17
+    substituteAll ${./create-grant.sh} $out/bin/create-grant.sh
18
+    substituteAll ${./nologin.sh}      $out/bin/nologin.sh
19
+
20
+    chmod 555 $out/bin/*.sh
16 21
   '';
17 22
 
18 23
   meta = with lib; {

Loading…
Cancel
Save