示例#1
0
def verify_spec(rendered_template, spec, cursor, verbose, attributes,
                memberships, ownerships, privileges):
    assert isinstance(spec, dict)
    dbcontext = context.DatabaseContext(cursor, verbose)

    error_messages = []

    # Having all roles represented exactly once is critical for all submodules
    # so we check this regardless of which submodules are being used
    error_messages += ensure_no_duplicate_roles(rendered_template)
    error_messages += ensure_no_undocumented_roles(spec, dbcontext)
    error_messages += ensure_no_except_on_schema(spec)

    if ownerships:
        for objkind in context.PRIVILEGE_MAP.keys():
            if objkind == 'schemas':
                error_messages += ensure_no_unowned_schemas(spec, dbcontext)
                error_messages += ensure_no_schema_owned_twice(spec)
            else:
                # We run each of these functions once per object kind as it is possible that
                # two objects of different kinds could have the same name in the same schema
                error_messages += ensure_no_missing_objects(
                    spec, dbcontext, objkind)
                error_messages += ensure_no_object_owned_twice(
                    spec, dbcontext, objkind)
                error_messages += ensure_no_dependent_object_is_owned(
                    spec, dbcontext, objkind)

    if privileges:
        error_messages += ensure_no_redundant_privileges(spec)

    if error_messages:
        common.fail('\n'.join(error_messages))
示例#2
0
def verify_spec(rendered_template, spec):
    assert isinstance(spec, dict)

    error_messages = []
    error_messages += detect_multiple_role_definitions(rendered_template)
    verification_functions = (verify_schema, check_for_multi_schema_owners,
                              check_read_write_obj_references)
    for fn in verification_functions:
        error_messages += fn(spec)
    if error_messages:
        common.fail('\n'.join(error_messages))
示例#3
0
    def identify_desired_objects(self):
        """
        Create the sets of desired privileges. The sets will look like the following:

            self.desired_nondefaults:
                {(ObjectName(schema, unqualified_name), priv_name), ...}
                Example: {('myschema.mytable', 'SELECT'), ...}

            self.desired_defaults:
                {(grantor, schema, priv_name), ...}
                Example: {('svc-hr-etl', 'hr_schema', 'SELECT'), ...}
        """
        desired_nondefault_objs = set()
        schemas = []
        for objname in self.desired_items:
            if objname == common.ObjectName(
                    'personal_schemas') and self.object_kind == 'schemas':
                desired_nondefault_objs.update(self.personal_schemas)
            elif objname == common.ObjectName(
                    'personal_schemas') and self.object_kind != 'schemas':
                # The end-user is asking something impossible
                common.fail(
                    PERSONAL_SCHEMAS_ERROR_MSG.format(self.rolename,
                                                      self.object_kind,
                                                      self.access))
            elif objname == common.ObjectName('personal_schemas', '*'):
                schemas.extend(self.personal_schemas)
            elif objname.unqualified_name != '*':
                # This is a single non-default privilege ask
                owner = self.get_object_owner(objname)
                if owner != self.rolename:
                    desired_nondefault_objs.add(objname)
            else:
                # We were given a schema.*; we'll process those below
                schemas.append(objname.only_schema())

        for schema in schemas:
            # For schemas, we wish to have privileges for all existing objects, so get all
            # existing objects not owned by this role and add them to self.desired_nondefaults
            schema_objects = self.get_schema_objects(schema.qualified_name)
            desired_nondefault_objs.update(schema_objects)

        #Remove excepted elements
        desired_nondefault_objs.difference_update(self.excepted_items)

        # Cross our desired objects with the desired privileges
        priv_types = PRIVILEGE_MAP[self.object_kind][self.access]
        self.desired_nondefaults = set(
            itertools.product(desired_nondefault_objs, priv_types))

        if self.default_acl_possible:
            self.determine_desired_defaults(schemas)
示例#4
0
 def get_object_owner(self, objname, objkind=None):
     objkind = objkind or self.object_kind
     object_owners = self.all_object_attrs.get(objkind, dict()).get(
         objname.schema, dict())
     owner = object_owners.get(objname, dict()).get('owner', None)
     if owner:
         return owner
     else:
         obj_kind_singular = objkind[:-1]
         common.fail(
             OBJECT_DOES_NOT_EXIST_ERROR_MSG.format(obj_kind_singular,
                                                    objname.qualified_name,
                                                    self.rolename))
