The sudo schema now defines sudoOrder, sudoNotBefore and sudoNotAfter but these weren't available in the sudorule plugin.

I've added support for these. sudoOrder enforces uniqueness because duplicates are undefined.

I also added support for a GeneralizedTime parameter type. This is similar to the existing AccessTime parameter but it only handles a single time value.

The sudo patch relies on my patch 916 or you'll have merge issues.

rob
>From 5709d765abd4e983a1ba4317930cd169af17b448 Mon Sep 17 00:00:00 2001
From: Rob Crittenden <rcrit...@redhat.com>
Date: Tue, 13 Dec 2011 22:31:02 -0500
Subject: [PATCH 1/2] Add Generalized Time parameter type and unit test

Per RFC 4517.

https://fedorahosted.org/freeipa/ticket/1314
---
 ipalib/__init__.py                   |    2 +-
 ipalib/parameters.py                 |  133 ++++++++++++++++++++++++++++++++++
 tests/test_ipalib/test_parameters.py |   49 +++++++++++++
 3 files changed, 183 insertions(+), 1 deletions(-)

diff --git a/ipalib/__init__.py b/ipalib/__init__.py
index 29ba0bb..2ee1f27 100644
--- a/ipalib/__init__.py
+++ b/ipalib/__init__.py
@@ -879,7 +879,7 @@ from frontend import Command, LocalOrRemote, Updater
 from frontend import Object, Method, Property
 from crud import Create, Retrieve, Update, Delete, Search
 from parameters import DefaultFrom, Bool, Flag, Int, Float, Bytes, Str, IA5Str, Password
-from parameters import BytesEnum, StrEnum, AccessTime, File
+from parameters import BytesEnum, StrEnum, AccessTime, File, GeneralizedTime
 from errors import SkipPluginModule
 from text import _, ngettext, GettextFactory, NGettextFactory
 
diff --git a/ipalib/parameters.py b/ipalib/parameters.py
index 287304d..debcac3 100644
--- a/ipalib/parameters.py
+++ b/ipalib/parameters.py
@@ -1769,6 +1769,139 @@ class AccessTime(Str):
             )
         return None
 
