From da7e5fa75ad8271f7320676ea1e09222b899eb20 Mon Sep 17 00:00:00 2001
From: Stanislav Laznicka <slaznick@redhat.com>
Date: Fri, 19 Feb 2016 08:35:31 +0100
Subject: [PATCH] Prototype of timerules as LDAP objects

https://fedorahosted.org/freeipa/ticket/547
---
 ACI.txt                               |  20 ++-
 API.txt                               |  98 +++++++++++
 VERSION                               |   4 +-
 freeipa.spec.in                       |   2 +
 install/share/60basev2.ldif           |   3 +
 install/share/bootstrap-template.ldif |   6 +
 install/share/indices.ldif            |  10 ++
 install/share/memberof-conf.ldif      |   4 +-
 install/updates/20-indices.update     |   9 ++
 install/updates/40-delegation.update  |   8 +
 install/updates/45-roles.update       |   3 +
 install/updates/45-timerules.update   |   5 +
 install/updates/Makefile.am           |   1 +
 ipaclient/plugins/timerule.py         |  59 +++++++
 ipalib/constants.py                   |   1 +
 ipaserver/plugins/hbacrule.py         | 160 +++++++++---------
 ipaserver/plugins/timerule.py         | 295 ++++++++++++++++++++++++++++++++++
 17 files changed, 604 insertions(+), 84 deletions(-)
 create mode 100644 install/updates/45-timerules.update
 create mode 100644 ipaclient/plugins/timerule.py
 create mode 100644 ipaserver/plugins/timerule.py

