~launchpad-pqm/launchpad/devel

10637.3.7 by Guilherme Salgado
merge devel
1
#!/usr/bin/python -S
8687.15.9 by Karl Fogel
Add the copyright header block to more files (everything under database/).
2
#
7675.895.1 by Jeroen Vermeulen
Speed up security.py by an order of magnitude by batching permission changes.
3
# Copyright 2009-2010 Canonical Ltd.  This software is licensed under the
8687.15.9 by Karl Fogel
Add the copyright header block to more files (everything under database/).
4
# GNU Affero General Public License version 3 (see the file LICENSE).
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
5
6
__metaclass__ = type
7
7370.2.2 by Barry Warsaw
pick lint
8
# pylint: disable-msg=W0403
1831 by Canonical.com Patch Queue Manager
New config machinery, database helpers and oddsnsods required for staging
9
import _pythonpath
10
7675.395.152 by Stuart Bishop
Add --no-revoke to security.py for use live, and --dry-run and improve logging for shits and giggles.
11
from collections import defaultdict
12
from ConfigParser import SafeConfigParser
7658.6.3 by Stuart Bishop
Review feedback from allenap
13
from itertools import chain
7675.395.152 by Stuart Bishop
Add --no-revoke to security.py for use live, and --dry-run and improve logging for shits and giggles.
14
from optparse import OptionParser
7370.2.2 by Barry Warsaw
pick lint
15
import os
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
16
import sys
17
18
import psycopg2
7370.2.2 by Barry Warsaw
pick lint
19
1831 by Canonical.com Patch Queue Manager
New config machinery, database helpers and oddsnsods required for staging
20
from canonical.database.sqlbase import connect
21
from canonical.launchpad.scripts import logger_options, logger, db_options
7675.395.152 by Stuart Bishop
Add --no-revoke to security.py for use live, and --dry-run and improve logging for shits and giggles.
22
from fti import quote_identifier
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
23
import replication.helpers
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
24
7370.2.2 by Barry Warsaw
pick lint
25
7675.396.2 by Stuart Bishop
Blacklist read permissions on some tables to the read group
26
# The 'read' group does not get given select permission on the following
27
# tables. This is to stop the ro user being given access to secrurity
28
# sensitive information that interactive sessions don't need.
29
SECURE_TABLES = [
30
    'public.accountpassword',
7675.395.31 by Stuart Bishop
More secure tables
31
    'public.oauthnonce',
32
    'public.openidnonce',
33
    'public.openidconsumernonce',
7675.396.2 by Stuart Bishop
Blacklist read permissions on some tables to the read group
34
    ]
35
36
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
37
class DbObject(object):
7675.1072.1 by Jeroen Vermeulen
security.py cleanup and speedup, ported over from devel branch.
38
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
39
    def __init__(
7658.3.16 by Stuart Bishop
Reapply backed out db changes
40
            self, schema, name, type_, owner, arguments=None, language=None):
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
41
        self.schema = schema
42
        self.name = name
43
        self.type = type_
44
        self.owner = owner
45
        self.arguments = arguments
46
        self.language = language
47
1339 by Canonical.com Patch Queue Manager
Add generated database diagrams (initial cut)
48
    def __eq__(self, other):
49
        return self.schema == other.schema and self.name == other.name
50
7658.3.16 by Stuart Bishop
Reapply backed out db changes
51
    @property
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
52
    def fullname(self):
7675.1072.1 by Jeroen Vermeulen
security.py cleanup and speedup, ported over from devel branch.
53
        fn = "%s.%s" % (self.schema, self.name)
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
54
        if self.type == 'function':
55
            fn = "%s(%s)" % (fn, self.arguments)
56
        return fn
57
7658.3.16 by Stuart Bishop
Reapply backed out db changes
58
    @property
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
59
    def seqname(self):
60
        if self.type != 'table':
61
            return ''
62
        return "%s.%s" % (self.schema, self.name + '_id_seq')
63
64
65
class DbSchema(dict):
13465.2.20 by Stuart Bishop
delint
66
    groups = None  # List of groups defined in the db
