On 03/09/2016 05:24 PM, Martin Basti wrote:

On 04.03.2016 14:53, Stanislav Laznicka wrote:
Hello,

So in the previous month and a bit I was reworking the time-based policies according to the changes we agreed on (http://pad.engineering.redhat.com/ipa-time-based-HBAC-design, line 83). Let me briefly walk you through what was done (no TLDR, sorry, but split the text in chapters):

*Time rule templates*
In the attachment is the proposal how this could be done using costemplates. Currently, the time rule templates have their own directory in the realm tree. The idea is that it could be used for both HBAC and Sudo rules so it needs to be in a location both should be able to reach. Should we not want them used in Sudo rules, the template directory could be moved to HBAC directory. There are also some new permissions for accessing these time rule templates which may need to be revised if the templates should be used both for sudo and HBAC rules.

*iCalendar format validation
*So there is an iCalendar string validation now. During its creation, I came across several issues with python-icalendar which is basically why it took me so long to write the validation. I made several fixes to the python-icalendar library, most of them are already merged in the repository master (https://github.com/collective/icalendar), one should be pushed in the next library major release.

My pull requests:
https://github.com/collective/icalendar/pull/175
https://github.com/collective/icalendar/pull/179
https://github.com/collective/icalendar/pull/180
https://github.com/collective/icalendar/pull/183
https://github.com/collective/icalendar/pull/189

I still have one fix in the making, that one should force the strong types in iCalendar as these are also missing in python-icalendar but required by the RFC.

Also, obviously, if you want to try the patches, you will need the current python-icalendar implementation from Github. I haven't put python-icalendar dependency into the .spec file yet for this reason.
*
**Summary
*We are now able to import iCalendar strings from files and more or less be sure that the parts we need will be consistent with the RFC 5545 (basically, we are only checking that VEVENT components are correct, to bring strict checking to python-icalendar would take some time and I believe I spent way too much time with it already (there is an issue on their github page, though, it's 4 years old)).

*TODO now
*0)**Update the design*
*1a) The hbacrule-*-accesstime should probably be split into 2 commands, one that reads iCalendar strings from files, and one that creates those strings from "some kind of user input" (similarly for timeruletemplates). 1b) Create the format of user input we could expect for the second kind of command from 1a). We need to be able to convert it to iCalendar string and back so that we are able to present the data stored on the server in human readable form. http://jkbrzt.github.io/rrule/ NL part might be of help although it aims mostly on RRULE property of VEVENT components, whereas we may want to use DTEND, EXDATE, RDATE and DURATION as well to be able to specify events more properly. 2) Represent the HBAC time rules on SSSD side. I already have a skeleton of this based on libical (https://github.com/libical/libical), which hopefully seems to be more viable than python-icalendar. I do not mean to do the validation of received iCalendar string on the SSSD side anymore (at least not in an excessive way), just get the required properties from VEVENT components and evaluate them accordingly.

*Discuss
*I would really appreciate your input on these topics:*
*1)**How to represent the iCalendar strings on the client side in CLI (while thinking about WebUI as well)?
2a) Do we want to use the time rules for Sudo rules as well?
2b) If 2a), is the proposed location of time rule templates along with the privileges ok?

Standa


Hello,
thank you for the patchset, I have a few comments :)*
****
***1)
+attributeTypes: (2.16.840.1.113730.3.8.11.72 NAME 'timeruleClass' DESC 'CNs of the timerule classes'

OID above is registered as:
2.16.840.1.113730.3.8.11.72    accessTimeExclude
                                  Access time - exclude these values
I would like to stick with a known OID before we decide that the patch is in final version but it's of course a right point until then.
*
***2)*
***please add requires and buildrequires to specfile (python-icalendar)
Done.

3)
Pylint is running, please wait ...
************* Module ipalib.plugins.hbacrule
ipalib/plugins/hbacrule.py:166: [E1101(no-member), validate_icalfile] Instance of 'list' has no 'name' member) ipalib/plugins/hbacrule.py:175: [E1101(no-member), validate_icalfile] Instance of 'list' has no 'subcomponents' member) ipalib/plugins/hbacrule.py:177: [E1601(print-statement), validate_icalfile] print statement used) ipalib/plugins/hbacrule.py:190: [E1601(print-statement), validate_icalfile] print statement used)

first two errors must be disabled by # pylint: disable=no-member because it is too complicated for pylint

I'm pretty sure that print should not be in plugin implementation
Done, replaced the prints with root_logger.

4)
PEP8

./ipalib/plugins/hbacrule.py:255:80: E501 line too long (216 > 79 characters) ./ipalib/plugins/hbacrule.py:262:80: E501 line too long (225 > 79 characters) ./ipalib/plugins/hbacrule.py:270:80: E501 line too long (251 > 79 characters)* ***./ipalib/plugins/hbacrule.py:456:17: E127 continuation line over-indented for visual indent* ***./ipalib/plugins/hbacrule.py:646:80: E501 line too long (80 > 79 characters)* ***./ipalib/plugins/hbacrule.py:657:1: E302 expected 2 blank lines, found 1
./ipalib/plugins/hbacrule.py:663:1: E302 expected 2 blank lines, found 1*
****
***./ipalib/plugins/hbacrule.py:177:80: E501 line too long (80 > 79 characters)* ***./ipalib/plugins/hbacrule.py:215:80: E501 line too long (80 > 79 characters)*
*
Do you want me to fix this last 1 letter overflow as well? It would probably look worse if I did.
****./ipalib/plugins/hbacrule.py:542:80: E501 line too long (127 > 79 characters) ./ipalib/plugins/hbacrule.py:544:80: E501 line too long (127 > 79 characters)
These two were not really caused by me but I can fix these PEP8 errors along with some others in a separate patch if you want to.
****./ipalib/plugins/hbacrule.py:551:1: E303 too many blank lines (3)

