~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
1831 by Canonical.com Patch Queue Manager
New config machinery, database helpers and oddsnsods required for staging
8
import _pythonpath
9
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.
10
from collections import defaultdict
11
from ConfigParser import SafeConfigParser
7658.6.3 by Stuart Bishop
Review feedback from allenap
12
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.
13
from optparse import OptionParser
7370.2.2 by Barry Warsaw
pick lint
14
import os
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
15
import sys
16
17
import psycopg2
7370.2.2 by Barry Warsaw
pick lint
18
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.
19
from fti import quote_identifier
14606.3.4 by William Grant
Replace canonical.database usage everywhere, and format-imports.
20
from lp.services.database.sqlbase import connect
21
from lp.services.scripts import (
22
    db_options,
23
    logger,
24
    logger_options,
25
    )
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
26
import replication.helpers
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
27
7675.396.2 by Stuart Bishop
Blacklist read permissions on some tables to the read group
28
# The 'read' group does not get given select permission on the following
29
# tables. This is to stop the ro user being given access to secrurity
30
# sensitive information that interactive sessions don't need.
7675.1220.15 by William Grant
Set up the read role outside the loop, so we don't do it len(users) times per object.
31
SECURE_TABLES = set((
7675.396.2 by Stuart Bishop
Blacklist read permissions on some tables to the read group
32
    'public.accountpassword',
7675.1220.15 by William Grant
Set up the read role outside the loop, so we don't do it len(users) times per object.
33
    'public.accountpassword_id_seq',
7675.395.31 by Stuart Bishop
More secure tables
34
    'public.oauthnonce',
7675.1220.15 by William Grant
Set up the read role outside the loop, so we don't do it len(users) times per object.
35
    'public.oauthnonce_id_seq',
7675.395.31 by Stuart Bishop
More secure tables
36
    'public.openidnonce',
7675.1220.15 by William Grant
Set up the read role outside the loop, so we don't do it len(users) times per object.
37
    'public.openidnonce_id_seq',
7675.395.31 by Stuart Bishop
More secure tables
38
    'public.openidconsumernonce',
7675.1220.15 by William Grant
Set up the read role outside the loop, so we don't do it len(users) times per object.
39
    'public.openidconsumernonce_id_seq',
40
    ))
7675.396.2 by Stuart Bishop
Blacklist read permissions on some tables to the read group
41
7675.1220.2 by William Grant
Parse postgres ACLs into dicts.
42
POSTGRES_ACL_MAP = {
43
    'r': 'SELECT',
44
    'w': 'UPDATE',
45
    'a': 'INSERT',
46
    'd': 'DELETE',
47
    'D': 'TRUNCATE',
48
    'x': 'REFERENCES',
49
    't': 'TRIGGER',
50
    'X': 'EXECUTE',
51
    'U': 'USAGE',
52
    'C': 'CREATE',
53
    'c': 'CONNECT',
54
    'T': 'TEMPORARY',
55
    }
56
57
7675.1220.12 by William Grant
Replace unquote_identifier and bits of parse_postgres_acl with _split_postgres_aclitem, which handles quoted identifiers properly.
58
def _split_postgres_aclitem(aclitem):
59
    """Split a PostgreSQL aclitem textual representation.
60
61
    Returns the (grantee, privs, grantor), unquoted and separated.
62
    """
63
    components = {'grantee': '', 'privs': '', 'grantor': ''}
64
    current_component = 'grantee'
65
    inside_quoted = False
66
    maybe_finished_quoted = False
67
    for char in aclitem:
7675.1220.25 by William Grant
Invert _split_postgres_aclitem a bit.
68
        if inside_quoted:
69
            if maybe_finished_quoted:
70
                maybe_finished_quoted = False
71
                if char == '"':
72
                    components[current_component] += '"'
73
                    continue
74
                else:
75
                    inside_quoted = False
76
            elif char == '"':
77
                maybe_finished_quoted = True
78
                continue
79
        # inside_quoted may have just been made False, so no else block
80
        # for you.
81
        if not inside_quoted:
82
            if char == '"':
7675.1220.12 by William Grant
Replace unquote_identifier and bits of parse_postgres_acl with _split_postgres_aclitem, which handles quoted identifiers properly.
83
                inside_quoted = True
7675.1220.25 by William Grant
Invert _split_postgres_aclitem a bit.
84
                continue
85
            elif char == '=':
86
                current_component = 'privs'
87
                continue
88
            elif char == '/':
89
                current_component = 'grantor'
90
                continue
91
        components[current_component] += char
7675.1220.12 by William Grant
Replace unquote_identifier and bits of parse_postgres_acl with _split_postgres_aclitem, which handles quoted identifiers properly.
92
    return components['grantee'], components['privs'], components['grantor']
7675.1220.5 by William Grant
Fix parse_postgrs_acl to unquote names, and handle missing ACLs.
93
94
7675.1220.2 by William Grant
Parse postgres ACLs into dicts.
95
def parse_postgres_acl(acl):
7675.1220.3 by William Grant
Docstring.
96
    """Parse a PostgreSQL object ACL into a dict with permission names.
97
98
    The dict is of the form {user: {permission: grant option}}.
99
    """
7675.1220.2 by William Grant
Parse postgres ACLs into dicts.
100
    parsed = {}