67
    users = None  # List of users defined in the db
7675.1072.1 by Jeroen Vermeulen
security.py cleanup and speedup, ported over from devel branch.
68
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
69
    def __init__(self, con):
7370.2.2 by Barry Warsaw
pick lint
70
        super(DbSchema, self).__init__()
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
71
        cur = con.cursor()
72
        cur.execute('''
7370.2.1 by Barry Warsaw
Fixes exposed by testing on staging, along with more unittests and whitespace
73
            SELECT
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
74
                n.nspname as "Schema",
75
                c.relname as "Name",
76
                CASE c.relkind
77
                    WHEN 'r' THEN 'table'
78
                    WHEN 'v' THEN 'view'
79
                    WHEN 'i' THEN 'index'
80
                    WHEN 'S' THEN 'sequence'
81
                    WHEN 's' THEN 'special'
82
                END as "Type",
83
                u.usename as "Owner"
84
            FROM pg_catalog.pg_class c
85
                LEFT JOIN pg_catalog.pg_user u ON u.usesysid = c.relowner
86
                LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
87
            WHERE c.relkind IN ('r','v','S','')
88
                AND n.nspname NOT IN ('pg_catalog', 'pg_toast')
89
                AND pg_catalog.pg_table_is_visible(c.oid)
90
            ORDER BY 1,2
91
            ''')
92
        for schema, name, type_, owner in cur.fetchall():
7370.2.2 by Barry Warsaw
pick lint
93
            key = '%s.%s' % (schema, name)
94
            self[key] = DbObject(schema, name, type_, owner)
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
95
96
        cur.execute(r"""
97
            SELECT
98
                n.nspname as "schema",
99
                p.proname as "name",
100
                pg_catalog.oidvectortypes(p.proargtypes) as "Argument types",
101
                u.usename as "owner",
102
                l.lanname as "language"
103
            FROM pg_catalog.pg_proc p
104
                LEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace
105
                LEFT JOIN pg_catalog.pg_language l ON l.oid = p.prolang
106
                LEFT JOIN pg_catalog.pg_user u ON u.usesysid = p.proowner
7658.3.16 by Stuart Bishop
Reapply backed out db changes
107
                LEFT JOIN pg_catalog.pg_type r ON r.oid = p.prorettype
108
            WHERE
109
                r.typname NOT IN ('trigger', 'language_handler')
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
110
                AND pg_catalog.pg_function_is_visible(p.oid)
111
                AND n.nspname <> 'pg_catalog'
112
                """)
113
        for schema, name, arguments, owner, language in cur.fetchall():
114
            self['%s.%s(%s)' % (schema, name, arguments)] = DbObject(
7675.1072.1 by Jeroen Vermeulen
security.py cleanup and speedup, ported over from devel branch.
115
                    schema, name, 'function', owner, arguments, language)
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
116
        # Pull a list of groups
117
        cur.execute("SELECT groname FROM pg_group")
118
        self.groups = [r[0] for r in cur.fetchall()]
119
120
        # Pull a list of users
121
        cur.execute("SELECT usename FROM pg_user")
122
        self.users = [r[0] for r in cur.fetchall()]
123
7658.6.3 by Stuart Bishop
Review feedback from allenap
124
    @property
125
    def principals(self):
126
        return chain(self.groups, self.users)
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
127
128
129
class CursorWrapper(object):
7675.1072.1 by Jeroen Vermeulen
security.py cleanup and speedup, ported over from devel branch.
130
1831 by Canonical.com Patch Queue Manager
New config machinery, database helpers and oddsnsods required for staging
131
    def __init__(self, cursor):
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
132
        self.__dict__['_cursor'] = cursor
133
134
    def execute(self, cmd, params=None):
1318 by Canonical.com Patch Queue Manager
Install psycopg type converters so that psycopg automatically returns Unicode strings and datetime objects, and allow test suites to pass with new psycopgda
135
        cmd = cmd.encode('utf8')
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
136
        if params is None:
7675.395.152 by Stuart Bishop
Add --no-revoke to security.py for use live, and --dry-run and improve logging for shits and giggles.
137
            log.debug2('%s' % (cmd, ))
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
138
            return self.__dict__['_cursor'].execute(cmd)
139
        else:
7675.395.152 by Stuart Bishop
Add --no-revoke to security.py for use live, and --dry-run and improve logging for shits and giggles.
140
            log.debug2('%s [%r]' % (cmd, params))
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
141
            return self.__dict__['_cursor'].execute(cmd, params)
142
143
    def __getattr__(self, key):
144
        return getattr(self.__dict__['_cursor'], key)
145
146
    def __setattr__(self, key, value):
147
        return setattr(self.__dict__['_cursor'], key, value)
148
1520 by Canonical.com Patch Queue Manager
Review and fix database security update code
149
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
150
CONFIG_DEFAULTS = {
7675.1072.1 by Jeroen Vermeulen
security.py cleanup and speedup, ported over from devel branch.
151
    'groups': '',
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
152
    }
153
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
154
1520 by Canonical.com Patch Queue Manager
Review and fix database security update code
155
def main(options):
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
156
    # Load the config file
157
    config = SafeConfigParser(CONFIG_DEFAULTS)
1831 by Canonical.com Patch Queue Manager
New config machinery, database helpers and oddsnsods required for staging
158
    configfile_name = os.path.join(os.path.dirname(__file__), 'security.cfg')
159
    config.read([configfile_name])
160
161
    con = connect(options.dbuser)
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
162
163
    if options.cluster:
164
        nodes = replication.helpers.get_nodes(con, 1)
165
        if nodes:
166
            # If we have a replicated environment, reset permissions on all
167
            # Nodes.
168
            con.close()
169
            for node in nodes:
170
                log.info("Resetting permissions on %s (%s)" % (
171
                    node.nickname, node.connection_string))
172
                reset_permissions(
173
                    psycopg2.connect(node.connection_string), config, options)
13465.2.15 by Stuart Bishop
Invoke security.py in-process to save startup overhead
174
            return 0
7675.395.197 by Stuart Bishop
Full database update script for fast deployments
175
        log.warning("--cluster requested, but not a Slony-I cluster.")
176
    log.info("Resetting permissions on single database")
177
    reset_permissions(con, config, options)
13465.2.15 by Stuart Bishop
Invoke security.py in-process to save startup overhead
178
    return 0
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
179
180
7675.895.1 by Jeroen Vermeulen
Speed up security.py by an order of magnitude by batching permission changes.
181
def list_identifiers(identifiers):
182
    """List all of `identifiers` as SQL, quoted and separated by commas.
183
184
    :param identifiers: A sequence of SQL identifiers.
185
    :return: A comma-separated SQL string consisting of all identifiers
186
        passed in.  Each will be quoted for use in SQL.
187
    """
188
    return ', '.join([
189
        quote_identifier(identifier) for identifier in identifiers])
190
191
7675.1072.1 by Jeroen Vermeulen
security.py cleanup and speedup, ported over from devel branch.
192
class PermissionGatherer:
193
    """Gather permissions for bulk granting or revocation.
194
195
    Processing such statements in bulk (with multiple users, tables,
196
    or permissions in one statement) is faster than issuing very large
197
    numbers of individual statements.
198
    """
199
200
    def __init__(self, entity_keyword):
201
        """Gather for SQL entities of one kind (TABLE, FUNCTION, SEQUENCE).
202
203
        :param entity_keyword: The SQL keyword for the kind of entity
204
            that permissions will be gathered for.
205
        """
206
        self.entity_keyword = entity_keyword
207
        self.permissions = defaultdict(dict)
208
209
    def add(self, permission, entity, principal, is_group=False):
210
        """Add a permission.
211
212
        Add all privileges you want to grant or revoke first, then use
213
        `grant` or `revoke` to process them in bulk.
214
215
        :param permission: A permission: SELECT, INSERT, EXECUTE, etc.
216
        :param entity: Table, function, or sequence on which to grant
217
            or revoke a privilege.
218
        :param principal: User or group to which the privilege should
219
            apply.
220
        :param is_group: Is `principal` a group?
221
        """
