Exemple #1
0
 def get_free_uid(self):
     """Returns the next free uid from ``posix_uid_seq``"""
     while 1:
         # We pick an UID
         uid = self.nextval("posix_uid_seq")
         # We check if the UID is in any of the reserved ranges.
         # If it is, we'll skip past the range (call setval), and
         # pick a new UID that is past the reserved range.
         for x in sorted(cereconf.UID_RESERVED_RANGE):
             # TODO: Move this check to some unit-testing stuff sometime
             if x[1] < x[0]:
                 raise Errors.ProgrammingError(
                     'Wrong order in cereconf.UID_RESERVED_RANGE')
             if x[0] <= uid <= x[1]:
                 self._db.setval('posix_uid_seq', x[1])
                 uid = self.nextval("posix_uid_seq")
         # If the UID is not in use, we return it, else, we try over.
         try:
             self.query_1(
                 """
             SELECT posix_uid
             FROM [:table schema=cerebrum name=posix_user]
             WHERE posix_uid=:uid""", locals())
         except Errors.NotFoundError:
             return int(uid)
Exemple #2
0
    def get_sito_uname(self, fnr, name):
        """
        Generate a SITO account_name.

        SITO accounts have their own namespace. SITO usernames are named
        "XXXNNN-s", where:

            XXX = letters generated based on *name*
            NNN = unique numeric identifier
            s = The letter s
        """
        cstart = 0
        step = 1

        co = Factory.get('Constants')(self._db)
        try:
            self._get_person_by_extid(co.externalid_fodselsnr, fnr)
        except Errors.NotFoundError:
            raise Errors.ProgrammingError(
                "Trying to create account for person:%s that does not exist!" %
                fnr)

        # getting here implies that person does not have a previous account in
        # BAS create a new username
        inits = self.get_initials(name)
        return self.get_serial(inits,
                               cstart,
                               step=step,
                               postfix=self.sito_postfix)
Exemple #3
0
 def _get_gid(self):
     """Returns the next free GID from 'posix_gid_seq'"""
     while True:
         # Pick a new GID
         gid = self.nextval('posix_gid_seq')
         # We check if the GID is in any of the reserved ranges.
         # If it is, we'll skip past the range (call setval), and
         # pick a new GID that is past the reserved range.
         for x in sorted(cereconf.GID_RESERVED_RANGE):
             # TODO: Move this check to some unit-testing stuff sometime
             if x[1] < x[0]:
                 raise Errors.ProgrammingError(
                     'Wrong order in cereconf.GID_RESERVED_RANGE')
             if x[0] <= gid <= x[1]:
                 self._db.setval('posix_gid_seq', x[1])
                 gid = self.nextval('posix_gid_seq')
         # We check if the GID is in use, if not, return, else start over.
         try:
             self.query_1(
                 """
             SELECT posix_gid
             FROM [:table schema=cerebrum name=posix_group]
             WHERE posix_gid=:gid""", locals())
         except Errors.NotFoundError:
             return gid
Exemple #4
0
    def add_relationship(self, relationship_code, target_id):
        """Add a relationship of given type between this role and a target
        component (atom or role).

        @type relationship_code: int
        @param relationship_code:
            The relationship constant that defines the kind of relationship the
            source and target will have.
        """
        if self.illegal_relationship(relationship_code, target_id):
            # TODO: is ProgrammingError the correct exception to raise here?
            # Frontends should be able to give better feedbacks
            raise Errors.ProgrammingError('Illegal relationship')
        self.execute(
            """
            INSERT INTO [:table schema=cerebrum name=hostpolicy_relationship]
              (source_policy, relationship, target_policy)
            VALUES (:source, :rel, :target)""", {
                'source': self.entity_id,
                'rel': int(relationship_code),
                'target': target_id
            })
        self._db.log_change(self.entity_id,
                            self.clconst.hostpolicy_relationship_add,
                            target_id)
