e3ffab
From 3998814d3abb5143f06479d2dbf93bf28285a66e Mon Sep 17 00:00:00 2001
e3ffab
From: Martin Kosek <mkosek@redhat.com>
e3ffab
Date: Tue, 13 Jan 2015 18:09:17 +0100
e3ffab
Subject: [PATCH] Allow PassSync user to locate and update NT users
e3ffab
e3ffab
Add new PassSync Service privilege that have sufficient access to
e3ffab
let AD PassSync service search for NT users and update the password.
e3ffab
To make sure existing PassSync user keeps working, it is added as
e3ffab
a member of the new privilege.
e3ffab
e3ffab
New update plugin is added to add link to the new privilege to the
e3ffab
potentially existing PassSync user to avoid breaking the PassSync
e3ffab
service.
e3ffab
e3ffab
https://fedorahosted.org/freeipa/ticket/4837
e3ffab
e3ffab
Reviewed-By: David Kupka <dkupka@redhat.com>
e3ffab
---
e3ffab
 ACI.txt                                      |  2 +
e3ffab
 install/updates/40-delegation.update         | 30 +++++++++++
e3ffab
 ipalib/plugins/user.py                       | 12 +++++
e3ffab
 ipaserver/install/plugins/Makefile.am        |  1 +
e3ffab
 ipaserver/install/plugins/update_passsync.py | 78 ++++++++++++++++++++++++++++
e3ffab
 ipaserver/install/replication.py             | 54 ++++++++++---------
e3ffab
 6 files changed, 152 insertions(+), 25 deletions(-)
e3ffab
 create mode 100644 ipaserver/install/plugins/update_passsync.py
