-
Notifications
You must be signed in to change notification settings - Fork 59
/
Copy pathpostgres_mgmt.go
327 lines (282 loc) · 8.31 KB
/
postgres_mgmt.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
/*
2019 © Postgres.ai
*/
package postgres
import (
"fmt"
"strings"
"github.com/lib/pq"
"github.com/pkg/errors"
"gitlab.com/postgres-ai/database-lab/v3/internal/provision/resources"
"gitlab.com/postgres-ai/database-lab/v3/pkg/log"
)
// ResetPasswordsQuery provides a template for a reset password query.
const ResetPasswordsQuery = `do $$
declare
rec record;
sql text;
begin
for rec in
select * from pg_roles where rolcanlogin{{OPTIONAL_WHERE}}
loop
sql := format(
'alter role %I password %L',
rec.rolname,
md5(random()::text || clock_timestamp())
);
raise debug 'SQL: %', sql;
execute sql;
end loop;
end
$$;
`
// ResetPasswordsQueryWhere provides a template for a reset password where clause.
const ResetPasswordsQueryWhere = ` and rolname not in (%s)`
// ResetAllPasswords defines a method for resetting password of all Postgres users.
func ResetAllPasswords(c *resources.AppConfig, whitelistUsers []string) error {
optionalWhere := ""
if len(whitelistUsers) > 0 {
for i, user := range whitelistUsers {
if i != 0 {
optionalWhere += ", "
}
optionalWhere += fmt.Sprintf("'%s'", user)
}
optionalWhere = fmt.Sprintf(ResetPasswordsQueryWhere, optionalWhere)
}
query := strings.Replace(ResetPasswordsQuery,
"{{OPTIONAL_WHERE}}", optionalWhere, 1)
out, err := runSimpleSQL(query, getPgConnStr(c.Host, c.DB.DBName, c.DB.Username, c.Port))
if err != nil {
return errors.Wrap(err, "failed to run psql")
}
log.Dbg("ResetAllPasswords:", out)
return nil
}
// selectAllDatabases provides a query to list available databases.
const selectAllDatabases = "select datname from pg_catalog.pg_database where not datistemplate"
// CreateUser defines a method for creation of Postgres user.
func CreateUser(c *resources.AppConfig, user resources.EphemeralUser) error {
var query string
dbName := c.DB.DBName
if user.AvailableDB != "" {
dbName = user.AvailableDB
}
if user.Restricted {
// create restricted user
query = restrictedUserQuery(user.Name, user.Password)
out, err := runSimpleSQL(query, getPgConnStr(c.Host, dbName, c.DB.Username, c.Port))
if err != nil {
return fmt.Errorf("failed to create restricted user: %w", err)
}
log.Dbg("Restricted user has been created: ", out)
// set restricted user as owner for database objects
databaseList, err := runSQLSelectQuery(selectAllDatabases, getPgConnStr(c.Host, dbName, c.DB.Username, c.Port))
if err != nil {
return fmt.Errorf("failed list all databases: %w", err)
}
for _, database := range databaseList {
query = restrictedObjectsQuery(user.Name)
out, err = runSimpleSQL(query, getPgConnStr(c.Host, database, c.DB.Username, c.Port))
if err != nil {
return fmt.Errorf("failed to run objects restrict query: %w", err)
}
log.Dbg("Objects restriction applied", database, out)
}
} else {
query = superuserQuery(user.Name, user.Password)
out, err := runSimpleSQL(query, getPgConnStr(c.Host, dbName, c.DB.Username, c.Port))
if err != nil {
return fmt.Errorf("failed to create superuser: %w", err)
}
log.Dbg("Super user has been created: ", out)
}
return nil
}
func superuserQuery(username, password string) string {
return fmt.Sprintf(`create user %s with password %s login superuser;`, pq.QuoteIdentifier(username), pq.QuoteLiteral(password))
}
const restrictionUserCreationTemplate = `
-- create a new user
create user @username with password @password login;
do $$
declare
new_owner text;
object_type record;
r record;
begin
new_owner := @usernameStr;
-- Changing owner of all databases
for r in select datname from pg_catalog.pg_database where not datistemplate loop
raise debug 'Changing owner of %', r.datname;
execute format(
'alter database %s owner to "%s";',
r.datname,
new_owner
);
end loop;
end
$$;
`
const restrictionTemplate = `
do $$
declare
new_owner text;
object_type record;
r record;
begin
new_owner := @usernameStr;
-- Schemas
-- allow working with all schemas
for r in select * from pg_namespace loop
raise debug 'Changing ownership of schema % to %',
r.nspname, new_owner;
execute format(
'alter schema %I owner to %I;',
r.nspname,
new_owner
);
end loop;
-- Types and Domains
-- d: domain (assuming that ALTER TYPE will be equivalent to ALTER DOMAIN)
-- e: enum
-- r: range
-- m: multirange
for r in
select n.nspname, t.typname
from pg_type t
join pg_namespace n on
n.oid = t.typnamespace
and not n.nspname in ('pg_catalog', 'information_schema')
and t.typtype in ('d', 'e', 'r', 'm')
order by t.typname
loop
raise debug 'Changing ownership of type %.% to %',
r.nspname, r.typname, new_owner;
execute format(
'alter type %I.%I owner to %I;',
r.nspname,
r.typname,
new_owner
);
end loop;
-- Relations
-- c: composite type
-- p: partitioned table
-- i: index
-- r: table
-- v: view
-- m: materialized view
-- S: sequence
for object_type in
select
unnest('{type,table,table,view,materialized view,sequence}'::text[]) type_name,
unnest('{c,p,r,v,m,S}'::text[]) code
loop
for r in
execute format(
$sql$
select n.nspname, c.relname
from pg_class c
join pg_namespace n on
n.oid = c.relnamespace
and not n.nspname in ('pg_catalog', 'information_schema')
and c.relkind = %L
order by c.relname
$sql$,
object_type.code
)
loop
raise debug 'Changing ownership of % %.% to %',
object_type.type_name, r.nspname, r.relname, new_owner;
execute format(
'alter %s %I.%I owner to %I;',
object_type.type_name,
r.nspname,
r.relname,
new_owner
);
end loop;
end loop;
-- Functions and Procedures,
for r in
select
p.prokind,
p.proname,
n.nspname,
pg_catalog.pg_get_function_identity_arguments(p.oid) as args
from pg_catalog.pg_namespace as n
join pg_catalog.pg_proc as p on p.pronamespace = n.oid
where not n.nspname in ('pg_catalog', 'information_schema')
and p.proname not ilike 'dblink%' -- We do not want dblink to be involved (exclusion)
and p.prokind in ('f', 'p', 'a', 'w')
loop
raise debug 'Changing ownership of function %.%(%) to %',
r.nspname, r.proname, r.args, new_owner;
execute format(
'alter %s %I.%I(%s) owner to %I', -- todo: check support CamelStyle r.args,
case r.prokind
when 'f' then 'function'
when 'w' then 'function'
when 'p' then 'procedure'
when 'a' then 'aggregate'
else 'unknown'
end,
r.nspname,
r.proname,
r.args,
new_owner
);
end loop;
-- full text search dictionary
-- TODO: text search configuration
for r in
select *
from pg_catalog.pg_namespace n
join pg_catalog.pg_ts_dict d on d.dictnamespace = n.oid
where not n.nspname in ('pg_catalog', 'information_schema')
loop
raise debug 'Changing ownership of text search dictionary %.% to %',
r.nspname, r.dictname, new_owner;
execute format(
'alter text search dictionary %I.%I owner to %I',
r.nspname,
r.dictname,
new_owner
);
end loop;
-- domain
for r in
select typname, nspname
from pg_catalog.pg_type
join pg_catalog.pg_namespace on pg_namespace.oid = pg_type.typnamespace
where typtype = 'd' and not nspname in ('pg_catalog', 'information_schema')
loop
raise debug 'Changing ownership of domain %.% to %',
r.nspname, r.typname, new_owner;
execute format(
'alter domain %I.%I owner to %I',
r.nspname,
r.typname,
new_owner
);
end loop;
grant select on pg_stat_activity to @username;
end
$$;
`
func restrictedUserQuery(username, password string) string {
repl := strings.NewReplacer(
"@usernameStr", pq.QuoteLiteral(username),
"@username", pq.QuoteIdentifier(username),
"@password", pq.QuoteLiteral(password),
)
return repl.Replace(restrictionUserCreationTemplate)
}
func restrictedObjectsQuery(username string) string {
repl := strings.NewReplacer(
"@usernameStr", pq.QuoteLiteral(username),
"@username", pq.QuoteIdentifier(username),
)
return repl.Replace(restrictionTemplate)
}