Exemple #5
0
    def __init__(self, db, logger, u_sprd=None, g_sprd=None, n_sprd=None,
                 fd=None):
        """ Initiate database and import modules.

        Spreads are given in initiation and general constants which is
        used in more than one method.

        """
        timer = make_timer(logger, 'Initing PosixLDIF...')
        from Cerebrum.modules import PosixGroup
        self.db = db
        self.logger = logger
        self.const = Factory.get('Constants')(self.db)
        self.grp = Factory.get('Group')(self.db)
        self.posuser = Factory.get('PosixUser')(self.db)
        self.posgrp = PosixGroup.PosixGroup(self.db)
        self.user_dn = LDIFutils.ldapconf('USER', 'dn', None)
        # This is an odd one -- if set to False, then id2uname should be
        # populated with users exported in the users export -- which makes the
        # group exports filter group members by *actually* exported users...
        self.get_name = True
        self.fd = fd
        self.spread_d = {}
        # Validate spread from arg or from cereconf
        for x, y in zip(['USER', 'FILEGROUP', 'NETGROUP'],
                        [u_sprd, g_sprd, n_sprd]):
            spread = LDIFutils.map_spreads(
                y or getattr(cereconf, 'LDAP_' + x).get('spread'), list)
            if spread:
                self.spread_d[x.lower()] = spread
        if 'user' not in self.spread_d:
            raise Errors.ProgrammingError(
                "Must specify spread-value as 'arg' or in cereconf")
        self.account2name = dict()
        self.group2gid = dict()
        self.groupcache = defaultdict(dict)
        self.group2groups = defaultdict(set)
        self.group2users = defaultdict(set)
        self.group2persons = defaultdict(list)
        self.shell_tab = dict()
        self.quarantines = dict()
        self.user_exporter = UserExporter(self.db)
        if len(self.spread_d['user']) > 1:
            logger.warning('Exporting users with multiple spreads, '
                           'ignoring homedirs from %r',
                           self.spread_d['user'][1:])
        self.homedirs = HomedirResolver(db, self.spread_d['user'][0])
        self.owners = OwnerResolver(db)

        auth_attr = LDIFutils.ldapconf('USER', 'auth_attr', None)
        self.user_password = AuthExporter.make_exporter(
            db,
            auth_attr['userPassword'])
        timer('... done initing PosixLDIF.')
Exemple #6
0
    def __init__(self,
                 db,
                 logger,
                 u_sprd=None,
                 g_sprd=None,
                 n_sprd=None,
                 fd=None):
        """ Initiate database and import modules.

        Spreads are given in initiation and general constants which is
        used in more than one method.

        """
        timer = make_timer(logger, 'Initing PosixLDIF...')
        from Cerebrum.modules import PosixGroup
        self.db = db
        self.logger = logger
        self.const = Factory.get('Constants')(self.db)
        self.grp = Factory.get('Group')(self.db)
        self.posuser = Factory.get('PosixUser')(self.db)
        self.posgrp = PosixGroup.PosixGroup(self.db)
        self.user_dn = LDIFutils.ldapconf('USER', 'dn', None)
        self.get_name = True
        self.fd = fd

        self.spread_d = {}
        # Validate spread from arg or from cereconf
        for x, y in zip(['USER', 'FILEGROUP', 'NETGROUP'],
                        [u_sprd, g_sprd, n_sprd]):
            spread = LDIFutils.map_spreads(
                y or getattr(cereconf, 'LDAP_' + x).get('spread'), list)
            if spread:
                self.spread_d[x.lower()] = spread
        if 'user' not in self.spread_d:
            raise Errors.ProgrammingError(
                "Must specify spread-value as 'arg' or in cereconf")
        self.account2name = dict()
        self.groupcache = defaultdict(dict)
        self.group2groups = defaultdict(set)
        self.group2users = defaultdict(set)
        self.group2persons = defaultdict(list)
        timer('... done initing PosixLDIF.')