e3ffab
e3ffab
diff --git a/ACI.txt b/ACI.txt
e3ffab
index 6680f658ee1aa0f961b2681f700557ce6b9238f8..fe45d063e7d48c487e380ca3568b0f9368762c6d 100644
e3ffab
--- a/ACI.txt
e3ffab
+++ b/ACI.txt
e3ffab
@@ -267,6 +267,8 @@ aci: (targetattr = "krblastadminunlock || krblastfailedauth || krblastpwdchange
e3ffab
 dn: cn=users,cn=accounts,dc=ipa,dc=example
e3ffab
 aci: (targetattr = "memberof")(targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:System: Read User Membership";allow (compare,read,search) userdn = "ldap:///all";)
e3ffab
 dn: cn=users,cn=accounts,dc=ipa,dc=example
e3ffab
+aci: (targetattr = "ntuniqueid || ntuseracctexpires || ntusercodepage || ntuserdeleteaccount || ntuserdomainid || ntuserlastlogoff || ntuserlastlogon")(targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:System: Read User NT Attributes";allow (compare,read,search) groupdn = "ldap:///cn=System: Read User NT Attributes,cn=permissions,cn=pbac,dc=ipa,dc=example";)
e3ffab
+dn: cn=users,cn=accounts,dc=ipa,dc=example
e3ffab
 aci: (targetattr = "cn || createtimestamp || description || displayname || entryusn || gecos || gidnumber || givenname || homedirectory || initials || ipantsecurityidentifier || loginshell || manager || modifytimestamp || objectclass || sn || title || uid || uidnumber")(targetfilter = "(objectclass=posixaccount)")(version 3.0;acl "permission:System: Read User Standard Attributes";allow (compare,read,search) userdn = "ldap:///anyone";)
e3ffab
 dn: dc=ipa,dc=example
e3ffab
 aci: (targetattr = "cn || createtimestamp || entryusn || gecos || gidnumber || homedirectory || loginshell || modifytimestamp || objectclass || uid || uidnumber")(target = "ldap:///cn=users,cn=*,cn=views,cn=compat,dc=ipa,dc=example")(version 3.0;acl "permission:System: Read User Views Compat Tree";allow (compare,read,search) userdn = "ldap:///anyone";)
e3ffab
diff --git a/install/updates/40-delegation.update b/install/updates/40-delegation.update
e3ffab
index 988de5e1962fabc6787f5914522b8f133e71a8ff..a79f906ea3e29b8b6755a62ac84d318d6abdd6cc 100644
e3ffab
--- a/install/updates/40-delegation.update
e3ffab
+++ b/install/updates/40-delegation.update
e3ffab
@@ -184,3 +184,33 @@ default:description: Read list of IPA masters
e3ffab
 dn: cn=masters,cn=ipa,cn=etc,$SUFFIX
e3ffab
 add:aci:'(targetfilter = "(objectClass=nsContainer)")(targetattr = "cn || objectClass || ipaConfigString")(version 3.0; acl "Read IPA Masters"; allow (read, search, compare) userdn = "ldap:///fqdn=$FQDN,cn=computers,cn=accounts,$SUFFIX";)'
e3ffab
 add:aci:'(targetfilter = "(objectClass=nsContainer)")(targetattr = "ipaConfigString")(version 3.0; acl "Modify IPA Masters"; allow (write) userdn = "ldap:///fqdn=$FQDN,cn=computers,cn=accounts,$SUFFIX";)'
e3ffab
+
e3ffab
+# PassSync
e3ffab
+dn: cn=PassSync Service,cn=privileges,cn=pbac,$SUFFIX
e3ffab
+default:objectClass: nestedgroup
e3ffab
+default:objectClass: groupofnames
e3ffab
+default:objectClass: top
e3ffab
+default:cn: PassSync Service
e3ffab
+default:description: PassSync Service
e3ffab
+
e3ffab
+dn: cn=Read PassSync Managers Configuration,cn=permissions,cn=pbac,$SUFFIX
e3ffab
+default:objectClass: groupofnames
e3ffab
+default:objectClass: ipapermission
e3ffab
+default:objectClass: top
e3ffab
+default:cn: Read PassSync Managers Configuration
e3ffab
+default:member: cn=Replication Administrators,cn=privileges,cn=pbac,$SUFFIX
e3ffab
+default:ipapermissiontype: SYSTEM
e3ffab
+
e3ffab
+dn: cn=config
e3ffab
+add:aci: '(targetattr = "cn || createtimestamp || entryusn || modifytimestamp || objectclass || passsyncmanagersdns*")(target = "ldap:///cn=ipa_pwd_extop,cn=plugins,cn=config")(version 3.0;acl "permission:Read PassSync Managers Configuration";allow (compare,read,search) groupdn = "ldap:///cn=Read PassSync Managers Configuration,cn=permissions,cn=pbac,$SUFFIX";)'
e3ffab
+
e3ffab
+dn: cn=Modify PassSync Managers Configuration,cn=permissions,cn=pbac,$SUFFIX
e3ffab
+default:objectClass: groupofnames
e3ffab
+default:objectClass: ipapermission
e3ffab
+default:objectClass: top
e3ffab
+default:cn: Modify PassSync Managers Configuration
e3ffab
+default:member: cn=Replication Administrators,cn=privileges,cn=pbac,$SUFFIX
e3ffab
+default:ipapermissiontype: SYSTEM
e3ffab
+
e3ffab
+dn: cn=config
e3ffab
+add:aci: '(targetattr = "passsyncmanagersdns*")(target = "ldap:///cn=ipa_pwd_extop,cn=plugins,cn=config")(version 3.0;acl "permission:Modify PassSync Managers Configuration";allow (write) groupdn = "ldap:///cn=Modify PassSync Managers Configuration,cn=permissions,cn=pbac,$SUFFIX";)'
e3ffab
diff --git a/ipalib/plugins/user.py b/ipalib/plugins/user.py
e3ffab
index e206289248dfe9ae79bd87271ff2c7672fb98b4f..56585b9f86593c0c5879139103bc71707b88e15f 100644
e3ffab
--- a/ipalib/plugins/user.py
e3ffab
+++ b/ipalib/plugins/user.py
e3ffab
@@ -373,10 +373,12 @@ class user(LDAPObject):
e3ffab
             'replaces': [
e3ffab
                 '(target = "ldap:///uid=*,cn=users,cn=accounts,$SUFFIX")(targetattr = "userpassword || krbprincipalkey || sambalmpassword || sambantpassword || passwordhistory")(version 3.0;acl "permission:Change a user password";allow (write) groupdn = "ldap:///cn=Change a user password,cn=permissions,cn=pbac,$SUFFIX";)',
e3ffab
                 '(targetfilter = "(!(memberOf=cn=admins,cn=groups,cn=accounts,$SUFFIX))")(target = "ldap:///uid=*,cn=users,cn=accounts,$SUFFIX")(targetattr = "userpassword || krbprincipalkey || sambalmpassword || sambantpassword || passwordhistory")(version 3.0;acl "permission:Change a user password";allow (write) groupdn = "ldap:///cn=Change a user password,cn=permissions,cn=pbac,$SUFFIX";)',
e3ffab
+                '(targetattr = "userPassword || krbPrincipalKey || sambaLMPassword || sambaNTPassword || passwordHistory")(version 3.0; acl "Windows PassSync service can write passwords"; allow (write) userdn="ldap:///uid=passsync,cn=sysaccounts,cn=etc,$SUFFIX";)',
e3ffab
             ],
e3ffab
             'default_privileges': {
e3ffab
                 'User Administrators',
e3ffab
                 'Modify Users and Reset passwords',
e3ffab
+                'PassSync Service',
e3ffab
             },
e3ffab
         },
e3ffab
         'System: Manage User SSH Public Keys': {
e3ffab
@@ -446,6 +448,16 @@ class user(LDAPObject):
e3ffab
                 'homedirectory', 'loginshell',
e3ffab
             },
e3ffab
         },
e3ffab
+        'System: Read User NT Attributes': {
e3ffab
+            'ipapermbindruletype': 'permission',
e3ffab
+            'ipapermright': {'read', 'search', 'compare'},
e3ffab
+            'ipapermdefaultattr': {
e3ffab
+                'ntuserdomainid', 'ntuniqueid', 'ntuseracctexpires',
e3ffab
+                'ntusercodepage', 'ntuserdeleteaccount', 'ntuserlastlogoff',
e3ffab
+                'ntuserlastlogon',
e3ffab
+            },
e3ffab
+            'default_privileges': {'PassSync Service'},
e3ffab
+        },
e3ffab
     }