222
        if is_group:
223
            full_principal = "GROUP " + principal
224
        else:
225
            full_principal = principal
226
        self.permissions[permission].setdefault(entity, set()).add(
227
            full_principal)
228
229
    def tabulate(self):
230
        """Group privileges into single-statement work items.
231
232
        Each entry returned by this method represents a batch of
233
        privileges that can be granted or revoked in a single SQL
234
        statement.
235
236
        :return: A sequence of tuples of strings: permission(s) to
237
            grant/revoke, entity or entities to act on, and principal(s)
238
            to grant or revoke for.  Each is a string.
239
        """
240
        result = []
241
        for permission, parties in self.permissions.iteritems():
242
            for entity, principals in parties.iteritems():
243
                result.append(
244
                    (permission, entity, ", ".join(principals)))
245
        return result
246
247
    def countPermissions(self):
248
        """Count the number of different permissions."""
249
        return len(self.permissions)
250
251
    def countEntities(self):
252
        """Count the number of different entities."""
253
        return len(set(sum([
254
            entities.keys()
255
            for entities in self.permissions.itervalues()], [])))
256
257
    def countPrincipals(self):
258
        """Count the number of different principals."""
259
        principals = set()
260
        for entities_and_principals in self.permissions.itervalues():
261
            for extra_principals in entities_and_principals.itervalues():
262
                principals.update(extra_principals)
263
        return len(principals)
264
265
    def grant(self, cur):
266
        """Grant all gathered permissions.
267
268
        :param cur: A cursor to operate on.
269
        """
270
        log.debug(
271
            "Granting %d permission(s) on %d %s(s) for %d user(s)/group(s).",
272
            self.countPermissions(),
273
            self.countEntities(),
274
            self.entity_keyword,
275
            self.countPrincipals())
276
        grant_count = 0
277
        for permissions, entities, principals in self.tabulate():
278
            grant = "GRANT %s ON %s %s TO %s" % (
279
                permissions, self.entity_keyword, entities, principals)
280
            log.debug2(grant)
281
            cur.execute(grant)
282
            grant_count += 1
283
        log.debug("Issued %d GRANT statement(s).", grant_count)
284
285
    def revoke(self, cur):
286
        """Revoke all gathered permissions.
287
288
        :param cur: A cursor to operate on.
289
        """
290
        log.debug(
291
            "Revoking %d permission(s) on %d %s(s) for %d user(s)/group(s).",
292
            self.countPermissions(),
293
            self.countEntities(),
294
            self.entity_keyword,
295
            self.countPrincipals())
296
        revoke_count = 0
297
        for permissions, entities, principals in self.tabulate():
298
            revoke = "REVOKE %s ON %s %s FROM %s" % (
299
                permissions, self.entity_keyword, entities, principals)
300
            log.debug2(revoke)
301
            cur.execute(revoke)
302
            revoke_count += 1
303
        log.debug("Issued %d REVOKE statement(s).", revoke_count)
304
305
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
306
def reset_permissions(con, config, options):
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
307
    schema = DbSchema(con)
7675.895.1 by Jeroen Vermeulen
Speed up security.py by an order of magnitude by batching permission changes.
308
    all_users = list_identifiers(schema.users)
309
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
310
    cur = CursorWrapper(con.cursor())
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
311
1520 by Canonical.com Patch Queue Manager
Review and fix database security update code
312
    # Add our two automatically maintained groups
1812 by Canonical.com Patch Queue Manager
[trivial] Create admin and read groups automatically
313
    for group in ['read', 'admin']:
314
        if group in schema.principals:
7675.395.152 by Stuart Bishop
Add --no-revoke to security.py for use live, and --dry-run and improve logging for shits and giggles.
315
            log.debug("Removing managed users from %s role" % group)
7675.895.1 by Jeroen Vermeulen
Speed up security.py by an order of magnitude by batching permission changes.
316
            cur.execute("ALTER GROUP %s DROP USER %s" % (
317
                    quote_identifier(group), all_users))
