class BofhdExtension(BofhdCommonMethods):
    """ Commands for getting, setting and unsetting consent. """

    hidden_commands = {}  # Not accessible through bofh
    all_commands = {}
    parent_commands = False
    authz = ConsentAuth

    def __init__(self, *args, **kwargs):
        """
        """
        super(BofhdExtension, self).__init__(*args, **kwargs)
        # POST:
        for attr in ('ConsentType', 'EntityConsent'):
            if not hasattr(self.const, attr):
                raise RuntimeError('consent: Missing consent constant types')

    @classmethod
    def get_help_strings(cls):
        """ Help strings for consent commands. """
        group, cmd, args = get_help_strings()

        group.setdefault('consent', 'Commands for handling consents')

        cmd.setdefault('consent', dict()).update({
            'consent_set':
            cls.consent_set.__doc__,
            'consent_unset':
            cls.consent_unset.__doc__,
            'consent_info':
            cls.consent_info.__doc__,
            'consent_list':
            cls.consent_list.__doc__,
        })

        args.update({
            'consent_type': [
                'type', 'Enter consent type',
                "'consent list' lists defined consents"
            ],
        })

        return (group, cmd, args)

    def check_consent_support(self, entity):
        """ Assert that entity has EntityConsentMixin.

        :param Cerebrum.Entity entity: The entity to check.

        :raise NotImplementedError: If entity lacks consent support.

        """
        entity_type = self.const.EntityType(entity.entity_type)
        if not isinstance(entity, EntityConsentMixin):
            raise NotImplementedError(
                "Entity type '%s' does not support consent." %
                six.text_type(entity_type))

    def _get_consent(self, consent_ident):
        u""" Get consent constant from constant strval or intval.

        :type consent_ident: int, str, Cerebrum.Constant.ConstantCode
        :param consent_ident: Something to lookup a consent constant by

        :return tuple:
            A tuple consisting of a EntityConsent constant and its ConsentType
            constant.

        :raise Cerebrum.Error.NotFoundError: If the constant cannot be found.

        """
        consent = self.const.human2constant(
            consent_ident, const_type=self.const.EntityConsent)
        if not consent:
            raise CerebrumError("No consent %r" % consent_ident)
        consent_type = self.const.ConsentType(consent.consent_type)
        return (consent, consent_type)

    #
    # consent set <ident> <consent_type>
    #
    all_commands['consent_set'] = Command(
        ('consent', 'set'),
        Id(help_ref="id:target:account"),
        ConsentType(),
        fs=FormatSuggestion(
            "OK: Set consent '%s' (%s) for %s '%s' (entity_id=%s)",
            ('consent_name', 'consent_type', 'entity_type', 'entity_name',
             'entity_id')),
        perm_filter='can_set_consent')

    def consent_set(self, operator, entity_ident, consent_ident):
        """ Set a consent for an entity. """
        entity = self.util.get_target(entity_ident, restrict_to=[])
        self.ba.can_set_consent(operator.get_entity_id(), entity)
        self.check_consent_support(entity)
        consent, consent_type = self._get_consent(consent_ident)
        entity_name = self._get_entity_name(entity.entity_id,
                                            entity.entity_type)
        entity.set_consent(consent)
        entity.write_db()
        return {
            'consent_name': six.text_type(consent),
            'consent_type': six.text_type(consent_type),
            'entity_id': entity.entity_id,
            'entity_type':
            six.text_type(self.const.EntityType(entity.entity_type)),
            'entity_name': entity_name,
        }

    #
    # consent unset <ident> <consent_type>
    #
    all_commands['consent_unset'] = Command(
        ('consent', 'unset'),
        Id(help_ref="id:target:account"),
        ConsentType(),
        fs=FormatSuggestion(
            "OK: Removed consent '%s' (%s) for %s '%s' (entity_id=%s)",
            ('consent_name', 'consent_type', 'entity_type', 'entity_name',
             'entity_id')),
        perm_filter='can_unset_consent')

    def consent_unset(self, operator, entity_ident, consent_ident):
        """ Remove a previously set consent. """
        entity = self.util.get_target(entity_ident, restrict_to=[])
        self.ba.can_unset_consent(operator.get_entity_id(), entity)
        self.check_consent_support(entity)
        consent, consent_type = self._get_consent(consent_ident)
        entity_name = self._get_entity_name(entity.entity_id,
                                            entity.entity_type)
        entity.remove_consent(consent)
        entity.write_db()
        return {
            'consent_name': six.text_type(consent),
            'consent_type': six.text_type(consent_type),
            'entity_id': entity.entity_id,
            'entity_type':
            six.text_type(self.const.EntityType(entity.entity_type)),
            'entity_name': entity_name,
        }

    #
    # consent info <ident>
    #
    all_commands['consent_info'] = Command(
        ('consent', 'info'),
        Id(help_ref="id:target:account"),
        fs=FormatSuggestion(
            '%-15s %-8s %-17s %-17s %s',
            ('consent_name', 'consent_type',
             format_datetime('consent_time_set'),
             format_datetime('consent_time_expire'), 'consent_description'),
            hdr='%-15s %-8s %-17s %-17s %s' %
            ('Name', 'Type', 'Set at', 'Expires at', 'Description')),
        perm_filter='can_show_consent_info')

    def consent_info(self, operator, ident):
        u""" View all set consents for a given entity. """
        entity = self.util.get_target(ident, restrict_to=[])
        self.check_consent_support(entity)
        self.ba.can_show_consent_info(operator.get_entity_id(), entity)

        consents = []
        for row in entity.list_consents(entity_id=entity.entity_id,
                                        filter_expired=False):
            consent, consent_type = self._get_consent(int(row['consent_code']))
            consents.append({
                'consent_name': six.text_type(consent),
                'consent_type': six.text_type(consent_type),
                'consent_time_set': row['time_set'],
                'consent_time_expire': row['expiry'],
                'consent_description': row['description'],
            })
        if not consents:
            name = self._get_entity_name(entity.entity_id, entity.entity_type)
            raise CerebrumError(
                "'%s' (entity_type=%s, entity_id=%s) has no consents set" %
                (name, six.text_type(self.const.EntityType(
                    entity.entity_type)), entity.entity_id))
        return consents

    #
    # consent list
    #
    all_commands['consent_list'] = Command(
        ('consent', 'list'),
        fs=FormatSuggestion(
            '%-15s  %-8s  %s',
            ('consent_name', 'consent_type', 'consent_description'),
            hdr='%-16s %-9s %s' % ('Name', 'Type', 'Description')),
        perm_filter='can_list_consents')

    def consent_list(self, operator):
        """ List all consent types. """
        self.ba.can_list_consents(operator.get_entity_id())
        consents = []
        for consent in self.const.fetch_constants(self.const.EntityConsent):
            consent, consent_type = self._get_consent(int(consent))
            consents.append({
                'consent_name': six.text_type(consent),
                'consent_type': six.text_type(consent_type),
                'consent_description': consent.description,
            })
        if not consents:
            raise CerebrumError("No consent types defined yet")
        return consents
