Jan Cholasta wrote:
Dne 13.1.2012 17:39, Rob Crittenden napsal(a):
Jan Cholasta wrote:
Dne 14.12.2011 16:21, Rob Crittenden napsal(a):
Jan Cholasta wrote:
Dne 14.12.2011 15:23, Rob Crittenden napsal(a):
Jan Cholasta wrote:
Dne 14.12.2011 05:20, Rob Crittenden napsal(a):
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.

You should parse the date/time part of the value with
time.strptime(timestr, '%Y%m%d%H%M%S') instead of doing it manually,
that way you'll get most of the validation for free.

Yes but it gives a crappy error message, just saying that some
data is
left over not what is wrong.

IMHO having a separate error message for every field in the time
string
(like you do in the patch) is an overkill, simple "invalid time"
and/or
"unknown time format" should suffice (we don't have errors like
"invalid
3rd octet" for IP adresses either).

Well, the work is done, hard to go back on a better error message.


Also, it would be nice to be able to enter the value in more
user-friendly format (e.g. "2011-12-14 13:01:25 +0100") and
normalize
that to LDAP generalized time.

When dealing with time there are so many ways to input and display
the
same values this becomes difficult.

I'd expect that the times for these two attributes will be relatively
simple and I somehow doubt users are going to want seconds, leap
seconds
or fractions, but we'll need to consider how to do it for future
consistency (otherwise we could have a case where time is entered in
one
format for some attributes and another for others).

If we input in a nice way we need to output in the same way.

We could make the preferred input/output time format
user-configurable,
defaulting to current locale time format. This format would be used
for
output. For input, we could go over a list of formats (first the
user-configured format, then current locale format, then a handful of
"standard" formats like YYYY-MM-DD HH:MM:SS) and use the first format
that can be successfully used to parse the time string.

See how far you get into the rabbit hole with even this simple format?

I don't mind, as long as it is the right thing to do (IMHO) :)

Anyway, I think this could be done on the client side, so we might use
your patch without changes. However, I would prefer if the parameter
class was more generic, so we could use it (hypothetically) to store
time in some other way than LDAP generalized time attribute (at least
name it DateTime please).


Ok, I'm fine with that.

Thanks.



The LDAP GeneralizedTime needs to be either in GMT or include a
differential. This gets us into the territory where the client could be
in a different timezone than the server which leads us to why we
dropped
AccessTime in the first place.

Speaking of time zones, the differential alone is not a sufficient time
zone description, as it doesn't account for DST. Is there a way to store
time in LDAP with full time zone name (just in case it's needed sometime
in future)?

There is no way to store DST in LDAP (probably for good reason). Oddly
enough the older LDAP v3 RFC (2252) strongly recommends using only GMT
but the RFC that obsoletes it (4517) does not include this.

Thanks for the info.



So I'd like the user to supply the
timezone themselves so I don't have to guess (wrongly) and let them
worry about differing timezones.

We don't have to guess, IIRC there is a way to get the local timezone
differential in both Python and JavaScript, so the client could supply
it automatically if necessary.

I was thinking more about non-IPA clients (like sudo and notBefore).

I think this can still be done at least in CLI, but it could be done in
a separate patch.


Updated patches attached.

rob

Patch 919 doesn't cleanly apply on current master (neither does 916 BTW).

Honza


Rebased patch (and 916 too, separately).

rob
>From f9fa7231ea0c03e98687116d73eb28255fd80f46 Mon Sep 17 00:00:00 2001
From: Rob Crittenden <rcrit...@redhat.com>
Date: Fri, 13 Jan 2012 11:06:42 -0500
Subject: [PATCH] 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..040d581 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, DateTime
 from errors import SkipPluginModule
 from text import _, ngettext, GettextFactory, NGettextFactory
 
diff --git a/ipalib/parameters.py b/ipalib/parameters.py
index be21086..fce0800 100644
--- a/ipalib/parameters.py
+++ b/ipalib/parameters.py
@@ -1776,6 +1776,139 @@ class AccessTime(Str):
             )
         return None
 
+class DateTime(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(DateTime, 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..c2ee192 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_DateTime(ClassChecker):
+    """
+    Test the `ipalib.parameters.DateTime` class.
+    """
+    _cls = parameters.DateTime
+
+    def test_init(self):
+        """
+        Test the `ipalib.parameters.DateTime.__init__` method.
+        """
+        # Test with no kwargs:
+        o = self.cls('my_time')
+        assert o.type is unicode
+        assert isinstance(o, parameters.DateTime)
+        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 f7b79aa094b45fa8145f769af0c11fc33c37b65b Mon Sep 17 00:00:00 2001
From: Rob Crittenden <rcrit...@redhat.com>
Date: Tue, 13 Dec 2011 22:31:32 -0500
Subject: [PATCH] Add support for sudoOrder, sudoNotBefore and sudoNotAfter
 attributes.

Update ipaSudoRule objectClass on upgrades to add new attributes.
Ensure uniqueness of sudoOrder in rules.
Use DateTime parameter type for sudoNotBefore and sudoNotAfter

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

diff --git a/API.txt b/API.txt
index 9048231bb1f349047f9790e5335778d4c3d637b0..46cf9fb21e50e05c1b56ca8fb8d563ea4ac8e78a 100644
--- a/API.txt
+++ b/API.txt
@@ -2848,7 +2848,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',))
@@ -2856,6 +2856,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: DateTime('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: DateTime('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)
@@ -2953,7 +2956,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)
@@ -2962,6 +2965,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: DateTime('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: DateTime('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)
@@ -2976,7 +2982,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',))
@@ -2984,6 +2990,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: DateTime('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: DateTime('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 80f8a2f07502ec8064c7dfbd6c5a8103e74b358d..1f973908c75f0986b4e2858e1c07e5facac94c33 100644
--- a/VERSION
+++ b/VERSION
@@ -79,4 +79,4 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=21
+IPA_API_VERSION_MINOR=22
diff --git a/install/updates/10-sudo.update b/install/updates/10-sudo.update
index 88bdc3ce111db32866461a59da5830f9c2bb19ca..a12da00434cee1b49147586f54139ecc12d99e64 100644
--- a/install/updates/10-sudo.update
+++ b/install/updates/10-sudo.update
@@ -38,3 +38,5 @@ 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 df395ead2e0380c3f9e15533f8e18904d9f697af..2474864180446a9bf2454d924924ad9edef456a1 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, DateTime
 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,
+        ),
+        DateTime('sudonotbefore?',
+            cli_name='notbefore',
+            label=_('Not before'),
+            doc=_('Start of time interval for which the entry is valid, YYMMDDHH[MM]Z)'),
+        ),
+        DateTime('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
@@ -236,6 +287,14 @@ 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 07d23c3d207151f063f2884d453bb019107aac2a..706bc77eab38deb6ee4d631de22a656b4de2d00a 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,8 +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_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_l_sudorule_del(self):
+    def test_m_sudorule_del(self):
         """
         Test deleting a Sudo rule using `xmlrpc.sudorule_del`.
         """
@@ -638,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