e3ffab
 
e3ffab
     label = _('Users')
e3ffab
diff --git a/ipaserver/install/plugins/Makefile.am b/ipaserver/install/plugins/Makefile.am
e3ffab
index d651297ac141b0f05831e7fabbb9b561cdd239c7..ead1d8f7d972c1b016bac8f2b8f7fd1f9a71b563 100644
e3ffab
--- a/ipaserver/install/plugins/Makefile.am
e3ffab
+++ b/ipaserver/install/plugins/Makefile.am
e3ffab
@@ -14,6 +14,7 @@ app_PYTHON = 			\
e3ffab
 	update_referint.py	\
e3ffab
 	ca_renewal_master.py	\
e3ffab
 	update_uniqueness.py	\
e3ffab
+	update_passsync.py	\
e3ffab
 	$(NULL)
e3ffab
 
e3ffab
 EXTRA_DIST =			\
e3ffab
diff --git a/ipaserver/install/plugins/update_passsync.py b/ipaserver/install/plugins/update_passsync.py
e3ffab
new file mode 100644
e3ffab
index 0000000000000000000000000000000000000000..d6595a06f4deb62b853d716012a8c594c6a76451
e3ffab
--- /dev/null
e3ffab
+++ b/ipaserver/install/plugins/update_passsync.py
e3ffab
@@ -0,0 +1,78 @@
e3ffab
+#
e3ffab
+# Copyright (C) 2014  FreeIPA Contributors see COPYING for license
e3ffab
+#
e3ffab
+
e3ffab
+from ipaserver.install.plugins import MIDDLE, LAST
e3ffab
+from ipaserver.install.plugins.baseupdate import PreUpdate, PostUpdate
e3ffab
+from ipalib import api, errors
e3ffab
+from ipapython.dn import DN
e3ffab
+from ipapython.ipa_log_manager import root_logger
e3ffab
+from ipaserver.install import sysupgrade
e3ffab
+
e3ffab
+class update_passync_privilege_check(PreUpdate):
e3ffab
+    order = MIDDLE
e3ffab
+
e3ffab
+    def execute(self, **options):
e3ffab
+        update_done = sysupgrade.get_upgrade_state('winsync', 'passsync_privilege_updated')
e3ffab
+        if update_done:
e3ffab
+            root_logger.debug("PassSync privilege update pre-check not needed")
e3ffab
+            return False, False, []
e3ffab
+
e3ffab
+        root_logger.debug("Check if there is existing PassSync privilege")
e3ffab
+
e3ffab
+        passsync_privilege_dn = DN(('cn','PassSync Service'),
e3ffab
+                self.api.env.container_privilege,
e3ffab
+                self.api.env.basedn)
e3ffab
+
e3ffab
+        ldap = self.obj.backend
e3ffab
+        try:
e3ffab
+            ldap.get_entry(passsync_privilege_dn, [''])
e3ffab
+        except errors.NotFound:
e3ffab
+            root_logger.debug("PassSync privilege not found, this is a new update")
e3ffab
+            sysupgrade.set_upgrade_state('winsync', 'passsync_privilege_updated', False)
e3ffab
+        else:
e3ffab
+            root_logger.debug("PassSync privilege found, skip updating PassSync")
e3ffab
+            sysupgrade.set_upgrade_state('winsync', 'passsync_privilege_updated', True)
e3ffab
+
e3ffab
+        return False, False, []
e3ffab
+
e3ffab
+api.register(update_passync_privilege_check)
e3ffab
+
e3ffab
+class update_passync_privilege_update(PostUpdate):
e3ffab
+    """
e3ffab
+        Add PassSync user as a member of PassSync privilege, if it exists
e3ffab
+    """
e3ffab
+
e3ffab
+    order = LAST
e3ffab
+
e3ffab
+    def execute(self, **options):
e3ffab
+        update_done = sysupgrade.get_upgrade_state('winsync', 'passsync_privilege_updated')
e3ffab
+        if update_done:
e3ffab
+            root_logger.debug("PassSync privilege update not needed")
e3ffab
+            return False, False, []
e3ffab
+
e3ffab
+        root_logger.debug("Add PassSync user as a member of PassSync privilege")
e3ffab
+        ldap = self.obj.backend
e3ffab
+        passsync_dn = DN(('uid','passsync'), ('cn', 'sysaccounts'), ('cn', 'etc'),
e3ffab
+            api.env.basedn)
e3ffab
+        passsync_privilege_dn = DN(('cn','PassSync Service'),
e3ffab
+                self.api.env.container_privilege,
e3ffab
+                self.api.env.basedn)
e3ffab
+
e3ffab
+        try:
e3ffab
+            entry = ldap.get_entry(passsync_dn, [''])
e3ffab
+        except errors.NotFound:
e3ffab
+            root_logger.debug("PassSync user not found, no update needed")
e3ffab
+            sysupgrade.set_upgrade_state('winsync', 'passsync_privilege_updated', True)
e3ffab
+            return False, False, []
e3ffab
+        else:
e3ffab
+            root_logger.debug("PassSync user found, do update")
e3ffab
+
e3ffab
+        update = {'dn': passsync_privilege_dn,
e3ffab
+                  'updates': ["add:member:'%s'" % passsync_dn]}
e3ffab
+        updates = {passsync_privilege_dn: update}
e3ffab
+
e3ffab
+        sysupgrade.set_upgrade_state('winsync', 'passsync_privilege_updated', True)
e3ffab
+        return (False, True, [updates])
e3ffab
+
e3ffab
+api.register(update_passync_privilege_update)
e3ffab
diff --git a/ipaserver/install/replication.py b/ipaserver/install/replication.py
e3ffab
index 5778cab036ad87ccb5b69254aa307a6bc8dec871..66764c22f69328942fe2e4581cfafb3806438d7c 100644
e3ffab
--- a/ipaserver/install/replication.py
e3ffab
+++ b/ipaserver/install/replication.py
e3ffab
@@ -528,39 +528,43 @@ class ReplicationManager(object):
e3ffab
         print "The user for the Windows PassSync service is %s" % pass_dn
