Example #1
0
 def test_addr_to_range(self):
     eggs = [('hostname', 'hostname'), ('192.168.1.1', '192.168.1.1/32'),
             ('192.168.1.0/24', '192.168.1.0/24'),
             ('::whatever::', '::whatever::/128'),
             ('::whatever::/64', '::whatever::/64'),
             ('unparseable nonsense', 'unparseable nonsense')]
     for addr, addr_range in eggs:
         with self.subTest(addr=addr):
             self.assertEqual(postgresql.addr_to_range(addr), addr_range)
Example #2
0
def incoming_addresses(relinfo):
    """Return the incoming address range(s) if present in relinfo.

    Address ranges are in CIDR format. eg. 192.168.1.0/24 or 2001::F00F/128.
    We look for information as provided by recent versions of Juju, and
    fall back to private-address if needed.

    Returns an empty list if no address information is present. An
    error is logged if this occurs, as something has gone seriously
    wrong.
    """
    # This helper could return a set, but a list with stable ordering is
    # easier to use without causing flapping.
    if "egress-subnets" in relinfo:
        return [n.strip() for n in relinfo["egress-subnets"].split(",") if n.strip()]
    if "ingress-address" in relinfo:
        return [postgresql.addr_to_range(relinfo["ingress-address"])]
    if "private-address" in relinfo:
        return [postgresql.addr_to_range(relinfo["private-address"])]
    return []
 def test_addr_to_range(self):
     eggs = [
         ("hostname", "hostname"),
         ("192.168.1.1", "192.168.1.1/32"),
         ("192.168.1.0/24", "192.168.1.0/24"),
         ("::whatever::", "::whatever::/128"),
         ("::whatever::/64", "::whatever::/64"),
         ("unparseable nonsense", "unparseable nonsense"),
     ]
     for addr, addr_range in eggs:
         with self.subTest(addr=addr):
             self.assertEqual(postgresql.addr_to_range(addr), addr_range)