Exemple #7
0
def _find_affiliations(cerebrum_person, hr_affs, affiliation_map,
                       source_system, mode):
    consider_affiliations = filter(lambda x: x, affiliation_map().values())

    cerebrum_affiliations = cerebrum_person.list_affiliations(
        person_id=cerebrum_person.entity_id,
        status=consider_affiliations,
        source_system=source_system)

    in_hr = map(
        lambda d: tuple(
            sorted(filter(lambda (k, v): k != u'precedence', d.items()))),
        hr_affs)

    in_cerebrum = map(
        lambda x: tuple(
            sorted(
                filter_elements(
                    translate_keys(
                        x, {
                            u'ou_id': u'ou_id',
                            u'affiliation': u'affiliation',
                            u'status': u'status'
                        })))), cerebrum_affiliations)

    if mode == u'remove':
        return [
            dict(
                filter(lambda (k, v): k in (u'ou_id', u'affiliation'), x) +
                ((u'source', source_system), ))
            for x in set(in_cerebrum) - set(in_hr)
        ]
    elif mode == u'add':
        to_add = set(in_hr) - set(in_cerebrum)
        to_ensure = set(in_hr) & set(in_cerebrum)
        return [dict(x) for x in to_add | to_ensure]
    else:
        from Cerebrum import Errors
        raise Errors.ProgrammingError(
            'Invalid mode {} supplied to _find_affiliations'.format(
                repr(mode)))