e3ffab
         try:
e3ffab
             conn.get_entry(pass_dn)
e3ffab
-            print "Windows PassSync entry exists, not resetting password"
e3ffab
-            return
e3ffab
+            print "Windows PassSync system account exists, not resetting password"
e3ffab
         except errors.NotFound:
e3ffab
-            pass
e3ffab
-
e3ffab
-        # The user doesn't exist, add it
e3ffab
-        entry = conn.make_entry(
e3ffab
-            pass_dn,
e3ffab
-            objectclass=["account", "simplesecurityobject"],
e3ffab
-            uid=["passsync"],
e3ffab
-            userPassword=[password],
e3ffab
-        )
e3ffab
-        conn.add_entry(entry)
e3ffab
+            # The user doesn't exist, add it
e3ffab
+            print "Adding Windows PassSync system account"
e3ffab
+            entry = conn.make_entry(
e3ffab
+                pass_dn,
e3ffab
+                objectclass=["account", "simplesecurityobject"],
e3ffab
+                uid=["passsync"],
e3ffab
+                userPassword=[password],
e3ffab
+            )
e3ffab
+            conn.add_entry(entry)
e3ffab
 
e3ffab
-        # Add it to the list of users allowed to bypass password policy
e3ffab
+        # Add the user to the list of users allowed to bypass password policy
e3ffab
         extop_dn = DN(('cn', 'ipa_pwd_extop'), ('cn', 'plugins'), ('cn', 'config'))