Example #4
0
def generate_pg_hba_conf(pg_hba, config, rels, _peer_rel=None):
    """Update the pg_hba.conf file (host based authentication)."""
    rules = []  # The ordered list, as tuples.

    # local      database  user  auth-method  [auth-options]
    # host       database  user  address  auth-method  [auth-options]
    # hostssl    database  user  address  auth-method  [auth-options]
    # hostnossl  database  user  address  auth-method  [auth-options]
    # host       database  user  IP-address  IP-mask  auth-method  [auth-opts]
    # hostssl    database  user  IP-address  IP-mask  auth-method  [auth-opts]
    # hostnossl  database  user  IP-address  IP-mask  auth-method  [auth-opts]
    def add(*record):
        rules.append(tuple(record))

    # The charm is running as the root user, and needs to be able to
    # connect as the postgres user to all databases.
    add("local", "all", "postgres", "peer", "map=juju_charm")

    # The local unit needs access to its own database. Let every local
    # user connect to their matching PostgreSQL user, if it exists, and
    # nagios with a password.
    add("local", "all", nagios.nagios_username(), "password")
    add("local", "all", "all", "peer")

    if _peer_rel is None:
        _peer_rel = helpers.get_peer_relation()

    # Peers need replication access as the charm replication user.
    if _peer_rel:
        for peer, relinfo in _peer_rel.items():
            for addr in incoming_addresses(relinfo):
                qaddr = postgresql.quote_identifier(addr)
                # Magic replication database, for replication.
                add(
                    "host",
                    "replication",
                    replication.replication_username(),
                    qaddr,
                    "md5",
                    "# {}".format(relinfo),
                )
                # postgres db, so leader can query replication status.
                add(
                    "host",
                    "postgres",
                    replication.replication_username(),
                    qaddr,
                    "md5",
                    "# {}".format(relinfo),
                )

    # Clients need access to the relation database as the relation users.
    for rel in rels["db"].values():
        if "user" in rel.local:
            for relinfo in rel.values():
                for addr in incoming_addresses(relinfo):
                    # Quote everything, including the address, to disenchant
                    # magic tokens like 'all'.
                    add(
                        "host",
                        postgresql.quote_identifier(rel.local["database"]),
                        postgresql.quote_identifier(rel.local["user"]),
                        postgresql.quote_identifier(addr),
                        "md5",
                        "# {}".format(relinfo),
                    )
                    add(
                        "host",
                        postgresql.quote_identifier(rel.local["database"]),
                        postgresql.quote_identifier(rel.local["schema_user"]),
                        postgresql.quote_identifier(addr),
                        "md5",
                        "# {}".format(relinfo),
                    )

    # Admin clients need access to all databases as any user, not just the
    # relation user. Most clients will just use the user provided them,
    # but proxies such as pgbouncer need to open connections as the accounts
    # it creates.
    for rel in rels["db-admin"].values():
        if "user" in rel.local:
            for relinfo in rel.values():
                for addr in incoming_addresses(relinfo):
                    add(
                        "host",
                        "all",
                        "all",
                        postgresql.quote_identifier(addr),
                        "md5",
                        "# {}".format(relinfo),
                    )

    # External replication connections. Somewhat different than before
    # as the relation gets its own user to avoid sharing credentials,
    # and logical replication connections will want to specify the
    # database name.
    for rel in rels["master"].values():
        for relinfo in rel.values():
            for addr in incoming_addresses(relinfo):
                add(
                    "host",
                    "replication",
                    postgresql.quote_identifier(rel.local["user"]),
                    postgresql.quote_identifier(addr),
                    "md5",
                    "# {}".format(relinfo),
                )
                if "database" in rel.local:
                    add(
                        "host",
                        postgresql.quote_identifier(rel.local["database"]),
                        postgresql.quote_identifier(rel.local["user"]),
                        postgresql.quote_identifier(addr),
                        "md5",
                        "# {}".format(relinfo),
                    )

    # External administrative addresses, if specified by the operator.
    for addr in config["admin_addresses"].split(","):
        if addr:
            add(
                "host",
                "all",
                "all",
                postgresql.quote_identifier(postgresql.addr_to_range(addr)),
                "md5",
                "# admin_addresses config",
            )

    # And anything-goes rules, if specified by the operator.
    for line in helpers.split_extra_pg_auth(config["extra_pg_auth"]):
        add(line + " # extra_pg_auth config")

    # Deny everything else
    add("local", "all", "all", "reject", "# Refuse by default")
    add("host", "all", "all", "all", "reject", "# Refuse by default")

    # Strip out the existing juju managed section
    start_mark = "### BEGIN JUJU SETTINGS ###"
    end_mark = "### END JUJU SETTINGS ###"
    pg_hba = re.sub(
        r"^\s*{}.*^\s*{}\s*$".format(re.escape(start_mark), re.escape(end_mark)),
        "",
        pg_hba,
        flags=re.I | re.M | re.DOTALL,
    )

    # Comment out any uncommented lines
    pg_hba = re.sub(r"^\s*([^#\s].*)$", r"# juju # \1", pg_hba, flags=re.M)

    # Spit out the updated file
    rules.insert(0, (start_mark,))
    rules.append((end_mark,))
    pg_hba += "\n" + "\n".join(" ".join(rule) for rule in rules)
    return pg_hba