1812 by Canonical.com Patch Queue Manager
[trivial] Create admin and read groups automatically
318
        else:
7675.395.152 by Stuart Bishop
Add --no-revoke to security.py for use live, and --dry-run and improve logging for shits and giggles.
319
            log.debug("Creating %s role" % group)
1812 by Canonical.com Patch Queue Manager
[trivial] Create admin and read groups automatically
320
            cur.execute("CREATE GROUP %s" % quote_identifier(group))
321
            schema.groups.append(group)
1520 by Canonical.com Patch Queue Manager
Review and fix database security update code
322
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
323
    # Create all required groups and users.
324
    for section_name in config.sections():
325
        if section_name.lower() == 'public':
326
            continue
7658.3.16 by Stuart Bishop
Reapply backed out db changes
327
7658.6.3 by Stuart Bishop
Review feedback from allenap
328
        assert not section_name.endswith('_ro'), (
329
            '_ro namespace is reserved (%s)' % repr(section_name))
330
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
331
        type_ = config.get(section_name, 'type')
7658.3.16 by Stuart Bishop
Reapply backed out db changes
332
        assert type_ in ['user', 'group'], 'Unknown type %s' % type_
333
334
        role_options = [
335
            'NOCREATEDB', 'NOCREATEROLE', 'NOCREATEUSER', 'INHERIT']
7658.5.1 by Stuart Bishop
Typo stopping db users from logging in
336
        if type_ == 'user':
7658.3.16 by Stuart Bishop
Reapply backed out db changes
337
            role_options.append('LOGIN')
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
338
        else:
7658.3.16 by Stuart Bishop
Reapply backed out db changes
339
            role_options.append('NOLOGIN')
340
341
        for username in [section_name, '%s_ro' % section_name]:
342
            if username in schema.principals:
343
                if type_ == 'group':
7675.395.152 by Stuart Bishop
Add --no-revoke to security.py for use live, and --dry-run and improve logging for shits and giggles.
344
                    if options.revoke:
345
                        log.debug("Revoking membership of %s role", username)
346
                        cur.execute("REVOKE %s FROM %s" % (
347
                            quote_identifier(username), all_users))
7658.3.16 by Stuart Bishop
Reapply backed out db changes
348
                else:
349
                    # Note - we don't drop the user because it might own
350
                    # objects in other databases. We need to ensure they are
351
                    # not superusers though!
7675.395.152 by Stuart Bishop
Add --no-revoke to security.py for use live, and --dry-run and improve logging for shits and giggles.
352
                    log.debug("Resetting role options of %s role.", username)
7658.3.16 by Stuart Bishop
Reapply backed out db changes
353
                    cur.execute(
354
                        "ALTER ROLE %s WITH %s" % (
355
                            quote_identifier(username),
356
                            ' '.join(role_options)))
357
            else:
7675.395.152 by Stuart Bishop
Add --no-revoke to security.py for use live, and --dry-run and improve logging for shits and giggles.
358
                log.debug("Creating %s role.", username)
7658.3.16 by Stuart Bishop
Reapply backed out db changes
359
                cur.execute(
360
                    "CREATE ROLE %s WITH %s"
361
                    % (quote_identifier(username), ' '.join(role_options)))
362
                schema.groups.append(username)
363
7658.5.2 by Stuart Bishop
Set default_transaction_read_only on database roles
364
        # Set default read-only mode for our roles.
365
        cur.execute(
366
            'ALTER ROLE %s SET default_transaction_read_only TO FALSE'
367
            % quote_identifier(section_name))
368
        cur.execute(
369
            'ALTER ROLE %s SET default_transaction_read_only TO TRUE'
370
            % quote_identifier('%s_ro' % section_name))
371
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
372
    # Add users to groups
373
    for user in config.sections():
374
        if config.get(user, 'type') != 'user':
375
            continue
376
        groups = [
377
            g.strip() for g in config.get(user, 'groups', '').split(',')
7675.1072.1 by Jeroen Vermeulen
security.py cleanup and speedup, ported over from devel branch.
378
            if g.strip()]