7675.1220.5 by William Grant
Fix parse_postgrs_acl to unquote names, and handle missing ACLs.
101
    if acl is None:
102
        return parsed
7675.1220.2 by William Grant
Parse postgres ACLs into dicts.
103
    for entry in acl:
7675.1220.12 by William Grant
Replace unquote_identifier and bits of parse_postgres_acl with _split_postgres_aclitem, which handles quoted identifiers properly.
104
        grantee, privs, grantor = _split_postgres_aclitem(entry)
105
        if grantee == '':
106
            grantee = 'public'
107
        parsed_privs = []
108
        for priv in privs:
109
            if priv == '*':
110
                parsed_privs[-1] = (parsed_privs[-1][0], True)
7675.1220.6 by William Grant
Fix grant option handling in parse_postgres_acl.
111
                continue
7675.1220.12 by William Grant
Replace unquote_identifier and bits of parse_postgres_acl with _split_postgres_aclitem, which handles quoted identifiers properly.
112
            parsed_privs.append((POSTGRES_ACL_MAP[priv], False))
113
        parsed[grantee] = dict(parsed_privs)
7675.1220.2 by William Grant
Parse postgres ACLs into dicts.
114
    return parsed
115
7675.396.2 by Stuart Bishop
Blacklist read permissions on some tables to the read group
116
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
117
class DbObject(object):
7675.1072.1 by Jeroen Vermeulen
security.py cleanup and speedup, ported over from devel branch.
118
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
119
    def __init__(
7675.1220.1 by William Grant
Grab the ACL for relations and functions.
120
        self, schema, name, type_, owner, acl, arguments=None, language=None):
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
121
        self.schema = schema
122
        self.name = name
123
        self.type = type_
124
        self.owner = owner
7675.1220.1 by William Grant
Grab the ACL for relations and functions.
125
        self.acl = acl
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
126
        self.arguments = arguments
127
        self.language = language
128
1339 by Canonical.com Patch Queue Manager
Add generated database diagrams (initial cut)
129
    def __eq__(self, other):
130
        return self.schema == other.schema and self.name == other.name
131
7658.3.16 by Stuart Bishop
Reapply backed out db changes
132
    @property
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
133
    def fullname(self):
7675.1072.1 by Jeroen Vermeulen
security.py cleanup and speedup, ported over from devel branch.
134
        fn = "%s.%s" % (self.schema, self.name)
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
135
        if self.type == 'function':
136
            fn = "%s(%s)" % (fn, self.arguments)
137
        return fn
138
7658.3.16 by Stuart Bishop
Reapply backed out db changes
139
    @property
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
140
    def seqname(self):
141
        if self.type != 'table':
142
            return ''
143
        return "%s.%s" % (self.schema, self.name + '_id_seq')
144
145
146
class DbSchema(dict):
13465.2.20 by Stuart Bishop
delint
147
    groups = None  # List of groups defined in the db
148
    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.
149
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
150
    def __init__(self, con):
7370.2.2 by Barry Warsaw
pick lint
151
        super(DbSchema, self).__init__()
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
152
        cur = con.cursor()
153
        cur.execute('''
7370.2.1 by Barry Warsaw
Fixes exposed by testing on staging, along with more unittests and whitespace
154
            SELECT
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
155
                n.nspname as "Schema",
156
                c.relname as "Name",
157
                CASE c.relkind
158
                    WHEN 'r' THEN 'table'
159
                    WHEN 'v' THEN 'view'
160
                    WHEN 'i' THEN 'index'
161
                    WHEN 'S' THEN 'sequence'
162
                    WHEN 's' THEN 'special'
163
                END as "Type",
7675.1220.1 by William Grant
Grab the ACL for relations and functions.
164
                u.usename as "Owner",
7675.1220.2 by William Grant
Parse postgres ACLs into dicts.
165
                c.relacl::text[] as "ACL"
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
166
            FROM pg_catalog.pg_class c
167
                LEFT JOIN pg_catalog.pg_user u ON u.usesysid = c.relowner
168
                LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
169
            WHERE c.relkind IN ('r','v','S','')
170
                AND n.nspname NOT IN ('pg_catalog', 'pg_toast')
171
                AND pg_catalog.pg_table_is_visible(c.oid)
172
            ORDER BY 1,2
173
            ''')
7675.1220.1 by William Grant
Grab the ACL for relations and functions.
174
        for schema, name, type_, owner, acl in cur.fetchall():
7370.2.2 by Barry Warsaw
pick lint
175
            key = '%s.%s' % (schema, name)
7675.1220.2 by William Grant
Parse postgres ACLs into dicts.
176
            self[key] = DbObject(
177
                schema, name, type_, owner, parse_postgres_acl(acl))
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
178
179
        cur.execute(r"""
180
            SELECT
181
                n.nspname as "schema",
182
                p.proname as "name",
183
                pg_catalog.oidvectortypes(p.proargtypes) as "Argument types",
184
                u.usename as "owner",
7675.1220.1 by William Grant
Grab the ACL for relations and functions.
185
                p.proacl::text[] as "acl",
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
186
                l.lanname as "language"
187
            FROM pg_catalog.pg_proc p
188
                LEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace
189
                LEFT JOIN pg_catalog.pg_language l ON l.oid = p.prolang
190
                LEFT JOIN pg_catalog.pg_user u ON u.usesysid = p.proowner
7658.3.16 by Stuart Bishop
Reapply backed out db changes
191
                LEFT JOIN pg_catalog.pg_type r ON r.oid = p.prorettype
192
            WHERE
193
                r.typname NOT IN ('trigger', 'language_handler')
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
194
                AND pg_catalog.pg_function_is_visible(p.oid)
195
                AND n.nspname <> 'pg_catalog'
196
                """)