示例#5
0
 def get_object_owner(self, item, objkind=None):
     objkind = objkind or self.object_kind
     schema = item.split('.', 1)[0]
     object_owners = self.all_object_owners.get(objkind,
                                                dict()).get(schema, dict())
     owner = object_owners.get(item, None)
     if owner:
         return owner
     else:
         obj_kind_singular = objkind[:-1]
         common.fail(
             OBJECT_DOES_NOT_EXIST_ERROR_MSG.format(obj_kind_singular, item,
                                                    self.rolename))
示例#6
0
def print_spec(spec_path):
    """ Validate a spec passes various checks and, if so, return the loaded spec. """
    rendered_template = render_template(spec_path)
    unconverted_spec = yaml.safe_load(rendered_template)

    # Validate the schema before verifying anything else about the spec. If the spec is invalid
    # then other checks may fail in erratic ways, so it is better to error out here
    error_messages = ensure_valid_schema(unconverted_spec)
    if error_messages:
        common.fail('\n'.join(error_messages))

    spec = convert_spec_to_objectnames(unconverted_spec)

    return spec
示例#7
0
def render_template(path):
    """ Load a spec. There may be templated password variables, which we render using Jinja. """
    try:
        dir_path, filename = os.path.split(path)
        environment = jinja2.Environment(loader=jinja2.FileSystemLoader(dir_path),
                                         undefined=jinja2.StrictUndefined)
        loaded = environment.get_template(filename)
        rendered = loaded.render(env=os.environ)
    except jinja2.exceptions.TemplateNotFound as err:
        common.fail(FILE_OPEN_ERROR_MSG.format(path, err))
    except jinja2.exceptions.UndefinedError as err:
        common.fail(MISSING_ENVVAR_MSG.format(err))
    else:
        return rendered
示例#8
0
    def identify_desired_objects(self):
        """
        Create the sets of desired privileges. The sets will look like the following:

            self.desired_nondefaults:
                {(objname, priv_name), ...}
                Example: {('myschema.mytable', 'SELECT'), ...}

            self.desired_defaults:
                {(grantor, schema, priv_name), ...}
                Example: {('svc-hr-etl', 'hr_schema', 'SELECT'), ...}
        """
        desired_nondefault_objs = set()
        schemas = []
        for item in self.desired_items:
            if item == 'personal_schemas' and self.object_kind == 'schemas':
                desired_nondefault_objs.update(self.personal_schemas)
            elif item == 'personal_schemas' and self.object_kind != 'schemas':
                # The end-user is asking something impossible
                common.fail(
                    PERSONAL_SCHEMAS_ERROR_MSG.format(self.rolename,
                                                      self.object_kind,
                                                      self.access))
            elif item == 'personal_schemas.*':
                schemas.extend(list(self.personal_schemas))
            elif not item.endswith('.*'):
                # This is a single non-default privilege ask
                quoted_item = ensure_quoted_identifier(item)
                owner = self.get_object_owner(quoted_item)
                if owner != self.rolename:
                    desired_nondefault_objs.add(quoted_item)
            else:
                # We were given a schema.*; we'll process those below
                schemas.append(item[:-2])

        for schema in schemas:
            # For schemas, we wish to have privileges for all existing objects, so get all
            # existing objects not owned by this role and add them to self.desired_nondefaults
            schema_objects = self.get_schema_objects(schema)
            desired_nondefault_objs.update(schema_objects)

        # Cross our desired objects with the desired privileges
        priv_types = PRIVILEGE_MAP[self.object_kind][self.access]
        self.desired_nondefaults = set(
            itertools.product(desired_nondefault_objs, priv_types))

        if self.default_acl_possible:
            self.determine_desired_defaults(schemas)
示例#9
0
def load_spec(spec_path, cursor, verbose, attributes, memberships, ownerships,
              privileges, attributes_source_table):
    """ Validate a spec passes various checks and, if so, return the loaded spec. """
    rendered_template = render_template(spec_path)
    unconverted_spec = yaml.safe_load(rendered_template)

    # Validate the schema before verifying anything else about the spec. If the spec is invalid
    # then other checks may fail in erratic ways, so it is better to error out here
    error_messages = ensure_valid_schema(unconverted_spec)
    if error_messages:
        common.fail('\n'.join(error_messages))

    spec = convert_spec_to_objectnames(unconverted_spec)
    verify_spec(rendered_template, spec, cursor, verbose, attributes,
                memberships, ownerships, privileges, attributes_source_table)
    return spec