7658.3.16 by Stuart Bishop
Reapply backed out db changes
379
        # Read-Only users get added to Read-Only groups.
380
        if user.endswith('_ro'):
381
            groups = ['%s_ro' % group for group in groups]
7675.395.152 by Stuart Bishop
Add --no-revoke to security.py for use live, and --dry-run and improve logging for shits and giggles.
382
        if groups:
383
            log.debug("Adding %s to %s roles", user, ', '.join(groups))
384
            for group in groups:
385
                cur.execute(r"""ALTER GROUP %s ADD USER %s""" % (
386
                    quote_identifier(group), quote_identifier(user)))
387
        else:
388
            log.debug("%s not in any roles", user)
7370.2.1 by Barry Warsaw
Fixes exposed by testing on staging, along with more unittests and whitespace
389
7675.395.152 by Stuart Bishop
Add --no-revoke to security.py for use live, and --dry-run and improve logging for shits and giggles.
390
    if options.revoke:
13465.2.19 by Stuart Bishop
security.py --no-revoke should not attempt ownership changes
391
        # Change ownership of all objects to OWNER.
392
        # We skip this in --no-revoke mode as ownership changes may
393
        # block on a live system.
394
        for obj in schema.values():
395
            if obj.type in ("function", "sequence"):
13465.2.20 by Stuart Bishop
delint
396
                pass  # Can't change ownership of functions or sequences
13465.2.19 by Stuart Bishop
security.py --no-revoke should not attempt ownership changes
397
            else:
398
                if obj.owner != options.owner:
399
                    log.info("Resetting ownership of %s", obj.fullname)
400
                    cur.execute("ALTER TABLE %s OWNER TO %s" % (
401
                        obj.fullname, quote_identifier(options.owner)))
402
7675.1072.1 by Jeroen Vermeulen
security.py cleanup and speedup, ported over from devel branch.
403
        # Revoke all privs from known groups. Don't revoke anything for
404
        # users or groups not defined in our security.cfg.
405
        table_revocations = PermissionGatherer("TABLE")
406
        function_revocations = PermissionGatherer("FUNCTION")
407
        sequence_revocations = PermissionGatherer("SEQUENCE")
408
409
        # Gather all revocations.
410
        for section_name in config.sections():
411
            role = quote_identifier(section_name)
412
            if section_name == 'public':
413
                ro_role = None
414
            else:
415
                ro_role = quote_identifier(section_name + "_ro")
416
417
            for obj in schema.values():
418
                if obj.type == 'function':
419
                    gatherer = function_revocations
420
                else:
421
                    gatherer = table_revocations
422
423
                gatherer.add("ALL", obj.fullname, role)
424
425
                if obj.seqname in schema:
426
                    sequence_revocations.add("ALL", obj.seqname, role)
427
                    if ro_role is not None:
428
                        sequence_revocations.add("ALL", obj.seqname, ro_role)
429
430
        table_revocations.revoke(cur)
431
        function_revocations.revoke(cur)
432
        sequence_revocations.revoke(cur)
7675.395.152 by Stuart Bishop
Add --no-revoke to security.py for use live, and --dry-run and improve logging for shits and giggles.
433
    else:
13465.2.19 by Stuart Bishop
security.py --no-revoke should not attempt ownership changes
434
        log.info("Not resetting ownership of database objects")
7675.395.152 by Stuart Bishop
Add --no-revoke to security.py for use live, and --dry-run and improve logging for shits and giggles.
435
        log.info("Not revoking permissions on database objects")
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
436
1520 by Canonical.com Patch Queue Manager
Review and fix database security update code
437
    # Set of all tables we have granted permissions on. After we have assigned
438
    # permissions, we can use this to determine what tables have been
439
    # forgotten about.
10293.3.1 by Max Bowsher
Remove use of the deprecated sets module.
440
    found = set()
1520 by Canonical.com Patch Queue Manager
Review and fix database security update code
441
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
442
    # Set permissions as per config file
7675.895.1 by Jeroen Vermeulen
Speed up security.py by an order of magnitude by batching permission changes.
443
7675.1072.1 by Jeroen Vermeulen
security.py cleanup and speedup, ported over from devel branch.
444
    table_permissions = PermissionGatherer("TABLE")
