Hi,

The moment's here, I'd like to share my code with you now. Let me comment on some additions from my last post here in August.

The methods for testing HBAC rules in hbactest module were modified so that a time zone can now also be picked in case there are some rules with the "host" time zone in the rule time policy. I also added few tests that test setting accessTime values.

The most important update of the previous month is the addition of negative values to the time rules language. Most of the keywords (all, except for timeofday and year) now accept negative values and negative value ranges. This should be useful for cases when the user should only be allowed access e.g. in the last 7 days of a month, last few weeks of a year etc. Also, it is a similar behavior to what iCalendar has.

The addition of negative values also made me re-think the ways the week of a year should be calculated. There are no 0th weeks of year anymore, a week of year can hold values ranging from 1 to 53 where the 1st week of a year may appear even on a date of the previous year (if 1st January is Tue-Thu) or the 52nd or 53rd week may appear on a date of the following year (when 31st December is Thu-Sat). If my explanation seems rather rough, please see https://docs.oracle.com/javase/8/docs/api/java/time/temporal/WeekFields.html.

The latter caused some changes to be made in my SSSD code. These changes took the most of my time last month alongside with generally polishing the code and adding comments where I thought necessary. I will push my SSSD code to the sssd-devel mailing list as a follow-up to this mail.

Another thing - I updated the design page on the FreeIPA wiki, so please check it out, too (http://www.freeipa.org/page/V4/Time-Based_Account_Policies).

Last thing I would like to mention - there is now a copr repo with both sssd and freeipa with time-based policies (https://copr.fedoraproject.org/coprs/stlaz/freeipa-sssd-timerules/). This was Martin K.'s idea and I think it's pretty dandy :) As the patches I am posting only contain CLI for HBAC time policies, you might be pleased that the repo includes at least basic WebUI for this purpose (although the WebUI is for some reason not updating the page on rule addition properly, I will be hopefully looking into that shortly). You will still need mkosek/freeipa-master copr repo for some dependencies. Should it not work properly for you, please, send me an email, it's my first time taking care of a copr repo.

That's it from me for now, thank you for your patience with my emails,
Standa

From 552d23f8c900cb3ef5437f020874f968bfce35e9 Mon Sep 17 00:00:00 2001
From: Stanislav Laznicka <slazn...@redhat.com>
Date: Tue, 7 Jul 2015 09:44:23 +0200
Subject: [PATCH 2/8] Added time-based policies types to LDAP schema.

https://fedorahosted.org/freeipa/ticket/547
---
 install/share/60basev2.ldif | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/install/share/60basev2.ldif b/install/share/60basev2.ldif
index 00712ddda2c548b7f7924a012f3f68499f2f01da..c3251a4331005ade1333f9e64b57a62a89706ce9 100644
--- a/install/share/60basev2.ldif
+++ b/install/share/60basev2.ldif
@@ -37,7 +37,9 @@ attributeTypes: (2.16.840.1.113730.3.8.3.11 NAME 'externalHost' DESC 'Multivalue
 attributeTypes: (2.16.840.1.113730.3.8.3.12 NAME 'sourceHostCategory' DESC 'Additional classification for hosts' EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'IPA v2' )
 attributeTypes: (2.16.840.1.113730.3.8.3.13 NAME 'accessRuleType' DESC 'The flag to represent if it is allow or deny rule.' EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'IPA v2' )
 attributeTypes: (2.16.840.1.113730.3.8.3.14 NAME 'accessTime' DESC 'Access time' EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'IPA v2' )
-objectClasses: (2.16.840.1.113730.3.8.4.7 NAME 'ipaHBACRule' SUP ipaAssociation STRUCTURAL MUST accessRuleType MAY ( sourceHost $ sourceHostCategory $ serviceCategory $ memberService $ externalHost $ accessTime ) X-ORIGIN 'IPA v2' )
+attributeTypes: (2.16.840.1.113730.3.8.11.72 NAME 'accessTimeExclude' DESC 'Access time - exclude these values' EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'IPA v4' )
+attributeTypes: (2.16.840.1.113730.3.8.11.73 NAME 'ipaTimeZone' DESC 'Olson database timezone name' EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'IPA v4' )
+objectClasses: (2.16.840.1.113730.3.8.4.7 NAME 'ipaHBACRule' SUP ipaAssociation STRUCTURAL MUST accessRuleType MAY ( sourceHost $ sourceHostCategory $ serviceCategory $ memberService $ externalHost $ ipaTimeZone $ accessTime $ accessTimeExclude ) X-ORIGIN 'IPA v2' )
 attributeTypes: (2.16.840.1.113730.3.8.3.15 NAME 'nisDomainName' DESC 'NIS domain name.' EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'IPA v2' )
 objectClasses: (2.16.840.1.113730.3.8.4.8 NAME 'ipaNISNetgroup' DESC 'IPA version of NIS netgroup' SUP ipaAssociation STRUCTURAL MAY ( externalHost $ nisDomainName $ member $ memberOf ) X-ORIGIN 'IPA v2' )
 attributeTypes: (1.3.6.1.1.1.1.31 NAME 'automountMapName' DESC 'automount Map Name' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE X-ORIGIN 'RFC 2307bis' )
-- 
2.4.3

From 54883bca2cc73162994d8e8ba4140ece5185db49 Mon Sep 17 00:00:00 2001
From: Stanislav Laznicka <slazn...@redhat.com>
Date: Tue, 7 Jul 2015 09:47:39 +0200
Subject: [PATCH 3/8] Added methods for setting time-based policies.

Methods for time-based policies in HBAC rules.

https://fedorahosted.org/freeipa/ticket/547
---
 ACI.txt                    |   4 +-
 API.txt                    |  50 ++++++-
 freeipa.spec.in            |   2 +
 ipalib/__init__.py         |   4 +-
 ipalib/parameters.py       | 180 -------------------------
 ipalib/plugins/hbacrule.py | 330 +++++++++++++++++++++++++++++++--------------
 6 files changed, 286 insertions(+), 284 deletions(-)

diff --git a/ACI.txt b/ACI.txt
index 40fa822217eaee8d0966491b10cdf7e0739a87ce..3372197d5b9a3ab2360df110ca4910a075b28a45 100644
--- a/ACI.txt
+++ b/ACI.txt
@@ -91,9 +91,9 @@ aci: (targetfilter = "(objectclass=ipahbacrule)")(version 3.0;acl "permission:Sy
 dn: cn=hbac,dc=ipa,dc=example
 aci: (targetattr = "externalhost || memberhost || memberservice || memberuser")(targetfilter = "(objectclass=ipahbacrule)")(version 3.0;acl "permission:System: Manage HBAC Rule Membership";allow (write) groupdn = "ldap:///cn=System: Manage HBAC Rule Membership,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: cn=hbac,dc=ipa,dc=example
-aci: (targetattr = "accessruletype || accesstime || cn || description || hostcategory || ipaenabledflag || servicecategory || sourcehost || sourcehostcategory || usercategory")(targetfilter = "(objectclass=ipahbacrule)")(version 3.0;acl "permission:System: Modify HBAC Rule";allow (write) groupdn = "ldap:///cn=System: Modify HBAC Rule,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+aci: (targetattr = "accessruletype || accesstime || accesstimeexclude || cn || description || hostcategory || ipaenabledflag || ipatimezone || servicecategory || sourcehost || sourcehostcategory || usercategory")(targetfilter = "(objectclass=ipahbacrule)")(version 3.0;acl "permission:System: Modify HBAC Rule";allow (write) groupdn = "ldap:///cn=System: Modify HBAC Rule,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: cn=hbac,dc=ipa,dc=example
-aci: (targetattr = "accessruletype || accesstime || cn || createtimestamp || description || entryusn || externalhost || hostcategory || ipaenabledflag || ipauniqueid || member || memberhost || memberservice || memberuser || modifytimestamp || objectclass || servicecategory || sourcehost || sourcehostcategory || usercategory")(targetfilter = "(objectclass=ipahbacrule)")(version 3.0;acl "permission:System: Read HBAC Rules";allow (compare,read,search) userdn = "ldap:///all";;)
+aci: (targetattr = "accessruletype || accesstime || accesstimeexclude || cn || createtimestamp || description || entryusn || externalhost || hostcategory || ipaenabledflag || ipatimezone || ipauniqueid || member || memberhost || memberservice || memberuser || modifytimestamp || objectclass || servicecategory || sourcehost || sourcehostcategory || usercategory")(targetfilter = "(objectclass=ipahbacrule)")(version 3.0;acl "permission:System: Read HBAC Rules";allow (compare,read,search) userdn = "ldap:///all";;)
 dn: cn=hbacservices,cn=hbac,dc=ipa,dc=example
 aci: (targetfilter = "(objectclass=ipahbacservice)")(version 3.0;acl "permission:System: Add HBAC Services";allow (add) groupdn = "ldap:///cn=System: Add HBAC Services,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: cn=hbacservices,cn=hbac,dc=ipa,dc=example
diff --git a/API.txt b/API.txt
index cf5446114a9ccffad8d87421b4cd75c92ff267ee..815320554c47d5da1ea208bb3d6abd4ddd1c5c43 100644
--- a/API.txt
+++ b/API.txt
@@ -1653,7 +1653,7 @@ output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDA
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: PrimaryKey('value', None, None)
 command: hbacrule_add
-args: 1,16,3
+args: 1,17,3
 arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, required=True)
 option: StrEnum('accessruletype', attribute=True, autofill=True, cli_name='type', default=u'allow', exclude='webui', multivalue=False, required=True, values=(u'allow', u'deny'))
 option: Str('addattr*', cli_name='addattr', exclude='webui')
@@ -1662,6 +1662,7 @@ option: Str('description', attribute=True, cli_name='desc', multivalue=False, re
 option: Str('externalhost', attribute=True, cli_name='externalhost', multivalue=True, required=False)
 option: StrEnum('hostcategory', attribute=True, cli_name='hostcat', multivalue=False, required=False, values=(u'all',))
 option: Bool('ipaenabledflag', attribute=True, cli_name='ipaenabledflag', multivalue=False, required=False)
+option: Str('ipatimezone', attribute=True, cli_name='timezone', multivalue=False, required=False)
 option: Flag('no_members', autofill=True, default=False, exclude='webui')
 option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
 option: StrEnum('servicecategory', attribute=True, cli_name='servicecat', multivalue=False, required=False, values=(u'all',))
@@ -1674,6 +1675,28 @@ option: Str('version?', exclude='webui')
 output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: PrimaryKey('value', None, None)
+command: hbacrule_add_accesstime
+args: 1,5,3
+arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
+option: Str('accesstime', alwaysask=True, attribute=True, cli_name='time', multivalue=True, required=False)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('version?', exclude='webui')
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: PrimaryKey('value', None, None)
+command: hbacrule_add_accesstimeexclude
+args: 1,5,3
+arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
+option: Str('accesstimeexclude', alwaysask=True, attribute=True, cli_name='excltime', multivalue=True, required=False)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('version?', exclude='webui')
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: PrimaryKey('value', None, None)
 command: hbacrule_add_host
 args: 1,6,3
 arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
@@ -1770,7 +1793,7 @@ output: ListOfEntries('result', (<type 'list'>, <type 'tuple'>), Gettext('A list
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: Output('truncated', <type 'bool'>, None)
 command: hbacrule_mod
-args: 1,18,3
+args: 1,19,3
 arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
 option: StrEnum('accessruletype', attribute=True, autofill=False, cli_name='type', default=u'allow', exclude='webui', multivalue=False, required=False, values=(u'allow', u'deny'))
 option: Str('addattr*', cli_name='addattr', exclude='webui')
@@ -1780,6 +1803,7 @@ option: Str('description', attribute=True, autofill=False, cli_name='desc', mult
 option: Str('externalhost', attribute=True, autofill=False, cli_name='externalhost', multivalue=True, required=False)
 option: StrEnum('hostcategory', attribute=True, autofill=False, cli_name='hostcat', multivalue=False, required=False, values=(u'all',))
 option: Bool('ipaenabledflag', attribute=True, autofill=False, cli_name='ipaenabledflag', multivalue=False, required=False)
+option: Str('ipatimezone', attribute=True, autofill=False, cli_name='timezone', multivalue=False, required=False)
 option: Flag('no_members', autofill=True, default=False, exclude='webui')
 option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
 option: Flag('rights', autofill=True, default=False)
@@ -1793,6 +1817,28 @@ option: Str('version?', exclude='webui')
 output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: PrimaryKey('value', None, None)
+command: hbacrule_remove_accesstime
+args: 1,5,3
+arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
+option: Str('accesstime', alwaysask=True, attribute=True, cli_name='time', multivalue=True, required=False)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('version?', exclude='webui')
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: PrimaryKey('value', None, None)
+command: hbacrule_remove_accesstimeexclude
+args: 1,5,3
+arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
+option: Str('accesstimeexclude', alwaysask=True, attribute=True, cli_name='excltime', multivalue=True, required=False)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Flag('no_members', autofill=True, default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('version?', exclude='webui')
+output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: PrimaryKey('value', None, None)
 command: hbacrule_remove_host
 args: 1,6,3
 arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
diff --git a/freeipa.spec.in b/freeipa.spec.in
index 38c76ce7f6f732f8731ff5f98de21f46d93998dd..087781623b74dc11432f28b54769df97155c2fb1 100644
--- a/freeipa.spec.in
+++ b/freeipa.spec.in
@@ -97,6 +97,7 @@ BuildRequires:  python-pytest-multihost >= 0.5
 BuildRequires:  python-pytest-sourceorder
 BuildRequires:  python-kdcproxy >= 0.3
 BuildRequires:  python-six
+BuildRequires:  pytz
 
 %description
 IPA is an integrated solution to provide centrally managed Identity (users,
@@ -322,6 +323,7 @@ Requires: wget
 Requires: dbus-python
 Requires: python-setuptools
 Requires: python-six
+Requires: pytz
 
 Conflicts: %{alt_name}-python
 Obsoletes: %{alt_name}-python < %{version}
diff --git a/ipalib/__init__.py b/ipalib/__init__.py
index e163b292c075e21a8032261a13ca7ea5d6422830..be8db7269d93a62c92b11fa0c1d7ea5623d157af 100644
--- a/ipalib/__init__.py
+++ b/ipalib/__init__.py
@@ -886,8 +886,8 @@ from ipalib.frontend import Command, LocalOrRemote, Updater
 from ipalib.frontend import Object, Method
 from ipalib.crud import Create, Retrieve, Update, Delete, Search
 from ipalib.parameters import DefaultFrom, Bool, Flag, Int, Decimal, Bytes, Str, IA5Str, Password, DNParam, DeprecatedParam
-from ipalib.parameters import (BytesEnum, StrEnum, IntEnum, AccessTime, File,
-                        DateTime, DNSNameParam)
+from ipalib.parameters import (BytesEnum, StrEnum, IntEnum, File,
+                               DateTime, DNSNameParam)
 from ipalib.errors import SkipPluginModule
 from ipalib.text import _, ngettext, GettextFactory, NGettextFactory
 
diff --git a/ipalib/parameters.py b/ipalib/parameters.py
index dab235a2482f6707a813ad891d28d5e3a56e2e96..db6e4b5273ee6b76a471bdfa91d2f8ff45a89276 100644
--- a/ipalib/parameters.py
+++ b/ipalib/parameters.py
@@ -1670,186 +1670,6 @@ class DateTime(Param):
         return super(DateTime, self)._convert_scalar(value, index)
 
 
-class AccessTime(Str):
-    """
-    Access time parameter type.
-
-    Accepts values conforming to generalizedTime as defined in RFC 4517
-    section 3.3.13 without time zone information.
-    """
-    def _check_HHMM(self, t):
-        if len(t) != 4:
-            raise ValueError('HHMM must be exactly 4 characters long')
-        if not t.isnumeric():
-            raise ValueError('HHMM non-numeric')
-        hh = int(t[0:2])
-        if hh < 0 or hh > 23:
-            raise ValueError('HH out of range')
-        mm = int(t[2:4])
-        if mm < 0 or mm > 59:
-            raise ValueError('MM out of range')
-
-    def _check_dotw(self, t):
-        if t.isnumeric():
-            value = int(t)
-            if value < 1 or value > 7:
-                raise ValueError('day of the week out of range')
-        elif t not in ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'):
-            raise ValueError('invalid day of the week')
-
-    def _check_dotm(self, t, month_num=1, year=4):
-        if not t.isnumeric():
-            raise ValueError('day of the month non-numeric')
-        value = int(t)
-        if month_num in (1, 3, 5, 7, 8, 10, 12):
-            if value < 1 or value > 31:
-                raise ValueError('day of the month out of range')
-        elif month_num in (4, 6, 9, 11):
-            if value < 1 or value > 30:
-                raise ValueError('day of the month out of range')
-        elif month_num == 2:
-            if year % 4 == 0 and (year % 100 != 0 or year % 400 == 0):
-                if value < 1 or value > 29:
-                    raise ValueError('day of the month out of range')
-            else:
-                if value < 1 or value > 28:
-                    raise ValueError('day of the month out of range')
-
-    def _check_wotm(self, t):
-        if not t.isnumeric():
-            raise ValueError('week of the month non-numeric')
-        value = int(t)
-        if value < 1 or value > 6:
-            raise ValueError('week of the month out of range')
-
-    def _check_woty(self, t):
-        if not t.isnumeric():
-            raise ValueError('week of the year non-numeric')
-        value = int(t)
-        if value < 1 or value > 52:
-            raise ValueError('week of the year out of range')
-
-    def _check_doty(self, t):
-        if not t.isnumeric():
-            raise ValueError('day of the year non-numeric')
-        value = int(t)
-        if value < 1 or value > 365:
-            raise ValueError('day of the year out of range')
-
-    def _check_month_num(self, t):
-        if not t.isnumeric():
-            raise ValueError('month number non-numeric')
-        value = int(t)
-        if value < 1 or value > 12:
-            raise ValueError('month number out of range')
-
-    def _check_interval(self, t, check_func):
-        intervals = t.split(',')
-        for i in intervals:
-            if not i:
-                raise ValueError('invalid time range')
-            values = i.split('-')
-            if len(values) > 2:
-                raise ValueError('invalid time range')
-            for v in values:
-                check_func(v)
-            if len(values) == 2:
-                if int(values[0]) > int(values[1]):
-                    raise ValueError('invalid time range')
-
-    def _check_W_spec(self, ts, index):
-        if ts[index] != 'day':
-            raise ValueError('invalid week specifier')
-        index += 1
-        self._check_interval(ts[index], self._check_dotw)
-        return index
-
-    def _check_M_spec(self, ts, index):
-        if ts[index] == 'week':
-            self._check_interval(ts[index + 1], self._check_wotm)
-            index = self._check_W_spec(ts, index + 2)
-        elif ts[index] == 'day':
-            index += 1
-            self._check_interval(ts[index], self._check_dotm)
-        else:
-            raise ValueError('invalid month specifier')
-        return index
-
-    def _check_Y_spec(self, ts, index):
-        if ts[index] == 'month':
-            index += 1
-            self._check_interval(ts[index], self._check_month_num)
-            month_num = int(ts[index])
-            index = self._check_M_spec(ts, index + 1)
-        elif ts[index] == 'week':
-            self._check_interval(ts[index + 1], self._check_woty)
-            index = self._check_W_spec(ts, index + 2)
-        elif ts[index] == 'day':
-            index += 1
-            self._check_interval(ts[index], self._check_doty)
-        else:
-            raise ValueError('invalid year specifier')
-        return index
-
-    def _check_generalized(self, t):
-        assert type(t) is unicode
-        if len(t) not in (10, 12, 14):
-            raise ValueError('incomplete generalized time')
-        if not t.isnumeric():
-            raise ValueError('time non-numeric')
-        # don't check year value, with time travel and all :)
-        self._check_month_num(t[4:6])
-        year_num = int(t[0:4])
-        month_num = int(t[4:6])
-        self._check_dotm(t[6:8], month_num, year_num)
-        if len(t) >= 12:
-            self._check_HHMM(t[8:12])
-        else:
-            self._check_HHMM('%s00' % t[8:10])
-        if len(t) == 14:
-            s = int(t[12:14])
-            if s < 0 or s > 60:
-                raise ValueError('seconds out of range')
-
-    def _check(self, time):
-        ts = time.split()
-        if ts[0] == 'absolute':
-            if len(ts) != 4:
-                raise ValueError('invalid format, must be \'absolute generalizedTime ~ generalizedTime\'')
-            self._check_generalized(ts[1])
-            if ts[2] != '~':
-                raise ValueError('invalid time range separator')
-            self._check_generalized(ts[3])
-            if int(ts[1]) >= int(ts[3]):
-                raise ValueError('invalid time range')
-        elif ts[0] == 'periodic':
-            index = None
-            if ts[1] == 'yearly':
-                index = self._check_Y_spec(ts, 2)
-            elif ts[1] == 'monthly':
-                index = self._check_M_spec(ts, 2)
-            elif ts[1] == 'weekly':
-                index = self._check_W_spec(ts, 2)
-            elif ts[1] == 'daily':
-                index = 1
-            if index is None:
-                raise ValueError('period must be yearly, monthy or daily, got \'%s\'' % ts[1])
-            self._check_interval(ts[index + 1], self._check_HHMM)
-        else:
-            raise ValueError('time neither absolute or periodic')
-
-    def _rule_required(self, _, value):
-        try:
-            self._check(value)
-        except ValueError as e:
-            raise ValidationError(name=self.get_param_name(), error=e.args[0])
-        except IndexError:
-            raise ValidationError(
-                name=self.get_param_name(), error=ugettext('incomplete time value')
-            )
-        return None
-
-
 class DNParam(Param):
     type = DN
 
diff --git a/ipalib/plugins/hbacrule.py b/ipalib/plugins/hbacrule.py
index 82a52bd80f58ede43249264db69acd193233448d..384016075f0618d159dc2cf27c60afcecc150daa 100644
--- a/ipalib/plugins/hbacrule.py
+++ b/ipalib/plugins/hbacrule.py
@@ -17,8 +17,10 @@
 # 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 pytz import timezone, UnknownTimeZoneError
+
 from ipalib import api, errors
-from ipalib import AccessTime, Password, Str, StrEnum, Bool, DeprecatedParam
+from ipalib import Password, Str, StrEnum, Bool, DeprecatedParam
 from ipalib.plugable import Registry
 from ipalib.plugins.baseldap import *
 from ipalib import _, ngettext
@@ -34,6 +36,8 @@ You can also specify a category of users and target hosts.
 This is currently limited to "all", but might be expanded in the
 future.
 
+You can control the times when the rule is active, too.
+
 Target hosts in HBAC rules must be hosts managed by IPA.
 
 The available services and groups of services are controlled by the
@@ -69,25 +73,18 @@ EXAMPLES:
 
  Remove a named HBAC rule:
    ipa hbacrule-del allow_server
+
+ Specify that the rule "test1" should be active every day between 0800 and 1400:
+   ipa hbacrule-add-accesstime --time='timeofday=0800-1400' test1
+
+ Specify that the rule "test1" should be active once, from 10:32 until 10:33 on
+ December 16, 2010:
+   ipa hbacrule-add-accesstime --time='timeofday=1032-1033
+      dayofmonth=16 monthofyear=12 year=2010' test1
 """)
 
 register = Registry()
 
-# AccessTime support is being removed for now.
-#
-# You can also control the times that the rule is active.
-#
-# The access time(s) of a host are cumulative and are not guaranteed to be
-# applied in the order displayed.
-#
-# Specify that the rule "test1" be active every day between 0800 and 1400:
-#   ipa hbacrule-add-accesstime --time='periodic daily 0800-1400' test1
-#
-# Specify that the rule "test1" be active once, from 10:32 until 10:33 on
-# December 16, 2010:
-#   ipa hbacrule-add-accesstime --time='absolute 201012161032 ~ 201012161033' test1
-
-
 topic = ('hbac', _('Host-based access control commands'))
 
 def validate_type(ugettext, type):
@@ -109,6 +106,182 @@ def is_all(options, attribute):
         return False
 
 
+# The following functions serve for validation of access time strings
+def _timearg_to_list(time):
+    '''
+    Parses the argument of either of the access time language keywords
+
+    Example: '1-5,12' into [[1,5], 12]
+    '''
+    return (t.split('-') for t in time.split(','))
+
+
+def _validate_range(keyword, lst, r1, r2, unit, unitlen):
+    '''
+    Checks the range of each keyword
+
+    keyword is the keyword string
+    lst is a list from _timearg_to_list,
+    r1 and r2 are the range borders for each keyword
+    unit is the unit of each keyword (e.g. day, year, ...)
+    unitlen is the maximum lenght of the argument (e.g. 2 for monthofyear
+        because max. month number is 12)
+    '''
+    for elempair in lst:
+        if(len(elempair) > 2):
+            return _('range may only consist of two numbers at once')
+        elold = 0
+        for i, elem in enumerate(elempair):
+            if not(elem):
+                # - or , used with no values -> got ''
+                return _('extra \'-\' or \',\'')
+            if len(elem) > unitlen:
+                return _('{kw} requires {unit} number {lb}-{rb}'
+                         .format(kw=keyword, unit=unit, lb=r1, rb=r2))
+            if not(elem.isdigit()):
+                return _('{unit} in {kw} is not a number'
+                         .format(unit=unit, kw=keyword))
+            elnum = int(elem)
+            if elnum < r1 or elnum > r2:
+                return _('{unit} in {kw} out of range. It needs '
+                         'number {lb}-{rb}'.format(unit=unit,
+                          kw=keyword, lb=r1, rb=r2))
+            if not(i):
+                elold = elnum
+            elif elold > elnum:
+                return _('first part of {kw} range is greater than '
+                         'the second'.format(kw=keyword))
+
+
+def _validate_timeofday(t):
+    t = _timearg_to_list(t)
+
+    for time in t:
+        hhmmval = 0
+        for i, hhmm in enumerate(time):
+            if len(hhmm) != 4:
+                return _('timeofday requires time in HHMM format')
+            if not(hhmm.isdigit()):
+                return _('time in timeofday is not a number')
+            hours = int(hhmm[0:2])
+            minutes = int(hhmm[2:4])
+            if hours < 0 or hours > 23:
+                return _('hours value out of range')
+            if minutes < 0 or minutes > 59:
+                return _('minutes value out of range')
+            if not(i % 2):
+                hhmmval = hours*100 + minutes
+            elif hhmmval > (hours*100 + minutes):
+                return _('first part of timeofday range is greater than the second')
+
+
+def _validate_dayofweek(t):
+    t = _timearg_to_list(t)
+    return _validate_range('dayofweek', t, 1, 7, 'day', 1)
+
+
+def _validate_dayofmonth(t):
+    t = _timearg_to_list(t)
+    return _validate_range('dayofmonth', t, 1, 31, 'day', 2)
+
+
+def _validate_weekofmonth(t):
+    t = _timearg_to_list(t)
+    return _validate_range('weekofmonth', t, 0, 5, 'week', 1)
+
+
+def _validate_monthofyear(t):
+    t = _timearg_to_list(t)
+    return _validate_range('monthofyear', t, 1, 12, 'month', 2)
+
+
+def _validate_year(t):
+    t = _timearg_to_list(t)
+    return _validate_range('year', t, 1970, 9999, 'year', 4)
+
+
+def _validate_dayofyear(t):
+    t = _timearg_to_list(t)
+    return _validate_range('dayofyear', t, 1, 366, 'day', 3)
+
+
+def _validate_weekofyear(t):
+    t = _timearg_to_list(t)
+    return _validate_range('weekofyear', t, 0, 53, 'week', 2)
+
+
+def normalize_accesstime(t):
+    # multiple whitespaces to one
+    t = ' '.join(t.split())
+    t = t.lower()
+    # no spaces between intervals
+    t = re.sub(r'\s?,\s?', r',', t)
+    # no spaces around '=' sign
+    t = re.sub(r'\s?=\s?', r'=', t)
+    # no spaces around '-' sign
+    t = re.sub(r'\s?-\s?', r'-', t)
+    # if keyword=1234keyword make a space after 1234
+    t = re.sub(r'(\d)([a-zA-Z])', r'\1 \2', t)
+
+    return t
+
+
+def validate_accesstime(ugettext, value):
+    value = normalize_accesstime(value)
+    ts = value.split()
+    TOD, DOW, DOM, DOY, WOM, WOY, MOY, YEAR = range(0, 8)
+    found = [False] * (YEAR + 1)
+
+    res = None
+    for i, el in enumerate(ts):
+        if(el.startswith('timeofday=')):
+            res = _validate_timeofday(el[10:]) if not(found[TOD]) else \
+                _("multiple appearance of timeofday")
+            found[TOD] = True
+        elif(el.startswith('dayofweek=')):
+            res = _validate_dayofweek(el[10:]) if not(found[DOW]) else \
+                _("multiple appearance of dayofweek")
+            found[DOW] = True
+        elif(el.startswith('dayofmonth=')):
+            res = _validate_dayofmonth(el[11:]) if not(found[DOM]) else \
+                _("multiple appearance of dayofmonth")
+            found[DOM] = True
+        elif(el.startswith('dayofyear=')):
+            res = _validate_dayofyear(el[10:]) if not(found[DOY]) else \
+                _("multiple appearance of dayofyear")
+            found[DOY] = True
+        elif(el.startswith('weekofmonth=')):
+            res = _validate_weekofmonth(el[12:]) if not(found[WOM]) else \
+                _("multiple appearance of weekofmonth")
+            found[WOM] = True
+        elif(el.startswith('weekofyear=')):
+            res = _validate_weekofyear(el[11:]) if not(found[WOY]) else \
+                _("multiple appearance of weekofyear")
+            found[WOY] = True
+        elif(el.startswith('monthofyear=')):
+            res = _validate_monthofyear(el[12:]) if not(found[MOY]) else \
+                _("multiple appearance of monthofyear")
+            found[MOY] = True
+        elif(el.startswith('year=')):
+            res = _validate_year(el[5:]) if not(found[YEAR]) else \
+                _("multiple appearance of year")
+            found[YEAR] = True
+        else:
+            res = _('Unknown expression {exp} at position {pos}'
+                    .format(exp=el, pos=i))
+        if res:
+            return res
+
+
+def validate_timezone(ugettext, value):
+    if value.lower() == 'host':
+        return
+    try:
+        timezone(value)
+    except UnknownTimeZoneError, e:
+        return _('Time zone unknown: {tz}'.format(tz=e.args[0]))
+
+
 @register()
 class hbacrule(LDAPObject):
     """
@@ -124,7 +297,8 @@ class hbacrule(LDAPObject):
         'description', 'usercategory', 'hostcategory',
         'servicecategory', 'ipaenabledflag',
         'memberuser', 'sourcehost', 'memberhost', 'memberservice',
-        'externalhost',
+        'memberhostgroup', 'externalhost', 'ipatimezone',
+        'accesstime', 'accesstimeexclude'
     ]
     uuid_attribute = 'ipauniqueid'
     rdn_attribute = 'ipauniqueid'
@@ -140,7 +314,8 @@ class hbacrule(LDAPObject):
             'ipapermbindruletype': 'all',
             'ipapermright': {'read', 'search', 'compare'},
             'ipapermdefaultattr': {
-                'accessruletype', 'accesstime', 'cn', 'description',
+                'accessruletype', 'ipatimezone', 'accesstime',
+                'accesstimeexclude', 'cn', 'description',
                 'externalhost', 'hostcategory', 'ipaenabledflag',
                 'ipauniqueid', 'memberhost', 'memberservice', 'memberuser',
                 'servicecategory', 'sourcehost', 'sourcehostcategory',
@@ -174,7 +349,8 @@ class hbacrule(LDAPObject):
         'System: Modify HBAC Rule': {
             'ipapermright': {'write'},
             'ipapermdefaultattr': {
-                'accessruletype', 'accesstime', 'cn', 'description',
+                'accessruletype', 'ipatimezone', 'accesstime',
+                'accesstimeexclude', 'cn', 'description',
                 'hostcategory', 'ipaenabledflag', 'servicecategory',
                 'sourcehost', 'sourcehostcategory', 'usercategory'
             },
@@ -224,10 +400,23 @@ class hbacrule(LDAPObject):
             doc=_('Service category the rule applies to'),
             values=(u'all', ),
         ),
-#        AccessTime('accesstime?',
-#            cli_name='time',
-#            label=_('Access time'),
-#        ),
+        Str('ipatimezone?', validate_timezone,
+            cli_name = 'timezone',
+            label = _('Time zone'),
+            flags = {'no_search'},
+        ),
+        Str('accesstime*', validate_accesstime,
+            cli_name='time',
+            label=_('Access time'),
+            flags = {'no_search', 'no_create', 'no_update'},
+            normalizer=normalize_accesstime,
+        ),
+        Str('accesstimeexclude*', validate_accesstime,
+            cli_name='excltime',
+            label=_('Access time exceptions'),
+            flags = {'no_search', 'no_create', 'no_update'},
+            normalizer=normalize_accesstime,
+        ),
         Str('description?',
             cli_name='desc',
             label=_('Description'),
@@ -397,86 +586,32 @@ class hbacrule_disable(LDAPQuery):
         )
 
 
+@register()
+class hbacrule_add_accesstime(LDAPAddAttribute):
+    __doc__ = _('Add allowed access times to an HBAC Rule')
+    msg_summary = _('Added allowed access times to the rule "%(value)s"')
+    attribute = 'accesstime'
 
-class hbacrule_add_accesstime(LDAPQuery):
-    """
-    Add an access time to an HBAC rule.
-    """
 
-    takes_options = (
-        AccessTime('accesstime',
-            cli_name='time',
-            label=_('Access time'),
-        ),
-    )
+@register()
+class hbacrule_remove_accesstime(LDAPRemoveAttribute):
+    __doc__ = _('Remove access times from an HBAC Rule')
+    msg_summary = _('Removed access times from rule "%(value)s"')
+    attribute = 'accesstime'
 
-    def execute(self, cn, **options):
-        ldap = self.obj.backend
 
-        dn = self.obj.get_dn(cn)
+@register()
+class hbacrule_add_accesstimeexclude(LDAPAddAttribute):
+    __doc__ = _('Add exceptions in access time to an HBAC Rule')
+    msg_summary = _('Added exceptions in access times to the rule "%(value)s"')
+    attribute = 'accesstimeexclude'
 
-        entry_attrs = ldap.get_entry(dn, ['accesstime'])
-        entry_attrs.setdefault('accesstime', []).append(
-            options['accesstime']
-        )
-        try:
-            ldap.update_entry(entry_attrs)
-        except errors.EmptyModlist:
-            pass
-        except errors.NotFound:
-            self.obj.handle_not_found(cn)
 
-        return dict(result=True)
-
-    def output_for_cli(self, textui, result, cn, **options):
-        textui.print_name(self.name)
-        textui.print_dashed(
-            'Added access time "%s" to HBAC rule "%s"' % (
-                options['accesstime'], cn
-            )
-        )
-
-#api.register(hbacrule_add_accesstime)
-
-
-class hbacrule_remove_accesstime(LDAPQuery):
-    """
-    Remove access time to HBAC rule.
-    """
-    takes_options = (
-        AccessTime('accesstime?',
-            cli_name='time',
-            label=_('Access time'),
-        ),
-    )
-
-    def execute(self, cn, **options):
-        ldap = self.obj.backend
-
-        dn = self.obj.get_dn(cn)
-
-        entry_attrs = ldap.get_entry(dn, ['accesstime'])
-        try:
-            entry_attrs.setdefault('accesstime', []).remove(
-                options['accesstime']
-            )
-            ldap.update_entry(entry_attrs)
-        except (ValueError, errors.EmptyModlist):
-            pass
-        except errors.NotFound:
-            self.obj.handle_not_found(cn)
-
-        return dict(result=True)
-
-    def output_for_cli(self, textui, result, cn, **options):
-        textui.print_name(self.name)
-        textui.print_dashed(
-            'Removed access time "%s" from HBAC rule "%s"' % (
-                options['accesstime'], cn
-            )
-        )
-
-#api.register(hbacrule_remove_accesstime)
+@register()
+class hbacrule_remove_accesstimeexclude(LDAPRemoveAttribute):
+    __doc__ = _('Remove exceptions in access times from an HBAC Rule')
+    msg_summary = _('Removed exceptions in access times from rule "%(value)s"')
+    attribute = 'accesstimeexclude'
 
 
 @register()
@@ -593,4 +728,3 @@ class hbacrule_remove_service(LDAPRemoveMember):
 
     member_attributes = ['memberservice']
     member_count_out = ('%i object removed.', '%i objects removed.')
-
-- 
2.4.3

From a064527ebb271c349bffec546ff7f6bc04d25f24 Mon Sep 17 00:00:00 2001
From: Stanislav Laznicka <slazn...@redhat.com>
Date: Tue, 11 Aug 2015 07:31:59 +0200
Subject: [PATCH 4/8] Added the "repeat" keyword.

The repeat keyword serves for setting recurrent events similar to
recurrent event rules in iCalendar.

https://fedorahosted.org/freeipa/ticket/547
---
 ipalib/plugins/hbacrule.py | 116 +++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 112 insertions(+), 4 deletions(-)

diff --git a/ipalib/plugins/hbacrule.py b/ipalib/plugins/hbacrule.py
index 384016075f0618d159dc2cf27c60afcecc150daa..779a75749dcb0bcdb6de6c22b6998be744b850c4 100644
--- a/ipalib/plugins/hbacrule.py
+++ b/ipalib/plugins/hbacrule.py
@@ -18,6 +18,7 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 from pytz import timezone, UnknownTimeZoneError
+from calendar import isleap
 
 from ipalib import api, errors
 from ipalib import Password, Str, StrEnum, Bool, DeprecatedParam
@@ -81,6 +82,16 @@ EXAMPLES:
  December 16, 2010:
    ipa hbacrule-add-accesstime --time='timeofday=1032-1033
       dayofmonth=16 monthofyear=12 year=2010' test1
+   OR
+     ipa hbacrule-add-accesstime --time='timeofday=1032-1033
+       repeat=20101216+1d' test1
+
+ Specify that the rule "test1" should be active every Monday through Wednesday
+ and on Friday on the third week of every other month but not in February
+ starting October 3, 2015:
+      ipa hbacrule-add-accesstime --time='dayofweek=1-3,5 weekofmonth=3
+         repeat=20151003+2m' test1
+      ipa hbacrule-add-accesstimeexclude --excltime='monthofyear=2' test1
 """)
 
 register = Registry()
@@ -210,6 +221,95 @@ def _validate_weekofyear(t):
     return _validate_range('weekofyear', t, 0, 53, 'week', 2)
 
 
+def _validate_ymd_range(y1, m1, d1, y2, m2, d2):
+    y1 = int(y1)
+    m1 = int(m1)
+    d1 = int(d1)
+    y2 = int(y2)
+    m2 = int(m2)
+    d2 = int(d2)
+
+    if y1 < y2:
+        return
+    elif y1 > y2:
+        return _("first year is greater than the second")
+    # y1 == y2
+    if m1 < m2:
+        return
+    elif m1 > m2:
+        return _("first month is greater than the second with equal years")
+    # y1 == y2 and m1 == m2
+    if d1 <= d2:
+        return
+    else:
+        return _("first day is greater than the second with equal "
+                 "years and months")
+
+
+def _validate_date(y, m, d):
+    res = _validate_year(y)
+    if res:
+        return _("invalid year in date")
+    res = _validate_monthofyear(m)
+    if res:
+        return _("invalid month in year")
+    res = _validate_dayofmonth(d)
+    if res:
+        return _("invalid day in date")
+
+    year = int(y)
+    mon = int(m)
+    day = int(d)
+    if mon in (4, 6, 9, 11):
+        if day > 30:
+            return _("invalid day in date")
+
+    if mon == 2:
+        if isleap(year):
+            if day > 29:
+                return _("invalid day in date")
+        elif day > 28:
+            return _("invalid day in date")
+
+
+def _validate_repeat(t):
+    date1 = t[0:8]
+
+    y1 = date1[0:4]
+    m1 = date1[4:6]
+    d1 = date1[6:8]
+    res = _validate_date(y1, m1, d1)
+    if res:
+        return _("'repeat': ") + res
+
+    date2 = t[9:17] if t[8] == '-' else None
+    # control at pos 8 if there is no date2
+    ctlpos = 8
+    if date2:
+        y2 = date2[0:4]
+        m2 = date2[4:6]
+        d2 = date2[6:8]
+        res = _validate_date(y2, m2, d2)
+        if res:
+            return _("'repeat': ") + res
+
+        res = _validate_ymd_range(y1, m1, d1, y2, m2, d2)
+        if res:
+            return _("'repeat': ") + res
+
+        ctlpos = 17
+
+    if t[ctlpos] != '+':
+        return _("'repeat': wrong syntax, '+' expected")
+    ctlpos += 1
+    if not('1' <= t[ctlpos] <= '9'):
+        return _("'repeat': wrong syntax, expected a number after '+'")
+    while t[ctlpos].isdigit():
+        ctlpos += 1
+    if t[ctlpos] not in ('d', 'w', 'm', 'y'):
+        return _("'repeat': wrong syntax, d/w/m/y expected")
+
+
 def normalize_accesstime(t):
     # multiple whitespaces to one
     t = ' '.join(t.split())
@@ -221,16 +321,20 @@ def normalize_accesstime(t):
     # no spaces around '-' sign
     t = re.sub(r'\s?-\s?', r'-', t)
     # if keyword=1234keyword make a space after 1234
-    t = re.sub(r'(\d)([a-zA-Z])', r'\1 \2', t)
-
+    # don't make space after num in range=... (that's why {2})
+    t = re.sub(r'(\d)([a-z]{2})', r'\1 \2', t)
+    # no spaces around '+' in repeat
+    t = re.sub(r'\s+\s', r'+', t)
+    # no spaces after interval number in repeat
+    t = re.sub(r'(\d)\s(d|w|m|y)\s', r'\1\2', t)
     return t
 
 
 def validate_accesstime(ugettext, value):
     value = normalize_accesstime(value)
     ts = value.split()
-    TOD, DOW, DOM, DOY, WOM, WOY, MOY, YEAR = range(0, 8)
-    found = [False] * (YEAR + 1)
+    TOD, DOW, DOM, DOY, WOM, WOY, MOY, YEAR, RPT = range(0, 9)
+    found = [False] * (RPT + 1)
 
     res = None
     for i, el in enumerate(ts):
@@ -266,6 +370,10 @@ def validate_accesstime(ugettext, value):
             res = _validate_year(el[5:]) if not(found[YEAR]) else \
                 _("multiple appearance of year")
             found[YEAR] = True
+        elif(el.startswith('repeat=')):
+            res = _validate_repeat(el[7:]) if not(found[RPT]) else \
+                _("multiple appearance of repeat")
+            found[RPT] = True
         else:
             res = _('Unknown expression {exp} at position {pos}'
                     .format(exp=el, pos=i))
-- 
2.4.3

From b8915c92389675e985f5bc29de9f3ee78b0c5ae4 Mon Sep 17 00:00:00 2001
From: Stanislav Laznicka <slazn...@redhat.com>
Date: Tue, 1 Sep 2015 15:42:19 +0200
Subject: [PATCH 5/8] HBAC test module support for time-based policies

https://fedorahosted.org/freeipa/ticket/547
---
 API.txt                    |  3 ++-
 ipalib/plugins/hbactest.py | 22 +++++++++++++++++++++-
 2 files changed, 23 insertions(+), 2 deletions(-)

diff --git a/API.txt b/API.txt
index 815320554c47d5da1ea208bb3d6abd4ddd1c5c43..1cd3456be5c820fb1efd0c2059a5a537e46a6e3e 100644
--- a/API.txt
+++ b/API.txt
@@ -2047,9 +2047,10 @@ output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDA
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: PrimaryKey('value', None, None)
 command: hbactest
-args: 0,10,6
+args: 0,11,6
 option: Flag('disabled?', autofill=True, cli_name='disabled', default=False)
 option: Flag('enabled?', autofill=True, cli_name='enabled', default=False)
+option: Str('hosttimezone?', cli_name='timezone')
 option: Flag('nodetail?', autofill=True, cli_name='nodetail', default=False)
 option: Str('rules*', cli_name='rules', csv=True)
 option: Str('service', cli_name='service')
diff --git a/ipalib/plugins/hbactest.py b/ipalib/plugins/hbactest.py
index b528707f785c711ce6449237ea789d56cda2dcbb..9ace736c3b0d5ea2792a4b9636729ddab60a2f3b 100644
--- a/ipalib/plugins/hbactest.py
+++ b/ipalib/plugins/hbactest.py
@@ -24,6 +24,7 @@ from ipalib.cli import to_cli
 from ipalib import _, ngettext
 from ipapython.dn import DN
 from ipalib.plugable import Registry
+from ipalib.plugins.hbacrule import validate_timezone
 if api.env.in_server and api.env.context in ['lite', 'server']:
     try:
         import ipaserver.dcerpc
@@ -31,6 +32,7 @@ if api.env.in_server and api.env.context in ['lite', 'server']:
     except ImportError:
         _dcerpc_bindings_installed = False
 
+import time
 import pyhbac
 import six
 
@@ -239,6 +241,12 @@ def convert_to_ipa_rule(rule):
             attr_name = '%s_%s' % (element[1], element[3])
             if attr_name in rule:
                 element[4].groups = rule[attr_name]
+    if 'ipatimezone' in rule:
+        ipa_rule.timerules.timezone = rule['ipatimezone'][0]
+    if 'accesstime' in rule:
+        ipa_rule.timerules.accesstimes = rule['accesstime']
+    if 'accesstimeexclude' in rule:
+        ipa_rule.timerules.exceptions = rule['accesstimeexclude']
     if 'externalhost' in rule:
             ipa_rule.srchosts.names.extend(rule['externalhost']) #pylint: disable=E1101
     return ipa_rule
@@ -268,6 +276,10 @@ class hbactest(Command):
             cli_name='host',
             label=_('Target host'),
         ),
+        Str('hosttimezone?', validate_timezone,
+            cli_name='timezone',
+            label=_('Host\'s time zone'),
+        ),
         Str('service',
             cli_name='service',
             label=_('Service'),
@@ -375,6 +387,7 @@ class hbactest(Command):
 
         # Rules are converted to pyhbac format, build request and then test it
         request = pyhbac.HbacRequest()
+        request.req_time = int(time.time())
 
         if options['user'] != u'all':
             # check first if this is not a trusted domain user
@@ -457,6 +470,14 @@ class hbactest(Command):
         warning_rules = []
 
         result = {'warning':None, 'matched':None, 'notmatched':None, 'error':None}
+
+        if 'hosttimezone' in options:
+            # The 'host' time zone settings needs to be changed
+            # If 'hosttimezone' is not set, leave it to local time zone
+            for ipa_rule in rules:
+                if ipa_rule.timerules.timezone.lower() == 'host':
+                        ipa_rule.timerules.timezone = options['hosttimezone']
+
         if not options['nodetail']:
             # Validate runs rules one-by-one and reports failed ones
             for ipa_rule in rules:
@@ -518,4 +539,3 @@ class hbactest(Command):
 
         # Propagate integer value for result. It will give proper command line result for scripts
         return int(not output['value'])
-
-- 
2.4.3

From fae365648324f80e3fea9dbc08d2c4ab9e908727 Mon Sep 17 00:00:00 2001
From: Stanislav Laznicka <slazn...@redhat.com>
Date: Wed, 2 Sep 2015 11:50:14 +0200
Subject: [PATCH 6/8] Removed old AccessTime parameter tests.

https://fedorahosted.org/freeipa/ticket/547
---
 ipatests/test_ipalib/test_parameters.py | 39 ---------------------------------
 1 file changed, 39 deletions(-)

diff --git a/ipatests/test_ipalib/test_parameters.py b/ipatests/test_ipalib/test_parameters.py
index 929289258ca2ff02f78900bfcc91d701ab276898..fe3ab7b042f0f7dfb23b7907c8023d47b1194fd3 100644
--- a/ipatests/test_ipalib/test_parameters.py
+++ b/ipatests/test_ipalib/test_parameters.py
@@ -1487,45 +1487,6 @@ class test_Decimal(ClassChecker):
             else:
                 param(value)
 
-class test_AccessTime(ClassChecker):
-    """
-    Test the `ipalib.parameters.AccessTime` class.
-    """
-    _cls = parameters.AccessTime
-
-    def test_init(self):
-        """
-        Test the `ipalib.parameters.AccessTime.__init__` method.
-        """
-        # Test with no kwargs:
-        o = self.cls('my_time')
-        assert o.type is unicode
-        assert isinstance(o, parameters.AccessTime)
-        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 rules
-        for value in (u'absolute 201012161032 ~ 201012161033',
-                      u'periodic monthly week 2 day Sat,Sun 0900-1300',
-                      u'periodic yearly month 4 day 1-31 0800-1400',
-                      u'periodic weekly day 7 0800-1400',
-                      u'periodic daily 0800-1400',
-            ):
-            assert rule(dummy, value) is None
-            assert dummy.called() is False
-
-        # And some bad ones
-        for value in (u'absolute 201012161032 - 201012161033',
-                      u'absolute 201012161032 ~',
-                      u'periodic monthly day Sat,Sun 0900-1300',
-                      u'periodical yearly month 4 day 1-31 0800-1400',
-                      u'periodic weekly day 8 0800-1400',
-            ):
-            e = raises(ValidationError, o._rule_required, None, value)
-
 def test_create_param():
     """
     Test the `ipalib.parameters.create_param` function.
-- 
2.4.3

From b729c720bd7e9d5c003cf15148d05ac8ef80a72e Mon Sep 17 00:00:00 2001
From: Stanislav Laznicka <slazn...@redhat.com>
Date: Thu, 3 Sep 2015 09:48:04 +0200
Subject: [PATCH 7/8] Tests for HBAC time rules language

https://fedorahosted.org/freeipa/ticket/547
---
 ipatests/test_xmlrpc/test_hbac_plugin.py     | 205 +++++++++++++++++++++++----
 ipatests/test_xmlrpc/test_hbactest_plugin.py |   4 +-
 2 files changed, 180 insertions(+), 29 deletions(-)

diff --git a/ipatests/test_xmlrpc/test_hbac_plugin.py b/ipatests/test_xmlrpc/test_hbac_plugin.py
index a9677bfdebecd8eaa746640834e067832d516728..5b259d879f15fee8d3eaba9db66e116b15357165 100644
--- a/ipatests/test_xmlrpc/test_hbac_plugin.py
+++ b/ipatests/test_xmlrpc/test_hbac_plugin.py
@@ -34,10 +34,11 @@ class test_hbac(XMLRPC_test):
     rule_type = u'allow'
     rule_type_fail = u'value not allowed'
     rule_service = u'ssh'
-    rule_time = u'absolute 20081010000000 ~ 20081015120000'
-    rule_time2 = u'absolute 20081010000000 ~ 20081016120000'
-    # wrong time, has 30th day in February in first date
-    rule_time_fail = u'absolute 20080230000000 ~ 20081015120000'
+    rule_time = u'timeofday=0000-2359 dayofmonth=1-15,16-31 ' \
+                 'dayofyear=23-47,26-77 year=1970-2563'
+    rule_time2 = u'weekofmonth=0,1,4,5 monthofyear=1-3,7,9,11-12 ' \
+                 'repeat=20150522-20150630+12d'
+    rule_time3 = u'timeofday=0700-1600 dayofmonth=1,3,5,22 repeat=19980217+4w'
     rule_desc = u'description'
     rule_desc_mod = u'description modified'
 
@@ -93,30 +94,178 @@ class test_hbac(XMLRPC_test):
         entry = ret['result']
         assert_attr_equal(entry, 'description', self.rule_desc_mod)
 
-#    def test_4_hbacrule_add_accesstime(self):
-#        """
-#        Test adding access time to HBAC rule using `xmlrpc.hbacrule_add_accesstime`.
-#        """
-#        return
-#        ret = api.Command['hbacrule_add_accesstime'](
-#            self.rule_name, accesstime=self.rule_time2
-#        )
-#        entry = ret['result']
-#        assert_attr_equal(entry, 'accesstime', self.rule_time);
-#        assert_attr_equal(entry, 'accesstime', self.rule_time2);
-
-#    def test_5_hbacrule_add_accesstime(self):
-#        """
-#        Test adding invalid access time to HBAC rule using `xmlrpc.hbacrule_add_accesstime`.
-#        """
-#        try:
-#            api.Command['hbacrule_add_accesstime'](
-#                self.rule_name, accesstime=self.rule_time_fail
-#            )
-#        except errors.ValidationError:
-#            pass
-#        else:
-#            assert False
+    def test_4_hbacrule_add_accesstime(self):
+        """
+        Test adding access time to HBAC rule using `xmlrpc.hbacrule_add_accesstime`.
+        """
+        ret = api.Command['hbacrule_add_accesstime'](
+            self.rule_name, accesstime=(self.rule_time, self.rule_time2,
+                                        self.rule_time3)
+        )
+        entry = ret['result']
+        assert_attr_equal(entry, 'accesstime', self.rule_time)
+        assert_attr_equal(entry, 'accesstime', self.rule_time2)
+        assert_attr_equal(entry, 'accesstime', self.rule_time3)
+
+    @raises(errors.ValidationError)
+    def test_5_hbacrule_add_accesstime(self):
+        """
+        This and the following add_accesstime tests test adding invalid access
+        times to HBAC rule using `xmlrpc.hbacrule_add_accesstime`.
+        """
+        api.Command['hbacrule_add_accesstime'](
+            self.rule_name, accesstime=u'nonsense'
+        )
+
+    @raises(errors.ValidationError)
+    def test_hbacrule_add_accesstime_TOD_1(self):
+        api.Command['hbacrule_add_accesstime'](
+            self.rule_name, accesstime=u'timeofday=2400'
+        )
+
+    @raises(errors.ValidationError)
+    def test_hbacrule_add_accesstime_TOD_2(self):
+        api.Command['hbacrule_add_accesstime'](
+            self.rule_name, accesstime=u'timeofday=1060'
+        )
+
+    @raises(errors.ValidationError)
+    def test_hbacrule_add_accesstime_TOD_3(self):
+        api.Command['hbacrule_add_accesstime'](
+            self.rule_name, accesstime=u'timeofday=7800'
+        )
+
+    @raises(errors.ValidationError)
+    def test_hbacrule_add_accesstime_DOW_1(self):
+        api.Command['hbacrule_add_accesstime'](
+            self.rule_name, accesstime=u'dayofweek=57'
+        )
+
+    @raises(errors.ValidationError)
+    def test_hbacrule_add_accesstime_DOW_2(self):
+        api.Command['hbacrule_add_accesstime'](
+            self.rule_name, accesstime=u'dayofweek=0'
+        )
+
+    @raises(errors.ValidationError)
+    def test_hbacrule_add_accesstime_DOW_3(self):
+        api.Command['hbacrule_add_accesstime'](
+            self.rule_name, accesstime=u'dayofweek=8'
+        )
+
+    @raises(errors.ValidationError)
+    def test_hbacrule_add_accesstime_DOM_1(self):
+        api.Command['hbacrule_add_accesstime'](
+            self.rule_name, accesstime=u'dayofmonth=280'
+        )
+
+    @raises(errors.ValidationError)
+    def test_hbacrule_add_accesstime_DOM_2(self):
+        api.Command['hbacrule_add_accesstime'](
+            self.rule_name, accesstime=u'dayofmonth=0'
+        )
+
+    @raises(errors.ValidationError)
+    def test_hbacrule_add_accesstime_DOM_3(self):
+        api.Command['hbacrule_add_accesstime'](
+            self.rule_name, accesstime=u'dayofmonth=32'
+        )
+
+    @raises(errors.ValidationError)
+    def test_hbacrule_add_accesstime_WOM_1(self):
+        api.Command['hbacrule_add_accesstime'](
+            self.rule_name, accesstime=u'weekofmonth=350'
+        )
+
+    @raises(errors.ValidationError)
+    def test_hbacrule_add_accesstime_WOM_2(self):
+        api.Command['hbacrule_add_accesstime'](
+            self.rule_name, accesstime=u'weekofmonth=6'
+        )
+
+    @raises(errors.ValidationError)
+    def test_hbacrule_add_accesstime_MOY_1(self):
+        api.Command['hbacrule_add_accesstime'](
+            self.rule_name, accesstime=u'monthofyear=789'
+        )
+
+    @raises(errors.ValidationError)
+    def test_hbacrule_add_accesstime_MOY_2(self):
+        api.Command['hbacrule_add_accesstime'](
+            self.rule_name, accesstime=u'monthofyear=0'
+        )
+
+    @raises(errors.ValidationError)
+    def test_hbacrule_add_accesstime_MOY_3(self):
+        api.Command['hbacrule_add_accesstime'](
+            self.rule_name, accesstime=u'monthofyear=13'
+        )
+
+    @raises(errors.ValidationError)
+    def test_hbacrule_add_accesstime_YEAR_1(self):
+        api.Command['hbacrule_add_accesstime'](
+            self.rule_name, accesstime=u'year=235'
+        )
+
+    @raises(errors.ValidationError)
+    def test_hbacrule_add_accesstime_YEAR_2(self):
+        api.Command['hbacrule_add_accesstime'](
+            self.rule_name, accesstime=u'year=1969'
+        )
+
+    @raises(errors.ValidationError)
+    def test_hbacrule_add_accesstime_YEAR_3(self):
+        api.Command['hbacrule_add_accesstime'](
+            self.rule_name, accesstime=u'year=10000'
+        )
+
+    @raises(errors.ValidationError)
+    def test_hbacrule_add_accesstime_DOY_1(self):
+        api.Command['hbacrule_add_accesstime'](
+            self.rule_name, accesstime=u'dayofyear=48646'
+        )
+
+    @raises(errors.ValidationError)
+    def test_hbacrule_add_accesstime_DOY_2(self):
+        api.Command['hbacrule_add_accesstime'](
+            self.rule_name, accesstime=u'dayofyear=0'
+        )
+
+    @raises(errors.ValidationError)
+    def test_hbacrule_add_accesstime_DOY_3(self):
+        api.Command['hbacrule_add_accesstime'](
+            self.rule_name, accesstime=u'dayofyear=367'
+        )
+
+    @raises(errors.ValidationError)
+    def test_hbacrule_add_accesstime_WOY_1(self):
+        api.Command['hbacrule_add_accesstime'](
+            self.rule_name, accesstime=u'weekofyear=6879754'
+        )
+
+    @raises(errors.ValidationError)
+    def test_hbacrule_add_accesstime_WOY_2(self):
+        api.Command['hbacrule_add_accesstime'](
+            self.rule_name, accesstime=u'weekofyear=54'
+        )
+
+    @raises(errors.ValidationError)
+    def test_hbacrule_add_accesstime_RPT_1(self):
+        """
+        Test invalid day of month
+        """
+        api.Command['hbacrule_add_accesstime'](
+            self.rule_name, accesstime=u'repeat=20150631+1d'
+        )
+
+    @raises(errors.ValidationError)
+    def test_hbacrule_add_accesstime_RPT_2(self):
+        """
+        Test second date lower than first
+        """
+        api.Command['hbacrule_add_accesstime'](
+            self.rule_name, accesstime=u'repeat=20150630-20150629+1d'
+        )
 
     def test_6_hbacrule_find(self):
         """
diff --git a/ipatests/test_xmlrpc/test_hbactest_plugin.py b/ipatests/test_xmlrpc/test_hbactest_plugin.py
index 6f7b0a6155fafa3eea5b870e49c3633bd0b48046..5d79d168e76c72b9b3794868dc1caf2aa4f67500 100644
--- a/ipatests/test_xmlrpc/test_hbactest_plugin.py
+++ b/ipatests/test_xmlrpc/test_hbactest_plugin.py
@@ -48,6 +48,7 @@ class test_hbactest(XMLRPC_test):
     test_sourcehost = u'hbacrule.testsrchost'
     test_sourcehostgroup = u'hbacrule_test_src_hostgroup'
     test_service = u'ssh'
+    test_timezone = u'America/Argentina/San_Juan'
 
     # Auxiliary funcion for checking existence of warning for specified rule
     def check_rule_presence(self,rule_name,warnings):
@@ -111,6 +112,7 @@ class test_hbactest(XMLRPC_test):
             user=self.test_user,
             targethost=self.test_host,
             service=self.test_service,
+            hosttimezone=self.test_timezone,
             rules=self.rule_names
         )
         assert ret['value'] == True
@@ -126,6 +128,7 @@ class test_hbactest(XMLRPC_test):
             user=self.test_user,
             targethost=self.test_host,
             service=self.test_service,
+            hosttimezone=self.test_timezone,
             rules=self.rule_names,
             nodetail=True
         )
@@ -214,4 +217,3 @@ class test_hbactest(XMLRPC_test):
         api.Command['host_del'](self.test_sourcehost)
         api.Command['hostgroup_del'](self.test_sourcehostgroup)
         api.Command['hbacsvc_del'](self.test_service)
-
-- 
2.4.3

From 31eb7e5aacbd879432065d83fb0c5c126042d957 Mon Sep 17 00:00:00 2001
From: Stanislav Laznicka <slazn...@redhat.com>
Date: Wed, 30 Sep 2015 12:42:19 +0200
Subject: [PATCH 8/8] Added negative values to the HBAC time policies

https://fedorahosted.org/freeipa/ticket/547
---
 ipalib/plugins/hbacrule.py               | 71 ++++++++++++++++++++++++--------
 ipatests/test_xmlrpc/test_hbac_plugin.py | 15 ++++---
 2 files changed, 62 insertions(+), 24 deletions(-)

diff --git a/ipalib/plugins/hbacrule.py b/ipalib/plugins/hbacrule.py
index 779a75749dcb0bcdb6de6c22b6998be744b850c4..66c587030d67608ee91b7d904cf115cabb742680 100644
--- a/ipalib/plugins/hbacrule.py
+++ b/ipalib/plugins/hbacrule.py
@@ -76,20 +76,20 @@ EXAMPLES:
    ipa hbacrule-del allow_server
 
  Specify that the rule "test1" should be active every day between 0800 and 1400:
-   ipa hbacrule-add-accesstime --time='timeofday=0800-1400' test1
+   ipa hbacrule-add-accesstime --time='timeofday=0800~1400' test1
 
  Specify that the rule "test1" should be active once, from 10:32 until 10:33 on
  December 16, 2010:
-   ipa hbacrule-add-accesstime --time='timeofday=1032-1033
+   ipa hbacrule-add-accesstime --time='timeofday=1032~1033
       dayofmonth=16 monthofyear=12 year=2010' test1
    OR
-     ipa hbacrule-add-accesstime --time='timeofday=1032-1033
+     ipa hbacrule-add-accesstime --time='timeofday=1032~1033
        repeat=20101216+1d' test1
 
  Specify that the rule "test1" should be active every Monday through Wednesday
  and on Friday on the third week of every other month but not in February
  starting October 3, 2015:
-      ipa hbacrule-add-accesstime --time='dayofweek=1-3,5 weekofmonth=3
+      ipa hbacrule-add-accesstime --time='dayofweek=1~3,5 weekofmonth=3
          repeat=20151003+2m' test1
       ipa hbacrule-add-accesstimeexclude --excltime='monthofyear=2' test1
 """)
@@ -122,12 +122,12 @@ def _timearg_to_list(time):
     '''
     Parses the argument of either of the access time language keywords
 
-    Example: '1-5,12' into [[1,5], 12]
+    Example: '1~5,12' into [[1,5], 12]
     '''
-    return (t.split('-') for t in time.split(','))
+    return (t.split('~') for t in time.split(','))
 
 
-def _validate_range(keyword, lst, r1, r2, unit, unitlen):
+def _validate_range(keyword, lst, r1, r2, unit, unitlen, loose=0):
     '''
     Checks the range of each keyword
 
@@ -137,15 +137,22 @@ def _validate_range(keyword, lst, r1, r2, unit, unitlen):
     unit is the unit of each keyword (e.g. day, year, ...)
     unitlen is the maximum lenght of the argument (e.g. 2 for monthofyear
         because max. month number is 12)
+    loose allows looser evaluation when there is a first negative value in
+        a range. This helps in cases e.g. for dayofmonth -28~1 vs. -31~1 where
+        there are multiple higher borders possible.
     '''
     for elempair in lst:
         if(len(elempair) > 2):
             return _('range may only consist of two numbers at once')
         elold = 0
+        neg = [False, False]
         for i, elem in enumerate(elempair):
             if not(elem):
-                # - or , used with no values -> got ''
-                return _('extra \'-\' or \',\'')
+                # ~ or , used with no values -> got ''
+                return _('extra \'~\' or \',\'')
+            if elem[0] == '-' and keyword != 'year':
+                elem = elem[1:]
+                neg[i] = True
             if len(elem) > unitlen:
                 return _('{kw} requires {unit} number {lb}-{rb}'
                          .format(kw=keyword, unit=unit, lb=r1, rb=r2))
@@ -153,12 +160,38 @@ def _validate_range(keyword, lst, r1, r2, unit, unitlen):
                 return _('{unit} in {kw} is not a number'
                          .format(unit=unit, kw=keyword))
             elnum = int(elem)
-            if elnum < r1 or elnum > r2:
-                return _('{unit} in {kw} out of range. It needs '
-                         'number {lb}-{rb}'.format(unit=unit,
-                          kw=keyword, lb=r1, rb=r2))
+            if keyword == 'weekofmonth' and neg[i]:
+                if elnum == 0:
+                    return _('negative zero is not allowed')
+                if elnum < r1 or elnum > (r2+1):
+                    return _('{unit} in {kw} out of range. It needs '
+                             'negative number in range from -{lb} to -{rb}'
+                             .format(unit=unit, kw=keyword, lb=r1, rb=r2+1))
+            else:
+                if elnum < r1 or elnum > r2:
+                    return _('{unit} in {kw} out of range. It needs '
+                             'number {lb}-{rb}'
+                             .format(unit=unit, kw=keyword, lb=r1, rb=r2))
+
             if not(i):
                 elold = elnum
+            elif True in neg:
+                if neg[0] == neg[1]:
+                    if elold < elnum:
+                        return _('first negative part of {kw} range makes '
+                                 'higher value than the second part'
+                                 .format(kw=keyword))
+                elif neg[0]:
+                    if (r2 - elold + 1) > (elnum + loose):
+                        return _('first part of {kw} range makes higher value '
+                                 'than the second part'
+                                 .format(kw=keyword))
+                else:
+                    #the second part is negative
+                    if (elold + elnum - 1) > r2:
+                        return _('first part of {kw} range is higher than the '
+                                 'second'
+                                 .format(kw=keyword))
             elif elold > elnum:
                 return _('first part of {kw} range is greater than '
                          'the second'.format(kw=keyword))
@@ -193,12 +226,12 @@ def _validate_dayofweek(t):
 
 def _validate_dayofmonth(t):
     t = _timearg_to_list(t)
-    return _validate_range('dayofmonth', t, 1, 31, 'day', 2)
+    return _validate_range('dayofmonth', t, 1, 31, 'day', 2, 3)
 
 
 def _validate_weekofmonth(t):
     t = _timearg_to_list(t)
-    return _validate_range('weekofmonth', t, 0, 5, 'week', 1)
+    return _validate_range('weekofmonth', t, 0, 5, 'week', 1, 1)
 
 
 def _validate_monthofyear(t):
@@ -213,12 +246,12 @@ def _validate_year(t):
 
 def _validate_dayofyear(t):
     t = _timearg_to_list(t)
-    return _validate_range('dayofyear', t, 1, 366, 'day', 3)
+    return _validate_range('dayofyear', t, 1, 366, 'day', 3, 1)
 
 
 def _validate_weekofyear(t):
     t = _timearg_to_list(t)
-    return _validate_range('weekofyear', t, 0, 53, 'week', 2)
+    return _validate_range('weekofyear', t, 0, 53, 'week', 2, 1)
 
 
 def _validate_ymd_range(y1, m1, d1, y2, m2, d2):
@@ -282,7 +315,7 @@ def _validate_repeat(t):
     if res:
         return _("'repeat': ") + res
 
-    date2 = t[9:17] if t[8] == '-' else None
+    date2 = t[9:17] if t[8] == '~' else None
     # control at pos 8 if there is no date2
     ctlpos = 8
     if date2:
@@ -320,6 +353,8 @@ def normalize_accesstime(t):
     t = re.sub(r'\s?=\s?', r'=', t)
     # no spaces around '-' sign
     t = re.sub(r'\s?-\s?', r'-', t)
+    # no spaces around - sign
+    t = re.sub(r'\s?~\s?', r'~', t)
     # if keyword=1234keyword make a space after 1234
     # don't make space after num in range=... (that's why {2})
     t = re.sub(r'(\d)([a-z]{2})', r'\1 \2', t)
diff --git a/ipatests/test_xmlrpc/test_hbac_plugin.py b/ipatests/test_xmlrpc/test_hbac_plugin.py
index 5b259d879f15fee8d3eaba9db66e116b15357165..973252ba51a874b5a5cb2562a4496ec71bf8fb2f 100644
--- a/ipatests/test_xmlrpc/test_hbac_plugin.py
+++ b/ipatests/test_xmlrpc/test_hbac_plugin.py
@@ -34,11 +34,13 @@ class test_hbac(XMLRPC_test):
     rule_type = u'allow'
     rule_type_fail = u'value not allowed'
     rule_service = u'ssh'
-    rule_time = u'timeofday=0000-2359 dayofmonth=1-15,16-31 ' \
-                 'dayofyear=23-47,26-77 year=1970-2563'
-    rule_time2 = u'weekofmonth=0,1,4,5 monthofyear=1-3,7,9,11-12 ' \
-                 'repeat=20150522-20150630+12d'
-    rule_time3 = u'timeofday=0700-1600 dayofmonth=1,3,5,22 repeat=19980217+4w'
+    rule_time = u'timeofday=0000~2359 dayofmonth=1~15,16~31 ' \
+                 'dayofyear=23~47,26~77 year=1970~2563'
+    rule_time2 = u'weekofmonth=0,1,4,5 monthofyear=1~3,7,9,11~12 ' \
+                 'repeat=20150522~20150630+12d'
+    rule_time3 = u'timeofday=0700~1600 dayofmonth=1,3,5,22 repeat=19980217+4w'
+    rule_time_neg = u'timeofday=2237~2352 monthofyear=1~3,-7~-1 ' \
+                    'dayofmonth=-25~-12,-15~-7,5~17,-5~-2 year=1973'
     rule_desc = u'description'
     rule_desc_mod = u'description modified'
 
@@ -100,12 +102,13 @@ class test_hbac(XMLRPC_test):
         """
         ret = api.Command['hbacrule_add_accesstime'](
             self.rule_name, accesstime=(self.rule_time, self.rule_time2,
-                                        self.rule_time3)
+                                        self.rule_time3, self.rule_time_neg)
         )
         entry = ret['result']
         assert_attr_equal(entry, 'accesstime', self.rule_time)
         assert_attr_equal(entry, 'accesstime', self.rule_time2)
         assert_attr_equal(entry, 'accesstime', self.rule_time3)
+        assert_attr_equal(entry, 'accesstime', self.rule_time_neg)
 
     @raises(errors.ValidationError)
     def test_5_hbacrule_add_accesstime(self):
-- 
2.4.3

-- 
Manage your subscription for the Freeipa-devel mailing list:
https://www.redhat.com/mailman/listinfo/freeipa-devel
Contribute to FreeIPA: http://www.freeipa.org/page/Contribute/Code

Reply via email to