Example #5
0
def generate_pg_hba_conf(pg_hba, config, rels):
    '''Update the pg_hba.conf file (host based authentication).'''
    rules = []  # The ordered list, as tuples.

    # local      database  user  auth-method  [auth-options]
    # host       database  user  address  auth-method  [auth-options]
    # hostssl    database  user  address  auth-method  [auth-options]
    # hostnossl  database  user  address  auth-method  [auth-options]
    # host       database  user  IP-address  IP-mask  auth-method  [auth-opts]
    # hostssl    database  user  IP-address  IP-mask  auth-method  [auth-opts]
    # hostnossl  database  user  IP-address  IP-mask  auth-method  [auth-opts]
    def add(*record):
        rules.append(tuple(record))

    # The charm is running as the root user, and needs to be able to
    # connect as the postgres user to all databases.
    add('local', 'all', 'postgres', 'peer', 'map=juju_charm')

    # The local unit needs access to its own database. Let every local
    # user connect to their matching PostgreSQL user, if it exists, and
    # nagios with a password.
    add('local', 'all', nagios.nagios_username(), 'password')
    add('local', 'all', 'all', 'peer')

    # Peers need replication access as the charm replication user.
    if rels.peer:
        for peer, relinfo in rels.peer.items():
            if 'private-address' not in relinfo:
                continue  # Other end not yet provisioned?
            addr = postgresql.addr_to_range(relinfo['private-address'])
            qaddr = postgresql.quote_identifier(addr)
            # Magic replication database, for replication.
            add('host', 'replication', replication.replication_username(),
                qaddr, 'md5', '# {}'.format(relinfo))
            # postgres database, so the leader can query replication status.
            add('host', 'postgres', replication.replication_username(),
                qaddr, 'md5', '# {}'.format(relinfo))

    # Clients need access to the relation database as the relation users.
    for rel in rels['db'].values():
        if 'user' in rel.local:
            for relinfo in rel.values():
                if 'private-address' not in relinfo:
                    continue  # Other end not yet provisioned?
                addr = postgresql.addr_to_range(relinfo['private-address'])
                # Quote everything, including the address, to disenchant
                # magic tokens like 'all'.
                add('host',
                    postgresql.quote_identifier(rel.local['database']),
                    postgresql.quote_identifier(rel.local['user']),
                    postgresql.quote_identifier(addr),
                    'md5', '# {}'.format(relinfo))
                add('host',
                    postgresql.quote_identifier(rel.local['database']),
                    postgresql.quote_identifier(rel.local['schema_user']),
                    postgresql.quote_identifier(addr),
                    'md5', '# {}'.format(relinfo))

    # Admin clients need access to all databases as any user, not just the
    # relation user. Most clients will just use the user provided them,
    # but proxies such as pgbouncer need to open connections as the accounts
    # it creates.
    for rel in rels['db-admin'].values():
        if 'user' in rel.local:
            for relinfo in rel.values():
                if 'private-address' not in relinfo:
                    continue  # Other end not yet provisioned?
                addr = postgresql.addr_to_range(relinfo['private-address'])
                add('host', 'all', 'all',
                    postgresql.quote_identifier(addr),
                    'md5', '# {}'.format(relinfo))

    # External replication connections. Somewhat different than before
    # as the relation gets its own user to avoid sharing credentials,
    # and logical replication connections will want to specify the
    # database name.
    for rel in rels['master'].values():
        for relinfo in rel.values():
            if 'private-address' not in relinfo:
                continue  # Other end not yet provisioned?
            addr = postgresql.addr_to_range(relinfo['private-address'])
            add('host', 'replication',
                postgresql.quote_identifier(rel.local['user']),
                postgresql.quote_identifier(addr),
                'md5', '# {}'.format(relinfo))
            if 'database' in rel.local:
                add('host',
                    postgresql.quote_identifier(rel.local['database']),
                    postgresql.quote_identifier(rel.local['user']),
                    postgresql.quote_identifier(addr),
                    'md5', '# {}'.format(relinfo))

    # External administrative addresses, if specified by the operator.
    for addr in config['admin_addresses'].split(','):
        if addr:
            add('host', 'all', 'all',
                postgresql.quote_identifier(postgresql.addr_to_range(addr)),
                'md5', '# admin_addresses config')

    # And anything-goes rules, if specified by the operator.
    for line in helpers.split_extra_pg_auth(config['extra_pg_auth']):
        add(line + ' # extra_pg_auth config')

    # Deny everything else
    add('local', 'all', 'all', 'reject', '# Refuse by default')
    add('host', 'all', 'all', 'all', 'reject', '# Refuse by default')

    # Strip out the existing juju managed section
    start_mark = '### BEGIN JUJU SETTINGS ###'
    end_mark = '### END JUJU SETTINGS ###'
    pg_hba = re.sub(r'^\s*{}.*^\s*{}\s*$'.format(re.escape(start_mark),
                                                 re.escape(end_mark)),
                    '', pg_hba, flags=re.I | re.M | re.DOTALL)

    # Comment out any uncommented lines
    pg_hba = re.sub(r'^\s*([^#\s].*)$', r'# juju # \1', pg_hba, flags=re.M)

    # Spit out the updated file
    rules.insert(0, (start_mark,))
    rules.append((end_mark,))
    pg_hba += '\n' + '\n'.join(' '.join(rule) for rule in rules)
    return pg_hba