e3ffab
         entry = conn.get_entry(extop_dn)
e3ffab
-        pass_mgrs = entry.get('passSyncManagersDNs')
e3ffab
-        if not pass_mgrs:
e3ffab
-            pass_mgrs = []
e3ffab
-        if not isinstance(pass_mgrs, list):
e3ffab
-            pass_mgrs = [pass_mgrs]
e3ffab
+        pass_mgrs = entry.get('passSyncManagersDNs', [])
e3ffab
         pass_mgrs.append(pass_dn)
e3ffab
         mod = [(ldap.MOD_REPLACE, 'passSyncManagersDNs', pass_mgrs)]
e3ffab
-        conn.modify_s(extop_dn, mod)
e3ffab
-
e3ffab
-        # And finally grant it permission to write passwords
e3ffab
-        mod = [(ldap.MOD_ADD, 'aci',
e3ffab
-            ['(targetattr = "userPassword || krbPrincipalKey || sambaLMPassword || sambaNTPassword || passwordHistory")(version 3.0; acl "Windows PassSync service can write passwords"; allow (write) userdn="ldap:///%s";)' % pass_dn])]
e3ffab
         try:
e3ffab
-            conn.modify_s(self.suffix, mod)
e3ffab
+            conn.modify_s(extop_dn, mod)
e3ffab
+        except ldap.TYPE_OR_VALUE_EXISTS:
e3ffab
+            root_logger.debug("Plugin '%s' already '%s' in passSyncManagersDNs",
e3ffab
+                    extop_dn, pass_dn)
e3ffab
+
e3ffab
+        # And finally add it is a member of PassSync privilege to allow
e3ffab
+        # displaying user NT attributes and reset passwords
e3ffab
+        passsync_privilege_dn = DN(('cn','PassSync Service'),
e3ffab
+                api.env.container_privilege,
e3ffab
+                api.env.basedn)
e3ffab
+        members = entry.get('member', [])
e3ffab
+        members.append(pass_dn)
e3ffab
+        mod = [(ldap.MOD_REPLACE, 'member', members)]
e3ffab
+        try:
e3ffab
+            conn.modify_s(passsync_privilege_dn, mod)
e3ffab
         except ldap.TYPE_OR_VALUE_EXISTS:
e3ffab
-            root_logger.debug("passsync aci already exists in suffix %s on %s" % (self.suffix, conn.host))
e3ffab
+            root_logger.debug("PassSync service '%s' already have '%s' as member",
e3ffab
+                    passsync_privilege_dn, pass_dn)
e3ffab
 
e3ffab
     def setup_winsync_agmt(self, entry, win_subtree=None):
e3ffab
         if win_subtree is None:
e3ffab
-- 
e3ffab
2.1.0
e3ffab