+class GeneralizedTime(Str):
+    """
+    LDAP GeneralizedTime per RFC 4517.
+
+    GeneralizedTime = century year month day hour
+                         [ minute [ second / leap-second ] ]
+                         [ fraction ]
+                         g-time-zone
+
+    century = 2(%x30-39) ; "00" to "99"
+    year    = 2(%x30-39) ; "00" to "99"
+    month   =   ( %x30 %x31-39 ) ; "01" (January) to "09"
+              / ( %x31 %x30-32 ) ; "10" to "12"
+    day     =   ( %x30 %x31-39 )    ; "01" to "09"
+              / ( %x31-32 %x30-39 ) ; "10" to "29"
+              / ( %x33 %x30-31 )    ; "30" to "31"
+    hour    = ( %x30-31 %x30-39 ) / ( %x32 %x30-33 ) ; "00" to "23"
+    minute  = %x30-35 %x30-39                        ; "00" to "59"
+
+    second      = ( %x30-35 %x30-39 ) ; "00" to "59"
+    leap-second = ( %x36 %x30 )       ; "60"
+
+    fraction        = ( DOT / COMMA ) 1*(%x30-39)
+    g-time-zone     = %x5A  ; "Z"
+                      / g-differential
+    g-differential  = ( MINUS / PLUS ) hour [ minute ]
+    MINUS           = %x2D  ; minus sign ("-")
+    """
+    type = unicode
+
+    def __init__(self, name, *rules, **kw):
+        kw['minlength'] = 11
+        kw['pattern'] = u'^[0-9]+[.,]{0,1}[0-9]+[-+]{0,1}[0-9]+Z{0,1}$'
+        kw['pattern_errmsg'] = u'Must be of the form YYYYMMDDHH[MM]Z or YYMMDDHHSS[+-]HHMM'
+        super(GeneralizedTime, self).__init__(name, *rules, **kw)
+
+    def _check_date(self, value):
+        # We know the minimum length is 11 so some length processing can
+        # be skipped
+        end = len(value) - 1
+        year = value[0:4]
+        month = value[4:6]
+        day = value[6:8]
+        hour = value[8:10]
+        minutes = '00'
+        seconds = '00'
+
+        if len(value) >= 12:
+            minutes = value[10:12]
+            last = 12
+            if len(value) >= 14:
+                last = 14
+                seconds = value[12:14]
+            if last > end: # not enough for a differential
+                if value[last-1] == 'Z':
+                    raise ValueError(_('Malformed seconds'))
+                else:
+                    raise ValueError(_('Missing Z'))
+            fraction = ''
+            if value[last] in ('.,'): # fraction
+                 last += 1
+                 while (last < len(value) and value[last].isdigit()):
+                     fraction += value[last]
+                     last += 1
+
+            # At this point we should have either a 'Z' or a differential
+            if end == last:
+                if value[last] != 'Z':
+                    raise ValueError(_('Missing Z or differential'))
+            else:
+                if last > end or value[last] not in ('+-'): # differential
+                    raise ValueError(_('Missing operator for differential or malformed time string'))
+                last += 1
+                if last + 3 > end:
+                    raise ValueError(_('Missing Z or malformed differential'))
+                diffhour = value[last:last+2]
+                if int(diffhour[0]) not in (0,1,2) or \
+                  (diffhour[0] == '2' and int(diffhour[1]) not in xrange(4)):
+                    raise ValueError(_('Malformed hour in differential'))
+                diffmin = value[last+2:last+4]
+                if int(diffmin[0]) not in xrange(6):
+                    raise ValueError(_('Malformed minute in differential'))
+                if last + 3 != end:
+                    raise ValueError(_('Trailing characters %(chars)s' % dict(chars=value[last+4:end+1])))
+
+        # Year can be anything, no validation
+
+        if (int(month[0]) not in (0,1)) or \
+          (month == '00') or \
+          (month[0] == '1' and int(month[1]) not in (0,1,2)):
+            raise ValueError(_('Invalid month'))
+
+        if (int(day[0]) not in (0,1,2,3)) or \
+          (day == '00') or \
+          (day[0] == '3' and int(day[1]) not in (0,1)):
+            raise ValueError(_('Invalid day'))
+
+        if (int(hour[0]) not in (0,1,2)) or \
+          (hour[0] == '2' and int(hour[1]) not in xrange(4)):
+            raise ValueError(_('Invalid hour'))
+
+        if (int(minutes[0]) not in xrange(6)):
+            raise ValueError(_('Invalid minutes'))
+
+        return None
+
+    def _rule_required(self, _, value):
+        """
+        Use this rule to cheat and apply additional validation to the
+        value.
+        """
+        # all_rules is constsructed so that the class rules are executed
+        # first and we don't want this because we want to check minlength
+        # and pattern first, so run those first.
+        for rule in self.all_rules:
+            if rule.im_func.func_name == '_rule_required': continue
+            error = rule(ugettext, value)
+            if error is not None:
+                raise ValidationError(
+                    name=self.name,
+                    value=value,
+                    error=error,
+                    rule=rule,
+                )
+        try:
+            self._check_date(value)
+        except ValueError, e:
+            name = self.cli_name
+            if not name:
+                name = self.name
+            raise ValidationError(name=name, error=e.args[0])
+
+        return None
 
 def create_param(spec):
     """
diff --git a/tests/test_ipalib/test_parameters.py b/tests/test_ipalib/test_parameters.py
index 5cb7abf..d0ed46b 100644
--- a/tests/test_ipalib/test_parameters.py
+++ b/tests/test_ipalib/test_parameters.py
@@ -1413,6 +1413,55 @@ class test_AccessTime(ClassChecker):
             ):
             e = raises(ValidationError, o._rule_required, None, value)
 
+class test_GeneralizedTime(ClassChecker):
+    """
+    Test the `ipalib.parameters.GeneralizedTime` class.
+    """
+    _cls = parameters.GeneralizedTime
+
+    def test_init(self):
+        """
+        Test the `ipalib.parameters.GeneralizedTime.__init__` method.
+        """
+        # Test with no kwargs:
+        o = self.cls('my_time')
+        assert o.type is unicode
+        assert isinstance(o, parameters.GeneralizedTime)
+        assert o.multivalue is False
+        translation = u'length=%(length)r'
+        dummy = dummy_ugettext(translation)
+        assert dummy.translation is translation
+        rule = o._rule_required
+
+        # Check some good values
+        for value in (u'201012161032Z',
+                      u'201112160000Z',
+                      u'2011121610Z',
+                      u'2012012323Z',
+                      u'9999121610Z',
+                      u'20111223182455.888Z',
+                      u'20111223182455.888+0500',
+                      u'20111223182455.1234567-0500',
+        ):
+            assert rule(dummy, value) is None
+            assert dummy.called() is False
+
+        # Check some bad values
+        for value in (u'201013161032Z', # Invalid month (13)
+                      u'201000161032Z', # Invalid month (00)
+                      u'2011123610Z',   # Invalid day (36)
+                      u'2011120010Z',   # Invalid day (00)
+                      u'201112163019Z', # Invalid hour (30)
+                      u'201112162419Z', # Invalid hour (24)
+                      u'201112160099Z,' # Invalid minutes (99)
+                      u'201112160060Z', # Invalid minutes (60)
+                      u'20111223182455.888',      # Missing Z
+                      u'20111223182455.888+3900', # Bad hour in diff
+                      u'20111223182455.888+0599', # Bad minutes in diff
+                      u'20111223182455.1234567+1900Z',# trailing Z
+        ):
+            e = raises(ValidationError, o._rule_required, None, value)
+
 def test_create_param():
     """
     Test the `ipalib.parameters.create_param` function.