diff --git a/ACI.txt b/ACI.txt
index fddd598..95f4643 100644
--- a/ACI.txt
+++ b/ACI.txt
@@ -97,15 +97,15 @@ aci: (targetattr = "businesscategory || cn || createtimestamp || description ||
 dn: cn=groups,cn=accounts,dc=ipa,dc=example
 aci: (targetfilter = "(|(objectclass=ipausergroup)(objectclass=posixgroup))")(version 3.0;acl "permission:System: Remove Groups";allow (delete) groupdn = "ldap:///cn=System: Remove Groups,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: cn=hbac,dc=ipa,dc=example
-aci: (targetfilter = "(objectclass=ipahbacrule)")(version 3.0;acl "permission:System: Add HBAC Rule";allow (add) groupdn = "ldap:///cn=System: Add HBAC Rule,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+aci: (targetfilter = "(|(objectclass=ipahbacrule)(objectclass=ipahbacrulev2))")(version 3.0;acl "permission:System: Add HBAC Rule";allow (add) groupdn = "ldap:///cn=System: Add HBAC Rule,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: cn=hbac,dc=ipa,dc=example
-aci: (targetfilter = "(objectclass=ipahbacrule)")(version 3.0;acl "permission:System: Delete HBAC Rule";allow (delete) groupdn = "ldap:///cn=System: Delete HBAC Rule,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+aci: (targetfilter = "(|(objectclass=ipahbacrule)(objectclass=ipahbacrulev2))")(version 3.0;acl "permission:System: Delete HBAC Rule";allow (delete) groupdn = "ldap:///cn=System: Delete HBAC Rule,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 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";)
+aci: (targetattr = "externalhost || ipamembertimerule || memberhost || memberservice || memberuser")(targetfilter = "(|(objectclass=ipahbacrule)(objectclass=ipahbacrulev2))")(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 || cn || description || hostcategory || ipaenabledflag || ipamembertimerule || servicecategory || sourcehost || sourcehostcategory || usercategory")(targetfilter = "(|(objectclass=ipahbacrule)(objectclass=ipahbacrulev2))")(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 || cn || createtimestamp || description || entryusn || externalhost || hostcategory || ipaenabledflag || ipamembertimerule || ipauniqueid || member || memberhost || memberservice || memberuser || modifytimestamp || objectclass || servicecategory || sourcehost || sourcehostcategory || usercategory")(targetfilter = "(|(objectclass=ipahbacrule)(objectclass=ipahbacrulev2))")(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
@@ -326,6 +326,16 @@ 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=timerules,dc=ipa,dc=example
+aci: (targetfilter = "(objectclass=ipatimerule)")(version 3.0;acl "permission:System: Add Time Rule";allow (add) groupdn = "ldap:///cn=System: Add Time Rule,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=timerules,dc=ipa,dc=example
+aci: (targetfilter = "(objectclass=ipatimerule)")(version 3.0;acl "permission:System: Delete Time Rule";allow (delete) groupdn = "ldap:///cn=System: Delete Time Rule,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=timerules,dc=ipa,dc=example
+aci: (targetattr = "accesstime || description")(targetfilter = "(objectclass=ipatimerule)")(version 3.0;acl "permission:System: Modify Time Rule";allow (write) groupdn = "ldap:///cn=System: Modify Time Rule,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=timerules,dc=ipa,dc=example
+aci: (targetattr = "accesstime || cn || createtimestamp || description || entryusn || modifytimestamp || objectclass")(targetfilter = "(objectclass=ipatimerule)")(version 3.0;acl "permission:System: Read Time Rule";allow (compare,read,search) userdn = "ldap:///all";)
+dn: cn=timerules,dc=ipa,dc=example
+aci: (targetattr = "memberof")(targetfilter = "(objectclass=ipatimerule)")(version 3.0;acl "permission:System: Read Time Rule Membership";allow (compare,read,search) userdn = "ldap:///all";)
 dn: cn=trusts,dc=ipa,dc=example
 aci: (targetattr = "cn || createtimestamp || entryusn || ipantadditionalsuffixes || 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 5b83bfb..8e4c16c 100644
--- a/API.txt
+++ b/API.txt
@@ -1955,6 +1955,17 @@ option: Str('version?')
 output: Output('completed', type=[<type 'int'>])
 output: Output('failed', type=[<type 'dict'>])
 output: Entry('result')
+command: hbacrule_add_timerule/1
+args: 1,5,3
+arg: Str('cn', cli_name='name')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Flag('no_members', autofill=True, default=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False)
+option: Str('timerule*', alwaysask=True, cli_name='timerules')
+option: Str('version?')
+output: Output('completed', type=[<type 'int'>])
+output: Output('failed', type=[<type 'dict'>])
+output: Entry('result')
 command: hbacrule_add_user/1
 args: 1,6,3
 arg: Str('cn', cli_name='name')
@@ -2070,6 +2081,17 @@ option: Str('version?')
 output: Output('completed', type=[<type 'int'>])
 output: Output('failed', type=[<type 'dict'>])
 output: Entry('result')
+command: hbacrule_remove_timerule/1
+args: 1,5,3
+arg: Str('cn', cli_name='name')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Flag('no_members', autofill=True, default=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False)
+option: Str('timerule*', alwaysask=True, cli_name='timerules')
+option: Str('version?')
+output: Output('completed', type=[<type 'int'>])
+output: Output('failed', type=[<type 'dict'>])
+output: Entry('result')
 command: hbacrule_remove_user/1
 args: 1,6,3
 arg: Str('cn', cli_name='name')
@@ -5297,6 +5319,74 @@ option: Str('version?')
 output: Entry('result')
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
 output: PrimaryKey('value')
+command: timerule_add/1
+args: 1,8,3
+arg: Str('cn', cli_name='name')
+option: Str('accesstime?', cli_name='time')
+option: Str('addattr*', cli_name='addattr')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Str('description?')
+option: Flag('no_members', autofill=True, default=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False)
+option: Str('setattr*', cli_name='setattr')
+option: Str('version?')
+output: Entry('result')
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
+output: PrimaryKey('value')
+command: timerule_del/1
+args: 1,2,3
+arg: Str('cn+', cli_name='name')
+option: Flag('continue', autofill=True, cli_name='continue', default=False)
+option: Str('version?')
+output: Output('result', type=[<type 'dict'>])
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
+output: ListOfPrimaryKeys('value')
+command: timerule_find/1
+args: 1,12,4
+arg: Str('criteria?')
+option: Str('accesstime?', autofill=False, cli_name='time')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Str('cn?', autofill=False, cli_name='name')
+option: Str('description?', autofill=False)
+option: Str('in_hbacrule*', cli_name='in_hbacrules')
+option: Flag('no_members', autofill=True, default=True)
+option: Str('not_in_hbacrule*', cli_name='not_in_hbacrules')
+option: Flag('pkey_only?', autofill=True, default=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False)
+option: Int('sizelimit?', autofill=False)
+option: Int('timelimit?', autofill=False)
+option: Str('version?')
+output: Output('count', type=[<type 'int'>])
+output: ListOfEntries('result')
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
+output: Output('truncated', type=[<type 'bool'>])
+command: timerule_mod/1
+args: 1,10,3
+arg: Str('cn', cli_name='name')
+option: Str('accesstime?', autofill=False, cli_name='time')
+option: Str('addattr*', cli_name='addattr')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Str('delattr*', cli_name='delattr')
+option: Str('description?', autofill=False)
+option: Flag('no_members', autofill=True, default=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False)
+option: Flag('rights', autofill=True, default=False)
+option: Str('setattr*', cli_name='setattr')
+option: Str('version?')
+output: Entry('result')
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
+output: PrimaryKey('value')
+command: timerule_show/1
+args: 1,5,3
+arg: Str('cn', cli_name='name')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Flag('no_members', autofill=True, default=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False)
+option: Flag('rights', autofill=True, default=False)
+option: Str('version?')
+output: Entry('result')
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
+output: PrimaryKey('value')
 command: topic_find/1
 args: 1,4,4
 arg: Str('criteria?')
@@ -6366,6 +6456,7 @@ default: hbacrule_add/1
 default: hbacrule_add_host/1
 default: hbacrule_add_service/1
 default: hbacrule_add_sourcehost/1
+default: hbacrule_add_timerule/1
 default: hbacrule_add_user/1
 default: hbacrule_del/1
 default: hbacrule_disable/1
@@ -6375,6 +6466,7 @@ default: hbacrule_mod/1
 default: hbacrule_remove_host/1
 default: hbacrule_remove_service/1
 default: hbacrule_remove_sourcehost/1
+default: hbacrule_remove_timerule/1
 default: hbacrule_remove_user/1
 default: hbacrule_show/1
 default: hbacsvc/1
@@ -6644,6 +6736,12 @@ default: sudorule_remove_runasgroup/1
 default: sudorule_remove_runasuser/1
 default: sudorule_remove_user/1
 default: sudorule_show/1
+default: timerule/1
+default: timerule_add/1
+default: timerule_del/1
+default: timerule_find/1
+default: timerule_mod/1
+default: timerule_show/1
 default: topic/1
 default: topic_find/1
 default: topic_show/1
diff --git a/VERSION b/VERSION
index a8b89ed..5559e7e 100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=212
-# Last change: ab: service: add flag to allow S4U2Self
+IPA_API_VERSION_MINOR=213
+# Last change: slaznick: added commands for handling time rules
diff --git a/freeipa.spec.in b/freeipa.spec.in
index e3ad5b6..2ec4a10 100644
--- a/freeipa.spec.in
+++ b/freeipa.spec.in
@@ -109,6 +109,7 @@ BuildRequires:  dbus-python
 BuildRequires:  python-netifaces >= 0.10.4
 BuildRequires:  python-libsss_nss_idmap
 BuildRequires:  python-sss
+BuildRequires:  python-icalendar >= 3.9.2
 
 # Build dependencies for unit tests
 BuildRequires:  libcmocka-devel
@@ -344,6 +345,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/install/share/60basev2.ldif b/install/share/60basev2.ldif
index 00712dd..27f7b00 100644
--- a/install/share/60basev2.ldif
+++ b/install/share/60basev2.ldif
@@ -38,6 +38,9 @@ attributeTypes: (2.16.840.1.113730.3.8.3.12 NAME 'sourceHostCategory' DESC 'Addi
 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' )
+objectClasses: (2.16.840.1.113730.3.8.12.37 NAME 'ipaTimeRule' SUP top STRUCTURAL MUST ( cn $ accessTime ) MAY ( memberOf $ description ) X-ORIGIN 'IPA v4.4')
+attributeTypes: (2.16.840.1.113730.3.8.11.76 NAME 'ipaMemberTimeRule' DESC 'Reference to a time rule describing some period of time' SUP distinguishedName EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 X-ORIGIN 'IPA v4.4' )
+objectClasses: (2.16.840.1.113730.3.8.12.38 NAME 'ipaHBACRuleV2' SUP ipaAssociation STRUCTURAL MUST accessRuleType MAY ( sourceHost $ sourceHostCategory $ serviceCategory $ memberService $ externalHost $ accessTime $ ipaMemberTimeRule ) X-ORIGIN 'IPA v4.4' )
 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 da12ddf..08ef422 100644
--- a/install/share/bootstrap-template.ldif
+++ b/install/share/bootstrap-template.ldif
@@ -284,6 +284,12 @@ description: IPA server hosts
 cn: ipaservers
 ipaUniqueID: autogenerate
 
+dn: cn=timerules,$SUFFIX
+changetype: add
+objectClass: top
+objectClass: nsContainer
+cn: timerules
+
 dn: cn=sshd,cn=hbacservices,cn=hbac,$SUFFIX
 changetype: add
 objectclass: ipahbacservice
diff --git a/install/share/indices.ldif b/install/share/indices.ldif
index d853266..3dd2f56 100644
--- a/install/share/indices.ldif
+++ b/install/share/indices.ldif
@@ -158,6 +158,16 @@ nsIndexType: eq
 nsIndexType: pres
 nsIndexType: sub
 
+dn: cn=ipaMemberTimeRule,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
+changetype: add
+cn: ipaMemberTimeRule
+ObjectClass: top
+ObjectClass: nsIndex
+nsSystemIndex: false
+nsIndexType: eq
+nsIndexType: pres
+nsIndexType: sub
+
 dn: cn=managedby,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
 changetype: add
 cn: managedby
diff --git a/install/share/memberof-conf.ldif b/install/share/memberof-conf.ldif
index 79ad647..7a83b0c 100644
--- a/install/share/memberof-conf.ldif
+++ b/install/share/memberof-conf.ldif
@@ -8,4 +8,6 @@ memberofgroupattr: memberUser
 -
 add: memberofgroupattr
 memberofgroupattr: memberHost
-
+-
+add: memberofgroupattr
+memberofgroupattr: ipaMemberTimeRule
diff --git a/install/updates/20-indices.update b/install/updates/20-indices.update
index 74961d7..049b0fe 100644
--- a/install/updates/20-indices.update
+++ b/install/updates/20-indices.update
@@ -99,6 +99,15 @@ only:nsIndexType: eq
 only:nsIndexType: pres
 only:nsIndexType: sub
 
+dn: cn=ipaMemberTimeRule,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
+default:cn: ipaMemberTimeRule
+default:ObjectClass: top
+default:ObjectClass: nsIndex
+default:nsSystemIndex: false
+only:nsIndexType: eq
+only:nsIndexType: pres
+only:nsIndexType: sub
+
 dn: cn=managedby,cn=index,cn=userRoot,cn=ldbm database,cn=plugins,cn=config
 default:cn: managedby
 default:ObjectClass: top
diff --git a/install/updates/40-delegation.update b/install/updates/40-delegation.update
index 259cbdb..2ef98f2 100644
--- a/install/updates/40-delegation.update
+++ b/install/updates/40-delegation.update
@@ -34,6 +34,14 @@ default:objectClass: top
 default:cn: Sudo Administrator
 default:description: Sudo Administrator
 
+# Time Rules
+dn: cn=Time Rules Administrator,cn=privileges,cn=pbac,$SUFFIX
+default:objectClass: nestedgroup
+default:objectClass: groupofnames
+default:objectClass: top
+default:cn: Time Rules Administrator
+default:description: Time Rules Administrator
+
 # Password Policy
 dn: cn=Password Policy Administrator,cn=privileges,cn=pbac,$SUFFIX
 default:objectClass: nestedgroup
diff --git a/install/updates/45-roles.update b/install/updates/45-roles.update
index fb28464..f2863f1 100644
--- a/install/updates/45-roles.update
+++ b/install/updates/45-roles.update
@@ -71,6 +71,9 @@ add:member: cn=IT Security Specialist,cn=roles,cn=accounts,$SUFFIX
 dn: cn=Sudo administrator,cn=privileges,cn=pbac,$SUFFIX
 add:member: cn=IT Security Specialist,cn=roles,cn=accounts,$SUFFIX
 
+dn: cn=Time Rules Administrator,cn=privileges,cn=pbac,$SUFFIX
+add:member: cn=IT Security Specialist,cn=roles,cn=accounts,$SUFFIX
+
 dn: cn=Security Architect,cn=roles,cn=accounts,$SUFFIX
 default:objectClass: groupofnames
 default:objectClass: nestedgroup
diff --git a/install/updates/45-timerules.update b/install/updates/45-timerules.update
new file mode 100644
index 0000000..6dade65
--- /dev/null
+++ b/install/updates/45-timerules.update
@@ -0,0 +1,5 @@
+# Add needed LDAP objects
+dn: cn=timerules,$SUFFIX
+default: objectClass: top
+default: objectClass: nsContainer
+default: cn: timerules
diff --git a/install/updates/Makefile.am b/install/updates/Makefile.am
index 455fd20..15ec842 100644
--- a/install/updates/Makefile.am
+++ b/install/updates/Makefile.am
@@ -41,6 +41,7 @@ app_DATA =				\
 	41-caacl.update			\
 	41-lightweight-cas.update	\
 	45-roles.update			\
+	45-timerules.update		\
 	50-7_bit_check.update	        \
 	50-dogtag10-migration.update	\
 	50-groupuuid.update		\
diff --git a/ipaclient/plugins/timerule.py b/ipaclient/plugins/timerule.py
new file mode 100644
index 0000000..989b26f
--- /dev/null
+++ b/ipaclient/plugins/timerule.py
@@ -0,0 +1,59 @@
+#
+# Copyright (C) 2016  FreeIPA Contributors see COPYING for license
+#
+
+import six
+
+from ipaclient.frontend import MethodOverride
+from ipalib import File
+from ipalib import errors
+from ipalib.plugable import Registry
+from ipalib import _
+
+if six.PY3:
+    unicode = str
+
+register = Registry()
+
+
+def get_accesstime(options):
+    if not('icalfile' in options or 'accesstime' in options):
+        raise errors.RequirementError(name='accesstime')
+    if 'icalfile' in options:
+        if 'accesstime' in options:
+            raise errors.OverlapError(names=['time', 'icalfile'])
+        options['accesstime'] = unicode(
+            options['icalfile'].encode('unicode-escape'))
+        del(options['icalfile'])
+    return options['accesstime']
+
+
+@register(override=True, no_fail=True)
+class timerule_add(MethodOverride):
+    takes_options = (
+        File('icalfile?',
+             label=_("iCalendar file"),
+             doc=_("File containing the iCalendar string"),
+             ),
+    )
+
+    def forward(self, *args, **options):
+        options['accesstime'] = get_accesstime(options)
+        return super(timerule_add, self).forward(*args, **options)
+
+
+@register(override=True, no_fail=True)
+class timerule_mod(MethodOverride):
+    takes_options = (
+        File('icalfile?',
+             label=_("iCalendar file"),
+             doc=_("File containing the iCalendar string"),
+             ),
+    )
+
+    def forward(self, *args, **options):
+        try:
+            options['accesstime'] = get_accesstime(options)
+        except errors.RequirementError:
+            pass
+        return super(timerule_mod, self).forward(*args, **options)
diff --git a/ipalib/constants.py b/ipalib/constants.py
index 9b351e2..03ed289 100644
--- a/ipalib/constants.py
+++ b/ipalib/constants.py
@@ -125,6 +125,7 @@
     ('container_ca', DN(('cn', 'cas'), ('cn', 'ca'))),
     ('container_dnsservers', DN(('cn', 'servers'), ('cn', 'dns'))),
     ('container_custodia', DN(('cn', 'custodia'), ('cn', 'ipa'), ('cn', 'etc'))),
+    ('container_timerules', DN(('cn', 'timerules'))),
 
     # Ports, hosts, and URIs:
     ('xmlrpc_uri', 'http://localhost:8888/ipa/xml'),
diff --git a/ipaserver/plugins/hbacrule.py b/ipaserver/plugins/hbacrule.py
index 7d3e485..9ab8aa2 100644
--- a/ipaserver/plugins/hbacrule.py
+++ b/ipaserver/plugins/hbacrule.py
@@ -18,7 +18,7 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 from ipalib import api, errors
-from ipalib import AccessTime, Str, StrEnum, Bool
+from ipalib import Str, StrEnum, Bool
 from ipalib.plugable import Registry
 from .baseldap import (
     pkey_to_value,
@@ -86,21 +86,6 @@
 
 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'
 
 def validate_type(ugettext, type):
@@ -130,14 +115,15 @@ class hbacrule(LDAPObject):
     container_dn = api.env.container_hbac
     object_name = _('HBAC rule')
     object_name_plural = _('HBAC rules')
-    object_class = ['ipaassociation', 'ipahbacrule']
-    permission_filter_objectclasses = ['ipahbacrule']
+    object_class = ['ipaassociation']
+    possible_objectclasses = ['ipahbacrule', 'ipahbacrulev2']
+    permission_filter_objectclasses = ['ipahbacrule', 'ipahbacrulev2']
     default_attributes = [
         'cn', 'ipaenabledflag',
         'description', 'usercategory', 'hostcategory',
         'servicecategory', 'ipaenabledflag',
         'memberuser', 'sourcehost', 'memberhost', 'memberservice',
-        'externalhost',
+        'externalhost', 'ipamembertimerule'
     ]
     uuid_attribute = 'ipauniqueid'
     rdn_attribute = 'ipauniqueid'
@@ -146,6 +132,7 @@ class hbacrule(LDAPObject):
         'memberhost': ['host', 'hostgroup'],
         'sourcehost': ['host', 'hostgroup'],
         'memberservice': ['hbacsvc', 'hbacsvcgroup'],
+        'ipamembertimerule': ['timerule'],
     }
     managed_permissions = {
         'System: Read HBAC Rules': {
@@ -157,7 +144,7 @@ class hbacrule(LDAPObject):
                 'externalhost', 'hostcategory', 'ipaenabledflag',
                 'ipauniqueid', 'memberhost', 'memberservice', 'memberuser',
                 'servicecategory', 'sourcehost', 'sourcehostcategory',
-                'usercategory', 'objectclass', 'member',
+                'usercategory', 'objectclass', 'member', 'ipamembertimerule',
             },
         },
         'System: Add HBAC Rule': {
@@ -177,7 +164,8 @@ class hbacrule(LDAPObject):
         'System: Manage HBAC Rule Membership': {
             'ipapermright': {'write'},
             'ipapermdefaultattr': {
-                'externalhost', 'memberhost', 'memberservice', 'memberuser'
+                'externalhost', 'memberhost', 'memberservice', 'memberuser',
+                'ipamembertimerule'
             },
             'replaces': [
                 '(targetattr = "memberuser || externalhost || memberservice || memberhost")(target = "ldap:///ipauniqueid=*,cn=hbac,$SUFFIX")(version 3.0;acl "permission:Manage HBAC rule membership";allow (write) groupdn = "ldap:///cn=Manage HBAC rule membership,cn=permissions,cn=pbac,$SUFFIX";)',
@@ -189,7 +177,8 @@ class hbacrule(LDAPObject):
             'ipapermdefaultattr': {
                 'accessruletype', 'accesstime', 'cn', 'description',
                 'hostcategory', 'ipaenabledflag', 'servicecategory',
-                'sourcehost', 'sourcehostcategory', 'usercategory'
+                'sourcehost', 'sourcehostcategory', 'usercategory',
+                'ipamembertimerule'
             },
             'replaces': [
                 '(targetattr = "servicecategory || sourcehostcategory || cn || description || ipaenabledflag || accesstime || usercategory || hostcategory || accessruletype || sourcehost")(target = "ldap:///ipauniqueid=*,cn=hbac,$SUFFIX")(version 3.0;acl "permission:Modify HBAC rule";allow (write) groupdn = "ldap:///cn=Modify HBAC rule,cn=permissions,cn=pbac,$SUFFIX";)',
@@ -244,10 +233,6 @@ class hbacrule(LDAPObject):
             doc=_('Service category the rule applies to'),
             values=(u'all', ),
         ),
-#        AccessTime('accesstime?',
-#            cli_name='time',
-#            label=_('Access time'),
-#        ),
         Str('description?',
             cli_name='desc',
             label=_('Description'),
@@ -272,6 +257,10 @@ class hbacrule(LDAPObject):
             label=_('Host Groups'),
             flags=['no_create', 'no_update', 'no_search'],
         ),
+        Str('ipamembertimerule_timerule?',
+            label=_('Time Rules'),
+            flags=['no_create', 'no_update', 'no_search']
+        ),
         Str('sourcehost_host?',
             deprecated=True,
             label=_('Source Hosts'),
@@ -305,6 +294,8 @@ def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
         assert isinstance(dn, DN)
         # HBAC rules are enabled by default
         entry_attrs['ipaenabledflag'] = 'TRUE'
+        # start as an old type HBAC
+        entry_attrs['objectclass'].append('ipahbacrule')
         return dn
 
 
@@ -349,7 +340,6 @@ def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
         return dn
 
 
-
 @register()
 class hbacrule_find(LDAPSearch):
     __doc__ = _('Search for HBAC rules.')
@@ -358,6 +348,16 @@ class hbacrule_find(LDAPSearch):
         '%(count)d HBAC rule matched', '%(count)d HBAC rules matched', 0
     )
 
+    def pre_callback(self, ldap, filter, attrs_list, base_dn,
+                     scope, *args, **options):
+        assert isinstance(base_dn, DN)
+        filters = [
+            ldap.make_filter({'objectclass': ['ipahbacrule', 'ipahbacrulev2']},
+                             rules=ldap.MATCH_ANY)
+        ]
+        filters.append(filter)
+        filter = ldap.combine_filters(filters, rules=ldap.MATCH_ALL)
+        return (filter, base_dn, scope)
 
 
 @register()
@@ -425,67 +425,76 @@ def execute(self, cn, **options):
         )
 
 
-# @register()
-class hbacrule_add_accesstime(LDAPQuery):
-    """
-    Add an access time to an HBAC rule.
-    """
+@register()
+class hbacrule_add_timerule(LDAPAddMember):
+    __doc__ = _('Add time rules to an HBAC rule.')
 
-    takes_options = (
-        AccessTime('accesstime',
-            cli_name='time',
-            label=_('Access time'),
-        ),
-    )
+    member_attributes = ['ipamembertimerule']
+    member_count_out = ('%i object added.', '%i objects added.')
 
-    def execute(self, cn, **options):
+    def execute(self, *args, **options):
         ldap = self.obj.backend
+        dn = self.obj.get_dn(*args, **options)
+        assert(isinstance(dn, DN))
 
-        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
+            entry_attrs = ldap.get_entry(dn, ['objectclass'])
         except errors.NotFound:
-            self.obj.handle_not_found(cn)
+            self.obj.handle_not_found(*args)
+        objclass_updated = False
+        # ipaHBACRuleV2 objectclass marks new version HBAC rules with new
+        # capabilities such as time policies
+        if ('ipahbacrulev2' not in
+                (o.lower() for o in entry_attrs['objectclass'])):
+            entry_attrs['objectclass'] = [cls for cls in
+                                          entry_attrs['objectclass']
+                                          if cls != 'ipahbacrule']
+            entry_attrs['objectclass'].append('ipahbacrulev2')
+            ldap.update_entry(entry_attrs)
+            objclass_updated = True
 
-        return dict(result=True)
+        try:
+            result = super(hbacrule_add_timerule, self).execute(*args, **options)
+        except Exception as e:
+            self.log.error("Failed to add a timerule: {err}".format(err=e))
+            if objclass_updated:
+                # there was an error adding time rule to an HBAC rule which was
+                # of old version before, switch it back to ipaHBACRule class
+                entry_attrs['objectclass'] = [cls for cls in
+                                              entry_attrs['objectclass']
+                                              if cls != 'ipahbacrulev2']
+                entry_attrs['objectclass'].append('ipahbacrule')
+                ldap.update_entry(entry_attrs)
+            raise
+        return result
 
 
-# @register()
-class hbacrule_remove_accesstime(LDAPQuery):
-    """
-    Remove access time to HBAC rule.
-    """
-    takes_options = (
-        AccessTime('accesstime?',
-            cli_name='time',
-            label=_('Access time'),
-        ),
-    )
+@register()
+class hbacrule_remove_timerule(LDAPRemoveMember):
+    __doc__ = _('Remove users and groups from an HBAC rule.')
 
-    def execute(self, cn, **options):
-        ldap = self.obj.backend
+    member_attributes = ['ipamembertimerule']
+    member_count_out = ('%i object removed.', '%i objects removed.')
 
-        dn = self.obj.get_dn(cn)
+    def execute(self, *args, **options):
+        result = super(hbacrule_remove_timerule, self).execute(*args,
+                                                               **options)
+        dn = result['result']['dn']
+        assert(isinstance(dn, DN))
+        timerules = result['result'].get('membertimerule_timerule', [])
 
-        entry_attrs = ldap.get_entry(dn, ['accesstime'])
-        try:
-            entry_attrs.setdefault('accesstime', []).remove(
-                options['accesstime']
-            )
+        ldap = self.obj.backend
+        entry_attrs = ldap.get_entry(dn, ['objectclass'])
+        if (not timerules and 'ipahbacrulev2' in
+           (o.lower() for o in entry_attrs['objectclass'])):
+            # there are no more time rules left in the HBAC rule, switch
+            # to old type rules
+            entry_attrs['objectclass'] = [cls for cls in
+                                          entry_attrs['objectclass']
+                                          if cls != 'ipahbacrulev2']
+            entry_attrs['objectclass'].append('ipahbacrule')
             ldap.update_entry(entry_attrs)
-        except (ValueError, errors.EmptyModlist):
-            pass
-        except errors.NotFound:
-            self.obj.handle_not_found(cn)
-
-        return dict(result=True)
+        return result
 
 
 @register()
@@ -602,4 +611,3 @@ class hbacrule_remove_service(LDAPRemoveMember):
 
     member_attributes = ['memberservice']
     member_count_out = ('%i object removed.', '%i objects removed.')
-
diff --git a/ipaserver/plugins/timerule.py b/ipaserver/plugins/timerule.py
new file mode 100644
index 0000000..a70f086
--- /dev/null
+++ b/ipaserver/plugins/timerule.py
@@ -0,0 +1,295 @@
+#
+# Copyright (C) 2016  FreeIPA Contributors see COPYING for license
+#
+
+import icalendar
+from datetime import date
+from ipalib import api, errors
+from ipalib import Str
+from ipalib import _, ngettext
+from ipalib.plugable import Registry
+from ipaserver.plugins.baseldap import (
+    LDAPObject,
+    LDAPCreate,
+    LDAPDelete,
+    LDAPRetrieve,
+    LDAPUpdate,
+    LDAPSearch)
+from ipapython.ipa_log_manager import root_logger
+from ipapython.dn import DN
+
+__doc__ = _("""
+Time Rules
+
+Time Rules are rules used to describe time periods (mostly recurring events)
+for different purposes. Most of the time the  purpose of a time rule is to
+set a restrictive time policy, e.g. in HBAC rules where time rules are used
+to restrict when the HBAC rule should apply.
+
+These time rules are based on the iCalendar format
+(https://tools.ietf.org/html/rfc5545).
+
+There are multiple ways to create a time rule:
+1) from an iCalendar string
+    ipa timerule-add ruleName \\
+    --time="BEGIN:VCALENDAR\\nPRODID:-//The Empire//iCal4j 1.0//EN\\n
+VERSION:2.0\\nCALSCALE:GREGORIAN\\nMETHOD:REQUEST\\nBEGIN:VEVENT\\n
+DTSTAMP:20160406T112129Z\\nDTSTART;VALUE=DATE:20160505\\nUID:1@darkside.com\\n
+END:VEVENT\\nEND:VCALENDAR\\n"
+    - note that the --time option requires an escaped string
+
+2) from an iCalendar file (must be run from the client context)
+    ipa timerule-add ruleName --icalfile=icalfile.ics
+    - the --icalfile option requires an iCalendar file containing valid
+      iCalendar string of a VCALENDAR component containing a single VEVENT
+      subcomponent
+""")
+
+register = Registry()
+
+topic = 'timerule'
+
+
+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():
+        if prop not in (comp.singletons + comp.multiple):
+            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_icalstring(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: %(err)s'
+                    ) % {'err': 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)
+
+    if len(vcal.subcomponents) > 1:  # pylint: disable=no-member
+        raise errors.ValidationError(
+            name=name,
+            error=_('Only one VEVENT component may be present in the '
+                    'iCalendar string at a time in this IPA version.'))
+
+    comp = vcal.subcomponents[0]  # pylint: disable=no-member
+
+    if comp.name != 'VEVENT':
+        raise errors.ValidationError(
+            name=name,
+            error=_('Found "%(comp)s" but only VEVENT component is '
+                    'supported.') % {'comp': comp.name}
+            )
+
+    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 "%(sub)s".') % {'sub': 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.')
+                )
+
+
+@register()
+class timerule(LDAPObject):
+    """
+    Time Rule object
+    """
+    container_dn = api.env.container_timerules
+    object_name = _('Time Rule')
+    object_name_plural = _('Time Rules')
+    object_class = ['ipatimerule']
+    permission_filter_objectclasses = ['ipatimerule']
+    default_attributes = ['cn', 'description', 'accesstime', 'memberof']
+    attribute_members = {
+        'memberof': ['hbacrule']
+    }
+    label = _('Time Rules')
+    label_singular = _('Time Rule')
+    managed_permissions = {
+        'System: Read Time Rule': {
+            'replaces_global_anonymous_aci': True,
+            'ipapermbindruletype': 'all',
+            'ipapermright': {'read', 'search', 'compare'},
+            'ipapermdefaultattr': {
+                'cn', 'objectclass', 'description', 'accesstime'
+            },
+        },
+        'System: Read Time Rule Membership': {
+            'replaces_global_anonymous_aci': True,
+            'ipapermbindruletype': 'all',
+            'ipapermright': {'read', 'search', 'compare'},
+            'ipapermdefaultattr': {
+                'memberof',
+            },
+        },
+        'System: Add Time Rule': {
+            'ipapermright': {'add'},
+            'default_privileges': {'Time Rules Administrator'},
+        },
+        'System: Delete Time Rule': {
+            'ipapermright': {'delete'},
+            'default_privileges': {'Time Rules Administrator'},
+        },
+        'System: Modify Time Rule': {
+            'ipapermright': {'write'},
+            'ipapermdefaultattr': {'description', 'accesstime'},
+            'default_privileges': {'Time Rules Administrator'},
+        },
+    }
+
+    takes_params = (
+        Str('cn', cli_name='name',
+            label=_('Rule name'),
+            primary_key=True),
+        Str('description?',
+            label=_('Description')
+            ),
+        # we need to have '?' here so that accessTime is not asked for if it is
+        # set using options/from icalfile
+        Str('accesstime?',
+            cli_name='time',
+            label=_('Access time'),
+            ),
+    )
+
+
+@register()
+class timerule_add(LDAPCreate):
+    __doc__ = _('Create new time rule.')
+
+    msg_summary = _('Added time rule "%(value)s"')
+
+    def pre_callback(self, ldap, dn, entry_attrs, *keys, **options):
+        assert(isinstance(dn, DN))
+        if not options.get('accesstime', False):
+            raise errors.RequirementError(name='accesstime')
+        entry_attrs['accesstime'] = \
+            entry_attrs['accesstime'].decode('unicode-escape')
+        # perform the validation here so that there's no need to decode
+        # the string twice (validation and before storing it to LDAP)
+        validate_icalstring(entry_attrs['accesstime'])
+        return dn
+
+
+@register()
+class timerule_del(LDAPDelete):
+    __doc__ = _('Delete a time rule.')
+
+    msg_summary = _('Deleted time rule "%(value)s"')
+
+
+@register()
+class timerule_mod(LDAPUpdate):
+    __doc__ = _('Modify a time rule.')
+
+    msg_summary = _('Modified a time rule "%(value)s"')
+
+    def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
+        assert isinstance(dn, DN)
+        if 'accesstime' in entry_attrs:
+            entry_attrs['accesstime'] = \
+                entry_attrs['accesstime'].decode('unicode-escape')
+            validate_icalstring(entry_attrs['accesstime'])
+        return dn
+
+
+@register()
+class timerule_find(LDAPSearch):
+    __doc__ = _('Search for time rules.')
+
+    member_attributes = ['memberof']
+
+    msg_summary = ngettext(
+        '%(count)d time rule matched',
+        '%(count)d time rules matched',
+        0,
+    )
+
+
+@register()
+class timerule_show(LDAPRetrieve):
+    __doc__ = _('Display the properties of a time rule object')