7675.1220.1 by William Grant
Grab the ACL for relations and functions.
197
        for schema, name, arguments, owner, acl, language in cur.fetchall():
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
198
            self['%s.%s(%s)' % (schema, name, arguments)] = DbObject(
7675.1220.2 by William Grant
Parse postgres ACLs into dicts.
199
                    schema, name, 'function', owner, parse_postgres_acl(acl),
200
                    arguments, language)
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
201
        # Pull a list of groups
202
        cur.execute("SELECT groname FROM pg_group")
203
        self.groups = [r[0] for r in cur.fetchall()]
204
205
        # Pull a list of users
206
        cur.execute("SELECT usename FROM pg_user")
207
        self.users = [r[0] for r in cur.fetchall()]
208
7658.6.3 by Stuart Bishop
Review feedback from allenap
209
    @property
210
    def principals(self):
211
        return chain(self.groups, self.users)
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
212
213
214
class CursorWrapper(object):
7675.1072.1 by Jeroen Vermeulen
security.py cleanup and speedup, ported over from devel branch.
215
1831 by Canonical.com Patch Queue Manager
New config machinery, database helpers and oddsnsods required for staging
216
    def __init__(self, cursor):
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
217
        self.__dict__['_cursor'] = cursor
218
219
    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
220
        cmd = cmd.encode('utf8')
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
221
        if params is None:
13465.2.27 by Stuart Bishop
Lower the volumn of security.py DEBUG output, making full-update.py -v readable
222
            log.debug3('%s' % (cmd, ))
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
223
            return self.__dict__['_cursor'].execute(cmd)
224
        else:
13465.2.27 by Stuart Bishop
Lower the volumn of security.py DEBUG output, making full-update.py -v readable
225
            log.debug3('%s [%r]' % (cmd, params))
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
226
            return self.__dict__['_cursor'].execute(cmd, params)
227
228
    def __getattr__(self, key):
229
        return getattr(self.__dict__['_cursor'], key)
230
231
    def __setattr__(self, key, value):
232
        return setattr(self.__dict__['_cursor'], key, value)
233
1520 by Canonical.com Patch Queue Manager
Review and fix database security update code
234
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
235
CONFIG_DEFAULTS = {
7675.1072.1 by Jeroen Vermeulen
security.py cleanup and speedup, ported over from devel branch.
236
    'groups': '',
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
237
    }
238
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
239
1520 by Canonical.com Patch Queue Manager
Review and fix database security update code
240
def main(options):
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
241
    # Load the config file
242
    config = SafeConfigParser(CONFIG_DEFAULTS)
1831 by Canonical.com Patch Queue Manager
New config machinery, database helpers and oddsnsods required for staging
243
    configfile_name = os.path.join(os.path.dirname(__file__), 'security.cfg')
244
    config.read([configfile_name])
245
13879.1.3 by William Grant
Drop now-obsolete connect(user) args.
246
    con = connect()
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
247
248
    if options.cluster:
249
        nodes = replication.helpers.get_nodes(con, 1)
250
        if nodes:
251
            # If we have a replicated environment, reset permissions on all
252
            # Nodes.
253
            con.close()
254
            for node in nodes:
255
                log.info("Resetting permissions on %s (%s)" % (
256
                    node.nickname, node.connection_string))
257
                reset_permissions(
258
                    psycopg2.connect(node.connection_string), config, options)
13465.2.15 by Stuart Bishop
Invoke security.py in-process to save startup overhead
259
            return 0
7675.395.197 by Stuart Bishop
Full database update script for fast deployments
260
        log.warning("--cluster requested, but not a Slony-I cluster.")
261
    log.info("Resetting permissions on single database")
262
    reset_permissions(con, config, options)
13465.2.15 by Stuart Bishop
Invoke security.py in-process to save startup overhead
263
    return 0
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
264
265
7675.895.1 by Jeroen Vermeulen
Speed up security.py by an order of magnitude by batching permission changes.
266
def list_identifiers(identifiers):
267
    """List all of `identifiers` as SQL, quoted and separated by commas.
268
269
    :param identifiers: A sequence of SQL identifiers.
270
    :return: A comma-separated SQL string consisting of all identifiers
271
        passed in.  Each will be quoted for use in SQL.
272
    """
273
    return ', '.join([
274
        quote_identifier(identifier) for identifier in identifiers])
275
276
7675.1072.1 by Jeroen Vermeulen
security.py cleanup and speedup, ported over from devel branch.
277
class PermissionGatherer:
278
    """Gather permissions for bulk granting or revocation.
279
280
    Processing such statements in bulk (with multiple users, tables,
281
    or permissions in one statement) is faster than issuing very large
282
    numbers of individual statements.
283
    """
284
285
    def __init__(self, entity_keyword):