class BofhdExtension(BofhdCommandBase):

    all_commands = {}
    hidden_commands = {}
    authz = RequestsAuth

    #
    # misc change_request <request-id> <datetime>
    #
    all_commands['misc_change_request'] = Command(("misc", "change_request"),
                                                  Id(help_ref="id:request_id"),
                                                  DateTimeString())

    def misc_change_request(self, operator, request_id, datetime):
        if not request_id:
            raise CerebrumError('Request id required')
        if not datetime:
            raise CerebrumError('Date required')
        datetime = self._parse_date(datetime)
        br = BofhdRequests(self.db, self.const)
        old_req = br.get_requests(request_id=request_id)
        if not old_req:
            raise CerebrumError("There is no request with id=%r" % request_id)
        else:
            # If there is anything, it's at most one
            old_req = old_req[0]
        # If you are allowed to cancel a request, you can change it :)
        self.ba.can_cancel_request(operator.get_entity_id(), request_id)
        br.delete_request(request_id=request_id)
        br.add_request(operator.get_entity_id(), datetime,
                       old_req['operation'], old_req['entity_id'],
                       old_req['destination_id'], old_req['state_data'])
        return "OK, altered request %s" % request_id

    #
    # misc list_bofhd_request_types
    #
    all_commands['misc_list_bofhd_request_types'] = Command(
        ("misc", "list_bofhd_request_types"),
        fs=FormatSuggestion("%-20s %s", ("code_str", "description"),
                            hdr="%-20s %s" % ("Code", "Description")))

    def misc_list_bofhd_request_types(self, operator):
        br = BofhdRequests(self.db, self.const)
        result = []
        for row in br.get_operations():
            br_code = self.const.BofhdRequestOp(row['code_str'])
            result.append({
                'code_str': six.text_type(br_code).lstrip('br_'),
                'description': br_code.description,
            })
        return result

    #
    # misc cancel_request
    #
    all_commands['misc_cancel_request'] = Command(
        ("misc", "cancel_request"), SimpleString(help_ref='id:request_id'))

    def misc_cancel_request(self, operator, req):
        if req.isdigit():
            req_id = int(req)
        else:
            raise CerebrumError("Request-ID must be a number")
        br = BofhdRequests(self.db, self.const)
        if not br.get_requests(request_id=req_id):
            raise CerebrumError("Request ID %d not found" % req_id)
        self.ba.can_cancel_request(operator.get_entity_id(), req_id)
        br.delete_request(request_id=req_id)
        return "OK, %d canceled" % req_id

    #
    # misc list_requests
    #
    all_commands['misc_list_requests'] = Command(
        ("misc", "list_requests"),
        SimpleString(help_ref='string_bofh_request_search_by',
                     default='requestee'),
        SimpleString(help_ref='string_bofh_request_target', default='<me>'),
        fs=FormatSuggestion("%-7i %-10s %-16s %-16s %-10s %-20s %s",
                            ("id", "requestee", format_time("when"), "op",
                             "entity", "destination", "args"),
                            hdr="%-7s %-10s %-16s %-16s %-10s %-20s %s" %
                            ("Id", "Requestee", "When", "Op", "Entity",
                             "Destination", "Arguments")))

    def misc_list_requests(self, operator, search_by, destination):
        br = BofhdRequests(self.db, self.const)
        ret = []

        if destination == '<me>':
            destination = self._get_account(operator.get_entity_id(),
                                            idtype='id')
            destination = destination.account_name
        if search_by == 'requestee':
            account = self._get_account(destination)
            rows = br.get_requests(operator_id=account.entity_id, given=True)
        elif search_by == 'operation':
            try:
                destination = int(
                    self.const.BofhdRequestOp('br_' + destination))
            except Errors.NotFoundError:
                raise CerebrumError("Unknown request operation %s" %
                                    destination)
            rows = br.get_requests(operation=destination)
        elif search_by == 'disk':
            disk_id = self._get_disk(destination)[1]
            rows = br.get_requests(destination_id=disk_id)
        elif search_by == 'account':
            account = self._get_account(destination)
            rows = br.get_requests(entity_id=account.entity_id)
        else:
            raise CerebrumError("Unknown search_by criteria")

        for r in rows:
            op = self.const.BofhdRequestOp(r['operation'])
            dest = None
            ent_name = None
            if op in (self.const.bofh_move_user, self.const.bofh_move_request,
                      self.const.bofh_move_user_now):
                disk = self._get_disk(r['destination_id'])[0]
                dest = disk.path
            elif op in (self.const.bofh_move_give, ):
                dest = self._get_entity_name(r['destination_id'],
                                             self.const.entity_group)
            elif op in (self.const.bofh_email_create,
                        self.const.bofh_email_delete):
                dest = self._get_entity_name(r['destination_id'],
                                             self.const.entity_host)
            elif op in (self.const.bofh_sympa_create,
                        self.const.bofh_sympa_remove):
                ea = Email.EmailAddress(self.db)
                if r['destination_id'] is not None:
                    ea.find(r['destination_id'])
                    dest = ea.get_address()
                ea.clear()
                try:
                    ea.find(r['entity_id'])
                except Errors.NotFoundError:
                    ent_name = "<not found>"
                else:
                    ent_name = ea.get_address()
            if ent_name is None:
                ent_name = self._get_entity_name(r['entity_id'],
                                                 self.const.entity_account)
            if r['requestee_id'] is None:
                requestee = ''
            else:
                requestee = self._get_entity_name(r['requestee_id'],
                                                  self.const.entity_account)
            ret.append({
                'when': r['run_at'],
                'requestee': requestee,
                'op': six.text_type(op),
                'entity': ent_name,
                'destination': dest,
                'args': r['state_data'],
                'id': r['request_id']
            })
        ret.sort(lambda a, b: cmp(a['id'], b['id']))
        return ret