5)
Following imports in hbac rule should be before ipalib imports

+import icalendar
+from datetime import date
+

Done.
6)
+        ical_errors = ('{comp}: {err}'
+                       .format(comp=x, err=y) for x, y in comp.errors)

it is not clear for me what is x, and y. A component of the component?
can you named it better than x,y. We are not limited by length of identifiers in python too much :)
Good eye, it was actually a property of a component. Modified it a bit. Components may appear in components too, though.

/7)//
//ugettext string is wrong (all places)//
//+ error=_('There were errors parsing the iCalendar string:\n{errs}'//
//+                    .format(errs='\n'.join(ical_errors)))//
//it should be//
////
//error=_('There were errors parsing the iCalendar string:\n{errs}').format(errs='\n'.join(ical_errors))//
/
//Had to go with the "%" syntax in the end because "Instance of 'Gettext' has no 'format' member". However, this brought kind of inconsistency of how strings are handled in my code. Would it be better to use the "%" syntax everywhere, then?

8)
+        # TODO: comp.required might be removed when
+        # https://github.com/collective/icalendar/pull/183 is merged

I'm not fan of TODO's in code, you provides copr repo anyway, so please build package with this merge and remove TODO, we should create workaround when upstream refuse your patches.
Let me just leave that there as long as we're in WIP phase so that I might eventually remove that part of code when the mentioned patch is merged or just remove the comments if not.
/
///9)
+    if api.env.context == 'cli':
+        if ics and os.path.exists(ics):
+            return

The param is File class, so this check should eb done automatically
Done. I just followed the code from cert.py.

10)
+    icalstr = ics

this statement is useless, please use ics directly

Done, of course.
11)
+def validate_icalfile(ugettext, ics):

This and other similar restriction are required by IPA or it is invalid icalfile? (sorry for question but I'm not familiar with icalendar enough yet)
It is an invalid icalfile.
+                    error=_('A VEVENT component can\'t contain '
+                            'subcomponent "{}".'.format(sub.name))
+                    )

In second case (icalfile is invalid) shouldn't be this validation done in python-icalendar module instead of IPA validators?
I believe all (or most of) the checks should be done in python-icalendar, actually. As I started working with libical again, I am getting the impression that the iCalendar parsers are there only to get the information from what seems to be an iCalendar string by any means possible no matter how wrong the input is. I believe that theory tells us that parser should be able to distinguish invalid input from valid one, yet this is not true at all for python-icalendar and for libical. While such behavior might be desirable in some cases, these parsers should at least have means to check the validity of the input if it's not performed by default. Yet they don't and I can say it literally drives me insane.

12)
+        if 'DTEND' in comp.keys() and 'DURATION' in comp.keys():
+            raise errors.ValidationError(
+                name=name,
+                error=_('Both DURATION and DTEND set in a VEVENT.')
+            )
+
+        elif 'DTEND' in comp.keys():
+            if type(comp['DTSTART'].dt) != type(comp['DTEND'].dt):
+                raise errors.ValidationError(
+                    name=name,
+                    error=_('Different types of DTSTART and DTEND '
+                            'component in VEVENT.')
+                    )

IMO following way is better for readability
if 'DTEND' in comp.keys():
    if 'DURATION' in comp.keys():
          something1
    elif type(comp['DTSTART'].dt) != type(comp['DTEND'].dt):
         something2
You're right, modified it.

13)
PATCH: Templating of access time rules for HBAC
There is missing upgrade path for:
+dn: cn=timeruleTemplates,$SUFFIX
+dn: cn=cosTimerulesDef,cn=hbac,$SUFFIX
Should be ok now.
*****
***14)
+    container_dn = DN(('cn', 'timeruletemplates'))
Please define this in constants.py*
*
Done.
*
*15)
Your managed ACI are completely new, so there should not be 'replaces' definition (several times)
+            'replaces': [
+ '(target = "ldap:///cn=*,cn=timeruletemplates,$SUFFIX";)(version 3.0;acl "permission:Delete Time Rule Template";allow (delete) groupdn = "ldap:///cn=Delete Time Rule Template,cn=permissions,cn=pbac,$SUFFIX";)',
+            ],
*
*
Oh ok.
**16)
+ 'System: Read Time Rule Template' should have also 'objectclass' as allowed attribute
Seems like it's already there.
**
/17)//
////
///+        File('accesstime', validate_icalfile,
+             cli_name='time',
+             label=_('Access time'),
+             ),
I prefer to have file option named like icalfile, instead of accesstime, as you mentioned we may need to add more options to be UX friendly