-- 
1.7.6

>From 4097c44a3f6aad38a72cc7f6486dcf24e8e94a74 Mon Sep 17 00:00:00 2001
From: Rob Crittenden <rcrit...@redhat.com>
Date: Tue, 13 Dec 2011 22:31:32 -0500
Subject: [PATCH 2/2] Add support for sudoOrder, sudoNotBefore and
 sudoNotAfter attributes.

Update ipaSudoRule objectClass on upgrades.
Ensure uniqueness of sudoOrder in rules.
Use GeneralizedTime parameter type for sudoNotBefore and sudoNotAfter

https://fedorahosted.org/freeipa/ticket/1314
---
 API.txt                                   |   15 ++++++-
 VERSION                                   |    2 +-
 install/updates/10-sudo.update            |    3 +
 ipalib/plugins/sudorule.py                |   60 ++++++++++++++++++++++++++++-
 tests/test_xmlrpc/test_sudorule_plugin.py |   22 ++++++++++-
 5 files changed, 96 insertions(+), 6 deletions(-)

diff --git a/API.txt b/API.txt
index aba3d8a..d2fe259 100644
--- a/API.txt
+++ b/API.txt
@@ -2664,7 +2664,7 @@ output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
 output: Output('value', <type 'unicode'>, None)
 command: sudorule_add
-args: 1,14,3
+args: 1,17,3
 arg: Str('cn', attribute=True, cli_name='sudorule_name', multivalue=False, primary_key=True, required=True)
 option: Str('description', attribute=True, cli_name='desc', multivalue=False, required=False)
 option: StrEnum('usercategory', attribute=True, cli_name='usercat', multivalue=False, required=False, values=(u'all',))