Esempio n. 3
0
class BofhdExtension(BofhdCommandBase):
    """The BofhdExctension for AD related commands and functionality."""

    all_commands = {}
    parent_commands = False
    authz = BofhdAuth

    @classmethod
    def get_help_strings(cls):
        group_help = {
            'ad': "Commands for AD related functionality",
            }

        # The texts in command_help are automatically line-wrapped, and should
        # not contain \n
        command_help = {
            'ad': {
                'ad_attributetypes':
                    'List out all defined AD-attribute types',
                'ad_list_attributes':
                    'List all attributes related to given spread',
                'ad_info':
                    'Get AD-related information about an entity',
                'ad_set_attribute':
                    'Set a given AD-attribute for an entity',
                'ad_remove_attribute':
                    'Remove a given AD-attribute for an entity',
                },
            }

        arg_help = {
            'attr_type':
                ['attr_type', 'Type of AD-attribute',
                 'See "ad attributetypes" for a list of defined types'],
            'attr_value':
                ['value', 'Value of AD-attribute',
                 'A string value for the AD-attribute'],
            'id':
                ['id', 'Id',
                 'The identity of an entity'],
            'spread':
                ['spread', 'Spread',
                 'The spread for the attribute'],
            }
        return (group_help, command_help, arg_help)

    # ad_attributetypes
    all_commands['ad_attributetypes'] = Command(
        ("ad", "attributetypes"),
        fs=FormatSuggestion(
            "%-14s %-8s %s", ('name', 'multi', 'desc'),
            hdr="%-14s %-8s %s" % ('Name', 'Multival', 'Description')
        )
    )

    def ad_attributetypes(self, operator):
        """List out all types of AD-attributes defined in Cerebrum."""
        return [
            {
                'name': six.text_type(c),
                'multi': c.multivalued,
                'desc': c.description,
            }
            for c in self.const.fetch_constants(self.const.ADAttribute)
        ]

    #
    # ad_list_attributes
    #
    all_commands['ad_list_attributes'] = Command(
        ("ad", "list_attributes"),
        AttributeType(optional=True),
        Spread(optional=True),
        fs=FormatSuggestion(
            "%-20s %-20s %-20s %s", ('attr_type', 'spread', 'entity', 'value'),
            hdr="%-20s %-20s %-20s %s" % ('Attribute', 'Spread', 'Entity',
                                          'Value')
        ),
        perm_filter='is_superuser'
    )  # TODO: fix BA!

    def ad_list_attributes(self, operator, attr_type=None, spread=None):
        """List all attributes, limited to given input."""
        if not self.ba.is_superuser(operator.get_entity_id()):
            raise PermissionDenied("Only for superusers, for now")
        # TODO: check if operator has access to the entity

        atr = spr = None
        if attr_type:
            atr = _get_attr(self.const, attr_type)
        if spread:
            spr = _get_spread(self.const, spread)

        ent = EntityADMixin(self.db)
        return [
            {
                'attr_type': six.text_type(
                    self.const.ADAttribute(row['attr_code'])),
                'spread': six.text_type(
                    self.const.Spread(row['spread_code'])),
                'entity': self._get_entity_name(row['entity_id']),
                'value': row['value'],
            }
            for row in ent.list_ad_attributes(spread=spr, attribute=atr)
        ]

    #
    # ad_info
    #
    all_commands['ad_info'] = Command(
        ("ad", "info"),
        EntityType(),
        Id(),
        fs=FormatSuggestion([
            ('AD-id: %-12s %s', ('id_type', 'ad_id')),
            ('%-20s %-20s %s', ('spread', 'attr_type', 'attr_value'),
             '%-20s %-20s %s' % ('Spread', 'Attribute', 'Value')),
        ]),
        perm_filter='is_superuser'
    )

    def ad_info(self, operator, entity_type, ident):
        """Return AD related information about a given entity."""
        if not self.ba.is_superuser(operator.get_entity_id()):
            raise PermissionDenied("Only for superusers, for now")
        ent = self._get_entity(entity_type, ident)
        # TODO: check if operator has access to the entity?
        ret = []
        for row in ent.search_external_ids(
                source_system=self.const.system_ad,
                entity_id=ent.entity_id, fetchall=False):
            ret.append({
                'id_type': six.text_type(
                    self.const.EntityExternalId(row['id_type'])),
                'ad_id': row['external_id'],
            })
        for row in ent.list_ad_attributes(entity_id=ent.entity_id):
            ret.append({
                'attr_type': six.text_type(
                    self.const.ADAttribute(row['attr_code'])),
                'spread': six.text_type(
                    self.const.Spread(row['spread_code'])),
                'attr_value': row['value'],
            })
        return ret

    #
    # ad_set_attribute <entity-type> <entity-id> <attr> <spread> <value>
    #
    all_commands['ad_set_attribute'] = Command(
        ("ad", "set_attribute"),
        EntityType(),
        Id(),
        AttributeType(),
        Spread(),
        SimpleString(help_ref='attr_value'),
        fs=FormatSuggestion([
            ("AD-attribute %s set for %s, limited to spread %s: %s",
             ('attribute', 'entity_name', 'spread', 'value')),
            ('WARNING: %s', ('warning', ))
        ]),
        perm_filter='is_superuser'
    )  # TODO: fix BA!

    def ad_set_attribute(self, operator,
                         entity_type, ident, attr_type, spread, value):
        """Set an attribute for a given entity."""
        if not self.ba.is_superuser(operator.get_entity_id()):
            raise PermissionDenied("Only for superusers, for now")
        # TODO: check if operator has access to the entity
        ent = self._get_entity(entity_type, ident)
        atr = _get_attr(self.const, attr_type)
        spr = _get_spread(self.const, spread)
        ent.set_ad_attribute(spread=spr, attribute=atr, value=value)
        ent.write_db()

        # We keep using the constant strvals:
        spread = six.text_type(spr)
        attr_type = six.text_type(atr)

        # Check if the spread and attribute is defined for an AD-sync. If not,
        # add a warning to the output.

        retval = [{
            'attribute': attr_type,
            'entity_name': self._get_entity_name(ent.entity_id),
            'entity_type': six.text_type(ent.entity_type),
            'spread': spread,
            'value': value,
        }]

        config = getattr(cereconf, 'AD_SPREADS', None)
        if config:
            if spread not in config:
                retval.append({
                    'warning': 'No AD-sync defined for spread: %s' % spread,
                })
            elif attr_type not in config[spread].get('attributes', ()):
                retval.append({
                    'warning': 'AD-sync for %s does not know of: %s' % (
                        attr_type, spread),
                })

        return retval

    #
    # ad_remove_attribute <entity-type> <entity-id> <attr> <spread>
    #
    all_commands['ad_remove_attribute'] = Command(
        ("ad", "remove_attribute"),
        EntityType(),
        Id(),
        AttributeType(),
        Spread(),
        fs=FormatSuggestion(
            "AD-attribute %s removed for %s, for spread %s",
            ('attribute', 'entity_name', 'spread')
        ),
        perm_filter='is_superuser'
    )  # TODO: fix BA!

    def ad_remove_attribute(self, operator,
                            entity_type, id, attr_type, spread):
        """Remove an AD-attribute for a given entity."""
        if not self.ba.is_superuser(operator.get_entity_id()):
            raise PermissionDenied("Only for superusers, for now")
        ent = self._get_entity(entity_type, id)
        atr = _get_attr(self.const, attr_type)
        spr = _get_spread(self.const, spread)
        try:
            ent.delete_ad_attribute(spread=spr, attribute=atr)
        except Errors.NotFoundError:
            raise CerebrumError(
                '%s does not have AD-attribute %s with spread %s' %
                (ent.entity_id,
                 six.text_type(atr),
                 six.text_type(spr))
            )
        ent.write_db()

        return {
            'attribute': six.text_type(atr),
            'entity_name': self._get_entity_name(ent.entity_id),
            'entity_type': six.text_type(ent.entity_type),
            'spread': six.text_type(spr),
        }