示例#10
0
def fail_if_undocumented_schemas(spec, dbcontext):
    """
    Refuse to continue if schemas are in the database but are not documented in spec. This is done
    (vs. just deleting the schemas programmatically) because the schema likely contains tables,
    those tables may contain permissions, etc. There's enough going on that if the user just made
    a mistake by forgetting to add a schema to their spec we've caused serious damage; better to
    ask them to manually resolve this
    """
    current_schemas_and_owners = dbcontext.get_all_schemas_and_owners()
    current_schemas = set(current_schemas_and_owners.keys())
    spec_schemas = get_spec_schemas(spec)
    undocumented_schemas = current_schemas.difference(spec_schemas)
    if undocumented_schemas:
        undocumented_schemas_fmtd = '"' + '", "'.join(
            sorted(undocumented_schemas)) + '"'
        common.fail(
            msg=UNDOCUMENTED_SCHEMAS_MSG.format(undocumented_schemas_fmtd))
示例#11
0
def fail_if_undocumented_roles(spec, dbcontext):
    """
    Refuse to continue if roles are in the database cluster but are not documented in spec. This
    is done (vs. just deleting the roles programmatically) because the roles may own schemas,
    tables, functions, etc. There's enough going on that if the user just made a mistake by
    forgetting to add a role to their spec then we've caused serious damage; it's safer to ask
    them to manually resolve this.
    """
    current_role_attributes = dbcontext.get_all_role_attributes()
    spec_roles = set(spec.keys())
    current_roles = set(current_role_attributes.keys())
    undocumented_roles = current_roles.difference(spec_roles)

    if undocumented_roles:
        undocumented_roles_fmtd = '"' + '", "'.join(
            sorted(undocumented_roles)) + '"'
        common.fail(msg=UNDOCUMENTED_ROLES_MSG.format(undocumented_roles_fmtd))
示例#12
0
def run_password_sql(cursor, all_password_sql_to_run):
    """
    Run one or more SQL statements that contains a password. We do this outside of the
    common.run_query() framework for two reasons:
        1) If verbose mode is requested then common.run_query() will show the password in its
        reporting of the queries that are executed
        2) The input to common.run_query() is the module output. This output is faithfully rendered
        as-is to STDOUT upon pgbedrock's completion, so we would leak the password there as well.

    By running password-containing queries outside of the common.run_query() approach we can avoid
    these issues
    """
    query = '\n'.join(all_password_sql_to_run)

    try:
        cursor.execute(query)
    except Exception as e:
        common.fail(msg=common.FAILED_QUERY_MSG.format(query, e))
示例#13
0
    def converted_attributes(self):
        """ Convert the list of attributes provided in the spec to postgres-compatible
        keywords and values.
        """
        converted_attributes = {}
        for spec_attribute in self.spec_attributes:

            # We do spec_attribute.upper() in each spot in order to leave the original
            # spec_attribute unchanged in case it is a password, in which case we don't want to
            # change the case
            if spec_attribute.upper().startswith('CONNECTION LIMIT'):
                val = spec_attribute[17:].strip()
                converted_attributes['rolconnlimit'] = int(val)

            elif spec_attribute.upper().startswith('VALID UNTIL'):
                val = spec_attribute[12:].strip()
                converted_attributes['rolvaliduntil'] = val

            elif spec_attribute.upper().startswith('IGNORE PASSWORD'):
                # This is the case where we don't want to check the password, so
                # set the 'rollpassword' attribute to False, which will cause
                # the password comparison check to be skipped
                converted_attributes['rolpassword'] = False

            elif 'PASSWORD' in spec_attribute.upper():
                # Regardless whether the spec specified ENCRYPTED or UNENCRYPTED for the password,
                # we throw this away as we will be storing the password in encrypted form
                val = spec_attribute.split('PASSWORD ', 1)[-1]

                # Trim leading and ending quotes, if there are any
                if val[0] == '"' or val[0] == "'":
                    val = val[1:]
                if val[-1] == '"' or val[-1] == "'":
                    val = val[:-1]

                if "'" in val or '"' in val:
                    common.fail(msg=UNSUPPORTED_CHAR_MSG.format(self.rolename))

                converted_attributes['rolpassword'] = val

            elif spec_attribute.upper().startswith('NO'):
                keyword = spec_attribute.upper()[2:]
                colname = PG_COLUMN_NAME.get(keyword)
                if not colname:
                    common.fail(UNKNOWN_ATTRIBUTE_MSG.format(spec_attribute))

                converted_attributes[colname] = False

            else:
                keyword = spec_attribute.upper()
                colname = PG_COLUMN_NAME.get(keyword)
                if not colname:
                    common.fail(UNKNOWN_ATTRIBUTE_MSG.format(spec_attribute))

                converted_attributes[colname] = True

        return converted_attributes