Should we have option, which will take ical string directly from CLI, IMO yes.
(this applies for both, hbacrule, and timerule template)
While I might be wrong here, it seems that the api commands won't accept newline as a part of a single input, separating the input as multivalued by newlines. Typical iCalendar string is a lot of newlines.

I currently left the 'accesstime' option there. Probably, in the end, there will have to be a separate method to load the content from an iCalendar file and store it to the database. So the current accesstime methods are mere placeholders.

18)
IMO timeruletemplate should be in separate module, we may reuse this later, and I don't see why it should be in HBAC module*
*
There's the validation part that's the same for both adding accesstime from ical file to hbacrule and for adding it to timerule template. A possible solution for splitting the modules would be to add a certain class to parameters.py just for validation. Then timerule_templates could have their own module as well as the validation would be usable in multiple other places. There's probably another way around this, though. Ideas are welcome :)
*****
***19)
+        Str('timeruleclass*',
+                cli_name='class'),

why option --class? it does not look descriptive enough for me. How about --timerule-template. Also timeruletemplate instead of timeruleclass looks better to me.
*
*
That seems right, modified it to timeruletemplate.
****20)
+ result = ldap.get_entry(DN(('cn', options['timeruleclass'][0]),
+                                       ('cn', 'timeruletemplates'),
+                                       api.env.basedn))
Please use constants for container timeruletemplates
result is unused var, remove it please.
Done.
*****
***
I have to look closer to icalendar and DS templates :)
So review is not finished, but feel free to fix issues I listed.
For iCalendar, there's of course the RFC: http://tools.ietf.org/html/rfc5545 and the Class of Service is thoroughly described at https://access.redhat.com/documentation/en-US/Red_Hat_Directory_Server/9.0/html/Administration_Guide/Advanced_Entry_Management-Assigning_Class_of_Service.html.

Martin^2

From 86be2ba39e1cb0541ff1892c619c778b67def0b2 Mon Sep 17 00:00:00 2001
From: Stanislav Laznicka <slazn...@redhat.com>
Date: Fri, 19 Feb 2016 08:35:31 +0100
Subject: [PATCH 1/2] HBAC Access Time Rules: icalendar format validation

https://fedorahosted.org/freeipa/ticket/547
---
 API.txt                    |  31 +++++-
 freeipa.spec.in            |   2 +
 ipalib/plugins/hbacrule.py | 230 ++++++++++++++++++++++++++++-----------------
 3 files changed, 174 insertions(+), 89 deletions(-)

diff --git a/API.txt b/API.txt
index e2976e0e2897355bdb7ead438d4b67524f2fb1e8..f54f00036d277094eb3568805169ae337d10561e 100644
--- a/API.txt
+++ b/API.txt
@@ -1656,9 +1656,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: 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: File('accesstime', attribute=True, cli_name='time', multivalue=True, required=False)
 option: Str('addattr*', cli_name='addattr', exclude='webui')
 option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
 option: Str('description', attribute=True, cli_name='desc', multivalue=False, required=False)