Esempio n. 4
0
class BofhdHistoryCmds(BofhdCommandBase):
    """BofhdExtension for history related commands and functionality."""

    all_commands = {}
    authz = BofhdHistoryAuth

    @property
    def util(self):
        try:
            return self.__util
        except AttributeError:
            self.__util = BofhdUtils(self.db)
            return self.__util

    @classmethod
    def get_help_strings(cls):
        """Get help strings."""
        group_help = {
            'history': "History related commands",
        }

        command_help = {
            'history': {
                'history_show': 'List changes made to an entity',
            }
        }

        argument_help = {
            'limit_number_of_results': [
                'limit', 'Number of changes to list',
                'Upper limit for how many changes to include, counting '
                'backwards from the most recent. Default (when left empty) '
                'is 0, which means no limit'
            ],
            'yes_no_all_changes': [
                'all', 'All involved changes?',
                'List all changes where the entity is involved (yes), or '
                'only the ones where the entity itself is changed (no)'
            ],
        }

        return merge_help_strings(
            get_help_strings(),
            (group_help, command_help, argument_help),
        )

    #
    # history show <entity> [yes|no] [n]
    #
    default_any_entity = 'yes'
    default_num_changes = '0'  # 0 means no limit

    all_commands['history_show'] = Command(
        ('history', 'show'),
        Id(help_ref='id:target:entity'),
        YesNo(help_ref='yes_no_all_changes',
              optional=True,
              default=default_any_entity),
        Integer(help_ref='limit_number_of_results',
                optional=True,
                default=default_num_changes),
        fs=FormatSuggestion('%s [%s]: %s',
                            ('timestamp', 'change_by', 'message')),
        perm_filter='can_show_history',
    )

    def history_show(self,
                     operator,
                     entity,
                     any_entity=default_any_entity,
                     limit_number_of_results=default_num_changes):
        """ show audit log for a given entity. """
        ent = self.util.get_target(entity, restrict_to=[])
        self.ba.can_show_history(operator.get_entity_id(), ent)
        ret = []
        try:
            n = int(limit_number_of_results)
        except ValueError:
            raise CerebrumError('Illegal range limit, must be an integer: '
                                '{}'.format(limit_number_of_results))

        record_db = AuditLogAccessor(self.db)
        rows = list(record_db.search(entities=ent.entity_id))
        if self._get_boolean(any_entity):
            rows.extend(list(record_db.search(targets=ent.entity_id)))
        rows = sorted(rows, key=lambda r: (r.timestamp, r.record_id))

        _process = AuditRecordProcessor()
        for r in rows[-n:]:
            processed_row = _process(r)
            ret.append({
                'timestamp': processed_row.timestamp,
                'change_by': processed_row.change_by,
                'message': processed_row.message
            })
        return ret
