summaryrefslogtreecommitdiffstats
path: root/ipaserver/plugins/pwpolicy.py
diff options
context:
space:
mode:
authorJan Cholasta <jcholast@redhat.com>2016-04-28 10:30:05 +0200
committerJan Cholasta <jcholast@redhat.com>2016-06-03 09:00:34 +0200
commit6e44557b601f769d23ee74555a72e8b5cc62c0c9 (patch)
treeeedd3e054b0709341b9f58c190ea54f999f7d13a /ipaserver/plugins/pwpolicy.py
parentec841e5d7ab29d08de294b3fa863a631cd50e30a (diff)
downloadfreeipa-6e44557b601f769d23ee74555a72e8b5cc62c0c9.tar.gz
freeipa-6e44557b601f769d23ee74555a72e8b5cc62c0c9.tar.xz
freeipa-6e44557b601f769d23ee74555a72e8b5cc62c0c9.zip
ipalib: move server-side plugins to ipaserver
Move the remaining plugin code from ipalib.plugins to ipaserver.plugins. Remove the now unused ipalib.plugins package. https://fedorahosted.org/freeipa/ticket/4739 Reviewed-By: David Kupka <dkupka@redhat.com>
Diffstat (limited to 'ipaserver/plugins/pwpolicy.py')
-rw-r--r--ipaserver/plugins/pwpolicy.py611
1 files changed, 611 insertions, 0 deletions
diff --git a/ipaserver/plugins/pwpolicy.py b/ipaserver/plugins/pwpolicy.py
new file mode 100644
index 000000000..5a2202aa0
--- /dev/null
+++ b/ipaserver/plugins/pwpolicy.py
@@ -0,0 +1,611 @@
+# Authors:
+# Pavel Zuna <pzuna@redhat.com>
+# Martin Kosek <mkosek@redhat.com>
+#
+# Copyright (C) 2010 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from ipalib import api
+from ipalib import Int, Str, DNParam
+from ipalib import errors
+from .baseldap import (
+ LDAPObject,
+ LDAPCreate,
+ LDAPDelete,
+ LDAPUpdate,
+ LDAPRetrieve,
+ LDAPSearch)
+from ipalib import _
+from ipalib.plugable import Registry
+from ipalib.request import context
+from ipapython.ipautil import run
+from ipapython.dn import DN
+from distutils import version
+
+import six
+
+if six.PY3:
+ unicode = str
+
+__doc__ = _("""
+Password policy
+
+A password policy sets limitations on IPA passwords, including maximum
+lifetime, minimum lifetime, the number of passwords to save in
+history, the number of character classes required (for stronger passwords)
+and the minimum password length.
+
+By default there is a single, global policy for all users. You can also
+create a password policy to apply to a group. Each user is only subject
+to one password policy, either the group policy or the global policy. A
+group policy stands alone; it is not a super-set of the global policy plus
+custom settings.
+
+Each group password policy requires a unique priority setting. If a user
+is in multiple groups that have password policies, this priority determines
+which password policy is applied. A lower value indicates a higher priority
+policy.
+
+Group password policies are automatically removed when the groups they
+are associated with are removed.
+
+EXAMPLES:
+
+ Modify the global policy:
+ ipa pwpolicy-mod --minlength=10
+
+ Add a new group password policy:
+ ipa pwpolicy-add --maxlife=90 --minlife=1 --history=10 --minclasses=3 --minlength=8 --priority=10 localadmins
+
+ Display the global password policy:
+ ipa pwpolicy-show
+
+ Display a group password policy:
+ ipa pwpolicy-show localadmins
+
+ Display the policy that would be applied to a given user:
+ ipa pwpolicy-show --user=tuser1
+
+ Modify a group password policy:
+ ipa pwpolicy-mod --minclasses=2 localadmins
+""")
+
+register = Registry()
+
+@register()
+class cosentry(LDAPObject):
+ """
+ Class of Service object used for linking policies with groups
+ """
+ NO_CLI = True
+
+ container_dn = DN(('cn', 'costemplates'), api.env.container_accounts)
+ object_class = ['top', 'costemplate', 'extensibleobject', 'krbcontainer']
+ permission_filter_objectclasses = ['costemplate']
+ default_attributes = ['cn', 'cospriority', 'krbpwdpolicyreference']
+ managed_permissions = {
+ 'System: Read Group Password Policy costemplate': {
+ 'replaces_global_anonymous_aci': True,
+ 'ipapermright': {'read', 'search', 'compare'},
+ 'ipapermdefaultattr': {
+ 'cn', 'cospriority', 'krbpwdpolicyreference', 'objectclass',
+ },
+ 'default_privileges': {
+ 'Password Policy Readers',
+ 'Password Policy Administrator',
+ },
+ },
+ 'System: Add Group Password Policy costemplate': {
+ 'ipapermright': {'add'},
+ 'replaces': [
+ '(target = "ldap:///cn=*,cn=costemplates,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Add Group Password Policy costemplate";allow (add) groupdn = "ldap:///cn=Add Group Password Policy costemplate,cn=permissions,cn=pbac,$SUFFIX";)',
+ ],
+ 'default_privileges': {'Password Policy Administrator'},
+ },
+ 'System: Delete Group Password Policy costemplate': {
+ 'ipapermright': {'delete'},
+ 'replaces': [
+ '(target = "ldap:///cn=*,cn=costemplates,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Delete Group Password Policy costemplate";allow (delete) groupdn = "ldap:///cn=Delete Group Password Policy costemplate,cn=permissions,cn=pbac,$SUFFIX";)',
+ ],
+ 'default_privileges': {'Password Policy Administrator'},
+ },
+ 'System: Modify Group Password Policy costemplate': {
+ 'ipapermright': {'write'},
+ 'ipapermdefaultattr': {'cospriority'},
+ 'replaces': [
+ '(targetattr = "cospriority")(target = "ldap:///cn=*,cn=costemplates,cn=accounts,$SUFFIX")(version 3.0;acl "permission:Modify Group Password Policy costemplate";allow (write) groupdn = "ldap:///cn=Modify Group Password Policy costemplate,cn=permissions,cn=pbac,$SUFFIX";)',
+ ],
+ 'default_privileges': {'Password Policy Administrator'},
+ },
+ }
+
+ takes_params = (
+ Str('cn', primary_key=True),
+ DNParam('krbpwdpolicyreference'),
+ Int('cospriority', minvalue=0),
+ )
+
+ priority_not_unique_msg = _(
+ 'priority must be a unique value (%(prio)d already used by %(gname)s)'
+ )
+
+ def get_dn(self, *keys, **options):
+ group_dn = self.api.Object.group.get_dn(keys[-1])
+ return self.backend.make_dn_from_attr(
+ 'cn', group_dn, DN(self.container_dn, api.env.basedn)
+ )
+
+ def check_priority_uniqueness(self, *keys, **options):
+ if options.get('cospriority') is not None:
+ entries = self.methods.find(
+ cospriority=options['cospriority']
+ )['result']
+ if len(entries) > 0:
+ group_name = self.api.Object.group.get_primary_key_from_dn(
+ DN(entries[0]['cn'][0]))
+ raise errors.ValidationError(
+ name='priority',
+ error=self.priority_not_unique_msg % {
+ 'prio': options['cospriority'],
+ 'gname': group_name,
+ }
+ )
+
+
+@register()
+class cosentry_add(LDAPCreate):
+ NO_CLI = True
+
+ def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
+ assert isinstance(dn, DN)
+
+ # check for existence of the group
+ group_dn = self.api.Object.group.get_dn(keys[-1])
+ try:
+ result = ldap.get_entry(group_dn, ['objectclass'])
+ except errors.NotFound:
+ self.api.Object.group.handle_not_found(keys[-1])
+
+ oc = [x.lower() for x in result['objectclass']]
+ if 'mepmanagedentry' in oc:
+ raise errors.ManagedPolicyError()
+ self.obj.check_priority_uniqueness(*keys, **options)
+ del entry_attrs['cn']
+ return dn
+
+
+@register()
+class cosentry_del(LDAPDelete):
+ NO_CLI = True
+
+
+@register()
+class cosentry_mod(LDAPUpdate):
+ NO_CLI = True
+
+ def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
+ assert isinstance(dn, DN)
+ new_cospriority = options.get('cospriority')
+ if new_cospriority is not None:
+ cos_entry = self.api.Command.cosentry_show(keys[-1])['result']
+ old_cospriority = int(cos_entry['cospriority'][0])
+
+ # check uniqueness only when the new priority differs
+ if old_cospriority != new_cospriority:
+ self.obj.check_priority_uniqueness(*keys, **options)
+ return dn
+
+
+@register()
+class cosentry_show(LDAPRetrieve):
+ NO_CLI = True
+
+
+@register()
+class cosentry_find(LDAPSearch):
+ NO_CLI = True
+
+
+global_policy_name = 'global_policy'
+global_policy_dn = DN(('cn', global_policy_name), ('cn', api.env.realm), ('cn', 'kerberos'), api.env.basedn)
+
+@register()
+class pwpolicy(LDAPObject):
+ """
+ Password Policy object
+ """
+ container_dn = DN(('cn', api.env.realm), ('cn', 'kerberos'))
+ object_name = _('password policy')
+ object_name_plural = _('password policies')
+ object_class = ['top', 'nscontainer', 'krbpwdpolicy']
+ permission_filter_objectclasses = ['krbpwdpolicy']
+ default_attributes = [
+ 'cn', 'cospriority', 'krbmaxpwdlife', 'krbminpwdlife',
+ 'krbpwdhistorylength', 'krbpwdmindiffchars', 'krbpwdminlength',
+ 'krbpwdmaxfailure', 'krbpwdfailurecountinterval',
+ 'krbpwdlockoutduration',
+ ]
+ managed_permissions = {
+ 'System: Read Group Password Policy': {
+ 'replaces_global_anonymous_aci': True,
+ 'ipapermbindruletype': 'permission',
+ 'ipapermright': {'read', 'search', 'compare'},
+ 'ipapermdefaultattr': {
+ 'cn', 'cospriority', 'krbmaxpwdlife', 'krbminpwdlife',
+ 'krbpwdfailurecountinterval', 'krbpwdhistorylength',
+ 'krbpwdlockoutduration', 'krbpwdmaxfailure',
+ 'krbpwdmindiffchars', 'krbpwdminlength', 'objectclass',
+ },
+ 'default_privileges': {
+ 'Password Policy Readers',
+ 'Password Policy Administrator',
+ },
+ },
+ 'System: Add Group Password Policy': {
+ 'ipapermright': {'add'},
+ 'replaces': [
+ '(target = "ldap:///cn=*,cn=$REALM,cn=kerberos,$SUFFIX")(version 3.0;acl "permission:Add Group Password Policy";allow (add) groupdn = "ldap:///cn=Add Group Password Policy,cn=permissions,cn=pbac,$SUFFIX";)',
+ ],
+ 'default_privileges': {'Password Policy Administrator'},
+ },
+ 'System: Delete Group Password Policy': {
+ 'ipapermright': {'delete'},
+ 'replaces': [
+ '(target = "ldap:///cn=*,cn=$REALM,cn=kerberos,$SUFFIX")(version 3.0;acl "permission:Delete Group Password Policy";allow (delete) groupdn = "ldap:///cn=Delete Group Password Policy,cn=permissions,cn=pbac,$SUFFIX";)',
+ ],
+ 'default_privileges': {'Password Policy Administrator'},
+ },
+ 'System: Modify Group Password Policy': {
+ 'ipapermright': {'write'},
+ 'ipapermdefaultattr': {
+ 'krbmaxpwdlife', 'krbminpwdlife', 'krbpwdfailurecountinterval',
+ 'krbpwdhistorylength', 'krbpwdlockoutduration',
+ 'krbpwdmaxfailure', 'krbpwdmindiffchars', 'krbpwdminlength'
+ },
+ 'replaces': [
+ '(targetattr = "krbmaxpwdlife || krbminpwdlife || krbpwdhistorylength || krbpwdmindiffchars || krbpwdminlength || krbpwdmaxfailure || krbpwdfailurecountinterval || krbpwdlockoutduration")(target = "ldap:///cn=*,cn=$REALM,cn=kerberos,$SUFFIX")(version 3.0;acl "permission:Modify Group Password Policy";allow (write) groupdn = "ldap:///cn=Modify Group Password Policy,cn=permissions,cn=pbac,$SUFFIX";)',
+ ],
+ 'default_privileges': {'Password Policy Administrator'},
+ },
+ }
+
+ MIN_KRB5KDC_WITH_LOCKOUT = "1.8"
+ has_lockout = False
+ lockout_params = ()
+
+ result = run(['klist', '-V'], raiseonerr=False, capture_output=True)
+ if result.returncode == 0:
+ verstr = result.output.split()[-1]
+ ver = version.LooseVersion(verstr)
+ min = version.LooseVersion(MIN_KRB5KDC_WITH_LOCKOUT)
+ if ver >= min:
+ has_lockout = True
+
+ if has_lockout:
+ lockout_params = (
+ Int('krbpwdmaxfailure?',
+ cli_name='maxfail',
+ label=_('Max failures'),
+ doc=_('Consecutive failures before lockout'),
+ minvalue=0,
+ ),
+ Int('krbpwdfailurecountinterval?',
+ cli_name='failinterval',
+ label=_('Failure reset interval'),
+ doc=_('Period after which failure count will be reset (seconds)'),
+ minvalue=0,
+ ),
+ Int('krbpwdlockoutduration?',
+ cli_name='lockouttime',
+ label=_('Lockout duration'),
+ doc=_('Period for which lockout is enforced (seconds)'),
+ minvalue=0,
+ ),
+ )
+
+ label = _('Password Policies')
+ label_singular = _('Password Policy')
+
+ takes_params = (
+ Str('cn?',
+ cli_name='group',
+ label=_('Group'),
+ doc=_('Manage password policy for specific group'),
+ primary_key=True,
+ ),
+ Int('krbmaxpwdlife?',
+ cli_name='maxlife',
+ label=_('Max lifetime (days)'),
+ doc=_('Maximum password lifetime (in days)'),
+ minvalue=0,
+ maxvalue=20000, # a little over 54 years
+ ),
+ Int('krbminpwdlife?',
+ cli_name='minlife',
+ label=_('Min lifetime (hours)'),
+ doc=_('Minimum password lifetime (in hours)'),
+ minvalue=0,
+ ),
+ Int('krbpwdhistorylength?',
+ cli_name='history',
+ label=_('History size'),
+ doc=_('Password history size'),
+ minvalue=0,
+ ),
+ Int('krbpwdmindiffchars?',
+ cli_name='minclasses',
+ label=_('Character classes'),
+ doc=_('Minimum number of character classes'),
+ minvalue=0,
+ maxvalue=5,
+ ),
+ Int('krbpwdminlength?',
+ cli_name='minlength',
+ label=_('Min length'),
+ doc=_('Minimum length of password'),
+ minvalue=0,
+ ),
+ Int('cospriority',
+ cli_name='priority',
+ label=_('Priority'),
+ doc=_('Priority of the policy (higher number means lower priority'),
+ minvalue=0,
+ flags=('virtual_attribute',),
+ ),
+ ) + lockout_params
+
+ def get_dn(self, *keys, **options):
+ if keys[-1] is not None:
+ return self.backend.make_dn_from_attr(
+ self.primary_key.name, keys[-1],
+ DN(self.container_dn, api.env.basedn)
+ )
+ return global_policy_dn
+
+ def convert_time_for_output(self, entry_attrs, **options):
+ # Convert seconds to hours and days for displaying to user
+ if not options.get('raw', False):
+ if 'krbmaxpwdlife' in entry_attrs:
+ entry_attrs['krbmaxpwdlife'][0] = unicode(
+ int(entry_attrs['krbmaxpwdlife'][0]) // 86400
+ )
+ if 'krbminpwdlife' in entry_attrs:
+ entry_attrs['krbminpwdlife'][0] = unicode(
+ int(entry_attrs['krbminpwdlife'][0]) // 3600
+ )
+
+ def convert_time_on_input(self, entry_attrs):
+ # Convert hours and days to seconds for writing to LDAP
+ if 'krbmaxpwdlife' in entry_attrs and entry_attrs['krbmaxpwdlife']:
+ entry_attrs['krbmaxpwdlife'] = entry_attrs['krbmaxpwdlife'] * 86400
+ if 'krbminpwdlife' in entry_attrs and entry_attrs['krbminpwdlife']:
+ entry_attrs['krbminpwdlife'] = entry_attrs['krbminpwdlife'] * 3600
+
+ def validate_lifetime(self, entry_attrs, add=False, *keys):
+ """
+ Ensure that the maximum lifetime is greater than the minimum.
+ If there is no minimum lifetime set then don't return an error.
+ """
+ maxlife=entry_attrs.get('krbmaxpwdlife', None)
+ minlife=entry_attrs.get('krbminpwdlife', None)
+ existing_entry = {}
+ if not add: # then read existing entry
+ existing_entry = self.api.Command.pwpolicy_show(keys[-1],
+ all=True,
+ )['result']
+ if minlife is None and 'krbminpwdlife' in existing_entry:
+ minlife = int(existing_entry['krbminpwdlife'][0]) * 3600
+ if maxlife is None and 'krbmaxpwdlife' in existing_entry:
+ maxlife = int(existing_entry['krbmaxpwdlife'][0]) * 86400
+
+ if maxlife is not None and minlife is not None:
+ if minlife > maxlife:
+ raise errors.ValidationError(
+ name='maxlife',
+ error=_('Maximum password life must be greater than minimum.'),
+ )
+
+ def add_cospriority(self, entry, pwpolicy_name, rights=True):
+ if pwpolicy_name and pwpolicy_name != global_policy_name:
+ cos_entry = self.api.Command.cosentry_show(
+ pwpolicy_name,
+ rights=rights, all=rights
+ )['result']
+ if cos_entry.get('cospriority') is not None:
+ entry['cospriority'] = cos_entry['cospriority']
+ if rights:
+ entry['attributelevelrights']['cospriority'] = \
+ cos_entry['attributelevelrights']['cospriority']
+
+
+@register()
+class pwpolicy_add(LDAPCreate):
+ __doc__ = _('Add a new group password policy.')
+
+ def get_args(self):
+ yield self.obj.primary_key.clone(attribute=True, required=True)
+
+ def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
+ assert isinstance(dn, DN)
+ self.obj.convert_time_on_input(entry_attrs)
+ self.obj.validate_lifetime(entry_attrs, True)
+ self.api.Command.cosentry_add(
+ keys[-1], krbpwdpolicyreference=dn,
+ cospriority=options.get('cospriority')
+ )
+ return dn
+
+ def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
+ assert isinstance(dn, DN)
+ self.log.info('%r' % entry_attrs)
+ # attribute rights are not allowed for pwpolicy_add
+ self.obj.add_cospriority(entry_attrs, keys[-1], rights=False)
+ self.obj.convert_time_for_output(entry_attrs, **options)
+ return dn
+
+
+@register()
+class pwpolicy_del(LDAPDelete):
+ __doc__ = _('Delete a group password policy.')
+
+ def get_args(self):
+ yield self.obj.primary_key.clone(
+ attribute=True, required=True, multivalue=True
+ )
+
+ def pre_callback(self, ldap, dn, *keys, **options):
+ assert isinstance(dn, DN)
+ if dn == global_policy_dn:
+ raise errors.ValidationError(
+ name='group',
+ error=_('cannot delete global password policy')
+ )
+ return dn
+
+ def post_callback(self, ldap, dn, *keys, **options):
+ assert isinstance(dn, DN)
+ try:
+ self.api.Command.cosentry_del(keys[-1])
+ except errors.NotFound:
+ pass
+ return True
+
+
+@register()
+class pwpolicy_mod(LDAPUpdate):
+ __doc__ = _('Modify a group password policy.')
+
+ def execute(self, cn=None, **options):
+ return super(pwpolicy_mod, self).execute(cn, **options)
+
+ def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
+ assert isinstance(dn, DN)
+ self.obj.convert_time_on_input(entry_attrs)
+ self.obj.validate_lifetime(entry_attrs, False, *keys)
+ setattr(context, 'cosupdate', False)
+ if options.get('cospriority') is not None:
+ if keys[-1] is None:
+ raise errors.ValidationError(
+ name='priority',
+ error=_('priority cannot be set on global policy')
+ )
+ try:
+ self.api.Command.cosentry_mod(
+ keys[-1], cospriority=options['cospriority']
+ )
+ except errors.EmptyModlist as e:
+ if len(entry_attrs) == 1: # cospriority only was passed
+ raise e
+ else:
+ setattr(context, 'cosupdate', True)
+ return dn
+
+ def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
+ assert isinstance(dn, DN)
+ rights = options.get('all', False) and options.get('rights', False)
+ self.obj.add_cospriority(entry_attrs, keys[-1], rights)
+ self.obj.convert_time_for_output(entry_attrs, **options)
+ return dn
+
+ def exc_callback(self, keys, options, exc, call_func, *call_args, **call_kwargs):
+ if call_func.__name__ == 'update_entry':
+ if isinstance(exc, errors.EmptyModlist):
+ entry_attrs = call_args[0]
+ cosupdate = getattr(context, 'cosupdate')
+ if not entry_attrs or cosupdate:
+ return
+ raise exc
+
+
+@register()
+class pwpolicy_show(LDAPRetrieve):
+ __doc__ = _('Display information about password policy.')
+
+ takes_options = LDAPRetrieve.takes_options + (
+ Str('user?',
+ label=_('User'),
+ doc=_('Display effective policy for a specific user'),
+ ),
+ )
+
+ def execute(self, cn=None, **options):
+ return super(pwpolicy_show, self).execute(cn, **options)
+
+ def pre_callback(self, ldap, dn, attrs_list, *keys, **options):
+ assert isinstance(dn, DN)
+ if options.get('user') is not None:
+ user_entry = self.api.Command.user_show(
+ options['user'], all=True
+ )['result']
+ if 'krbpwdpolicyreference' in user_entry:
+ return user_entry.get('krbpwdpolicyreference', [dn])[0]
+ return dn
+
+ def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
+ assert isinstance(dn, DN)
+ rights = options.get('all', False) and options.get('rights', False)
+ self.obj.add_cospriority(entry_attrs, keys[-1], rights)
+ self.obj.convert_time_for_output(entry_attrs, **options)
+ return dn
+
+
+@register()
+class pwpolicy_find(LDAPSearch):
+ __doc__ = _('Search for group password policies.')
+
+ # this command does custom sorting in post_callback
+ sort_result_entries = False
+
+ def priority_sort_key(self, entry):
+ """Key for sorting password policies
+
+ returns a pair: (is_global, priority)
+ """
+ # global policy will be always last in the output
+ if entry['cn'][0] == global_policy_name:
+ return True, 0
+ else:
+ # policies with higher priority (lower number) will be at the
+ # beginning of the list
+ try:
+ cospriority = int(entry['cospriority'][0])
+ except KeyError:
+ # if cospriority is not present in the entry, rather return 0
+ # than crash
+ cospriority = 0
+ return False, cospriority
+
+ def post_callback(self, ldap, entries, truncated, *args, **options):
+ for e in entries:
+ # When pkey_only flag is on, entries should contain only a cn.
+ # Add a cospriority attribute that will be used for sorting.
+ # Attribute rights are not allowed for pwpolicy_find.
+ self.obj.add_cospriority(e, e['cn'][0], rights=False)
+
+ self.obj.convert_time_for_output(e, **options)
+
+ # do custom entry sorting by its cospriority
+ entries.sort(key=self.priority_sort_key)
+
+ if options.get('pkey_only', False):
+ # remove cospriority that was used for sorting
+ for e in entries:
+ try:
+ del e['cospriority']
+ except KeyError:
+ pass
+
+ return truncated