286
        """Gather for SQL entities of one kind (TABLE, FUNCTION, SEQUENCE).
287
288
        :param entity_keyword: The SQL keyword for the kind of entity
289
            that permissions will be gathered for.
290
        """
291
        self.entity_keyword = entity_keyword
292
        self.permissions = defaultdict(dict)
293
7675.1220.4 by William Grant
groups and users are now roles, so the GROUP option to GRANT can be removed.
294
    def add(self, permission, entity, principal):
7675.1072.1 by Jeroen Vermeulen
security.py cleanup and speedup, ported over from devel branch.
295
        """Add a permission.
296
297
        Add all privileges you want to grant or revoke first, then use
298
        `grant` or `revoke` to process them in bulk.
299
300
        :param permission: A permission: SELECT, INSERT, EXECUTE, etc.
301
        :param entity: Table, function, or sequence on which to grant
302
            or revoke a privilege.
303
        :param principal: User or group to which the privilege should
304
            apply.
305
        """
7675.1220.11 by William Grant
Group GRANTs/REVOKEs by (permission, principal) rather than (permission, entity); there are many more entities than there are principals, plus we always grant ALL to admin so should optimise for that case.
306
        self.permissions[permission].setdefault(principal, set()).add(entity)
7675.1072.1 by Jeroen Vermeulen
security.py cleanup and speedup, ported over from devel branch.
307
308
    def tabulate(self):
309
        """Group privileges into single-statement work items.
310
311
        Each entry returned by this method represents a batch of
312
        privileges that can be granted or revoked in a single SQL
313
        statement.
314
315
        :return: A sequence of tuples of strings: permission(s) to
316
            grant/revoke, entity or entities to act on, and principal(s)
317
            to grant or revoke for.  Each is a string.
318
        """
319
        result = []
320
        for permission, parties in self.permissions.iteritems():
7675.1220.11 by William Grant
Group GRANTs/REVOKEs by (permission, principal) rather than (permission, entity); there are many more entities than there are principals, plus we always grant ALL to admin so should optimise for that case.
321
            for principal, entities in parties.iteritems():
7675.1072.1 by Jeroen Vermeulen
security.py cleanup and speedup, ported over from devel branch.
322
                result.append(
7675.1220.11 by William Grant
Group GRANTs/REVOKEs by (permission, principal) rather than (permission, entity); there are many more entities than there are principals, plus we always grant ALL to admin so should optimise for that case.
323
                    (permission, ", ".join(entities), principal))
7675.1072.1 by Jeroen Vermeulen
security.py cleanup and speedup, ported over from devel branch.
324
        return result
325
326
    def countPermissions(self):
327
        """Count the number of different permissions."""
328
        return len(self.permissions)
329
330
    def countEntities(self):
331
        """Count the number of different entities."""
7675.1220.11 by William Grant
Group GRANTs/REVOKEs by (permission, principal) rather than (permission, entity); there are many more entities than there are principals, plus we always grant ALL to admin so should optimise for that case.
332
        entities = set()
333
        for entities_and_entities in self.permissions.itervalues():
334
            for extra_entities in entities_and_entities.itervalues():
335
                entities.update(extra_entities)
336
        return len(entities)
7675.1072.1 by Jeroen Vermeulen
security.py cleanup and speedup, ported over from devel branch.
337
338
    def countPrincipals(self):
339
        """Count the number of different principals."""
7675.1220.11 by William Grant
Group GRANTs/REVOKEs by (permission, principal) rather than (permission, entity); there are many more entities than there are principals, plus we always grant ALL to admin so should optimise for that case.
340
        return len(set(sum([
341
            principals.keys()
342
            for principals in self.permissions.itervalues()], [])))
7675.1072.1 by Jeroen Vermeulen
security.py cleanup and speedup, ported over from devel branch.
343
344
    def grant(self, cur):
345
        """Grant all gathered permissions.
346
347
        :param cur: A cursor to operate on.
348
        """
349
        log.debug(
350
            "Granting %d permission(s) on %d %s(s) for %d user(s)/group(s).",
351
            self.countPermissions(),
352
            self.countEntities(),
353
            self.entity_keyword,
354
            self.countPrincipals())
355
        grant_count = 0
356
        for permissions, entities, principals in self.tabulate():
357
            grant = "GRANT %s ON %s %s TO %s" % (
358
                permissions, self.entity_keyword, entities, principals)
359
            log.debug2(grant)
360
            cur.execute(grant)
361
            grant_count += 1
362
        log.debug("Issued %d GRANT statement(s).", grant_count)
363
364
    def revoke(self, cur):
365
        """Revoke all gathered permissions.
366
367
        :param cur: A cursor to operate on.
368
        """
369
        log.debug(
370
            "Revoking %d permission(s) on %d %s(s) for %d user(s)/group(s).",
371
            self.countPermissions(),
372
            self.countEntities(),
373
            self.entity_keyword,
374
            self.countPrincipals())
375
        revoke_count = 0
376
        for permissions, entities, principals in self.tabulate():
377
            revoke = "REVOKE %s ON %s %s FROM %s" % (
378
                permissions, self.entity_keyword, entities, principals)
379
            log.debug2(revoke)
380
            cur.execute(revoke)
381
            revoke_count += 1
382
        log.debug("Issued %d REVOKE statement(s).", revoke_count)