Esempio n. 5
0
class BofhdExtension(BofhdCommonMethods):
    u""" Extends bofhd with a 'note' command group. """

    all_commands = {}
    parent_commands = False
    authz = BofhdAuth

    @classmethod
    def get_help_strings(cls):
        group_help = {
            'note': 'Entity note related commands',
        }

        command_help = {
            'note': {
                'note_show': 'Show notes associated with an entity',
                'note_add': 'Add a new note to an entity',
                'note_remove': 'Remove a note associated with an entity',
            },
        }

        arg_help = {
            'note_id':
                ['note_id', 'Enter note ID',
                 'Enter the ID of the note'],
            'note_subject':
                ['note_subject', 'Enter subject',
                 'Enter the subject of the note'],
            'note_description':
                ['note_description', 'Enter description',
                 'Enter the description of the note'],
        }

        return (group_help, command_help, arg_help)

    #
    # note show <subject-id>
    #
    all_commands['note_show'] = Command(
        ('note', 'show'),
        Id(help_ref='id:target:entity'),
        fs=FormatSuggestion([
            ("%d note(s) found for %s:\n", ("notes_total", "entity_target")),
            ("Note #%d added by %s on %s:\n"
             "%s: %s\n",
             ("note_id", "creator", format_time("create_date"),
              "subject", "description"))
        ]),
        perm_filter='can_show_notes')

    def note_show(self, operator, entity_target):
        self.ba.can_show_notes(operator.get_entity_id())

        entity = self.util.get_target(
            entity_target, default_lookup="account", restrict_to=[]
        )
        enote = Note.EntityNote(self.db)
        enote.find(entity.entity_id)

        notes = enote.get_notes()
        result = []

        if len(notes) is 0:
            return "No notes were found for %s" % (entity_target)
        else:
            result.append({
                'notes_total': len(notes),
                'entity_target': entity_target,
            })

        for note_row in notes:
            note = {}

            for key, value in note_row.items():
                note[key] = value

                if key in ('subject', 'description') and len(value) is 0:
                    note[key] = '<not set>'

            # translate creator_id to username
            acc = self._get_account(note_row['creator_id'], idtype='id')
            note['creator'] = acc.account_name
            result.append(note)

        return result

    #
    # note add <subject-id> <title> <contents>
    #
    all_commands['note_add'] = Command(
        ('note', 'add'),
        Id(help_ref='id:target:entity'),
        SimpleString(help_ref='note_subject'),
        SimpleString(help_ref='note_description'),
        perm_filter='can_add_notes')

    def note_add(self, operator, entity_target, subject, description):
        self.ba.can_add_notes(operator.get_entity_id())

        if len(subject) > 70:
            raise CerebrumError(
                u"Subject field cannot be longer than 70 characters")
        if len(description) > 1024:
            raise CerebrumError(
                u"Description field cannot be longer than 1024 characters")

        entity = self.util.get_target(entity_target, restrict_to=[])
        enote = Note.EntityNote(self.db)
        enote.find(entity.entity_id)
        note_id = enote.add_note(operator.get_entity_id(),
                                 subject,
                                 description)
        return "Note #%s was added to entity %s" % (note_id, entity_target)

    #
    # note remove <subject-id> <note-id>
    #
    all_commands['note_remove'] = Command(
        ('note', 'remove'),
        Id(help_ref='id:target:entity'),
        SimpleString(help_ref='note_id'),
        perm_filter='can_remove_notes')

    def note_remove(self, operator, entity_target, note_id):
        self.ba.can_remove_notes(operator.get_entity_id())

        entity = self.util.get_target(entity_target, restrict_to=[])
        enote = Note.EntityNote(self.db)
        enote.find(entity.entity_id)

        if not note_id.isdigit():
            raise CerebrumError("Note ID must be a numeric value")

        for note_row in enote.get_notes():
            if int(note_row['note_id']) is int(note_id):
                enote.delete_note(note_id)
                return "Note #%s associated with entity %s was removed" % (
                    note_id, entity_target)

        raise CerebrumError("Note #%s is not associated with entity %s" % (
            note_id, entity_target))