445
    function_permissions = PermissionGatherer("FUNCTION")
446
    sequence_permissions = PermissionGatherer("SEQUENCE")
7675.895.1 by Jeroen Vermeulen
Speed up security.py by an order of magnitude by batching permission changes.
447
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
448
    for username in config.sections():
449
        for obj_name, perm in config.items(username):
450
            if '.' not in obj_name:
451
                continue
2837.1.3 by Stuart Bishop
Make 'unknown object' be a warning instead of a failure to ease
452
            if obj_name not in schema.keys():
453
                log.warn('Bad object name %r', obj_name)
454
                continue
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
455
            obj = schema[obj_name]
1520 by Canonical.com Patch Queue Manager
Review and fix database security update code
456
457
            found.add(obj)
458
459
            perm = perm.strip()
460
            if not perm:
461
                # No perm means no rights. We can't grant no rights, so skip.
462
                continue
463
7658.3.16 by Stuart Bishop
Reapply backed out db changes
464
            who = quote_identifier(username)
465
            if username == 'public':
466
                who_ro = who
1520 by Canonical.com Patch Queue Manager
Review and fix database security update code
467
            else:
7658.3.16 by Stuart Bishop
Reapply backed out db changes
468
                who_ro = quote_identifier('%s_ro' % username)
1520 by Canonical.com Patch Queue Manager
Review and fix database security update code
469
7675.395.152 by Stuart Bishop
Add --no-revoke to security.py for use live, and --dry-run and improve logging for shits and giggles.
470
            log.debug(
471
                "Granting %s on %s to %s", perm, obj.fullname, who)
1520 by Canonical.com Patch Queue Manager
Review and fix database security update code
472
            if obj.type == 'function':
7675.1072.1 by Jeroen Vermeulen
security.py cleanup and speedup, ported over from devel branch.
473
                function_permissions.add(perm, obj.fullname, who)
474
                function_permissions.add("EXECUTE", obj.fullname, who_ro)
475
                function_permissions.add(
476
                    "EXECUTE", obj.fullname, "read", is_group=True)
477
                function_permissions.add(
478
                    "ALL", obj.fullname, "admin", is_group=True)
1520 by Canonical.com Patch Queue Manager
Review and fix database security update code
479
            else:
7675.1072.1 by Jeroen Vermeulen
security.py cleanup and speedup, ported over from devel branch.
480
                table_permissions.add(
481
                    "ALL", obj.fullname, "admin", is_group=True)
482
                table_permissions.add(perm, obj.fullname, who)
483
                table_permissions.add("SELECT", obj.fullname, who_ro)
484
                is_secure = (obj.fullname in SECURE_TABLES)
485
                if not is_secure:
486
                    table_permissions.add(
487
                        "SELECT", obj.fullname, "read", is_group=True)
488
                if obj.seqname in schema:
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
489
                    if 'INSERT' in perm:
490
                        seqperm = 'USAGE'
491
                    elif 'SELECT' in perm:
492
                        seqperm = 'SELECT'
7675.1072.1 by Jeroen Vermeulen
security.py cleanup and speedup, ported over from devel branch.
493
                    sequence_permissions.add(seqperm, obj.seqname, who)
494
                    if not is_secure:
495
                        sequence_permissions.add(
496
                            "SELECT", obj.seqname, "read", is_group=True)
497
                    sequence_permissions.add("SELECT", obj.seqname, who_ro)
498
                    sequence_permissions.add(
499
                        "ALL", obj.seqname, "admin", is_group=True)
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
500
7675.1072.1 by Jeroen Vermeulen
security.py cleanup and speedup, ported over from devel branch.
501
    function_permissions.grant(cur)
502
    table_permissions.grant(cur)
503
    sequence_permissions.grant(cur)
7675.895.1 by Jeroen Vermeulen
Speed up security.py by an order of magnitude by batching permission changes.
504
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
505
    # Set permissions on public schemas