383
384
7675.1220.7 by William Grant
First stab at permission diffing. Works for everything except revocation of admin privs.
385
def alter_permissions(cur, which, revoke=False):
386
    """Efficiently apply a set of permission changes.
387
388
    :param cur: a database cursor
389
    :param which: an iterable of (object, role, permissions)
390
    :param revoke: whether to revoke or grant permissions
391
    """
392
    gatherers = {
393
        'table': PermissionGatherer("TABLE"),
394
        'function': PermissionGatherer("FUNCTION"),
395
        'sequence': PermissionGatherer("SEQUENCE"),
396
        }
397
398
    for obj, role, perms in which:
399
        gatherers.get(obj.type, gatherers['table']).add(
400
            ', '.join(perms), obj.fullname, quote_identifier(role))
401
402
    for gatherer in gatherers.values():
403
        if revoke:
404
            gatherer.revoke(cur)
405
        else:
406
            gatherer.grant(cur)
407
408
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
409
def reset_permissions(con, config, options):
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
410
    schema = DbSchema(con)
7675.895.1 by Jeroen Vermeulen
Speed up security.py by an order of magnitude by batching permission changes.
411
    all_users = list_identifiers(schema.users)
412
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
413
    cur = CursorWrapper(con.cursor())
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
414
1520 by Canonical.com Patch Queue Manager
Review and fix database security update code
415
    # Add our two automatically maintained groups
1812 by Canonical.com Patch Queue Manager
[trivial] Create admin and read groups automatically
416
    for group in ['read', 'admin']:
417
        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.
418
            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.
419
            cur.execute("ALTER GROUP %s DROP USER %s" % (
420
                    quote_identifier(group), all_users))
1812 by Canonical.com Patch Queue Manager
[trivial] Create admin and read groups automatically
421
        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.
422
            log.debug("Creating %s role" % group)
1812 by Canonical.com Patch Queue Manager
[trivial] Create admin and read groups automatically
423
            cur.execute("CREATE GROUP %s" % quote_identifier(group))
424
            schema.groups.append(group)
1520 by Canonical.com Patch Queue Manager
Review and fix database security update code
425
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
426
    # Create all required groups and users.
427
    for section_name in config.sections():
428
        if section_name.lower() == 'public':
429
            continue
7658.3.16 by Stuart Bishop
Reapply backed out db changes
430
7658.6.3 by Stuart Bishop
Review feedback from allenap
431
        assert not section_name.endswith('_ro'), (
432
            '_ro namespace is reserved (%s)' % repr(section_name))
433
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
434
        type_ = config.get(section_name, 'type')
7658.3.16 by Stuart Bishop
Reapply backed out db changes
435
        assert type_ in ['user', 'group'], 'Unknown type %s' % type_
436
437
        role_options = [
438
            'NOCREATEDB', 'NOCREATEROLE', 'NOCREATEUSER', 'INHERIT']
7658.5.1 by Stuart Bishop
Typo stopping db users from logging in
439
        if type_ == 'user':
7658.3.16 by Stuart Bishop
Reapply backed out db changes
440
            role_options.append('LOGIN')
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
441
        else:
7658.3.16 by Stuart Bishop
Reapply backed out db changes
442
            role_options.append('NOLOGIN')
443
444
        for username in [section_name, '%s_ro' % section_name]:
445
            if username in schema.principals:
446
                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.
447
                    if options.revoke:
13465.2.27 by Stuart Bishop
Lower the volumn of security.py DEBUG output, making full-update.py -v readable
448
                        log.debug2("Revoking membership of %s role", username)
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.
449
                        cur.execute("REVOKE %s FROM %s" % (
450
                            quote_identifier(username), all_users))
7658.3.16 by Stuart Bishop
Reapply backed out db changes
451
                else:
452
                    # Note - we don't drop the user because it might own
453
                    # objects in other databases. We need to ensure they are
454
                    # not superusers though!
13465.2.27 by Stuart Bishop
Lower the volumn of security.py DEBUG output, making full-update.py -v readable
455
                    log.debug2("Resetting role options of %s role.", username)
7658.3.16 by Stuart Bishop
Reapply backed out db changes
456
                    cur.execute(
457
                        "ALTER ROLE %s WITH %s" % (
458
                            quote_identifier(username),
459
                            ' '.join(role_options)))
460
            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.
461
                log.debug("Creating %s role.", username)
7658.3.16 by Stuart Bishop
Reapply backed out db changes
462
                cur.execute(
463
                    "CREATE ROLE %s WITH %s"
464
                    % (quote_identifier(username), ' '.join(role_options)))
465
                schema.groups.append(username)
466
7658.5.2 by Stuart Bishop
Set default_transaction_read_only on database roles
467
        # Set default read-only mode for our roles.
468
        cur.execute(
469
            'ALTER ROLE %s SET default_transaction_read_only TO FALSE'
470
            % quote_identifier(section_name))
471
        cur.execute(
472
            'ALTER ROLE %s SET default_transaction_read_only TO TRUE'
473
            % quote_identifier('%s_ro' % section_name))
474
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
475
    # Add users to groups
476
    for user in config.sections():
477
        if config.get(user, 'type') != 'user':
478
            continue
479
        groups = [
480
            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.
481
            if g.strip()]