Exemple #8
0
    def get_email_target_info(self, target_id=None, target_entity=None):
        """Return the entity_type and entity_id of the entity the EmailTarget
        points at. Look up by _either_ target_id or target_entity.

        :type target_id: int
        :param target_id: The EmailTargets id.

        :type target_entity: int
        :param target_entity: The targets target entity id.

        :rtype: typle
        :return: (entity_id, target_entity_id, target_entity_type, hard_quota,
            soft_quota)."""
        self.et.clear()
        self.eq.clear()
        if target_id:
            self.et.find(target_id)
            try:
                self.eq.find(target_id)
            except Errors.NotFoundError:
                self.eq.clear()
        elif target_entity:
            self.et.find_by_target_entity(target_entity)
            try:
                self.eq.find(self.et.entity_id)
            except Errors.NotFoundError:
                self.eq.clear()
        else:
            self.db.rollback()
            raise Errors.ProgrammingError(
                'Must define either target_id og target_entity')
        ret = (self.et.entity_id, self.et.email_target_entity_id,
               self.et.email_target_entity_type, self.eq.get_quota_hard(),
               self.eq.get_quota_soft())
        self.db.rollback()
        return ret
    def process_person(self, person):
        """Sync spreads between a person and it's accounts."""

        # str2const is something we need if we create new accounts.
        self._make_str2const()

        if not self._ac:
            self._ac = Factory.get('Account')(self.db)

        def _diff_aff(new_aff, old_aff):
            """Return two lists. One with affiliations to remove and one
            with affiliations to add."""
            remove = list()
            add = list()
            for tpl in new_aff:
                if tpl not in old_aff:
                    add.append(tpl)
            for tpl in old_aff:
                if tpl not in new_aff:
                    remove.append(tpl)
            return remove, add

        # Find the person's affiliations
        person_affiliations = []
        for row in person.get_affiliations():
            person_affiliations.append((row['ou_id'], row['affiliation']))

        # If the person in question doesn't have any affiliations, we
        # don't create any account.
        accounts = person.get_accounts(filter_expired=False)
        if len(accounts) == 0:
            if person_affiliations:
                ac = self._maybe_create_account(person)
                if ac:
                    self.logger.info("Person '%d' got new account '%s'." %
                                     (person.entity_id, ac.account_name))
                # Take care of person affiliations too

            else:
                self.logger.info(
                    "Person '%d' have no affiliations. No account created." %
                    person.entity_id)

        # Clean up in spreads
        change = False
        for spread in procconf.PERSON_SPREADS:
            if person_affiliations:
                if not person.has_spread(int(self.str2const[spread])):
                    person.add_spread(int(self.str2const[spread]))
                    self.logger.info("Person '%d' got spread '%s'." %
                                     (person.entity_id, spread))
                    change = True
            else:
                if person.has_spread(int(self.str2const[spread])):
                    person.delete_spread(int(self.str2const[spread]))
                    self.logger.info("Person '%d' have no affiliations. "
                                     "Spread '%s' deleted." %
                                     (person.entity_id, spread))
                    change = True
        if change:
            person.write_db()

        # Loop over the person's account(s) and correct affiliations
        # and spreads
        for account in person.get_accounts(filter_expired=False):
            account_affiliations = []
            self._ac.clear()
            self._ac.find(account['account_id'])
            # Update affiliations
            for row in self._ac.get_account_types(filter_expired=False):
                account_affiliations.append((row['ou_id'], row['affiliation']))

            rem, add = _diff_aff(person_affiliations, account_affiliations)
            change = False
            for r in rem:
                self._ac.del_account_type(r[0], r[1])
                change = True
                self.logger.info("Account '%s' removed type '%s', '%s'." %
                                 (self._ac.account_name, r[0], r[1]))
            for a in add:
                self._ac.set_account_type(a[0], a[1])
                change = True
                self.logger.info("Account '%s' added type '%s', '%s'." %
                                 (self._ac.account_name, a[0], a[1]))
            # Set expire_date if no account_types
            if not person_affiliations:
                # Don't reset expire_date on an already expired account
                if not self._ac.expire_date:
                    self._ac.expire_date = DateTime.now()
                    change = True
                    self.logger.info("Account '%s' set to expired." %
                                     self._ac.account_name)
            else:
                # The account is about to get account_types so we restore it
                # by removing expire_date
                if self._ac.expire_date:
                    self._ac.expire_date = None
                    change = True
                    self.logger.info("Account '%s' is restored." %
                                     self._ac.account_name)
                    self._ac_add_new_traits(self._ac)

            # TODO: Limit the removal of spreads to types known by proc_entity

            # Update account spreads (if set in the config)
            if (hasattr(procconf, 'ACCOUNT_SPREADS')
                    and hasattr(procconf, 'OU2ACCOUNT_SPREADS')):
                raise Errors.ProgrammingError(
                    "Both ACCOUNT_SPREADS and OU2ACCOUNT_SPREADS in procconf.")
            elif (hasattr(procconf, 'ACCOUNT_SPREADS')
                  and procconf.ACCOUNT_SPREADS):
                acc_spreads = []
                for i in person_affiliations:
                    aff_str = str(_PersonAffiliationCode(i[1]))
                    if aff_str not in procconf.ACCOUNT_SPREADS:
                        continue
                    spreads = [
                        int(self.str2const[s])
                        for s in procconf.ACCOUNT_SPREADS[aff_str]
                    ]
                    acc_spreads += [s for s in spreads if s not in acc_spreads]
                for row in self._ac.get_spread():
                    # Annoying "feature". get_spread() return a tuple of
                    # one-element tuples.
                    if int(row[0]) not in acc_spreads:
                        self._ac.delete_spread(row[0])
                        self.logger.info("Account '%s' removed spread '%s'." %
                                         (self._ac.account_name,
                                          str(_SpreadCode(int(row[0])))))
                        change = True
                for spread in acc_spreads:
                    if not self._ac.has_spread(spread):
                        self._ac.add_spread(spread)
                        self.logger.info("Account '%s' added spread '%s'." %
                                         (self._ac.account_name,
                                          str(_SpreadCode(int(spread)))))
                        change = True
            elif (hasattr(procconf, 'OU2ACCOUNT_SPREADS')
                  and procconf.OU2ACCOUNT_SPREADS):
                self._populate_ou2spread()
                acc_spreads = []
                for ou, aff in person_affiliations:
                    for s in self.ou2spread[ou]:
                        if s not in acc_spreads:
                            acc_spreads.append(s)
                for row in self._ac.get_spread():
                    # Annoying "feature". get_spread() return a tuple of
                    # one-element tuples.
                    if int(row[0]) not in acc_spreads:
                        self._ac.delete_spread(row[0])
                        self.logger.info("Account '%s' removed spread '%s'." %
                                         (self._ac.account_name,
                                          str(_SpreadCode(int(row[0])))))
                        change = True
                for spread in acc_spreads:
                    if not self._ac.has_spread(spread):
                        self._ac.add_spread(spread)
                        self.logger.info("Account '%s' added spread '%s'." %
                                         (self._ac.account_name,
                                          str(_SpreadCode(int(spread)))))
                        change = True

            if change:
                self._ac.write_db()