506
    public_schemas = [
7675.1072.1 by Jeroen Vermeulen
security.py cleanup and speedup, ported over from devel branch.
507
        s.strip() for s in config.get('DEFAULT', 'public_schemas').split(',')
508
        if s.strip()]
7675.395.152 by Stuart Bishop
Add --no-revoke to security.py for use live, and --dry-run and improve logging for shits and giggles.
509
    log.debug("Granting access to %d public schemas", len(public_schemas))
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
510
    for schema_name in public_schemas:
511
        cur.execute("GRANT USAGE ON SCHEMA %s TO PUBLIC" % (
512
            quote_identifier(schema_name),
513
            ))
514
    for obj in schema.values():
515
        if obj.schema not in public_schemas:
516
            continue
1520 by Canonical.com Patch Queue Manager
Review and fix database security update code
517
        found.add(obj)
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
518
        if obj.type == 'function':
7370.2.2 by Barry Warsaw
pick lint
519
            cur.execute('GRANT EXECUTE ON FUNCTION %s TO PUBLIC' %
520
                        obj.fullname)
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
521
        else:
522
            cur.execute('GRANT SELECT ON TABLE %s TO PUBLIC' % obj.fullname)
523
1520 by Canonical.com Patch Queue Manager
Review and fix database security update code
524
    # Raise an error if we have database objects lying around that have not
525
    # had permissions assigned.
10293.3.1 by Max Bowsher
Remove use of the deprecated sets module.
526
    forgotten = set()
1520 by Canonical.com Patch Queue Manager
Review and fix database security update code
527
    for obj in schema.values():
528
        if obj not in found:
529
            forgotten.add(obj)
530
    forgotten = [obj.fullname for obj in forgotten
7675.1072.1 by Jeroen Vermeulen
security.py cleanup and speedup, ported over from devel branch.
531
        if obj.type in ['table', 'function', 'view']]
1520 by Canonical.com Patch Queue Manager
Review and fix database security update code
532
    if forgotten:
1831 by Canonical.com Patch Queue Manager
New config machinery, database helpers and oddsnsods required for staging
533
        log.warn('No permissions specified for %r', forgotten)
1520 by Canonical.com Patch Queue Manager
Review and fix database security update code
534
7675.395.152 by Stuart Bishop
Add --no-revoke to security.py for use live, and --dry-run and improve logging for shits and giggles.
535
    if options.dryrun:
536
        log.info("Dry run - rolling back changes")
537
        con.rollback()
538
    else:
539
        log.debug("Committing changes")
540
        con.commit()
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
541
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
542
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
543
if __name__ == '__main__':
1520 by Canonical.com Patch Queue Manager
Review and fix database security update code
544
    parser = OptionParser()
545
    parser.add_option(
7675.395.152 by Stuart Bishop
Add --no-revoke to security.py for use live, and --dry-run and improve logging for shits and giggles.
546
        "-n", "--dry-run", dest="dryrun", default=False,
547
        action="store_true", help="Don't commit any changes")
548
    parser.add_option(
549
        "--revoke", dest="revoke", default=True, action="store_true",
550
        help="Revoke privileges as well as add them")
551
    parser.add_option(
552
        "--no-revoke", dest="revoke", default=True, action="store_false",
553
        help="Do not revoke any privileges. Just add.")
554
    parser.add_option(
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
555
        "-o", "--owner", dest="owner", default="postgres",
556
        help="Owner of PostgreSQL objects")
557
    parser.add_option(
558
        "-c", "--cluster", dest="cluster", default=False,
559
        action="store_true",
560
        help="Rebuild permissions on all nodes in the Slony-I cluster.")
1831 by Canonical.com Patch Queue Manager
New config machinery, database helpers and oddsnsods required for staging
561
    db_options(parser)
562
    logger_options(parser)
563
1520 by Canonical.com Patch Queue Manager
Review and fix database security update code
564
    (options, args) = parser.parse_args()
1831 by Canonical.com Patch Queue Manager
New config machinery, database helpers and oddsnsods required for staging
565
566
    log = logger(options)
567
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
568
    sys.exit(main(options))