7658.3.16 by Stuart Bishop
Reapply backed out db changes
482
        # Read-Only users get added to Read-Only groups.
483
        if user.endswith('_ro'):
484
            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.
485
        if groups:
13465.2.27 by Stuart Bishop
Lower the volumn of security.py DEBUG output, making full-update.py -v readable
486
            log.debug2("Adding %s to %s roles", user, ', '.join(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.
487
            for group in groups:
488
                cur.execute(r"""ALTER GROUP %s ADD USER %s""" % (
489
                    quote_identifier(group), quote_identifier(user)))
490
        else:
13465.2.27 by Stuart Bishop
Lower the volumn of security.py DEBUG output, making full-update.py -v readable
491
            log.debug2("%s not in any roles", user)
7370.2.1 by Barry Warsaw
Fixes exposed by testing on staging, along with more unittests and whitespace
492
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.
493
    if options.revoke:
13465.2.19 by Stuart Bishop
security.py --no-revoke should not attempt ownership changes
494
        # Change ownership of all objects to OWNER.
495
        # We skip this in --no-revoke mode as ownership changes may
496
        # block on a live system.
497
        for obj in schema.values():
498
            if obj.type in ("function", "sequence"):
13465.2.20 by Stuart Bishop
delint
499
                pass  # Can't change ownership of functions or sequences
13465.2.19 by Stuart Bishop
security.py --no-revoke should not attempt ownership changes
500
            else:
501
                if obj.owner != options.owner:
502
                    log.info("Resetting ownership of %s", obj.fullname)
503
                    cur.execute("ALTER TABLE %s OWNER TO %s" % (
504
                        obj.fullname, quote_identifier(options.owner)))
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.
505
    else:
13465.2.19 by Stuart Bishop
security.py --no-revoke should not attempt ownership changes
506
        log.info("Not resetting ownership of database objects")
7675.1220.7 by William Grant
First stab at permission diffing. Works for everything except revocation of admin privs.
507
7675.1220.24 by William Grant
Controlled roles are now known as managed roles.
508
    managed_roles = set(['read', 'admin'])
7675.1220.7 by William Grant
First stab at permission diffing. Works for everything except revocation of admin privs.
509
    for section_name in config.sections():
7675.1220.24 by William Grant
Controlled roles are now known as managed roles.
510
        managed_roles.add(section_name)
7675.1220.7 by William Grant
First stab at permission diffing. Works for everything except revocation of admin privs.
511
        if section_name != 'public':
7675.1220.24 by William Grant
Controlled roles are now known as managed roles.
512
            managed_roles.add(section_name + "_ro")
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
513
1520 by Canonical.com Patch Queue Manager
Review and fix database security update code
514
    # Set of all tables we have granted permissions on. After we have assigned
515
    # permissions, we can use this to determine what tables have been
516
    # forgotten about.
10293.3.1 by Max Bowsher
Remove use of the deprecated sets module.
517
    found = set()
1520 by Canonical.com Patch Queue Manager
Review and fix database security update code
518
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
519
    # Set permissions as per config file
7675.1220.7 by William Grant
First stab at permission diffing. Works for everything except revocation of admin privs.
520
    desired_permissions = defaultdict(lambda: defaultdict(set))
7675.895.1 by Jeroen Vermeulen
Speed up security.py by an order of magnitude by batching permission changes.
521
7675.1220.14 by William Grant
Rip slow logging and dict key existence checks out of the permission generator loop.
522
    valid_objs = set(schema.iterkeys())
523
7675.1220.18 by William Grant
Rename controlled_objs to readable_objs.
524
    # Any object with permissions granted is accessible to the 'read'
525
    # role. Some (eg. the lp_* replicated tables and internal or trigger
526
    # functions) aren't readable.
7675.1220.19 by William Grant
Don't just control permissions on controlled_objs; deal with controlled_roles on all objects in the DB. This tweaking the admin handling to grant ALL on granted_objs, and revoke ALL from everything else that has admin in the ACL.
527
    granted_objs = set()
7675.1220.18 by William Grant
Rename controlled_objs to readable_objs.
528
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
529
    for username in config.sections():
7675.1220.14 by William Grant
Rip slow logging and dict key existence checks out of the permission generator loop.
530
        who = username
531
        if username == 'public':
532
            who_ro = who
533
        else:
534
            who_ro = '%s_ro' % username
535
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
536
        for obj_name, perm in config.items(username):
537
            if '.' not in obj_name:
538
                continue
7675.1220.14 by William Grant
Rip slow logging and dict key existence checks out of the permission generator loop.
539
            if obj_name not in valid_objs:
2837.1.3 by Stuart Bishop
Make 'unknown object' be a warning instead of a failure to ease
540
                log.warn('Bad object name %r', obj_name)
541
                continue
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
542
            obj = schema[obj_name]
1520 by Canonical.com Patch Queue Manager
Review and fix database security update code
543
544
            found.add(obj)
545
546
            perm = perm.strip()
547
            if not perm:
548
                # No perm means no rights. We can't grant no rights, so skip.
549
                continue
550
7675.1220.19 by William Grant
Don't just control permissions on controlled_objs; deal with controlled_roles on all objects in the DB. This tweaking the admin handling to grant ALL on granted_objs, and revoke ALL from everything else that has admin in the ACL.
551
            granted_objs.add(obj)
7675.1220.8 by William Grant
Granting ALL on everything to admin is more easily done at the end of the loop.
552
1520 by Canonical.com Patch Queue Manager
Review and fix database security update code
553
            if obj.type == 'function':
7675.1220.7 by William Grant
First stab at permission diffing. Works for everything except revocation of admin privs.
554
                desired_permissions[obj][who].update(perm.split(', '))
555
                if who_ro:
556
                    desired_permissions[obj][who_ro].add("EXECUTE")
1520 by Canonical.com Patch Queue Manager
Review and fix database security update code
557
            else:
7675.1220.7 by William Grant
First stab at permission diffing. Works for everything except revocation of admin privs.
558
                desired_permissions[obj][who].update(perm.split(', '))
559
                if who_ro:
560
                    desired_permissions[obj][who_ro].add("SELECT")
7675.1220.14 by William Grant
Rip slow logging and dict key existence checks out of the permission generator loop.
561
                if obj.seqname in valid_objs:
7675.1220.7 by William Grant
First stab at permission diffing. Works for everything except revocation of admin privs.
562
                    seq = schema[obj.seqname]
7675.1220.19 by William Grant
Don't just control permissions on controlled_objs; deal with controlled_roles on all objects in the DB. This tweaking the admin handling to grant ALL on granted_objs, and revoke ALL from everything else that has admin in the ACL.
563
                    granted_objs.add(seq)
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
564
                    if 'INSERT' in perm:
565
                        seqperm = 'USAGE'
566
                    elif 'SELECT' in perm:
567
                        seqperm = 'SELECT'
7675.1220.17 by William Grant
Fix an existing bug where if neither SELECT nor INSERT was granted on a table, the permission for the previous sequence would be granted to the current one. Only affects launchpad_main's buildfarmjob and packagebuild privileges, where it holds only DELETE.
568
                    else:
569
                        seqperm = None
570
                    if seqperm:
571
                        desired_permissions[seq][who].add(seqperm)
7675.1220.7 by William Grant
First stab at permission diffing. Works for everything except revocation of admin privs.
572
                    desired_permissions[seq][who_ro].add("SELECT")
573
7675.1220.21 by William Grant
Comments.
574
    # read gets read access to all non-secure objects that we've granted
575
    # anybody access to.
7675.1220.19 by William Grant
Don't just control permissions on controlled_objs; deal with controlled_roles on all objects in the DB. This tweaking the admin handling to grant ALL on granted_objs, and revoke ALL from everything else that has admin in the ACL.
576
    for obj in granted_objs:
7675.1220.15 by William Grant
Set up the read role outside the loop, so we don't do it len(users) times per object.
577
        if obj.type == 'function':
578
            desired_permissions[obj]['read'].add("EXECUTE")
579
        else:
7675.1220.22 by William Grant
functions can't be secure.
580
            if obj.fullname not in SECURE_TABLES:
7675.1220.15 by William Grant
Set up the read role outside the loop, so we don't do it len(users) times per object.
581
                desired_permissions[obj]['read'].add("SELECT")
582
7675.1220.20 by William Grant
Integrate public schema (ts2) handling into the main thing.
583
    # Set permissions on public schemas
584
    public_schemas = [
585
        s.strip() for s in config.get('DEFAULT', 'public_schemas').split(',')
586
        if s.strip()]
587
    log.debug("Granting access to %d public schemas", len(public_schemas))
588
    for schema_name in public_schemas:
589
        cur.execute("GRANT USAGE ON SCHEMA %s TO PUBLIC" % (
590
            quote_identifier(schema_name),
591
            ))
592
    for obj in schema.values():
593
        if obj.schema not in public_schemas:
594
            continue
595
        found.add(obj)
596
        if obj.type == 'function':
597
            desired_permissions[obj]['public'].add('EXECUTE')
598
        else:
599
            desired_permissions[obj]['public'].add('SELECT')
600
7675.1220.21 by William Grant
Comments.
601
    # For every object in the DB, ensure that the privileges held by our
7675.1220.24 by William Grant
Controlled roles are now known as managed roles.
602
    # managed roles match our expectations. If not, store the delta
7675.1220.21 by William Grant
Comments.
603
    # to be applied later.
604
    # Also grants/revokes access by the admin role, which isn't a
7675.1220.24 by William Grant
Controlled roles are now known as managed roles.
605
    # traditionally managed role.
7675.1220.23 by William Grant
Log unmanaged roles that hold privileges on managed objects.
606
    unmanaged_roles = set()
7675.1220.7 by William Grant
First stab at permission diffing. Works for everything except revocation of admin privs.
607
    required_grants = []
608
    required_revokes = []
7675.1220.19 by William Grant
Don't just control permissions on controlled_objs; deal with controlled_roles on all objects in the DB. This tweaking the admin handling to grant ALL on granted_objs, and revoke ALL from everything else that has admin in the ACL.
609
    for obj in schema.values():
7675.1220.21 by William Grant
Comments.
610
        # We only care about roles that are in either the desired or
7675.1220.24 by William Grant
Controlled roles are now known as managed roles.
611
        # existing ACL, and are also our managed roles. But skip admin,
612
        # because it's done at the end.
7675.1220.13 by William Grant
Optimise delta generation from ~250ms to 15ms.
613
        interesting_roles = set(desired_permissions[obj]).union(obj.acl)
7675.1220.24 by William Grant
Controlled roles are now known as managed roles.
614
        unmanaged_roles.update(interesting_roles.difference(managed_roles))
615
        for role in managed_roles.intersection(interesting_roles):
7675.1220.23 by William Grant
Log unmanaged roles that hold privileges on managed objects.
616
            if role == 'admin':
617
                continue
7675.1220.7 by William Grant
First stab at permission diffing. Works for everything except revocation of admin privs.
618
            new = desired_permissions[obj][role]
7675.1220.13 by William Grant
Optimise delta generation from ~250ms to 15ms.
619
            old_privs = obj.acl.get(role, {})
620
            old = set(old_privs)
621
            if any(old_privs.itervalues()):
7675.1220.7 by William Grant
First stab at permission diffing. Works for everything except revocation of admin privs.
622
                log.warning("%s has grant option on %s", role, obj.fullname)
7675.1220.13 by William Grant
Optimise delta generation from ~250ms to 15ms.
623
            if new == old:
624
                continue
7675.1220.7 by William Grant
First stab at permission diffing. Works for everything except revocation of admin privs.
625
            missing = new.difference(old)
626
            extra = old.difference(new)
627
            if missing:
628
                required_grants.append((obj, role, missing))
629
            if extra:
630
                required_revokes.append((obj, role, extra))
7675.1220.19 by William Grant
Don't just control permissions on controlled_objs; deal with controlled_roles on all objects in the DB. This tweaking the admin handling to grant ALL on granted_objs, and revoke ALL from everything else that has admin in the ACL.
631
632
        # admin get all privileges on anything with privileges granted
633
        # in security.cfg. We don't have a mapping from ALL to real
634
        # privileges for each object type, so we just grant or revoke ALL
635
        # each time.
636
        if obj in granted_objs:
637
            required_grants.append((obj, "admin", ("ALL",)))
638
        else:
639
            if "admin" in obj.acl:
640
                required_revokes.append((obj, "admin", ("ALL",)))
7675.1220.7 by William Grant
First stab at permission diffing. Works for everything except revocation of admin privs.
641
7675.1220.23 by William Grant
Log unmanaged roles that hold privileges on managed objects.
642
    log.debug("Unmanaged roles on managed objects: %r", list(unmanaged_roles))
643
7675.1220.7 by William Grant
First stab at permission diffing. Works for everything except revocation of admin privs.
644
    alter_permissions(cur, required_grants)
645
    if options.revoke:
646
        alter_permissions(cur, required_revokes, revoke=True)
7675.895.1 by Jeroen Vermeulen
Speed up security.py by an order of magnitude by batching permission changes.
647
1520 by Canonical.com Patch Queue Manager
Review and fix database security update code
648
    # Raise an error if we have database objects lying around that have not
649
    # had permissions assigned.
10293.3.1 by Max Bowsher
Remove use of the deprecated sets module.
650
    forgotten = set()
1520 by Canonical.com Patch Queue Manager
Review and fix database security update code
651
    for obj in schema.values():
652
        if obj not in found:
653
            forgotten.add(obj)
654
    forgotten = [obj.fullname for obj in forgotten
7675.1072.1 by Jeroen Vermeulen
security.py cleanup and speedup, ported over from devel branch.
655
        if obj.type in ['table', 'function', 'view']]
1520 by Canonical.com Patch Queue Manager
Review and fix database security update code
656
    if forgotten:
1831 by Canonical.com Patch Queue Manager
New config machinery, database helpers and oddsnsods required for staging
657
        log.warn('No permissions specified for %r', forgotten)
1520 by Canonical.com Patch Queue Manager
Review and fix database security update code
658
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.
659
    if options.dryrun:
660
        log.info("Dry run - rolling back changes")
661
        con.rollback()
662
    else:
663
        log.debug("Committing changes")
664
        con.commit()
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
665
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
666
1297 by Canonical.com Patch Queue Manager
Merge in database security branch
667
if __name__ == '__main__':
1520 by Canonical.com Patch Queue Manager
Review and fix database security update code
668
    parser = OptionParser()
669
    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.
670
        "-n", "--dry-run", dest="dryrun", default=False,
671
        action="store_true", help="Don't commit any changes")
672
    parser.add_option(
673
        "--revoke", dest="revoke", default=True, action="store_true",
674
        help="Revoke privileges as well as add them")
675
    parser.add_option(
676
        "--no-revoke", dest="revoke", default=True, action="store_false",
677
        help="Do not revoke any privileges. Just add.")
678
    parser.add_option(
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
679
        "-o", "--owner", dest="owner", default="postgres",
680
        help="Owner of PostgreSQL objects")
681
    parser.add_option(
682
        "-c", "--cluster", dest="cluster", default=False,
683
        action="store_true",
684
        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
685
    db_options(parser)
686
    logger_options(parser)
687
1520 by Canonical.com Patch Queue Manager
Review and fix database security update code
688
    (options, args) = parser.parse_args()
1831 by Canonical.com Patch Queue Manager
New config machinery, database helpers and oddsnsods required for staging
689
690
    log = logger(options)
691
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
692
    sys.exit(main(options))