Exemple #10
0
    def search(
        self,
        group_id=None,
        member_id=None,
        indirect_members=False,
        admin_id=None,
        admin_by_membership=False,
        moderator_id=None,
        moderator_by_membership=False,
        spread=None,
        name=None,
        description=None,
        group_type=None,
        filter_expired=True,
        creator_id=None,
        expired_only=False,
        fetchall=True,
    ):
        """Search for groups satisfying various filters.

        Search **for groups** where the results are filtered by a number of
        criteria. There are many filters that can be specified; the result
        returned by this method satisfies all of the filters. Not all of the
        filters are compatible (check the documentation)

        If a filter is None, it means that it will not be applied. Calling
        this method without any arguments will return all non-expired groups
        registered in group_info.

        :type group_id: int or sequence thereof or None.
        :param group_id:
          Group ids to look for. This is the most specific filter that can be
          given. With this filter, only the groups matching the specified
          id(s) will be returned.

          This filter cannot be combined with L{member_id}.

        :type member_id: int or sequence thereof or None.
        :param member_id:
          The resulting group list will be filtered by membership - only
          groups that have members specified by member_id will be returned. If
          member_id is a sequence, then a group g1 is returned if any of the
          ids in the sequence are a member of g1.

          This filter cannot be combined with L{group_id}.

        :type indirect_members: bool
        :param indirect_members:
          This parameter controls how the L{member_id} filter is applied. When
          False, only groups where L{member_id} is a/are direct member(s) will
          be returned. When True, the membership of L{member_id} does not have
          to be direct; if group g2 is a member of group g1, and member_id m1
          is a member of g2, specifying indirect_members=True will return g1
          as well as g2. Be careful, for some situations this can drastically
          increase the result size.

          This filter makes sense only when L{member_id} is set.

        :type admin_id: int or sequence thereof or None.
        :param admin_id:
          The resulting group list will be filtered by adminship - only
          groups that have admins specified by admin_id will be
          returned. If admin_id is a sequence, then a group g1 is returned
          if any of the ids in the sequence are a admin of g1.

        :type admin_by_membership: bool
        :param admin_by_membership:
          This parameter controls how the L{admin_id} filter is applied.
          When False, only groups where L{admin_id} is a/are direct
          admin(s) will be returned. When True, the adminship of
          L{admin_id} does not have to be direct; if group g2 is a
          admin of group g1, and admin_id m1 is a member of g2,
          specifying indirect_admins=True will return g1.

          This filter makes sense only when L{admin_id} is set.

        :type moderator_id: int or sequence thereof or None.
        :param moderator_id:
          The resulting group list will be filtered by moderatorship - only
          groups that have moderators specified by moderator_id will be
          returned. If moderator_id is a sequence, then a group g1 is returned
          if any of the ids in the sequence are a moderator of g1.

        :type moderator_by_membership: bool
        :param moderator_by_membership:
          This parameter controls how the L{moderator_id} filter is applied.
          When False, only groups where L{moderator_id} is a/are direct
          moderator(s) will be returned. When True, the moderatorship of
          L{moderator_id} does not have to be direct; if group g2 is a
          moderator of group g1, and moderator_id m1 is a member of g2,
          specifying indirect_moderators=True will return g1.

          This filter makes sense only when L{moderator_id} is set.

        :type spread: int or SpreadCode or sequence thereof or None.
        :param spread:
          Filter the resulting group list by spread. I.e. only groups with
          specified spread(s) will be returned.

        :type name: basestring
        :param name:
          Filter the resulting group list by name. The name may contain SQL
          wildcard characters.

        :type description: basestring
        :param description:
          Filter the resulting group list by group description. The
          description may contain SQL wildcard characters.

        :type group_type: int or GroupTypeCode or sequence thereof or None
        :param description:
          Filter the resulting group list by group type - i.e. only return
          groups with one of the given group types.

        :type filter_expired: bool
        :param filter_expired:
          Filter the resulting group list by expiration date. If set, do NOT
          return groups that have expired (i.e. have group_info.expire_date in
          the past relative to the call time).

        :type expired_only: bool
        :param expired_only:
          Filter the resulting group list by expiration date.
          If set, return ONLY groups
          that have expired_date set and expired (relative to the call time).
          N.B. filter_expired and filter_expired are mutually exclusive

        :rtype: iterable (yielding db-rows with group information)
        :return:
          An iterable (sequence or a generator) that yields successive db-rows
          matching all of the specified filters. Regardless of the filters,
          any given group_id is guaranteed to occur at most once in the
          result. The keys available in db_rows are the content of the
          group_info table and group's name (if it does not exist, None is
          assigned to the 'name' key).
        """
        # Sanity check: if indirect members is specified, then at least we
        # need one id to go on.
        if indirect_members and not member_id:
            raise Errors.ProgrammingError(
                'Cannot use indirect_members without member_id')

        if admin_by_membership and not admin_id:
            raise Errors.ProgrammingError(
                'Cannot use indirect_admins without admin_id')

        if moderator_by_membership and not moderator_id:
            raise Errors.ProgrammingError(
                'Cannot use indirect_moderators without moderator_id')

        # Sanity check: it is probably a bad idea to allow specifying both.
        if member_id and group_id:
            raise Errors.ProgrammingError(
                'member_id and group_id cannot be used simultaneously')

        def search_transitive_closure(member_id):
            """Return all groups where member_id is/are indirect member(s).

            :type member_id: int or sequence thereof.
            :param member_id:
              We are looking for groups where L{member_id} is/are indirect
              member(s).

            :rtype: set (of group_ids (ints))
            :result:
              Set of group_ids where member_id is/are indirect members.
            """

            result = set()
            if not isinstance(member_id, (tuple, set, list)):
                member_id = (member_id, )

            # workset contains ids of the entities that are members. in each
            # iteration we are looking for direct parents of whatever is in
            # workset.
            workset = set(int(x) for x in member_id)
            while workset:
                tmp = workset
                workset = set()
                for row in self.search(
                        member_id=tmp,
                        indirect_members=False,
                        # We need to be *least* restrictive
                        # here. Final filtering will take care
                        # of 'expiredness'.
                        filter_expired=False):
                    group_id = int(row["group_id"])
                    if group_id in result:
                        continue
                    result.add(group_id)
                    if group_id not in workset:
                        workset.add(group_id)

            return result

        stmt = """
          SELECT DISTINCT
            gi.group_id AS group_id,
            en.entity_name AS name,
            gi.description AS description,
            gi.visibility AS visibility,
            gi.creator_id AS creator_id,
            ei.created_at AS created_at,
            gi.expire_date AS expire_date,
            gi.group_type AS group_type
          FROM
            [:table schema=cerebrum name=group_info] gi
          LEFT OUTER JOIN
            [:table schema=cerebrum name=entity_name] en
          ON
            en.entity_id = gi.group_id AND
            en.value_domain = :vdomain
          LEFT OUTER JOIN
            [:table schema=cerebrum name=entity_info] ei
          ON
            ei.entity_id = gi.group_id AND
            ei.entity_type = :entity_type
          {extra_tables}
          {where}
        """
        extra_tables = []
        where = []
        binds = {
            "vdomain": int(self.const.group_namespace),
            "entity_type": int(self.const.entity_group),
        }

        #
        # group_id filter
        if group_id is not None:
            where.append(argument_to_sql(group_id, "gi.group_id", binds, int))

        #
        # member_id filters (all of them)
        if member_id is not None:
            if indirect_members:
                # NB! This can be a very large group set.
                group_ids = search_transitive_closure(member_id)
                if not group_ids:
                    return []

                where.append(
                    argument_to_sql(group_ids, "gi.group_id", binds, int))
            else:
                extra_tables.append(
                    "[:table schema=cerebrum name=group_member] gm")
                where.append("(gi.group_id = gm.group_id)")
                where.append(
                    argument_to_sql(member_id, "gm.member_id", binds, int))

        #
        # admin_id filter (all of them)
        if admin_id is not None:
            extra_tables.append("[:table schema=cerebrum name=group_admin] ga")
            where.append("(gi.group_id = ga.group_id)")

            if admin_by_membership:
                admin_ids = [admin_id]
                for group in self.search(member_id=admin_id):
                    admin_ids.append(group['group_id'])

                where.append(
                    argument_to_sql(admin_ids, "ga.admin_id", binds, int))
            else:
                where.append(
                    argument_to_sql(admin_id, "ga.admin_id", binds, int))

        #
        # moderator_id filter (all of them)
        if moderator_id is not None:
            extra_tables.append(
                "[:table schema=cerebrum name=group_moderator] gmod")
            where.append("(gi.group_id = gmod.group_id)")
            if moderator_by_membership:
                moderator_ids = [moderator_id]
                for group in self.search(member_id=moderator_id):
                    moderator_ids.append(group['group_id'])

                where.append(
                    argument_to_sql(moderator_ids, "gmod.moderator_id", binds,
                                    int))
            else:
                where.append(
                    argument_to_sql(moderator_id, "gmod.moderator_id", binds,
                                    int))

        #
        # spread filter
        if spread is not None:
            extra_tables.append(
                "[:table schema=cerebrum name=entity_spread] es")
            where.append("(gi.group_id = es.entity_id)")
            where.append(argument_to_sql(spread, "es.spread", binds, int))

        #
        # name filter
        if name is not None:
            name = prepare_string(name)
            where.append("(LOWER(en.entity_name) LIKE :name)")
            binds["name"] = name

        # description filter
        if description is not None:
            description = prepare_string(description)
            where.append("(LOWER(gi.description) LIKE :description)")
            binds["description"] = description

        #
        # group type filter
        if group_type is not None:
            where.append(
                argument_to_sql(group_type, "gi.group_type", binds, int))

        #
        # expired filter
        if filter_expired:
            where.append("(gi.expire_date IS NULL OR gi.expire_date > [:now])")

        #
        # creator_id filter
        if creator_id is not None:
            where.append(
                argument_to_sql(creator_id, "gi.creator_id", binds, int))

        #
        # expired_only filter
        if expired_only:
            where.append("(gi.expire_date IS NOT NULL AND gi.expire_date < "
                         "[:now])")

        prepared = stmt.format(
            extra_tables=(', ' +
                          ', '.join(extra_tables) if extra_tables else ''),
            where='WHERE ' + ' AND '.join(where) if where else '',
        )
        return self.query(prepared, binds, fetchall=fetchall)