@@ -1677,6 +1678,17 @@ 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: File('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_host
 args: 1,6,3
 arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
@@ -1748,9 +1760,10 @@ output: Output('result', <type 'bool'>, None)
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: PrimaryKey('value', None, None)
 command: hbacrule_find
-args: 1,18,4
+args: 1,19,4
 arg: Str('criteria?', noextrawhitespace=False)
 option: StrEnum('accessruletype', attribute=True, autofill=False, cli_name='type', default=u'allow', exclude='webui', multivalue=False, query=True, required=False, values=(u'allow', u'deny'))
+option: File('accesstime', attribute=True, autofill=False, cli_name='time', multivalue=True, query=True, required=False)
 option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
 option: Str('cn', attribute=True, autofill=False, cli_name='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)
@@ -1773,9 +1786,10 @@ 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: File('accesstime', attribute=True, autofill=False, cli_name='time', multivalue=True, required=False)
 option: Str('addattr*', cli_name='addattr', exclude='webui')
 option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
 option: Str('delattr*', cli_name='delattr', exclude='webui')
@@ -1796,6 +1810,17 @@ 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: File('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_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 bc47df4c916bd8f091fc2f70330d95bd116ad187..8c4b6d6e26607d6aee50f0890e8e2d514cefe543 100644
--- a/freeipa.spec.in
+++ b/freeipa.spec.in
@@ -103,6 +103,7 @@ BuildRequires:  python-jwcrypto
 BuildRequires:  custodia
 BuildRequires:  libini_config-devel >= 1.2.0
 BuildRequires:  dbus-python
+BuildRequires:  python-icalendar >= 3.9.2
 
 # Build dependencies for unit tests
 BuildRequires:  libcmocka-devel
@@ -334,6 +335,7 @@ Requires: libsss_autofs
 Requires: autofs
 Requires: libnfsidmap
 Requires: nfs-utils
+Requires: python-icalendar >= 3.9.2
 Requires(post): policycoreutils
 
 Provides: %{alt_name}-client = %{version}
diff --git a/ipalib/plugins/hbacrule.py b/ipalib/plugins/hbacrule.py
index 54487eded21637bcd9d78179ad51c4abdedfc561..401e450ae90a3fa05d3fe4e72aa65382d2480655 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/>.
 
+import icalendar
+from datetime import date
 from ipalib import api, errors
-from ipalib import AccessTime, Str, StrEnum, Bool, DeprecatedParam
+from ipalib import Str, StrEnum, Bool, File, DeprecatedParam
 from ipalib.plugable import Registry
 from ipalib.plugins.baseldap import (
     pkey_to_value,
@@ -31,10 +33,14 @@ from ipalib.plugins.baseldap import (
     LDAPSearch,
     LDAPQuery,
     LDAPAddMember,
-    LDAPRemoveMember)
+    LDAPRemoveMember,
+    LDAPAddAttribute,
+    LDAPRemoveAttribute)
 from ipalib import _, ngettext
 from ipalib import output
 from ipapython.dn import DN
+from ipapython.ipa_log_manager import root_logger
+
 
 __doc__ = _("""
 Host-based access control
@@ -101,6 +107,126 @@ register = Registry()
 #   ipa hbacrule-add-accesstime --time='absolute 201012161032 ~ 201012161033' test1
 
 
+def validate_ical_component(comp, name):
+    if comp.errors:
+        ical_errors = ('{prop}: {err}'
+                       .format(prop=prop, err=e) for prop, e in comp.errors)
+        raise errors.ValidationError(
+            name=name,
+            error=_('There were errors parsing the iCalendar string:\n%(errs)s'
+                    ) % {'errs': '\n'.join(ical_errors)}
+            )
+
+    for prop in comp.required:
+        if prop not in comp.keys():
+            raise errors.ValidationError(
+                name=name,
+                error=_('A required property "%(prop)s" not found '
+                        'in "%(comp)s".') % {'prop': prop, 'comp': comp.name}
+                )
+
+    for prop in comp.keys():
+        # TODO: comp.required might be removed when
+        # https://github.com/collective/icalendar/pull/183 is merged
+        if prop not in (comp.singletons + comp.multiple + comp.required):
+            raise errors.ValidationError(
+                name=name,
+                error=_('A "%(comp)s" component can\'t contain '
+                        'property "%(prop)s".'
+                        ) % {'comp': comp.name, 'prop': prop}
+                )
+
+        if (prop in comp.singletons and isinstance(comp[prop], list)
+                and len(comp[prop]) > 1):
+            raise errors.ValidationError(
+                name=name,
+                error=_('A "%(comp)s" component can\'t have more than '
+                        'one "%(prop)s" property."'
+                        ) % {'comp': comp.name, 'prop': prop}
+                )
+
+
+def validate_icalfile(ugettext, ics):
+    name = 'accesstime'
+
+    try:
+        vcal = icalendar.cal.Calendar().from_ical(ics)
+    except ValueError as e:
+        raise errors.ValidationError(
+            name=name,
+            error=_('Couln\'t parse iCalendar string: %s'
+                    ) % (e, )
+            )
+
+    if(vcal.name != 'VCALENDAR'):  # pylint: disable=no-member
+        raise errors.ValidationError(
+            name=name,
+            error=_('Received object is not a VCALENDAR')
+            )
+
+    validate_ical_component(vcal, name)
+
+    # get a list of all components of a VCALENDAR
+    for comp in vcal.subcomponents:  # pylint: disable=no-member
+        if comp.name != 'VEVENT':
+            root_logger.info(
+                'Found "{comp}" but only VEVENT component is supported.'
+                .format(comp=comp.name))
+            continue
+
+        validate_ical_component(comp, name)
+        for sub in comp.subcomponents:
+            if sub.name != 'VALARM':
+                raise errors.ValidationError(
+                    name=name,
+                    error=_('A VEVENT component can\'t contain '
+                            'subcomponent "%s".') % (sub.name, )
+                    )
+            else:
+                root_logger.info(
+                    'Found "{comp}" but only VEVENT component is '
+                    'supported.'
+                    .format(comp=sub.name))
+
+        # we WILL require DTSTART for VEVENTs
+        if 'DTSTART' not in comp.keys():
+            raise errors.ValidationError(
+                name=name,
+                error=_('DTSTART property is required in VEVENT.')
+                )
+
+        if 'DTEND' in comp.keys():
+            if 'DURATION' in comp.keys():
+                raise errors.ValidationError(
+                    name=name,
+                    error=_('Both DURATION and DTEND set in a VEVENT.')
+                )
+
+            if type(comp['DTSTART'].dt) != type(comp['DTEND'].dt):
+                raise errors.ValidationError(
+                    name=name,
+                    error=_('Different types of DTSTART and DTEND '
+                            'component in VEVENT.')
+                    )
+
+        elif 'DURATION' in comp.keys() and isinstance(comp['DTSTART'].dt, date):
+            """
+            python-icalendar represents DURATION as datetime.timedelta. This,
+            in some cases, blocks us from checking whether it was originally
+            set correctly.
+
+            Example: If DTSTART has value of type DATE, DURATION should be set
+            only as dur-day or dur-week. However, DURATION:PT24H will evaluate
+            as timedelta(1)
+            """
+            if comp['DURATION'].dt.seconds:
+                raise errors.ValidationError(
+                    name=name,
+                    error=_('DURATION is not of type dur-day or dur-week '
+                            'when DTSTART value type is DATE.')
+                    )
+
+
 topic = ('hbac', _('Host-based access control commands'))
 
 def validate_type(ugettext, type):
@@ -237,10 +363,10 @@ class hbacrule(LDAPObject):
             doc=_('Service category the rule applies to'),
             values=(u'all', ),
         ),
-#        AccessTime('accesstime?',
-#            cli_name='time',
-#            label=_('Access time'),
-#        ),
+        File('accesstime*', validate_icalfile,
+             cli_name='time',
+             label=_('Access time'),
+        ),
         Str('description?',
             cli_name='desc',
             label=_('Description'),
@@ -410,86 +536,18 @@ class hbacrule_disable(LDAPQuery):
         )
 
 
-
-class hbacrule_add_accesstime(LDAPQuery):
-    """
-    Add an access time to an 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'])
-        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_add_accesstime(LDAPAddAttribute):
+    __doc__ = _('Add an access time to an HBAC rule.')
+    msg_summary = _('Added allowed access times to the rule "%(value)s"')
+    attribute = 'accesstime'
+
+
+@register()
+class hbacrule_remove_accesstime(LDAPRemoveAttribute):
+    __doc__ = _('Remove access times from an HBAC Rule')
+    msg_summary = _('Removed access times from the rule "%(value)s"')
+    attribute = 'accesstime'
 
 
 @register()
-- 
2.5.0

From 1cce3ff5e755e902454cc5392db62d6f8d47b438 Mon Sep 17 00:00:00 2001
From: Stanislav Laznicka <slazn...@redhat.com>
Date: Fri, 19 Feb 2016 08:40:12 +0100
Subject: [PATCH 2/2] Templating of access time rules for HBAC

https://fedorahosted.org/freeipa/ticket/547
---
 ACI.txt                               |   8 +++
 API.txt                               |  80 +++++++++++++++++++++++++-
 install/share/60basev2.ldif           |   3 +-
 install/share/bootstrap-template.ldif |  16 ++++++
 install/updates/45-timerules.update   |  14 +++++
 install/updates/Makefile.am           |   1 +
 ipalib/constants.py                   |   1 +
 ipalib/plugins/hbacrule.py            | 103 +++++++++++++++++++++++++++++++++-
 8 files changed, 221 insertions(+), 5 deletions(-)
 create mode 100644 install/updates/45-timerules.update

diff --git a/ACI.txt b/ACI.txt
index 24cb332ce6e10c82a5bfab76d084fb6c0277800d..892aa182b6b0e1e40dfe1c432937042da7350b8c 100644
--- a/ACI.txt
+++ b/ACI.txt
@@ -298,6 +298,14 @@ dn: cn=sudorules,cn=sudo,dc=ipa,dc=example
 aci: (targetattr = "cmdcategory || cn || createtimestamp || description || entryusn || externalhost || externaluser || hostcategory || hostmask || ipaenabledflag || ipasudoopt || ipasudorunas || ipasudorunasextgroup || ipasudorunasextuser || ipasudorunasextusergroup || ipasudorunasgroup || ipasudorunasgroupcategory || ipasudorunasusercategory || ipauniqueid || member || memberallowcmd || memberdenycmd || memberhost || memberuser || modifytimestamp || objectclass || sudonotafter || sudonotbefore || sudoorder || usercategory")(targetfilter = "(objectclass=ipasudorule)")(version 3.0;acl "permission:System: Read Sudo Rules";allow (compare,read,search) userdn = "ldap:///all";;)
 dn: dc=ipa,dc=example
 aci: (targetattr = "cn || createtimestamp || description || entryusn || modifytimestamp || objectclass || ou || sudocommand || sudohost || sudonotafter || sudonotbefore || sudooption || sudoorder || sudorunas || sudorunasgroup || sudorunasuser || sudouser")(target = "ldap:///ou=sudoers,dc=ipa,dc=example";)(version 3.0;acl "permission:System: Read Sudoers compat tree";allow (compare,read,search) userdn = "ldap:///anyone";;)
+dn: cn=timeruletemplates,dc=ipa,dc=example
+aci: (targetfilter = "(objectclass=costemplate)")(version 3.0;acl "permission:System: Add Time Rule Template";allow (add) groupdn = "ldap:///cn=System: Add Time Rule Template,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=timeruletemplates,dc=ipa,dc=example
+aci: (targetfilter = "(objectclass=costemplate)")(version 3.0;acl "permission:System: Delete Time Rule Template";allow (delete) groupdn = "ldap:///cn=System: Delete Time Rule Template,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=timeruletemplates,dc=ipa,dc=example
+aci: (targetattr = "accesstime")(targetfilter = "(objectclass=costemplate)")(version 3.0;acl "permission:System: Modify Time Rule Template";allow (write) groupdn = "ldap:///cn=System: Modify Time Rule Template,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=timeruletemplates,dc=ipa,dc=example
+aci: (targetattr = "accesstime || cn || createtimestamp || entryusn || modifytimestamp || objectclass")(targetfilter = "(objectclass=costemplate)")(version 3.0;acl "permission:System: Read Time Rule Template";allow (compare,read,search) groupdn = "ldap:///cn=System: Read Time Rule Template,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: cn=trusts,dc=ipa,dc=example
 aci: (targetattr = "cn || createtimestamp || entryusn || ipantflatname || ipantsecurityidentifier || ipantsidblacklistincoming || ipantsidblacklistoutgoing || ipanttrustdirection || ipanttrusteddomainsid || ipanttrustpartner || modifytimestamp || objectclass")(version 3.0;acl "permission:System: Read Trust Information";allow (compare,read,search) userdn = "ldap:///all";;)
 dn: cn=trusts,dc=ipa,dc=example
diff --git a/API.txt b/API.txt
index f54f00036d277094eb3568805169ae337d10561e..118d960955a1e2e1d1dd508f8bb780a1c373af23 100644
--- a/API.txt
+++ b/API.txt
@@ -1656,7 +1656,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,17,3
+args: 1,18,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: File('accesstime', attribute=True, cli_name='time', multivalue=True, required=False)
@@ -1673,6 +1673,7 @@ option: Str('setattr*', cli_name='setattr', exclude='webui')
 option: DeprecatedParam('sourcehost_host', attribute=True, cli_name='sourcehost_host', multivalue=False, required=False)
 option: DeprecatedParam('sourcehost_hostgroup', attribute=True, cli_name='sourcehost_hostgroup', multivalue=False, required=False)
 option: DeprecatedParam('sourcehostcategory', attribute=True, cli_name='sourcehostcategory', multivalue=False, required=False)
+option: Str('timeruletemplate', attribute=True, cli_name='timerule_template', multivalue=True, required=False)
 option: StrEnum('usercategory', attribute=True, cli_name='usercat', multivalue=False, required=False, values=(u'all',))
 option: Str('version?', exclude='webui')
 output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
@@ -1689,6 +1690,17 @@ 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_template
+args: 1,5,3
+arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
+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('timeruletemplate', alwaysask=True, attribute=True, cli_name='timerule_template', multivalue=True, required=False)
+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)
@@ -1760,7 +1772,7 @@ output: Output('result', <type 'bool'>, None)
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: PrimaryKey('value', None, None)
 command: hbacrule_find
-args: 1,19,4
+args: 1,20,4
 arg: Str('criteria?', noextrawhitespace=False)
 option: StrEnum('accessruletype', attribute=True, autofill=False, cli_name='type', default=u'allow', exclude='webui', multivalue=False, query=True, required=False, values=(u'allow', u'deny'))
 option: File('accesstime', attribute=True, autofill=False, cli_name='time', multivalue=True, query=True, required=False)
@@ -1779,6 +1791,7 @@ option: DeprecatedParam('sourcehost_host', attribute=True, autofill=False, cli_n
 option: DeprecatedParam('sourcehost_hostgroup', attribute=True, autofill=False, cli_name='sourcehost_hostgroup', multivalue=False, query=True, required=False)
 option: DeprecatedParam('sourcehostcategory', attribute=True, autofill=False, cli_name='sourcehostcategory', multivalue=False, query=True, required=False)
 option: Int('timelimit?', autofill=False, minvalue=0)
+option: Str('timeruletemplate', attribute=True, autofill=False, cli_name='timerule_template', multivalue=True, query=True, required=False)
 option: StrEnum('usercategory', attribute=True, autofill=False, cli_name='usercat', multivalue=False, query=True, required=False, values=(u'all',))
 option: Str('version?', exclude='webui')
 output: Output('count', <type 'int'>, None)
@@ -1786,7 +1799,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,19,3
+args: 1,20,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: File('accesstime', attribute=True, autofill=False, cli_name='time', multivalue=True, required=False)
@@ -1805,6 +1818,7 @@ option: Str('setattr*', cli_name='setattr', exclude='webui')
 option: DeprecatedParam('sourcehost_host', attribute=True, autofill=False, cli_name='sourcehost_host', multivalue=False, required=False)
 option: DeprecatedParam('sourcehost_hostgroup', attribute=True, autofill=False, cli_name='sourcehost_hostgroup', multivalue=False, required=False)
 option: DeprecatedParam('sourcehostcategory', attribute=True, autofill=False, cli_name='sourcehostcategory', multivalue=False, required=False)
+option: Str('timeruletemplate', attribute=True, autofill=False, cli_name='timerule_template', multivalue=True, required=False)
 option: StrEnum('usercategory', attribute=True, autofill=False, cli_name='usercat', multivalue=False, required=False, values=(u'all',))
 option: Str('version?', exclude='webui')
 output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
@@ -1821,6 +1835,17 @@ 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_template
+args: 1,5,3
+arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
+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('timeruletemplate', alwaysask=True, attribute=True, cli_name='timerule_template', multivalue=True, required=False)
+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)
@@ -4859,6 +4884,55 @@ 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: timeruletemplate_add
+args: 1,6,3
+arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, required=True)
+option: File('accesstime', attribute=True, cli_name='time', multivalue=False, required=True)
+option: Str('addattr*', cli_name='addattr', exclude='webui')
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Str('setattr*', cli_name='setattr', 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: timeruletemplate_del
+args: 1,2,3
+arg: Str('cn', attribute=True, cli_name='name', multivalue=True, primary_key=True, query=True, required=True)
+option: Flag('continue', autofill=True, cli_name='continue', default=False)
+option: Str('version?', exclude='webui')
+output: Output('result', <type 'dict'>, None)
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: ListOfPrimaryKeys('value', None, None)
+command: timeruletemplate_find
+args: 1,8,4
+arg: Str('criteria?', noextrawhitespace=False)
+option: File('accesstime', attribute=True, autofill=False, cli_name='time', multivalue=False, query=True, required=False)
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('cn', attribute=True, autofill=False, cli_name='name', multivalue=False, primary_key=True, query=True, required=False)
+option: Flag('pkey_only?', autofill=True, default=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Int('sizelimit?', autofill=False, minvalue=0)
+option: Int('timelimit?', autofill=False, minvalue=0)
+option: Str('version?', exclude='webui')
+output: Output('count', <type 'int'>, None)
+output: ListOfEntries('result', (<type 'list'>, <type 'tuple'>), Gettext('A list of LDAP entries', domain='ipa', localedir=None))
+output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
+output: Output('truncated', <type 'bool'>, None)
+command: timeruletemplate_mod
+args: 1,8,3
+arg: Str('cn', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True)
+option: File('accesstime', attribute=True, autofill=False, cli_name='time', multivalue=False, required=False)
+option: Str('addattr*', cli_name='addattr', exclude='webui')
+option: Flag('all', autofill=True, cli_name='all', default=False, exclude='webui')
+option: Str('delattr*', cli_name='delattr', exclude='webui')
+option: Flag('raw', autofill=True, cli_name='raw', default=False, exclude='webui')
+option: Flag('rights', autofill=True, default=False)
+option: Str('setattr*', cli_name='setattr', 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: topologysegment_add
 args: 2,13,3
 arg: Str('topologysuffixcn', cli_name='topologysuffix', multivalue=False, primary_key=True, query=True, required=True)
diff --git a/install/share/60basev2.ldif b/install/share/60basev2.ldif
index 00712ddda2c548b7f7924a012f3f68499f2f01da..b070b32c6d6f978e66bd66f5ef15dfc7dd7b2dd0 100644
--- a/install/share/60basev2.ldif
+++ b/install/share/60basev2.ldif
@@ -37,7 +37,8 @@ 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 'timeruleTemplate' DESC 'CNs of the timerule templates' EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'IPA v4.3' )
+objectClasses: (2.16.840.1.113730.3.8.4.7 NAME 'ipaHBACRule' SUP ipaAssociation STRUCTURAL MUST accessRuleType MAY ( sourceHost $ sourceHostCategory $ serviceCategory $ memberService $ externalHost $ timeruleTemplate $ accessTime ) 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' )
diff --git a/install/share/bootstrap-template.ldif b/install/share/bootstrap-template.ldif
index 628a8e2e0f5483b9f6f565b0c7d11eb000a5912d..cfa7f39d27caa1f5fb2641a992b65742dfd31a4d 100644
--- a/install/share/bootstrap-template.ldif
+++ b/install/share/bootstrap-template.ldif
@@ -272,6 +272,22 @@ description: IPA server hosts
 cn: ipaservers
 ipaUniqueID: autogenerate
 
+dn: cn=timeruleTemplates,$SUFFIX
+changetype: add
+objectClass: top
+objectClass: nsContainer
+cn: timeruleTemplates
+
+dn: cn=cosTimerulesDef,cn=hbac,$SUFFIX
+changetype: add
+objectClass: top
+objectClass: ldapsubentry
+objectClass: cosSuperDefinition
+objectClass: cosClassicDefinition
+cosTemplateDn: cn=timeruleTemplates,$SUFFIX
+cosAttribute: accessTime default merge-schemes
+cosSpecifier: timeruleTemplate
+
 dn: cn=sshd,cn=hbacservices,cn=hbac,$SUFFIX
 changetype: add
 objectclass: ipahbacservice
diff --git a/install/updates/45-timerules.update b/install/updates/45-timerules.update
new file mode 100644
index 0000000000000000000000000000000000000000..beef69e50ce73351908ef8bac334e268626c2dd0
--- /dev/null
+++ b/install/updates/45-timerules.update
@@ -0,0 +1,14 @@
+# Add needed LDAP objects
+dn: cn=timeruleTemplates,$SUFFIX
+default: objectClass: top
+default: objectClass: nsContainer
+default: cn: timeruleTemplates
+
+dn: cn=cosTimerulesDef,cn=hbac,$SUFFIX
+default: objectClass: top
+default: objectClass: ldapsubentry
+default: objectClass: cosSuperDefinition
+default: objectClass: cosClassicDefinition
+default: cosTemplateDn: cn=timeruleTemplates,$SUFFIX
+default: cosAttribute: accessTime default merge-schemes
+default: cosSpecifier: timeruleTemplate
diff --git a/install/updates/Makefile.am b/install/updates/Makefile.am
index 3edc21473d676bd282e9ea2b88769c097fb8a63a..700429d6be93a94923da669420c74ace40a97703 100644
--- a/install/updates/Makefile.am
+++ b/install/updates/Makefile.am
@@ -38,6 +38,7 @@ app_DATA =				\
 	40-vault.update			\
 	41-caacl.update			\
 	45-roles.update			\
+	45-timerules.update		\
 	50-7_bit_check.update	        \
 	50-dogtag10-migration.update	\
 	50-groupuuid.update		\
diff --git a/ipalib/constants.py b/ipalib/constants.py
index 1ff9ccc7efe0c3be0fc3cf2f21352e89ef94ad6e..6f0ab4b423afebf2ae9e345603b791fffc537ad9 100644
--- a/ipalib/constants.py
+++ b/ipalib/constants.py
@@ -121,6 +121,7 @@ DEFAULT_CONFIG = (
     ('container_certprofile', DN(('cn', 'certprofiles'), ('cn', 'ca'))),
     ('container_topology', DN(('cn', 'topology'), ('cn', 'ipa'), ('cn', 'etc'))),
     ('container_caacl', DN(('cn', 'caacls'), ('cn', 'ca'))),
+    ('container_timerules', DN(('cn', 'timeruletemplates'))),
 
     # Ports, hosts, and URIs:
     ('xmlrpc_uri', 'http://localhost:8888/ipa/xml'),
diff --git a/ipalib/plugins/hbacrule.py b/ipalib/plugins/hbacrule.py
index 401e450ae90a3fa05d3fe4e72aa65382d2480655..b29dbd2627d7a614fe7f793547673e63cd8c35a5 100644
--- a/ipalib/plugins/hbacrule.py
+++ b/ipalib/plugins/hbacrule.py
@@ -227,6 +227,81 @@ def validate_icalfile(ugettext, ics):
                     )
 
 
+@register()
+class timeruletemplate(LDAPObject):
+    """
+    Timerule templates
+    """
+    container_dn = api.env.container_timerules
+    object_name = _('Time rule template')
+    object_name_plural = _('Time rule templates')
+    object_class = ['top', 'costemplate', 'extensibleobject']
+    permission_filter_objectclasses = ['costemplate']
+    default_attributes = ['cn', 'accesstime']
+    managed_permissions = {
+        'System: Read Time Rule Template': {
+            'replaces_global_anonymous_aci': True,
+            'ipapermright': {'read', 'search', 'compare'},
+            'ipapermdefaultattr': {
+                'cn', 'objectclass', 'accesstime'
+            },
+        },
+        'System: Add Time Rule Template': {
+            'ipapermright': {'add'},
+            'default_privileges': {'HBAC Administrator'},
+        },
+        'System: Delete Time Rule Template': {
+            'ipapermright': {'delete'},
+            'default_privileges': {'HBAC Administrator'},
+        },
+        'System: Modify Time Rule Template': {
+            'ipapermright': {'write'},
+            'ipapermdefaultattr': {'accesstime'},
+            'default_privileges': {'HBAC Administrator'},
+        },
+    }
+
+    takes_params = (
+        Str('cn', cli_name='name',
+            label=_('Template name'),
+            primary_key=True),
+        File('accesstime', validate_icalfile,
+             cli_name='time',
+             label=_('Access time'),
+             ),
+    )
+
+
+@register()
+class timeruletemplate_add(LDAPCreate):
+    __doc__ = _('Create new timerule template.')
+
+
+@register()
+class timeruletemplate_del(LDAPDelete):
+    __doc__ = _('Delete a timerule template.')
+
+    msg_summary = _('Deleted template "%(value)s"')
+
+
+@register()
+class timeruletemplate_mod(LDAPUpdate):
+    __doc__ = _('Modify a timerule template.')
+
+    msg_summary = _('Modified a template "%(value)s"')
+
+
+@register()
+class timeruletemplate_find(LDAPSearch):
+    __doc__ = _('Search for timerule templates.')
+
+    msg_summary = ngettext(
+        '%(count)d timerule template matched',
+        '%(count)d timerule templates matched',
+        0,
+    )
+
+
 topic = ('hbac', _('Host-based access control commands'))
 
 def validate_type(ugettext, type):
@@ -367,6 +442,9 @@ class hbacrule(LDAPObject):
              cli_name='time',
              label=_('Access time'),
         ),
+        Str('timeruletemplate*',
+            cli_name='timerule_template',
+            label=_('Template name')),
         Str('description?',
             cli_name='desc',
             label=_('Description'),
@@ -460,7 +538,6 @@ class hbacrule_mod(LDAPUpdate):
         return dn
 
 
-
 @register()
 class hbacrule_find(LDAPSearch):
     __doc__ = _('Search for HBAC rules.')
@@ -551,6 +628,30 @@ class hbacrule_remove_accesstime(LDAPRemoveAttribute):
 
 
 @register()
+class hbacrule_add_accesstime_template(LDAPAddAttribute):
+    __doc__ = _('Add access time from a timerule template to an HBAC rule')
+    msg_summary = _('Added template to the rule "%(value)s"')
+    attribute = 'timeruletemplate'
+
+    def pre_callback(self, ldap, dn, *keys, **options):
+        assert isinstance(dn, DN)
+        try:
+            ldap.get_entry(DN(('cn', options['timeruletemplate'][0]),
+                              api.env.container_timerules))
+        except errors.NotFound:
+            raise errors.NotFound('cn={}'.format(options['timeruletemplate']))
+
+        return dn
+
+
+@register()
+class hbacrule_remove_accesstime_template(LDAPRemoveAttribute):
+    __doc__ = _('Remove access time template from an HBAC rule')
+    msg_summary = _('Removed template from the rule "%(value)s"')
+    attribute = 'timeruletemplate'
+
+
+@register()
 class hbacrule_add_user(LDAPAddMember):
     __doc__ = _('Add users and groups to an HBAC rule.')
 
-- 
2.5.0

-- 
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