@@ -2672,6 +2672,9 @@ option: StrEnum('hostcategory', attribute=True, cli_name='hostcat', multivalue=F
 option: StrEnum('cmdcategory', attribute=True, cli_name='cmdcat', multivalue=False, required=False, values=(u'all',))
 option: StrEnum('ipasudorunasusercategory', attribute=True, cli_name='runasusercat', multivalue=False, required=False, values=(u'all',))
 option: StrEnum('ipasudorunasgroupcategory', attribute=True, cli_name='runasgroupcat', multivalue=False, required=False, values=(u'all',))
+option: Int('sudoorder', attribute=True, cli_name='order', default=0, multivalue=False, required=False)
+option: GeneralizedTime('sudonotbefore', attribute=True, cli_name='notbefore', minlength=11, multivalue=False, pattern=u'^[0-9]+[.,]{0,1}[0-9]+[-+]{0,1}[0-9]+Z{0,1}$', pattern_errmsg=u'Must be of the form YYYYMMDDHH[MM]Z or YYMMDDHHSS[+-]HHMM', required=False)
+option: GeneralizedTime('sudonotafter', attribute=True, cli_name='notafter', minlength=11, multivalue=False, pattern=u'^[0-9]+[.,]{0,1}[0-9]+[-+]{0,1}[0-9]+Z{0,1}$', pattern_errmsg=u'Must be of the form YYYYMMDDHH[MM]Z or YYMMDDHHSS[+-]HHMM', required=False)
 option: Str('externaluser', attribute=True, cli_name='externaluser', multivalue=False, required=False)
 option: Str('ipasudorunasextuser', attribute=True, cli_name='runasexternaluser', multivalue=False, required=False)
 option: Str('ipasudorunasextgroup', attribute=True, cli_name='runasexternalgroup', multivalue=False, required=False)
@@ -2769,7 +2772,7 @@ args: 1,0,1
 arg: Str('cn', attribute=True, cli_name='sudorule_name', multivalue=False, primary_key=True, query=True, required=True)
 output: Output('result', None, None)
 command: sudorule_find
-args: 1,16,4
+args: 1,19,4
 arg: Str('criteria?', noextrawhitespace=False)
 option: Str('cn', attribute=True, autofill=False, cli_name='sudorule_name', multivalue=False, primary_key=True, query=True, required=False)
 option: Str('description', attribute=True, autofill=False, cli_name='desc', multivalue=False, query=True, required=False)
@@ -2778,6 +2781,9 @@ option: StrEnum('hostcategory', attribute=True, autofill=False, cli_name='hostca
 option: StrEnum('cmdcategory', attribute=True, autofill=False, cli_name='cmdcat', multivalue=False, query=True, required=False, values=(u'all',))
 option: StrEnum('ipasudorunasusercategory', attribute=True, autofill=False, cli_name='runasusercat', multivalue=False, query=True, required=False, values=(u'all',))
 option: StrEnum('ipasudorunasgroupcategory', attribute=True, autofill=False, cli_name='runasgroupcat', multivalue=False, query=True, required=False, values=(u'all',))
+option: Int('sudoorder', attribute=True, autofill=False, cli_name='order', default=0, multivalue=False, query=True, required=False)
+option: GeneralizedTime('sudonotbefore', attribute=True, autofill=False, cli_name='notbefore', minlength=11, multivalue=False, pattern=u'^[0-9]+[.,]{0,1}[0-9]+[-+]{0,1}[0-9]+Z{0,1}$', pattern_errmsg=u'Must be of the form YYYYMMDDHH[MM]Z or YYMMDDHHSS[+-]HHMM', query=True, required=False)
+option: GeneralizedTime('sudonotafter', attribute=True, autofill=False, cli_name='notafter', minlength=11, multivalue=False, pattern=u'^[0-9]+[.,]{0,1}[0-9]+[-+]{0,1}[0-9]+Z{0,1}$', pattern_errmsg=u'Must be of the form YYYYMMDDHH[MM]Z or YYMMDDHHSS[+-]HHMM', query=True, required=False)
 option: Str('externaluser', attribute=True, autofill=False, cli_name='externaluser', multivalue=False, query=True, required=False)
 option: Str('ipasudorunasextuser', attribute=True, autofill=False, cli_name='runasexternaluser', multivalue=False, query=True, required=False)
 option: Str('ipasudorunasextgroup', attribute=True, autofill=False, cli_name='runasexternalgroup', multivalue=False, query=True, required=False)
@@ -2792,7 +2798,7 @@ output: ListOfEntries('result', (<type 'list'>, <type 'tuple'>), Gettext('A list
 output: Output('count', <type 'int'>, None)
 output: Output('truncated', <type 'bool'>, None)
 command: sudorule_mod
-args: 1,16,3
+args: 1,19,3
 arg: Str('cn', attribute=True, cli_name='sudorule_name', multivalue=False, primary_key=True, query=True, required=True)
 option: Str('description', attribute=True, autofill=False, cli_name='desc', multivalue=False, required=False)
 option: StrEnum('usercategory', attribute=True, autofill=False, cli_name='usercat', multivalue=False, required=False, values=(u'all',))
@@ -2800,6 +2806,9 @@ option: StrEnum('hostcategory', attribute=True, autofill=False, cli_name='hostca
 option: StrEnum('cmdcategory', attribute=True, autofill=False, cli_name='cmdcat', multivalue=False, required=False, values=(u'all',))
 option: StrEnum('ipasudorunasusercategory', attribute=True, autofill=False, cli_name='runasusercat', multivalue=False, required=False, values=(u'all',))
 option: StrEnum('ipasudorunasgroupcategory', attribute=True, autofill=False, cli_name='runasgroupcat', multivalue=False, required=False, values=(u'all',))
+option: Int('sudoorder', attribute=True, autofill=False, cli_name='order', default=0, multivalue=False, required=False)
+option: GeneralizedTime('sudonotbefore', attribute=True, autofill=False, cli_name='notbefore', minlength=11, multivalue=False, pattern=u'^[0-9]+[.,]{0,1}[0-9]+[-+]{0,1}[0-9]+Z{0,1}$', pattern_errmsg=u'Must be of the form YYYYMMDDHH[MM]Z or YYMMDDHHSS[+-]HHMM', required=False)
+option: GeneralizedTime('sudonotafter', attribute=True, autofill=False, cli_name='notafter', minlength=11, multivalue=False, pattern=u'^[0-9]+[.,]{0,1}[0-9]+[-+]{0,1}[0-9]+Z{0,1}$', pattern_errmsg=u'Must be of the form YYYYMMDDHH[MM]Z or YYMMDDHHSS[+-]HHMM', required=False)
 option: Str('externaluser', attribute=True, autofill=False, cli_name='externaluser', multivalue=False, required=False)
 option: Str('ipasudorunasextuser', attribute=True, autofill=False, cli_name='runasexternaluser', multivalue=False, required=False)
 option: Str('ipasudorunasextgroup', attribute=True, autofill=False, cli_name='runasexternalgroup', multivalue=False, required=False)
diff --git a/VERSION b/VERSION
index 0816437..b6ef09f 100644
--- a/VERSION
+++ b/VERSION
@@ -79,4 +79,4 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=18
+IPA_API_VERSION_MINOR=19
diff --git a/install/updates/10-sudo.update b/install/updates/10-sudo.update
index 88bdc3c..1c9fa6b 100644
--- a/install/updates/10-sudo.update
+++ b/install/updates/10-sudo.update
@@ -38,3 +38,6 @@ add:attributeTypes: ( 1.3.6.1.4.1.15953.9.1.10
      SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
      X-ORIGIN 'SUDO' )
 replace:objectClasses:( 1.3.6.1.4.1.15953.9.2.1 NAME 'sudoRole' DESC 'Sudoer Entries' STRUCTURAL MUST cn MAY ( sudoUser $$ sudoHost $$ sudoCommand $$ sudoRunAs $$ sudoOption $$ description ) X-ORIGIN 'SUDO' )::( 1.3.6.1.4.1.15953.9.2.1 NAME 'sudoRole' SUP top STRUCTURAL DESC 'Sudoer Entries' MUST ( cn ) MAY ( sudoUser $$ sudoHost $$ sudoCommand $$ sudoRunAs $$ sudoRunAsUser $$ sudoRunAsGroup $$ sudoOption $$ sudoNotBefore $$ sudoNotAfter $$ sudoOrder $$ description ) X-ORIGIN 'SUDO')
+
+replace:objectClasses: ( 2.16.840.1.113730.3.8.8.1 NAME 'ipaSudoRule' SUP ipaAssociation STRUCTURAL MAY ( externalUser $$ externalHost $$ hostMask $$ memberAllowCmd $$ memberDenyCmd $$ cmdCategory $$ ipaSudoOpt $$ ipaSudoRunAs $$ ipaSudoRunAsExtUser $$ ipaSudoRunAsUserCategory $$ ipaSudoRunAsGroup $$ ipaSudoRunAsExtGroup $$ ipaSudoRunAsGroupCategory ) X-ORIGIN 'IPA v2' )::(2.16.840.1.113730.3.8.8.1 NAME 'ipaSudoRule' SUP ipaAssociation STRUCTURAL MAY ( externalUser $$ externalHost $$ hostMask $$ memberAllowCmd $$ memberDenyCmd $$ cmdCategory $$ ipaSudoOpt $$ ipaSudoRunAs $$ ipaSudoRunAsExtUser $$ ipaSudoRunAsUserCategory $$ ipaSudoRunAsGroup $$ ipaSudoRunAsExtGroup $$ ipaSudoRunAsGroupCategory $$ sudoNotBefore $$ sudoNotAfter $$ sudoOrder) X-ORIGIN 'IPA v2' )
+
diff --git a/ipalib/plugins/sudorule.py b/ipalib/plugins/sudorule.py
index 3c998d9..8622cbe 100644
--- a/ipalib/plugins/sudorule.py
+++ b/ipalib/plugins/sudorule.py
@@ -18,7 +18,7 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 from ipalib import api, errors
-from ipalib import Str, StrEnum
+from ipalib import Str, StrEnum, Int, GeneralizedTime
 from ipalib.plugins.baseldap import *
 from ipalib.plugins.hbacrule import is_all
 from ipalib import _, ngettext
@@ -40,6 +40,19 @@ FreeIPA provides a means to configure the various aspects of Sudo:
    RunAsGroup: The group(s) whose gid rights Sudo will be invoked with.
    Options: The various Sudoers Options that can modify Sudo's behavior.
 
+An order can be added to a sudorule to control the order in which they
+are evaluated (if the client supports it). This order is an integer and
+must be unique.
+
+A date that defines when the rule becomes effective or is no longer effective
+can be defined using the --notbefore and --notafter options. The format
+of the date is Generalized Time (see RFC 4517). The basic format is
+YYMMDDHHSSZ where Z defines GMT. You can also set a time as an offset
+YYMMDDHHSS[+-]HHMM. Some examples are:
+
+ 201112161032Z
+ 201112160532-0500
+
 FreeIPA provides a designated binddn to use with Sudo located at:
 uid=sudo,cn=sysaccounts,cn=etc,dc=example,dc=com
 
@@ -80,6 +93,7 @@ class sudorule(LDAPObject):
         'memberallowcmd', 'memberdenycmd', 'ipasudoopt',
         'ipasudorunas', 'ipasudorunasgroup',
         'ipasudorunasusercategory', 'ipasudorunasgroupcategory',
+        'sudoorder', 'sudonotbefore', 'sudonotafter',
     ]
     uuid_attribute = 'ipauniqueid'
     rdn_attribute = 'ipauniqueid'
@@ -139,6 +153,22 @@ class sudorule(LDAPObject):
             doc=_('RunAs Group category the rule applies to'),
             values=(u'all', ),
         ),
+        Int('sudoorder?',
+            cli_name='order',
+            label=_('Sudo order'),
+            doc=_('integer to order the Sudo rules'),
+            default=0,
+        ),
+        GeneralizedTime('sudonotbefore?',
+            cli_name='notbefore',
+            label=_('Not before'),
+            doc=_('Start of time interval for which the entry is valid, YYMMDDHH[MM]Z)'),
+        ),
+        GeneralizedTime('sudonotafter?',
+            cli_name='notafter',
+            label=_('Not after'),
+            doc=_('End of time interval for which the entry is valid (YYMMDDHH[MM]Z)'),
+        ),
         Str('memberuser_user?',
             label=_('Users'),
             flags=['no_create', 'no_update', 'no_search'],
@@ -207,6 +237,26 @@ class sudorule(LDAPObject):
         ),
     )
 
+    order_not_unique_msg = _(
+        'order must be a unique value (%(order)d already used by %(rule)s)'
+    )
+
+    def check_order_uniqueness(self, *keys, **options):
+        if 'sudoorder' in options:
+            entries = self.methods.find(
+                sudoorder=options['sudoorder']
+            )['result']
+            if len(entries) > 0:
+                rule_name = entries[0]['cn'][0]
+                raise errors.ValidationError(
+                    name='order',
+                    error=self.order_not_unique_msg % {
+                        'order': options['sudoorder'],
+                        'rule': rule_name,
+                    }
+                )
+
+
 api.register(sudorule)
 
 
@@ -214,6 +264,7 @@ class sudorule_add(LDAPCreate):
     __doc__ = _('Create new Sudo Rule.')
 
     def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
+        self.obj.check_order_uniqueness(*keys, **options)
         # Sudo Rules are enabled by default
         entry_attrs['ipaenabledflag'] = 'TRUE'
         return dn
@@ -237,6 +288,13 @@ class sudorule_mod(LDAPUpdate):
     msg_summary = _('Modified Sudo Rule "%(value)s"')
 
     def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
+        new_order = options.get('sudoorder')
+        if new_order:
+            old_entry = self.api.Command.sudorule_show(keys[-1])['result']
+            if 'sudoorder' in old_entry:
+                old_order = int(old_entry['sudoorder'][0])
+                if old_order != new_order:
+                    self.obj.check_order_uniqueness(*keys, **options)
         try:
             (_dn, _entry_attrs) = ldap.get_entry(dn, self.obj.default_attributes)
         except errors.NotFound:
diff --git a/tests/test_xmlrpc/test_sudorule_plugin.py b/tests/test_xmlrpc/test_sudorule_plugin.py
index de1bba5..706bc77 100644
--- a/tests/test_xmlrpc/test_sudorule_plugin.py
+++ b/tests/test_xmlrpc/test_sudorule_plugin.py
@@ -30,6 +30,7 @@ class test_sudorule(XMLRPC_test):
     Test the `sudorule` plugin.
     """
     rule_name = u'testing_sudorule1'
+    rule_name2 = u'testing_sudorule2'
     rule_command = u'/usr/bin/testsudocmd1'
     rule_desc = u'description'
     rule_desc_mod = u'description modified'
@@ -625,7 +626,25 @@ class test_sudorule(XMLRPC_test):
         api.Command['sudocmdgroup_del'](self.test_sudoallowcmdgroup)
         api.Command['sudocmdgroup_del'](self.test_sudodenycmdgroup)
 
-    def test_l_sudorule_del(self):
+    def test_l_sudorule_order(self):
+        """
+        Test that order uniqueness is maintained
+        """
+        api.Command['sudorule_mod'](self.rule_name, sudoorder=1)
+        try:
+            api.Command['sudorule_add'](self.rule_name2, sudoorder=1)
+        except errors.ValidationError:
+            pass
+        api.Command['sudorule_add'](self.rule_name2, sudoorder=2)
+        try:
+            api.Command['sudorule_mod'](self.rule_name2, sudoorder=1)
+        except errors.ValidationError:
+            pass
+
+    # Note that time format for notbefore/notafter is handled in
+    # the GeneralizedTime parameter unit test.
+
+    def test_m_sudorule_del(self):
         """
         Test deleting a Sudo rule using `xmlrpc.sudorule_del`.
         """
@@ -637,3 +656,4 @@ class test_sudorule(XMLRPC_test):
             pass
         else:
             assert False
+        api.Command['sudorule_del'](self.rule_name2)
-- 
1.7.6

_______________________________________________
Freeipa-devel mailing list
Freeipa-devel@redhat.com
https://www.redhat.com/mailman/listinfo/freeipa-devel

Reply via email to